1 |
|
2 |
|
3 | var spawn = require("child_process").spawn
|
4 | , opened = {}
|
5 | , defaults = {
|
6 | bin: "sqlite3",
|
7 | detached: true
|
8 | }
|
9 | , escapeRe = /'/g
|
10 | , unescapeRe = /''/g
|
11 |
|
12 | module.exports = openDb
|
13 |
|
14 | function openDb(file, opts) {
|
15 | return opened[file] || new Db(file, opts)
|
16 | }
|
17 |
|
18 | function nop() {}
|
19 |
|
20 | function 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 |
|
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)
|
75 | type = (
|
76 | code === 39 ? 7 :
|
77 | code === 88 ? 6 :
|
78 | code === 99 ? 3 :
|
79 | code === 82 ? 4 :
|
80 | code === 78 ? (i+=3, 1) : 2
|
81 | )
|
82 | } else if (code === 10 || code === 44) {
|
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 |
|
160 | Db.prototype = {
|
161 |
|
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 |
|
255 | function 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 |
|