Add support to query books on specified lists (of award winners).
[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, Title Field = "aut", "lan", "lst", "ser", "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 var g_db *sql.DB = nil
47 var g_mutex = &sync.Mutex{}
48
49 // ============================================================================
50 func dbShutdown() {
51   if nil != g_db {
52     g_db.Close()
53   }
54 }
55
56 func getDb() (*sql.DB) {
57   if nil == g_db {
58     g_mutex.Lock()
59     defer g_mutex.Unlock()
60     if nil == g_db {
61       config := GetConfig()
62       g_db = openDb(config.user, config.pass, config.dbName)
63     }
64   }
65
66   return g_db
67 }
68
69 func niVal(ni sql.NullInt64) int {
70   if ni.Valid {
71     return int(ni.Int64)
72   }
73   return 0
74 }
75
76 func nsVal(ns sql.NullString) string {
77   if ns.Valid {
78     return ns.String
79   }
80   return ""
81 }
82
83 func openDb(user, pass, dbName string) (*sql.DB) {
84   db, err := sql.Open("postgres","user=" + user + " password=" + pass + " dbname=" + dbName + " sslmode=disable")
85   if nil != err {
86     report("Error:  DB arguments incorrect?", err)
87     return nil
88   }
89
90   err = db.Ping()
91   if nil != err {
92     report("Error:  could not connect to DB.", err)
93     db.Close()
94     return nil
95   }
96
97   return db
98 }
99
100 func queryBooksByIds(ids []int) []Book {
101   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
102             FROM Authors a 
103             INNER JOIN Books b ON a.id=b.author
104             LEFT OUTER JOIN Classifications c ON c.id=b.classification
105             LEFT OUTER JOIN Series s ON s.id=b.series
106             WHERE b.id=$1`
107
108   ps, err := getDb().Prepare(query)
109   if nil != err {
110     report("Error:  failed to prepare statement:  " + query, err)
111     return nil
112   }
113   defer ps.Close()
114
115   var count int = 0
116   for _, id := range ids {
117     if 0 != id {
118       count++
119     }
120   }
121     
122   res := make([]Book, count)
123   
124   count = 0
125   for _, id := range ids {
126     if 0 == id {
127       continue
128     }
129
130     row := ps.QueryRow(id)
131
132     var age, grouping, reading, sort, ddc, description, genre, language, lcc, name, title, volume sql.NullString
133     var cover sql.NullInt64
134
135     err = row.Scan(&age, &grouping, &reading, &sort, &cover, &ddc, &description, &genre, &language, &lcc, &name, &title, &volume)
136     if err != nil {
137       report("Error:  Failed to read book:" + strconv.Itoa(id) + ":", err)
138     } else {
139       var b Book
140       b.Id = id
141       b.Age = nsVal(age)
142       b.AuthorGrouping = nsVal(grouping)
143       b.AuthorReading = nsVal(reading)
144       b.AuthorSort = nsVal(sort)
145       b.CoverId = niVal(cover)
146       b.DDC = nsVal(ddc)
147       b.Description = nsVal(description)
148       b.Genre = nsVal(genre)
149       b.Language = nsVal(language)
150       b.LCC = nsVal(lcc)
151       b.SeriesName = nsVal(name)
152       b.Title = nsVal(title)
153       b.Volume = nsVal(volume)
154
155       res[count] = b
156       count++
157     }
158   }
159
160   if count < len(res) {
161     res = res[:count]
162   }
163
164   return res
165 }
166
167 func queryBookPathById(id int) (string) {
168   query := "SELECT b.path FROM Books b WHERE b.id=$1"
169
170   ps, err := getDb().Prepare(query)
171   if nil != err {
172     report("Failed to Prepare query:  " + query, err)
173     return ""
174   }
175   defer ps.Close()
176
177   row := ps.QueryRow(id)
178   var path sql.NullString
179   err = row.Scan(&path)
180   if nil != err {
181     report(fmt.Sprintf("Failed to retrieve path for book id %v: ", id), err)
182     return ""
183   }
184
185   return nsVal(path)
186 }
187
188 func queryIds(criteria []SearchTerm) []int {
189   query := "SELECT b.id FROM Books b" +
190            " INNER JOIN Authors a ON a.id=b.author" +
191            " LEFT OUTER JOIN Series s ON s.id=b.series" +
192            " LEFT OUTER JOIN Lists_Books lb ON b.id=lb.book" +
193            " LEFT OUTER JOIN Lists l ON l.id=lb.list"
194
195   args := make([]interface{}, 0)
196
197   i := 0
198
199   for _, criterion := range criteria {
200     if 0 == i {
201       query += " WHERE "
202     } else {
203       query += " AND "
204     }
205
206     switch criterion.Attribute {
207     case Author:
208       query += " UPPER(a.grouping) LIKE UPPER($" + strconv.Itoa(i + 1) + ")"
209       args = append(args, strings.Replace(criterion.Text, " ", "", -1))
210     case Language:
211       query += " UPPER(b.language) LIKE UPPER($" + strconv.Itoa(i + 1) + ")"
212       args = append(args, criterion.Text)
213     case List:
214       codes := strings.Split(criterion.Text, ",")
215       query += " UPPER(l.code) IN ("
216
217       for j, code := range codes {
218         if j > 0 {
219           query += ","
220         }
221         query += "UPPER($" + strconv.Itoa(i + j + 1) + ")"
222         args = append(args, code)
223       }
224       query += ")"
225       i += len(codes) - 1
226     case Series:
227       query += " UPPER(s.descr) LIKE UPPER($" + strconv.Itoa(i + 1) + ")"
228       args = append(args, criterion.Text)
229     case Title:
230       query += " UPPER(b.title) LIKE UPPER($" + strconv.Itoa(i + 1) + ")"
231       args = append(args, criterion.Text)
232     default:
233       report("Error:  unrecognized search field in queryIds():  " + criterion.Attribute.String(), nil)
234       return nil
235     }
236
237     i++
238   }
239
240   query += " ORDER BY a.grouping,s.descr,b.volume,b.title,b.path"
241
242   res := []int{}
243
244   ps, err := getDb().Prepare(query)
245   if nil != err {
246     report("Failed to Prepare query:  " + query, err)
247     return nil
248   }
249   defer ps.Close()
250
251   var rows *sql.Rows
252   rows, err = ps.Query(args...)
253   if nil != err {
254     report("Failed to execute query:  " + query, err)
255     return nil
256   }
257   defer rows.Close()
258
259   for rows.Next() {
260     var id int
261     rows.Scan(&id)
262     res = append(res, id)
263   }
264
265   return res
266 }
267
268 func queryMimeTypeByEfsId(efsId int) string {
269   const query = "SELECT mimeType FROM Efs WHERE id=$1"
270
271   ps, err := getDb().Prepare(query)
272   if nil != err {
273     report("Failed to Prepare query:  " + query, err)
274     return ""
275   }
276   defer ps.Close()
277
278   row := ps.QueryRow(efsId)
279   var mimeType sql.NullString
280   err = row.Scan(&mimeType)
281   if nil != err {
282     report(fmt.Sprintf("Failed to retrieve mimeType for id %v: ", efsId), err)
283     return ""
284   }
285   
286   return nsVal(mimeType)
287 }
288
289 func report(msg string, err error) {
290   fmt.Println("Error:  " + msg, err)
291 }