UNPKG

7.91 kBJavaScriptView Raw
1
2
3var spawn = require("child_process").spawn
4, opened = {}
5, defaults = {
6 bin: "sqlite3",
7 detached: true
8}
9, escapeRe = /'/g
10, unescapeRe = /''/g
11
12module.exports = openDb
13
14function openDb(file, opts) {
15 return opened[file] || new Db(file, opts)
16}
17
18function nop() {}
19
20function Db(file, opts) {
21 var db = Object.assign(this, defaults, opts)
22 , _col = 0, _len = 0, _type = 0
23 , _row = {}
24 , args = [db.bin, "-header", file || ""]
25 , bufs = []
26
27 if (file && file !== ":memory:") {
28 opened[db.file = file] = db
29 }
30
31 if (db.nice) args.unshift("nice", "-n", db.nice)
32
33 db.queue = []
34 db.headers = db.pending = false
35
36 db.child = spawn(args.shift(), args, db)
37 ;(
38 db.pipe ?
39 db.child.stdout.pipe(db.pipe) :
40 db.child.stdout
41 ).on("data", function(buf) {
42 var code
43 , cut = 0
44 , i = 0
45 , len = buf.length
46 , type = _type
47 , col = _col
48 , row = _row
49
50 if (db.headers === false) {
51 if (buf[0] === 89) {
52 // no response, wait stderr before calling callback
53 return setImmediate(_done)
54 }
55 if (buf[0] === 10 && buf.length === 1) return
56 i = cut = buf.indexOf(10) + 1
57 db.headers = buf.toString("utf8", 1, i - 2).split("','")
58 } else if (type === 9) {
59 type = 8
60 i = 1
61 if (buf[0] === 10 || buf[0] === 44) {
62 read(buf, i)
63 }
64 }
65
66 for (; i < len; ) {
67 if (type > 6 && (
68 buf[i++] !== 39 ||
69 buf[i] === 39 && (type = 8) && ++i ||
70 i === len && (type = 9)
71 )) continue
72 code = buf[i++]
73 if (type === 0) {
74 if (code === 89) return setImmediate(_done) // Y
75 type = (
76 code === 39 ? 7 : // '
77 code === 88 ? 6 : // X
78 code === 99 ? 3 : // changes: 1 total_changes: 5
79 code === 82 ? 4 : // Run Time: real 0.001 user 0.000069 sys 0.000079
80 code === 78 ? (i+=3, 1) : 2 // NULL : numbers
81 )
82 } else if (code === 10 || code === 44) { // \n || ,
83 read(buf, i)
84 }
85 }
86 _col = col
87 _row = row
88 _type = type
89 if (cut === len) return
90 if (bufs.push(buf) === 1) {
91 if (cut > 0) bufs[0] = bufs[0].slice(cut)
92 _len = bufs[0].length
93 } else {
94 _len += len
95 }
96 function read(buf, i) {
97 var j = i
98 if (bufs.length > 0) {
99 bufs.push(buf.slice(0, j))
100 buf = Buffer.concat(bufs, j += _len)
101 _len = _type = bufs.length = 0
102 }
103 if (type === 3) {
104 j = buf.toString("utf8", cut, j).split(/[\:\s]+/)
105 db.changes = +j[1]
106 db.totalChanges = +j[3]
107 } else if (type === 4) {
108 j = buf.toString("utf8", cut, j).split(/[\:\s]+/)
109 db.real = +j[3]
110 db.user = +j[5]
111 db.sys = +j[7]
112 } else {
113 row[db.headers[col]] = (
114 type === 1 ? null :
115 type === 2 ? 1 * buf.toString("utf8", cut, j-1) :
116 type === 6 ? (
117 cut + 6 === j ? buf[cut + 3] === 49 :
118 Buffer.from(buf.toString("utf8", cut+2, j-2), "hex")
119 ) :
120 type > 7 ? buf.toString("utf8", cut+1, j-2).replace(unescapeRe, "'") :
121 buf.toString("utf8", cut+1, j-2)
122 )
123 if (code === 10) {
124 if (db.firstRow === null) db.firstRow = row
125 if (db.onRow !== null) db.onRow.call(db, row)
126 row = {}
127 col = 0
128 } else {
129 col++
130 }
131 }
132 cut = i
133 type = 0
134 }
135 })
136 .on("end", _done)
137
138 db.child.stderr.on("data", function(buf) {
139 db.error = buf.toString("utf8", 0, buf.length - 1)
140 })
141
142 db.run(".mode quote", function(err) {
143 if (err) throw Error(err)
144 })
145
146 if (db.migration) migrate(db, db.migration)
147
148 function _done() {
149 _row = {}
150 _type = _col = 0
151 db.headers = db.pending = false
152 if (db.onDone !== null) db.onDone.call(db, db.error)
153 else if (db.error !== null) throw Error(db.error + "\n-- " + db.lastQuery)
154 if (db.queue.length > 0 && db.pending === false) {
155 db.each.apply(db, db.queue.shift())
156 }
157 }
158}
159
160Db.prototype = {
161 // Overwriting Db.prototype will ruin constructor
162 constructor: Db,
163 _add: function(query, values, onDone, onRow) {
164 var db = this
165 if (db.pending === true) {
166 db.queue.unshift([query, values, onRow, onDone])
167 } else {
168 db.each(query, values, onRow, onDone)
169 }
170 },
171 _esc: function _esc(value) {
172 return typeof value !== "string" ? (
173 value === true ? "X'01'" :
174 value === false ? "X'00'" :
175 value == null || value !== value ? "null" :
176 Array.isArray(value) ? value.map(_esc).join(",") :
177 Buffer.isBuffer(value) ? "X'" + value.toString("hex") + "'" :
178 value
179 ) :
180 "'" + value.replace(escapeRe, "''").replace(/\0/g, "") + "'"
181 },
182 each: function(query, values, onRow, onDone) {
183 var db = this
184
185 if (Array.isArray(values)) {
186 query = query.split("?")
187 for (var i = values.length; i--; ) {
188 query[i] += db._esc(values[i])
189 }
190 query = query.join("")
191 } else if (typeof values === "function") {
192 onDone = onRow
193 onRow = values
194 }
195 if (db.pending === true) {
196 db.queue.push([query, onRow, onDone])
197 } else {
198 db.pending = true
199 db.changes = 0
200 db.real = db.user = db.sys = null
201 db.error = db.firstRow = null
202 db.onRow = typeof onRow === "function" ? onRow : null
203 db.onDone = typeof onDone === "function" ? onDone : null
204 db.lastQuery = query
205 db.child.stdin.write(
206 query.charCodeAt(0) !== 46 && query.charCodeAt(query.length-1) !== 59 ? query + ";\n.print Y\n" :
207 query + "\n.print Y\n"
208 )
209 }
210 },
211 run: function(query, values, onDone) {
212 if (typeof values === "function") {
213 onDone = values
214 values = null
215 }
216 return this.each(query, values, nop, onDone)
217 },
218 all: function(query, values, onDone) {
219 if (typeof values === "function") {
220 onDone = values
221 values = null
222 }
223 var rows = []
224 this.each(query, values, rows.push.bind(rows), function(err) {
225 onDone.call(this, err, rows)
226 })
227 },
228 get: function(query, values, onDone) {
229 if (typeof values === "function") {
230 onDone = values
231 values = null
232 }
233 this.each(query, values, nop, function(err) {
234 if (typeof onDone === "function") {
235 onDone.call(this, err, this.firstRow)
236 }
237 })
238 },
239 insert: function(query, values, onDone) {
240 if (values && values.constructor === Object) {
241 query += "(" + Object.keys(values) + ")"
242 values = Object.values(values)
243 }
244 if (Array.isArray(values)) {
245 query += " VALUES (" + Array(values.length).join("?,") + "?)"
246 }
247 this.get("INSERT INTO " + query + ";SELECT last_insert_rowid() AS lastId;", values, onDone)
248 },
249 close: function(onDone) {
250 opened[this.file] = null
251 this.each(".quit", nop, onDone)
252 }
253}
254
255function migrate(db, dir) {
256 var fs = require("fs")
257 , path = require("./path")
258 , log = require("./log")("db")
259
260 db.get("PRAGMA user_version", function(err, res) {
261 if (err) return log.error(err)
262 var patch = ""
263 , current = res.user_version
264 , files = fs.readdirSync(dir).filter(isSql).sort()
265 , latest = parseInt(files[files.length - 1], 10)
266
267 log.info("Migrate %s current:%i latest:%i in:%s", db.file, current, latest, dir)
268
269 function saveVersion(err) {
270 if (err) throw Error(err)
271 db.run("PRAGMA user_version=?", [current], function(err) {
272 if (err) throw Error(err)
273 log.info("Migrated to", latest)
274 })
275 db.run("INSERT INTO db_schema_log(ver) VALUES (?)", [current], applyPatch)
276 }
277
278 function applyPatch(err) {
279 if (err) throw Error(err)
280 for (var ver, f, i = 0; f = files[i++]; ) {
281 ver = parseInt(f, 10)
282 if (ver > current) {
283 current = ver
284 log.info("Applying migration: %s", f)
285 f = fs.readFileSync(path.resolve(dir, f), "utf8").trim().split(/\s*^-- Down$\s*/m)
286 db.run(f[0])
287 db.run(
288 "REPLACE INTO db_schema(ver,up,down) VALUES(?,?,?)",
289 [ver, f[0], f[1]],
290 saveVersion
291 )
292 }
293 }
294 }
295
296 if (latest > current) {
297 applyPatch()
298 } else if (latest < current) {
299 var rows = []
300 db.run(
301 "SELECT down FROM db_schema WHERE id>? ORDER BY id DESC",
302 [current],
303 function(err) {
304 if (err) throw Error(err)
305 var patch = rows.map(r=>r.rollback).join("\n")
306 db.run(patch, null, saveVersion)
307 },
308 rows.push.bind(rows)
309 )
310 }
311 })
312
313 function isSql(name) {
314 return name.split(".").pop() === "sql"
315 }
316}
317
318