Add ability to select sort order
[quanweb.git] / main / db.go
1 package main
2
3 import (
4   "database/sql"
5   "fmt"
6   _ "github.com/lib/pq"
7   "strings"
8   "strconv"
9   "sync"
10 )
11
12 // ---------------------------------------------------------------------------
13 type Book struct {
14   Id             int
15   Age            string  // recommended age, e.g. "beginner", "junior", "ya" (Young Adult), "adult"
16   AuthorGrouping string  // unique rendering of the author's name, used for internal grouping 
17   AuthorReading  string  // reading order of author's name, e.g. "Charles Dickens"
18   AuthorSort     string  // sort order of author's name, e.g. "Dickens, Charles"
19   CoverId        int     // index into EFS table for cover, if there is one
20   DDC            string  // Dewey Decimal Classification
21   Description    string  // Back cover / inside flap blurb, describing the book
22   Genre          string  // e.g. "adventure", "historical", "mystery", "romance", "sf" (Science Fiction)
23   Language       string
24   LCC            string  // Library of Congress Classification
25   SeriesName     string  
26   Title          string
27   Volume         string
28 }
29
30 // ---------------------------------------------------------------------------
31 type Field string
32 const (
33   Author, Language, List, Series, Sort, Title Field = "aut", "lan", "lst", "ser", "srt", "tit"
34 )
35
36 func (f Field) String() string {
37   return string(f)
38 }
39
40 // ---------------------------------------------------------------------------
41 type SearchTerm struct {
42   Attribute Field
43   Text      string
44 }
45
46 // ---------------------------------------------------------------------------
47 type SortOrder string
48 const (
49   ByArrival, ByAuthor, ByPublication, ByTitle SortOrder = "arr", "aut", "pub", "tit"
50 )
51
52 func (so SortOrder) String() string {
53   return string(so)
54 }
55
56 // ---------------------------------------------------------------------------
57 var g_db *sql.DB = nil
58 var g_mutex = &sync.Mutex{}
59
60 // ============================================================================
61 func conditional(count int) (string, int) {
62   if (count == 0) {
63     return "WHERE", 1
64   } else {
65     return "AND", (count + 1)
66   }
67 }
68
69 func dbShutdown() {
70   if nil != g_db {
71     g_db.Close()
72   }
73 }
74
75 func getDb() (*sql.DB) {
76   if nil == g_db {
77     g_mutex.Lock()
78     defer g_mutex.Unlock()
79     if nil == g_db {
80       config := GetConfig()
81       g_db = openDb(config.user, config.pass, config.dbName)
82     }
83   }
84
85   return g_db
86 }
87
88 func niVal(ni sql.NullInt64) int {
89   if ni.Valid {
90     return int(ni.Int64)
91   }
92   return 0
93 }
94
95 func nsVal(ns sql.NullString) string {
96   if ns.Valid {
97     return ns.String
98   }
99   return ""
100 }
101
102 func openDb(user, pass, dbName string) (*sql.DB) {
103   db, err := sql.Open("postgres","user=" + user + " password=" + pass + " dbname=" + dbName + " sslmode=disable")
104   if nil != err {
105     report("Error:  DB arguments incorrect?", err)
106     return nil
107   }
108
109   err = db.Ping()
110   if nil != err {
111     report("Error:  could not connect to DB.", err)
112     db.Close()
113     return nil
114   }
115
116   return db
117 }
118
119 func queryBooksByIds(ids []int) []Book {
120   query := `SELECT s.age,a.grouping,a.reading,a.sort,b.cover,c.ddc,b.description,s.genre,b.language,c.lcc,s.descr,b.title,b.volume
121             FROM Authors a 
122             INNER JOIN Books b ON a.id=b.author
123             LEFT OUTER JOIN Classifications c ON c.id=b.classification
124             LEFT OUTER JOIN Series s ON s.id=b.series
125             WHERE b.id=$1`
126
127   ps, err := getDb().Prepare(query)
128   if nil != err {
129     report("Error:  failed to prepare statement:  " + query, err)
130     return nil
131   }
132   defer ps.Close()
133
134   var count int = 0
135   for _, id := range ids {
136     if 0 != id {
137       count++
138     }
139   }
140     
141   res := make([]Book, count)
142   
143   count = 0
144   for _, id := range ids {
145     if 0 == id {
146       continue
147     }
148
149     row := ps.QueryRow(id)
150
151     var age, grouping, reading, sort, ddc, description, genre, language, lcc, name, title, volume sql.NullString
152     var cover sql.NullInt64
153
154     err = row.Scan(&age, &grouping, &reading, &sort, &cover, &ddc, &description, &genre, &language, &lcc, &name, &title, &volume)
155     if err != nil {
156       report("Error:  Failed to read book:" + strconv.Itoa(id) + ":", err)
157     } else {
158       var b Book
159       b.Id = id
160       b.Age = nsVal(age)
161       b.AuthorGrouping = nsVal(grouping)
162       b.AuthorReading = nsVal(reading)
163       b.AuthorSort = nsVal(sort)
164       b.CoverId = niVal(cover)
165       b.DDC = nsVal(ddc)
166       b.Description = nsVal(description)
167       b.Genre = nsVal(genre)
168       b.Language = nsVal(language)
169       b.LCC = nsVal(lcc)
170       b.SeriesName = nsVal(name)
171       b.Title = nsVal(title)
172       b.Volume = nsVal(volume)
173
174       res[count] = b
175       count++
176     }
177   }
178
179   if count < len(res) {
180     res = res[:count]
181   }
182
183   return res
184 }
185
186 func queryBookPathById(id int) (string) {
187   query := "SELECT b.path FROM Books b WHERE b.id=$1"
188
189   ps, err := getDb().Prepare(query)
190   if nil != err {
191     report("Failed to Prepare query:  " + query, err)
192     return ""
193   }
194   defer ps.Close()
195
196   row := ps.QueryRow(id)
197   var path sql.NullString
198   err = row.Scan(&path)
199   if nil != err {
200     report(fmt.Sprintf("Failed to retrieve path for book id %v: ", id), err)
201     return ""
202   }
203
204   return nsVal(path)
205 }
206
207 func queryIds(criteria []SearchTerm) []int {
208   query := "SELECT b.id FROM Books b" +
209            " INNER JOIN Authors a ON a.id=b.author" +
210            " LEFT OUTER JOIN Series s ON s.id=b.series" +
211            " LEFT OUTER JOIN Lists_Books lb ON b.id=lb.book" +
212            " LEFT OUTER JOIN Lists l ON l.id=lb.list" +
213            " "
214
215   args := make([]interface{}, 0)
216   conjunction := "WHERE"
217   count := 0
218   sort := ByAuthor
219
220   for _, criterion := range criteria {
221     switch criterion.Attribute {
222     case Author:
223       conjunction, count = conditional(count)
224       query += conjunction + " UPPER(a.grouping) LIKE UPPER($" + strconv.Itoa(count) + ")"
225       args = append(args, strings.Replace(criterion.Text, " ", "", -1))
226     case Language:
227       conjunction, count = conditional(count)
228       query += conjunction + " UPPER(b.language) LIKE UPPER($" + strconv.Itoa(count) + ")"
229       args = append(args, criterion.Text)
230     case List:
231       conjunction, count = conditional(count)
232       codes := strings.Split(criterion.Text, ",")
233       query += conjunction + " UPPER(l.code) IN ("
234
235       for j, code := range codes {
236         if j > 0 {
237           query += ","
238         }
239         query += "UPPER($" + strconv.Itoa(count + j + 1) + ")"
240         args = append(args, code)
241       }
242       query += ")"
243       count += len(codes) - 1
244     case Series:
245       conjunction, count = conditional(count)
246       query += conjunction + " UPPER(s.descr) LIKE UPPER($" + strconv.Itoa(count) + ")"
247       args = append(args, criterion.Text)
248     case Sort:
249       sort = SortOrder(criterion.Text)
250     case Title:
251       conjunction, count = conditional(count)
252       query += conjunction + " UPPER(b.title) LIKE UPPER($" + strconv.Itoa(count) + ")"
253       args = append(args, criterion.Text)
254     default:
255       report("Error:  unrecognized search field in queryIds():  " + criterion.Attribute.String(), nil)
256       return nil
257     }
258   }
259
260   switch sort {
261   case ByArrival:
262     query += " ORDER BY b.arrived DESC,a.grouping,s.descr,b.volume,b.title,b.path"
263   case ByAuthor:
264     query += " ORDER BY a.grouping,s.descr,b.volume,b.title,b.path,b.arrived DESC"
265   case ByPublication:
266     report("Error: cannot sort by publication (not yet implemented)", nil)
267     return nil
268   case ByTitle:
269     query += " ORDER BY b.title,a.grouping,s.descr,b.volume,b.path,b.arrived DESC"
270   default:
271     report("Error: unrecognized sort order in queryIds(): " + sort.String(), nil)
272     return nil
273   }
274
275   res := []int{}
276
277   ps, err := getDb().Prepare(query)
278   if nil != err {
279     report("Failed to Prepare query:  " + query, err)
280     return nil
281   }
282   defer ps.Close()
283
284   var rows *sql.Rows
285   rows, err = ps.Query(args...)
286   if nil != err {
287     report("Failed to execute query:  " + query, err)
288     return nil
289   }
290   defer rows.Close()
291
292   for rows.Next() {
293     var id int
294     rows.Scan(&id)
295     res = append(res, id)
296   }
297
298   return res
299 }
300
301 func queryMimeTypeByEfsId(efsId int) string {
302   const query = "SELECT mimeType FROM Efs WHERE id=$1"
303
304   ps, err := getDb().Prepare(query)
305   if nil != err {
306     report("Failed to Prepare query:  " + query, err)
307     return ""
308   }
309   defer ps.Close()
310
311   row := ps.QueryRow(efsId)
312   var mimeType sql.NullString
313   err = row.Scan(&mimeType)
314   if nil != err {
315     report(fmt.Sprintf("Failed to retrieve mimeType for id %v: ", efsId), err)
316     return ""
317   }
318   
319   return nsVal(mimeType)
320 }
321
322 func report(msg string, err error) {
323   fmt.Println("Error:  " + msg, err)
324 }