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 | 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()
|
76 | type = (
|
77 | code === 39 ? 5 :
|
78 | code === 88 ? 4 :
|
79 | code === 99 ? 3 :
|
80 | code === 82 ? 3 :
|
81 | code === 78 ? (i+=3, 1) : 2
|
82 | )
|
83 | } else if (code === 10 || code === 44) {
|
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 |
|
155 | Db.prototype = {
|
156 |
|
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 |
|
248 | function 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 |
|