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