UNPKG

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