7 require_relative 'series'
8 require_relative 'tconn'
12 @basePath + '/csv/unclassified.csv'
15 def initialize(config_file)
18 config = IniFile.load(config_file)
20 puts 'FATAL: Failed to load config file "' + config_file + '". Aborting initialization.'
24 section = config['database']
25 @dbhost = section['host']
27 @dbname = section['name']
28 @dbuser = section['user']
29 @dbpass = section['pass']
31 section = config['filesystem']
32 @basePath = section['basePath']
36 @conn = TimedConn.new(PG.connect(@dbhost, @dbport, '', '', @dbname, @dbuser, @dbpass))
44 def construct_efs_path(efs_id)
45 id_str = sprintf('%010d', efs_id)
46 path = sprintf('%s/%s/%s/%s', id_str[0,2], id_str[2,2], id_str[4,2], id_str[6,2])
47 name = id_str + '.dat'
51 def cross_reference_lists
52 puts "@@@@@@@@@@@ CROSS-REF START @@@@@@@@@@@"
53 exec_update("TRUNCATE TABLE Lists CASCADE;", [])
56 puts "@@@@@@@@@@@ CROSS-REF DONE @@@@@@@@@@@"
59 def create_schema(skip_class)
62 CREATE TABLE Authors (
63 id INTEGER PRIMARY KEY,
73 id INTEGER PRIMARY KEY,
75 author INTEGER REFERENCES Authors(id),
76 classification INTEGER REFERENCES Classifications(id),
81 series INTEGER REFERENCES Series(id),
87 create_classification =
89 CREATE TABLE Classifications (
90 id INTEGER PRIMARY KEY,
93 author_grouping VARCHAR(64),
94 author_sort VARCHAR(128),
95 title_grouping VARCHAR(256),
103 id INTEGER PRIMARY KEY,
111 id VARCHAR(32) PRIMARY KEY,
116 # Associative entity, linking FAST and Classifications tables
117 # in a 0..n to 0..m relationship
118 create_fast_classifications =
120 CREATE TABLE FAST_Classifications (
121 fast VARCHAR(32) REFERENCES FAST(id),
122 classification INTEGER REFERENCES Classifications(id)
129 id INTEGER PRIMARY KEY,
131 category VARCHAR(32),
134 author INTEGER REFERENCES Authors(id),
139 # Associative entity, linking Lists and Books tables
140 # in a 0..n to 0..m relationship
143 CREATE TABLE Lists_Books (
144 list INTEGER REFERENCES Lists(id),
145 book INTEGER REFERENCES Books(id)
151 CREATE TABLE Series (
152 id INTEGER PRIMARY KEY,
155 grouping VARCHAR(64),
163 create_classification,
168 create_fast_classifications,
171 'CREATE SEQUENCE author_id;',
172 'CREATE SEQUENCE book_id;',
173 'CREATE SEQUENCE classification_id;',
174 'CREATE SEQUENCE efs_id;',
175 'CREATE SEQUENCE list_id;',
176 'CREATE SEQUENCE series_id;'
183 if skip_class == false
185 populate_classifications_table
188 populate_series_table
193 'DROP TABLE Lists_Books;',
196 'DROP TABLE FAST_Classifications;',
197 'DROP TABLE Authors;',
198 'DROP TABLE Classifications;',
201 'DROP TABLE Series;',
202 'DROP SEQUENCE author_id;',
203 'DROP SEQUENCE book_id;',
204 'DROP SEQUENCE classification_id;',
205 'DROP SEQUENCE efs_id;',
206 'DROP SEQUENCE list_id;',
207 'DROP SEQUENCE series_id;'
213 rescue Exception => exc
214 puts 'WARNING: "' + stmt + '" failed: ' + exc.to_s
219 def find_all_authors(author_name)
222 sqlSelect = "SELECT id FROM Authors WHERE grouping=$1;"
225 @conn.exec_params(sqlSelect, args) do |rs|
234 def find_author(author)
235 sqlSelect = "SELECT id FROM Authors WHERE grouping=$1 AND reading=$2 AND sort=$3;"
236 args = [author.grouping, author.reading_order, author.sort_order]
238 @conn.exec_params(sqlSelect, args) do |rs|
247 def init_db(skip_class)
248 sql = "SELECT 1 FROM pg_tables WHERE tableowner='quanlib' AND tablename='books'"
250 @conn.exec(sql).each do |row|
255 create_schema(skip_class)
260 sqlSelect = "SELECT grouping, reading, sort FROM Authors WHERE id=$1"
262 @conn.exec_params(sqlSelect, args) do |rs|
264 raise "Expected 1 row for " + id + " but got " + rs.ntuples + ": " + sqlSelect
267 author = Author.new(row['grouping'], row['reading'], row['sort'])
273 def store_author(author)
274 id = find_author(author)
276 id = next_id('author_id')
277 sqlInsert = "INSERT INTO Authors(id, grouping, reading, sort) VALUES ($1, $2, $3, $4);"
278 args = [id, author.grouping, author.reading_order, author.sort_order]
280 rs = @conn.exec_params(sqlInsert, args)
281 rescue Exception => e
282 puts sqlInsert + ": " + args.inspect()
293 sql = "SELECT author, classification, cover, description, language, path, series, title, volume FROM Books WHERE id=$1;"
297 @conn.exec_params(sql, [id]) do |rs|
299 raise 'Expected one row in Books for id ' + id + ', but found ' + rs.length + '.'
304 book = Book.new(self)
305 book.author = load_author(row['author'])
306 book.classification_id = row['classification']
307 book.cover = load_cover(row['cover'])
308 book.description = row['description']
309 book.language = row['language']
310 book.path = row['path']
311 book.series_id = row['series']
312 book.title = row['title']
313 book.volume = row['volume']
315 rescue Exception => e
325 sql = "INSERT INTO Books (id, arrived, author, classification, cover, description, language, path, series, title, volume) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);"
327 book_id = next_id('book_id')
329 author_id = store_author(book.author)
330 (efs_id, mime_type) = store_cover(book)
332 args = [book_id, book.arrived, author_id, book.classification_id, efs_id, book.description, book.language, book.path, book.series_id, book.title, book.volume]
335 rs = @conn.exec_params(sql, args)
336 rescue Exception => e
337 puts sql + ": " + args.inspect()
347 def find_classification(author_grouping, title_grouping)
348 sql = "SELECT id FROM Classifications WHERE author_grouping = $1 AND title_grouping = $2;"
349 @conn.exec_params(sql, [author_grouping, title_grouping]) do |rs|
357 def load_classification(id)
358 sql = "SELECT ddc, lcc, author_grouping, author_sort, title_grouping, title "
359 sql += " FROM Classifications WHERE id=$1"
360 @conn.exec_params(sql, [id]) do |rs|
365 author_grouping = row['author_grouping']
366 author = row['author_sort']
367 title_grouping = row['title_grouping']
370 result = Classification.new(ddc, lcc, author_grouping, author, title_grouping, title)
384 mime_type = 'application/octet-stream'
386 sql = "SELECT mimeType FROM Efs WHERE id=$1"
387 @conn.exec_params(sql, [id]) do |rs|
389 raise "Expected one row but got " + rs.ntuples + ": " + sql + ": " + id
391 mime_type = rs[0]['mimeType']
394 (efspath, efsname) = construct_efs_path(id)
396 fullpath = @basePath + '/efs/' + efspath + '/' + efsname
398 return Cover.new(nil, fullpath, mime_type)
401 def store_cover(book)
409 @conn.exec("SELECT nextval('efs_id')") do |rs|
410 efs_id = rs[0]['nextval']
417 (efspath, efsname) = construct_efs_path(efs_id)
419 efspath = @basePath + '/efs/' + efspath
421 FileUtils.mkdir_p(efspath)
423 (filepath, mimetype) = cover.write_image(efspath, efsname)
425 sql = "INSERT INTO efs VALUES ($1, $2)"
427 rs = @conn.exec_params(sql, [efs_id, mimetype])
428 rescue Exception => e
429 puts sql + ": " + efs_id + ", " + mimetype
436 return efs_id, mimetype
439 def exec_id_query(sql, args)
441 @conn.exec_params(sql, args) do |rs|
449 def exec_update(sql, args)
451 rs = @conn.exec_params(sql, args)
452 rescue Exception => e
453 puts sql + ": " + args.inspect()
461 def next_id(seq_name)
463 @conn.exec("SELECT nextval('" + seq_name + "');") do |rs|
464 id = rs[0]['nextval']
469 def get_series(grouping, code)
474 sql = "SELECT id FROM Series WHERE grouping=$1 AND code=$2;"
475 args = [grouping, code]
476 @conn.exec_params(sql, args).each do |row|
480 # TODO: Create a new series object here?
481 puts 'WARNING: series("' + grouping + '", "' + code + '") not found.'
486 sql = "SELECT age,genre,grouping,code,descr FROM Series WHERE id=$1;"
488 @conn.exec_params(sql, args) do |rs|
491 series = Series.new(id)
492 series.age = row['age']
493 series.genre = row['genre']
494 series.grouping = row['grouping']
495 series.code = row['code']
496 series.descr = row['descr']
503 def populate_classifications_table
504 puts "Populating the Classifications table..."
506 CSV.foreach(@basePath + '/csv/class.csv') do |row|
508 # skip the header row
512 # First, add a row to the Classifications table
514 id = next_id('classification_id')
517 author_grouping = row[2]
519 title_grouping = row[4]
522 sqlInsert = "INSERT INTO Classifications (id, ddc, lcc, author_grouping, author_sort, title_grouping, title) VALUES ($1, $2, $3, $4, $5, $6, $7);"
523 args = [id, ddc, lcc, author_grouping, author_sort, title_grouping, title]
524 exec_update(sqlInsert, args)
526 # Second, link up with the appropriate FAST table entries
531 fast = input.split(';')
534 fast.each do |fast_id|
535 sqlInsert = "INSERT INTO FAST_Classifications (fast, classification) VALUES ($1, $2);"
537 exec_update(sqlInsert, args)
543 def populate_fast_table
544 puts "Populating the FAST table..."
546 CSV.foreach(@basePath + '/csv/fast.csv') do |row|
548 first = false # skip the header row
552 sqlInsert = "INSERT INTO FAST (id, descr) VALUES ($1, $2);"
553 exec_update(sqlInsert, [id, descr])
558 def populate_lists_table
559 puts "Populating the Lists table..."
561 CSV.foreach(@basePath + "/csv/lists.csv", headers: true) do |row|
562 author_ids = find_all_authors(row['author'])
564 specification = [row['age'], row['category'], row['code'], row['year'], row['author'], row['title']]
565 .map { |x| x.inspect }
568 puts "WARNING: For list entry (#{specification}), no such author was found."
574 INSERT INTO Lists (id, age, category, code, year, author, title)
575 VALUES ($1, $2, $3, $4, $5, $6, $7);
577 author_ids.each do |author_id|
578 list_id = next_id('list_id')
579 args = [list_id, row['age'], row['category'], row['code'], row['year'], author_id, row['title']]
580 exec_update(sqlInsert, args)
582 update_lists_books_table(list_id, author_id, row['title'])
587 # Scan for books that match this Lists entry, and add any matches to the Lists_Books associative table
588 def update_lists_books_table(list_id, author_id, title)
589 title_pattern = Book.grouping_for_title(title).gsub('_', '%')
590 sqlSelect = "SELECT id FROM Books WHERE author = $1 AND title LIKE $2;"
591 args = [author_id, title_pattern]
593 @conn.exec_params(sqlSelect, args) do |rs|
595 sqlInsert = "INSERT INTO Lists_Books (list, book) VALUES ($1, $2)"
596 args = [list_id, row['id']]
597 exec_update(sqlInsert, args)
602 def populate_series_table
603 puts "Populating the Series table..."
604 CSV.foreach(@basePath + '/csv/series.csv') do |row|
605 id = next_id('series_id')
606 sqlInsert = "INSERT INTO Series (id, age, genre, grouping, code, descr) VALUES ($1, $2, $3, $4, $5, $6);"
608 exec_update(sqlInsert, args)
612 def query_books_by_author(pattern)
615 SELECT b.id FROM Authors a
616 INNER JOIN Books b ON b.author=a.id
617 LEFT OUTER JOIN Series s on s.id=b.series
618 WHERE upper(a.grouping) LIKE $1
619 ORDER BY a.grouping, b.series, b.volume, b.title
621 return exec_id_query(sql, [pattern])
624 def query_books_by_ddc
627 SELECT b.id FROM Classifications c
628 INNER JOIN Books b ON b.classification=c.id
631 return exec_id_query(sql, [])
634 def query_books_by_series_id(id)
637 SELECT b.id FROM Books b
639 ORDER BY b.volume,b.title
641 return exec_id_query(sql, [id])
644 def query_series_by_age(pattern)
650 ORDER BY s.grouping,s.descr
652 return exec_id_query(sql, [pattern])