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