946c43c4f9056a53a1e753b2240babeb90c08e81
[quanlib.git] / store.rb
1
2 require 'csv'
3 require 'fileutils'
4 require 'pg'
5
6 class Store
7   def initialize
8     @basepath = '/arc/quanlib'  # TODO: FIXME: configure this in a sane way
9     @conn = nil
10
11     #@dburl = 'dbi:Pg:quanlib:localhost'
12     @dbhost = "localhost"
13     @dbport = 5432
14     @dbname = 'quanlib'
15     @dbuser = 'quanlib'
16     @dbpass = 'quanlib'
17   end
18
19   def connect
20     # @conn = PGconn.connect('localhost', 5432, '', '', 'quanlib', 'quanlib', 'quanlib')
21     @conn = PG.connect('localhost', 5432, '', '', 'quanlib', 'quanlib', 'quanlib')
22     return @conn
23   end
24
25   def disconnect
26     @conn.close()
27   end
28
29   def construct_efs_path(efs_id)
30     id_str = sprintf('%010d', efs_id)
31     path = sprintf('%s/%s/%s/%s', id_str[0,2], id_str[2,2], id_str[4,2], id_str[6,2])
32     name = id_str + '.dat'
33     return path, name
34   end
35
36   def create_schema
37     create_authors = 
38 <<EOS
39       CREATE TABLE Authors (
40         id          INTEGER PRIMARY KEY,
41         grouping    VARCHAR(64),
42         reading     VARCHAR(128),
43         sort        VARCHAR(128)
44       );
45 EOS
46
47     create_books = 
48 <<EOS
49       CREATE TABLE Books (
50         id          INTEGER PRIMARY KEY,
51         author      INTEGER REFERENCES Authors(id),
52         cover       INTEGER,
53         description TEXT,
54         path        VARCHAR(256),
55         series      INTEGER REFERENCES Series(id),
56         title       VARCHAR(196),
57         volume      VARCHAR(16)
58       );
59 EOS
60
61     create_efs = 
62 <<EOS
63       CREATE TABLE EFS (
64         id          INTEGER,
65         mimetype    VARCHAR(64)
66       );
67 EOS
68
69     create_series = 
70 <<EOS
71       CREATE TABLE Series (
72         id          INTEGER PRIMARY KEY,
73         age         VARCHAR(32),
74         genre       VARCHAR(32),
75         grouping    VARCHAR(64),
76         code        VARCHAR(16),
77         descr       VARCHAR(128)
78       )
79 EOS
80
81     stmts = [
82       create_authors,
83       create_efs,
84       create_series,
85       create_books,
86       'CREATE SEQUENCE author_id;',
87       'CREATE SEQUENCE book_id;',
88       'CREATE SEQUENCE efs_id;',
89       'CREATE SEQUENCE series_id;'
90     ]
91
92     for stmt in stmts
93       @conn.exec(stmt)
94     end
95
96     populate_series_table()
97   end
98
99   def dropSchema
100     stmts = [
101       'DROP TABLE Books;',
102       'DROP TABLE Authors;',
103       'DROP TABLE EFS;',
104       'DROP TABLE Series;',
105       'DROP SEQUENCE author_id;',
106       'DROP SEQUENCE book_id;',
107       'DROP SEQUENCE efs_id;',
108       'DROP SEQUENCE series_id;'
109     ]
110
111     for stmt in stmts do
112       @conn.exec(stmt)
113     end
114   end
115
116   def find_author(author)
117     sqlSelect = "SELECT id FROM Authors WHERE grouping=$1 AND reading=$2 AND sort=$3;"
118     args = [author.grouping, author.reading_order, author.sort_order]
119     @conn.exec_params(sqlSelect, args) do |rs|
120       if rs.ntuples > 0
121         return rs[0]['id']
122       end
123     end
124     return nil
125   end
126
127   def init_db
128     sql = "SELECT 1 FROM pg_tables WHERE tableowner='quanlib' AND tablename='books'"
129     found = false
130     @conn.exec(sql).each do |row|
131       found = true
132     end
133
134     if ! found
135       create_schema()
136     end
137   end
138
139   def load_author(id)
140     #puts 'DEBUG:  load_author(' + id + ')'
141     sqlSelect = "SELECT grouping, reading, sort FROM Authors WHERE id=$1"
142     args = [id]
143     @conn.exec_params(sqlSelect, args) do |rs|
144       if rs.ntuples != 1
145         raise "Expected 1 row for " + id + " but got " + rs.ntuples + ":  " + sqlSelect
146       end
147       row = rs[0]
148       author = Author.new(row['grouping'], row['reading'], row['sort'])
149       #puts 'DEBUG:  author:  ' + author.inspect()
150       return author
151     end
152     #puts 'DEBUG:  NOT FOUND'
153     return nil
154   end
155
156   def store_author(author)
157     id = find_author(author)
158     if nil == id
159       id = next_id('author_id')
160       sqlInsert = "INSERT INTO Authors(id, grouping, reading, sort) VALUES ($1, $2, $3, $4);"
161       args = [id, author.grouping, author.reading_order, author.sort_order]
162       begin 
163         rs = @conn.exec_params(sqlInsert, args)
164       rescue Exception => e
165         puts sqlInsert + ":  " + args.inspect()
166         puts e.message
167         puts $@
168       ensure
169         rs.clear if rs
170       end
171     end
172     return id
173   end
174
175   def load_book(id)
176     #puts 'DEBUG:  load_book(' + id + ')'
177     sql = "SELECT author, cover, description, path, series, title, volume FROM Books WHERE id=$1;"
178     book = nil
179
180     begin
181       @conn.exec_params(sql, [id]) do |rs|
182         if 1 != rs.ntuples
183           raise 'Expected one row in Books for id ' + id + ', but found ' + rs.length + '.'
184           return nil
185         end
186         row = rs[0]
187
188         book = Book.new(self)
189         book.author = load_author(row['author'])
190         book.cover = load_cover(row['cover'])
191         book.description = row['description']
192         book.path = row['path']
193         book.series_id = row['series']
194         book.title = row['title']
195         book.volume = row['volume']
196       end    
197     rescue Exception => e
198       puts sql + ": " + id
199       puts e.message
200       puts $@
201     end
202
203     #puts 'DEBUG:  loaded book:   ' + book.inspect()
204     return book
205   end
206
207   def store_book(book)
208     sql = "INSERT INTO Books (id, author, cover, description, path, series, title, volume) VALUES ($1, $2, $3, $4, $5, $6, $7, $8);"
209
210     book_id = next_id('book_id')
211
212     author_id = store_author(book.author)
213     (efs_id, mime_type) = store_cover(book)
214
215     args = [book_id, author_id, efs_id, book.description(), book.path(), book.series_id(), book.title(), book.volume()]
216
217     begin
218       rs = @conn.exec_params(sql, args)
219     rescue Exception => e
220       puts sql + ": " + args.inspect()
221       puts e.message 
222       puts $@
223     ensure
224       rs.clear if rs
225     end
226
227     return book_id
228   end
229
230   def load_cover(id)
231     if nil == id
232       return nil
233     end
234
235     mime_type = 'application/octet-stream'
236
237     sql = "SELECT mimeType FROM Efs WHERE id=$1"
238     @conn.exec_params(sql, [id]) do |rs|
239       if rs.ntuples != 1
240         raise "Expected one row but got " + rs.ntuples + ": " + sql + ": " + id
241       end
242       mime_type = rs[0]['mimeType']
243     end
244
245     (efspath, efsname) = construct_efs_path(id)
246
247     File.open(@basepath + '/efs/' + efspath + '/' + efsname, 'rb') do |is|
248       return Cover.new(is, efsname, mime_type)
249     end
250
251     return nil
252   end
253
254   def store_cover(book)
255     efs_id = nil
256     cover = book.cover()
257
258     if nil == cover
259       return nil
260     end
261
262     @conn.exec("SELECT nextval('efs_id')") do |rs|
263       efs_id = rs[0]['nextval']
264     end
265
266     if nil == efs_id
267       return nil
268     end
269
270     (efspath, efsname) = construct_efs_path(efs_id)
271
272     efspath = @basepath + '/efs/' + efspath
273
274     FileUtils.mkdir_p(efspath)
275
276     (filepath, mimetype) = cover.write_image(efspath, efsname)
277
278     sql = "INSERT INTO efs VALUES ($1, $2)"
279     begin
280       rs = @conn.exec_params(sql, [efs_id, mimetype])
281     rescue Exception => e
282       puts sql + ": " + efs_id + ", " + mimetype
283       puts e.message
284       puts $@
285     ensure
286       rs.clear if rs
287     end
288     
289     return efs_id, mimetype
290   end
291
292   def next_id(seq_name)
293     id = nil
294     @conn.exec("SELECT nextval('" + seq_name + "');") do |rs|
295       id = rs[0]['nextval']
296     end 
297     return id
298   end
299
300   def get_series(grouping, code)
301     if nil == code
302       return nil
303     end
304
305     sql = "SELECT id FROM Series WHERE grouping=$1 AND code=$2;"
306     args = [grouping, code]
307     @conn.exec_params(sql, args).each do |row|
308       return row['id']
309     end
310
311     # TODO:  Create a new series object here?
312     puts 'WARNING:  series("' + grouping + '", "' + code + '") not found.'
313     return nil
314   end
315
316   def load_series(id)
317     sql = "SELECT descr FROM Series WHERE id=$1;"
318     args = [id]
319     @conn.exec_params(sql, args) do |rs|
320       if rs.ntuples > 0
321         return rs[0]['descr']
322       end
323     end
324     return nil
325   end
326
327   def populate_series_table
328     puts "Populating the Series table..."
329     CSV.foreach(@basepath + '/csv/series.csv') do |row|
330       id = next_id('series_id')
331       sqlInsert = "INSERT INTO Series (id, age, genre, grouping, code, descr) VALUES ($1, $2, $3, $4, $5, $6);"
332       args = [id] + row
333       begin
334         # DEBUG: puts 'SQL> ' + sqlInsert + ':  ' + args.inspect()
335         rs = @conn.exec_params(sqlInsert, args)
336       rescue Exception => e
337         puts sqlInsert + ":  " + args.inspect()
338         puts e.message
339         puts $@
340       ensure
341         rs.clear if rs
342       end
343     end
344   end
345
346   def query_books_by_author(pattern)
347     sql = 
348 <<EOS
349       SELECT b.id FROM Authors a 
350       INNER JOIN Books b ON b.author=a.id 
351       LEFT OUTER JOIN Series s on s.id=b.series
352       WHERE upper(a.grouping) LIKE $1 
353       ORDER BY a.grouping, b.series, b.volume, b.title
354 EOS
355     book_ids = []
356     @conn.exec_params(sql, [pattern]) do |rs|
357       rs.each do |row|
358         book_ids.push(row['id'])
359       end
360     end
361     return book_ids
362   end
363 end
364