cozy-pouch-link 60.30.2 → 60.30.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db/sqlite/sql.js +368 -81
- package/dist/db/sqlite/sql.mango.spec.js +225 -25
- package/dist/db/sqlite/sql.spec.js +64 -5
- package/dist/db/sqlite/sqliteDb.native.js +89 -30
- package/dist/db/sqlite/sqliteDb.native.spec.js +70 -0
- package/dist/errors.js +53 -2
- package/package.json +2 -2
- package/types/db/sqlite/sql.d.ts +2 -1
- package/types/db/sqlite/sqliteDb.native.d.ts +11 -0
- package/types/errors.d.ts +12 -0
package/dist/db/sqlite/sql.js
CHANGED
|
@@ -5,26 +5,42 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", {
|
|
6
6
|
value: true
|
|
7
7
|
});
|
|
8
|
-
exports.executeSQL = exports.deleteIndex = exports.createMangoIndex = exports.makeSQLCreateDeletedIndex = exports.makeSQLCreateDocIDIndex = exports.makeSQLCreateMangoIndex = exports.makeSQLDropIndex = exports.makeSQLQueryAll = exports.makeSQLQueryForIds = exports.makeSQLQueryForId = exports.makeSQLQueryFromMango = exports.makeSortClause = exports.makeWhereClause = exports.mangoSelectorToSQL = exports.parseResults = exports.keepDocWitHighestRev = void 0;
|
|
8
|
+
exports.executeSQL = exports.toWebSQLResult = exports.deleteIndex = exports.createMangoIndex = exports.makeSQLCreateDeletedIndex = exports.makeSQLCreateDocIDIndex = exports.makeSQLCreateMangoIndex = exports.makeSQLDropIndex = exports.makeSQLQueryAll = exports.makeSQLQueryForIds = exports.makeSQLQueryForId = exports.makeSQLQueryFromMango = exports.makeSortClause = exports.makeWhereClause = exports.mangoSelectorToSQL = exports.parseResults = exports.keepDocWitHighestRev = void 0;
|
|
9
9
|
|
|
10
10
|
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
|
|
11
11
|
|
|
12
12
|
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
|
|
13
13
|
|
|
14
|
+
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
15
|
+
|
|
14
16
|
var _jsonapi = require("../../jsonapi");
|
|
15
17
|
|
|
16
18
|
var _helpers = require("../helpers");
|
|
17
19
|
|
|
20
|
+
var _errors = require("../../errors");
|
|
21
|
+
|
|
22
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
|
|
23
|
+
|
|
24
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
|
|
25
|
+
|
|
26
|
+
// Binary operators with a direct SQL equivalent. Everything else needs its own
|
|
27
|
+
// SQL shape and is handled explicitly in parseCondition.
|
|
18
28
|
var MANGO_TO_SQL_OP = {
|
|
19
29
|
$eq: '=',
|
|
20
|
-
$ne: '!=',
|
|
21
30
|
$gt: '>',
|
|
22
31
|
$gte: '>=',
|
|
23
32
|
$lt: '<',
|
|
24
|
-
$lte: '<='
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
$lte: '<='
|
|
34
|
+
}; // Mango and SQLite do not spell types the same way: json_type() reports integers
|
|
35
|
+
// and reals separately, and booleans as 'true'/'false'.
|
|
36
|
+
|
|
37
|
+
var MANGO_TYPE_TO_JSON_TYPES = {
|
|
38
|
+
null: ['null'],
|
|
39
|
+
boolean: ['true', 'false'],
|
|
40
|
+
number: ['integer', 'real'],
|
|
41
|
+
string: ['text'],
|
|
42
|
+
array: ['array'],
|
|
43
|
+
object: ['object']
|
|
28
44
|
};
|
|
29
45
|
|
|
30
46
|
var extractRevPrefix = function extractRevPrefix(rev) {
|
|
@@ -130,70 +146,249 @@ var parseResults = function parseResults(client, result, doctype) {
|
|
|
130
146
|
skip: skip,
|
|
131
147
|
next: next
|
|
132
148
|
};
|
|
133
|
-
}; //
|
|
134
|
-
//
|
|
149
|
+
}; // Single quotes are doubled ('') so a value like "l'ete.txt" cannot break out of
|
|
150
|
+
// the literal (SQL error) or inject.
|
|
135
151
|
|
|
136
152
|
|
|
137
153
|
exports.parseResults = parseResults;
|
|
138
154
|
|
|
155
|
+
var escapeSQLString = function escapeSQLString(str) {
|
|
156
|
+
return str.replace(/'/g, "''");
|
|
157
|
+
}; // Quote a value for inline SQL.
|
|
158
|
+
// $in, $nin and $all are the operators whose operand is a list. A non-array
|
|
159
|
+
// operand would reach .map / .length and either throw a TypeError or, worse,
|
|
160
|
+
// silently iterate a string's characters.
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
var requireArrayOperand = function requireArrayOperand(operator, field, value) {
|
|
164
|
+
if (!Array.isArray(value)) {
|
|
165
|
+
throw new _errors.UnsupportedMangoSelectorError("".concat(operator, " on \"").concat(field, "\" expects an array, got ").concat(JSON.stringify(value)));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return value;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
var joinTests = function joinTests(tests, operator) {
|
|
172
|
+
return tests.length > 1 ? "(".concat(tests.join(" ".concat(operator, " ")), ")") : tests[0];
|
|
173
|
+
};
|
|
174
|
+
|
|
139
175
|
var quoteSQLValue = function quoteSQLValue(value) {
|
|
140
176
|
if (typeof value === 'string') {
|
|
141
|
-
return "'".concat(value
|
|
177
|
+
return "'".concat(escapeSQLString(value), "'");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
181
|
+
// NaN and +/-Infinity have no SQL literal: they would be interpolated as the
|
|
182
|
+
// bare words "NaN" / "Infinity", which SQLite reads as column names.
|
|
183
|
+
throw new _errors.UnsupportedMangoSelectorError("Cannot express ".concat(String(value), " as a SQL value"));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (value === undefined || typeof value === 'object' && value !== null) {
|
|
187
|
+
// undefined, objects and arrays have no SQL literal form. A template
|
|
188
|
+
// literal would stringify undefined to the word "undefined" - the very
|
|
189
|
+
// failure this translator exists to make impossible - and an object to
|
|
190
|
+
// "[object Object]".
|
|
191
|
+
throw new _errors.UnsupportedMangoSelectorError("Cannot express ".concat(JSON.stringify(value), " as a SQL value"));
|
|
142
192
|
}
|
|
143
193
|
|
|
144
194
|
return value;
|
|
195
|
+
}; // Mango forbids mixing operators and field names in the same object, so a single
|
|
196
|
+
// $-prefixed key is enough to tell an operator object ({ $gt: 1 }) from a nested
|
|
197
|
+
// document selector ({ b: 'x' }).
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
var isOperatorObject = function isOperatorObject(obj) {
|
|
201
|
+
return Object.keys(obj).some(function (key) {
|
|
202
|
+
return key.startsWith('$');
|
|
203
|
+
});
|
|
145
204
|
};
|
|
146
205
|
|
|
147
|
-
var
|
|
206
|
+
var isPlainObject = function isPlainObject(value) {
|
|
207
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
208
|
+
}; // json_extract yields SQL NULL for a missing path, and NOT NULL is NULL rather
|
|
209
|
+
// than true. CouchDB's $not / $nor match documents where the inner condition
|
|
210
|
+
// merely fails to hold, missing field included, so resolve the unknown to false
|
|
211
|
+
// before negating.
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
var negate = function negate(sql) {
|
|
215
|
+
return "NOT IFNULL((".concat(sql, "), 0)");
|
|
216
|
+
}; // json_each / json_type / json_array_length take (column, path) rather than a
|
|
217
|
+
// pre-extracted value: json_extract unwraps a JSON string into bare SQL text,
|
|
218
|
+
// which those functions then fail to re-parse as JSON.
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
var makeJSONFunctionArgs = function makeJSONFunctionArgs(field, columnName) {
|
|
222
|
+
return field ? "".concat(columnName, ", '$.").concat(escapeSQLString(field), "'") : columnName;
|
|
223
|
+
}; // "at least one element of the array satisfies elementWhere", where the element
|
|
224
|
+
// is bound to `elem.value`.
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
var makeArrayElementExists = function makeArrayElementExists(field, columnName, elementWhere) {
|
|
228
|
+
return "EXISTS (SELECT 1 FROM json_each(".concat(makeJSONFunctionArgs(field, columnName), ") AS elem WHERE ").concat(elementWhere, ")");
|
|
229
|
+
}; // A sub-selector made of operators applies to the array element itself
|
|
230
|
+
// ({ tags: { $elemMatch: { $eq: 'a' } } }); one made of field names applies to
|
|
231
|
+
// the element's subfields ({ referenced_by: { $elemMatch: { type: 'album' } } }).
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
var parseElemMatch = function parseElemMatch(field, subSelector, columnName) {
|
|
235
|
+
if (!isPlainObject(subSelector)) {
|
|
236
|
+
throw new _errors.UnsupportedMangoSelectorError("$elemMatch on \"".concat(field, "\" expects a selector"));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
var elementColumn = 'elem.value';
|
|
240
|
+
var where = isOperatorObject(subSelector) ? parseCondition('', subSelector, elementColumn) : mangoSelectorToSQL(subSelector, elementColumn);
|
|
241
|
+
return makeArrayElementExists(field, columnName, where);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
var parseCondition = function parseCondition(field, condition) {
|
|
245
|
+
var columnName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'data';
|
|
148
246
|
var conditions = [];
|
|
149
247
|
var sqlField = transformMangoFieldInJSONSQL(field, columnName);
|
|
150
248
|
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
249
|
+
if (!isPlainObject(condition)) {
|
|
250
|
+
// Implicit equality. Routed through $eq so `null` gets the same IS NULL
|
|
251
|
+
// treatment as its explicit form.
|
|
252
|
+
return parseCondition(field, {
|
|
253
|
+
$eq: condition
|
|
254
|
+
}, columnName);
|
|
255
|
+
}
|
|
157
256
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
conditions.push("".concat(sqlField, " ").concat(sqlOp, " (").concat(values, ")"));
|
|
165
|
-
}
|
|
166
|
-
} else if (operator === '$exists') {
|
|
167
|
-
var value = condition[operator];
|
|
257
|
+
if (!isOperatorObject(condition)) {
|
|
258
|
+
// A nested document means subfield equality: { a: { b: 'x' } } selects the
|
|
259
|
+
// same documents as { 'a.b': 'x' }.
|
|
260
|
+
for (var subField in condition) {
|
|
261
|
+
conditions.push(parseCondition("".concat(field, ".").concat(subField), condition[subField], columnName));
|
|
262
|
+
}
|
|
168
263
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
} else {
|
|
172
|
-
sqlOp += ' NULL';
|
|
173
|
-
}
|
|
264
|
+
return conditions.join(' AND ');
|
|
265
|
+
}
|
|
174
266
|
|
|
175
|
-
|
|
267
|
+
for (var operator in condition) {
|
|
268
|
+
var value = condition[operator];
|
|
269
|
+
var sqlOp = MANGO_TO_SQL_OP[operator]; // json_extract returns SQL NULL both for a JSON null and for a missing path,
|
|
270
|
+
// so every null comparison needs an IS [NOT] NULL form: `= NULL` and
|
|
271
|
+
// `!= NULL` are NULL, never true.
|
|
272
|
+
|
|
273
|
+
if (operator === '$eq' && value === null) {
|
|
274
|
+
conditions.push("".concat(sqlField, " IS NULL"));
|
|
275
|
+
} else if ((operator === '$gt' || operator === '$ne') && value === null) {
|
|
276
|
+
conditions.push("".concat(sqlField, " IS NOT NULL"));
|
|
277
|
+
} else if (value === null && sqlOp) {
|
|
278
|
+
// The remaining range operators have no meaningful null form here: `x >=
|
|
279
|
+
// NULL` is NULL for every row, so the query would silently match nothing.
|
|
280
|
+
// CouchDB orders null against every other type, which SQL comparison does
|
|
281
|
+
// not, so hand these to pouch-find rather than approximate them.
|
|
282
|
+
throw new _errors.UnsupportedMangoSelectorError("Cannot compare \"".concat(field, "\" to null with \"").concat(operator, "\""));
|
|
283
|
+
} else if (operator === '$ne') {
|
|
284
|
+
// CouchDB matches a missing field: `x != v` is NULL, not true, when
|
|
285
|
+
// json_extract finds nothing, so the absent case needs its own branch.
|
|
286
|
+
conditions.push("(".concat(sqlField, " IS NULL OR ").concat(sqlField, " != ").concat(quoteSQLValue(value), ")"));
|
|
287
|
+
} else if (operator === '$in' || operator === '$nin') {
|
|
288
|
+
var list = requireArrayOperand(operator, field, value);
|
|
289
|
+
|
|
290
|
+
if (list.length === 0) {
|
|
291
|
+
// "IN ()" is a syntax error. $in [] matches nothing (0 = false),
|
|
292
|
+
// $nin [] matches everything (1 = true).
|
|
293
|
+
conditions.push(operator === '$in' ? '0' : '1');
|
|
176
294
|
} else {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
295
|
+
// SQL never matches NULL through IN / NOT IN, so a null member has to
|
|
296
|
+
// become an explicit IS NULL test rather than sit in the list.
|
|
297
|
+
var hasNull = list.some(function (item) {
|
|
298
|
+
return item === null;
|
|
299
|
+
});
|
|
300
|
+
var values = list.filter(function (item) {
|
|
301
|
+
return item !== null;
|
|
302
|
+
}).map(quoteSQLValue);
|
|
303
|
+
var tests = [];
|
|
304
|
+
|
|
305
|
+
if (operator === '$in') {
|
|
306
|
+
if (values.length > 0) {
|
|
307
|
+
tests.push("".concat(sqlField, " IN (").concat(values.join(', '), ")"));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (hasNull) {
|
|
311
|
+
tests.push("".concat(sqlField, " IS NULL"));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
conditions.push(joinTests(tests, 'OR'));
|
|
180
315
|
} else {
|
|
181
|
-
|
|
316
|
+
if (values.length > 0) {
|
|
317
|
+
tests.push("".concat(sqlField, " NOT IN (").concat(values.join(', '), ")"));
|
|
318
|
+
} // $nin keeps documents missing the field, matching CouchDB - unless
|
|
319
|
+
// null is itself excluded, which is exactly what those rows hold.
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
tests.push(hasNull ? "".concat(sqlField, " IS NOT NULL") : "".concat(sqlField, " IS NULL"));
|
|
323
|
+
conditions.push(hasNull ? joinTests(tests, 'AND') : joinTests(tests.reverse(), 'OR'));
|
|
182
324
|
}
|
|
183
325
|
}
|
|
326
|
+
} else if (operator === '$exists') {
|
|
327
|
+
conditions.push("".concat(sqlField, " IS ").concat(value ? 'NOT NULL' : 'NULL'));
|
|
328
|
+
} else if (operator === '$not') {
|
|
329
|
+
conditions.push(negate(parseCondition(field, value, columnName)));
|
|
330
|
+
} else if (operator === '$elemMatch') {
|
|
331
|
+
conditions.push(parseElemMatch(field, value, columnName));
|
|
332
|
+
} else if (operator === '$all') {
|
|
333
|
+
var _list = requireArrayOperand(operator, field, value); // One scan per value: SQLite has no "contains all of" primitive, and the
|
|
334
|
+
// values may sit at any positions in the array.
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
conditions.push(_list.length === 0 ? '1' : _list.map(function (item) {
|
|
338
|
+
return makeArrayElementExists(field, columnName, "elem.value = ".concat(quoteSQLValue(item)));
|
|
339
|
+
}).join(' AND '));
|
|
340
|
+
} else if (operator === '$size') {
|
|
341
|
+
conditions.push("json_array_length(".concat(makeJSONFunctionArgs(field, columnName), ") = ").concat(quoteSQLValue(value)));
|
|
342
|
+
} else if (operator === '$mod') {
|
|
343
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
344
|
+
throw new _errors.UnsupportedMangoSelectorError("$mod on \"".concat(field, "\" expects a [divisor, remainder] pair"));
|
|
345
|
+
} // The numeric guard is not decoration: SQLite coerces a non-numeric
|
|
346
|
+
// operand of % to 0, so a text field would match any {$mod: [d, 0]}.
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
conditions.push("(json_type(".concat(makeJSONFunctionArgs(field, columnName), ") IN (").concat(MANGO_TYPE_TO_JSON_TYPES.number.map(quoteSQLValue).join(', '), ") AND ").concat(sqlField, " % ").concat(quoteSQLValue(value[0]), " = ").concat(quoteSQLValue(value[1]), ")"));
|
|
350
|
+
} else if (operator === '$type') {
|
|
351
|
+
var jsonTypes = MANGO_TYPE_TO_JSON_TYPES[value];
|
|
352
|
+
|
|
353
|
+
if (!jsonTypes) {
|
|
354
|
+
throw new _errors.UnsupportedMangoSelectorError("Unsupported mango $type \"".concat(value, "\" on field \"").concat(field, "\""));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
conditions.push("json_type(".concat(makeJSONFunctionArgs(field, columnName), ") IN (").concat(jsonTypes.map(quoteSQLValue).join(', '), ")"));
|
|
358
|
+
} else if (sqlOp) {
|
|
359
|
+
conditions.push("".concat(sqlField, " ").concat(sqlOp, " ").concat(quoteSQLValue(value)));
|
|
360
|
+
} else {
|
|
361
|
+
// The single point where an untranslatable selector is caught. $regex ends
|
|
362
|
+
// up here - op-sqlite exposes no way to register a REGEXP function - along
|
|
363
|
+
// with any operator this translator does not know. Throwing rather than
|
|
364
|
+
// interpolating an undefined operator is what guarantees the generated SQL
|
|
365
|
+
// never contains "undefined"; SQLiteQueryEngine turns it into a pouch-find
|
|
366
|
+
// fallback.
|
|
367
|
+
throw new _errors.UnsupportedMangoSelectorError("Unsupported mango operator \"".concat(operator, "\" on field \"").concat(field, "\""));
|
|
184
368
|
}
|
|
185
|
-
} else {
|
|
186
|
-
conditions.push("".concat(sqlField, " = ").concat(quoteSQLValue(condition)));
|
|
187
369
|
}
|
|
188
370
|
|
|
189
371
|
return conditions.join(' AND ');
|
|
190
372
|
};
|
|
191
373
|
|
|
192
374
|
var parseLogicalOperator = function parseLogicalOperator(operator, conditionsArray, columnName) {
|
|
193
|
-
|
|
375
|
+
if (!Array.isArray(conditionsArray) || conditionsArray.length === 0) {
|
|
376
|
+
throw new _errors.UnsupportedMangoSelectorError("".concat(operator, " expects a non-empty array of selectors"));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
var sqlOperator = operator === '$and' ? 'AND' : 'OR'; // An empty sub-selector translates to an empty string; wrapping it would emit
|
|
380
|
+
// "()" and break the statement.
|
|
381
|
+
|
|
194
382
|
var parsedConditions = conditionsArray.map(function (cond) {
|
|
195
|
-
return
|
|
383
|
+
return mangoSelectorToSQL(cond, columnName);
|
|
384
|
+
}).filter(Boolean).map(function (sql) {
|
|
385
|
+
return "(".concat(sql, ")");
|
|
196
386
|
});
|
|
387
|
+
|
|
388
|
+
if (parsedConditions.length === 0) {
|
|
389
|
+
throw new _errors.UnsupportedMangoSelectorError("".concat(operator, " expects at least one translatable selector"));
|
|
390
|
+
}
|
|
391
|
+
|
|
197
392
|
return parsedConditions.join(" ".concat(sqlOperator, " "));
|
|
198
393
|
}; // PouchDB keeps a document's id and rev in by-sequence COLUMNS and strips them
|
|
199
394
|
// from the stored JSON, so json_extract(json, '$._id') is NULL on every row. A
|
|
@@ -208,24 +403,53 @@ var PHYSICAL_COLUMNS = {
|
|
|
208
403
|
|
|
209
404
|
var transformMangoFieldInJSONSQL = function transformMangoFieldInJSONSQL(field) {
|
|
210
405
|
var columnName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'data';
|
|
211
|
-
var physicalColumn = PHYSICAL_COLUMNS[field];
|
|
406
|
+
var physicalColumn = PHYSICAL_COLUMNS[field]; // Only at document level: inside a json_each element (`elem.value`) the row
|
|
407
|
+
// is an array item, which has no doc_id / rev of its own.
|
|
212
408
|
|
|
213
|
-
if (physicalColumn) {
|
|
214
|
-
// `data` is the query alias, where by-sequence is joined with
|
|
215
|
-
// and the column needs qualifying; `json` is the CREATE
|
|
216
|
-
//
|
|
409
|
+
if (physicalColumn && (columnName === 'data' || columnName === 'json')) {
|
|
410
|
+
// `data` is the query alias, where by-sequence is joined with
|
|
411
|
+
// document-store and the column needs qualifying; `json` is the CREATE
|
|
412
|
+
// INDEX context, scoped to by-sequence alone, which must stay unqualified.
|
|
217
413
|
return columnName === 'json' ? physicalColumn : "'by-sequence'.".concat(physicalColumn);
|
|
218
414
|
}
|
|
219
415
|
|
|
220
|
-
|
|
416
|
+
if (!field) {
|
|
417
|
+
// No path: the value IS the column (a json_each element, for $elemMatch on
|
|
418
|
+
// an array of scalars). json_extract would re-parse it as JSON and fail on a
|
|
419
|
+
// bare string.
|
|
420
|
+
return columnName;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return "json_extract(".concat(columnName, ", '$.").concat(escapeSQLString(field), "')");
|
|
221
424
|
};
|
|
425
|
+
/**
|
|
426
|
+
* Translate a mango selector into a SQL boolean expression.
|
|
427
|
+
*
|
|
428
|
+
* @param {object} selector - The mango selector
|
|
429
|
+
* @param {string} [columnName] - The JSON column the paths are resolved against
|
|
430
|
+
* @returns {string} The SQL expression, never containing "undefined"
|
|
431
|
+
* @throws {UnsupportedMangoSelectorError} When the selector uses a mango feature
|
|
432
|
+
* this translator cannot express in SQL
|
|
433
|
+
*/
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
var mangoSelectorToSQL = function mangoSelectorToSQL(selector) {
|
|
437
|
+
var columnName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'data';
|
|
438
|
+
|
|
439
|
+
if (!isPlainObject(selector)) {
|
|
440
|
+
throw new _errors.UnsupportedMangoSelectorError("Expected a selector, got ".concat(JSON.stringify(selector)));
|
|
441
|
+
}
|
|
222
442
|
|
|
223
|
-
var mangoSelectorToSQL = function mangoSelectorToSQL(selector, columnName) {
|
|
224
443
|
var conditions = [];
|
|
225
444
|
|
|
226
445
|
for (var key in selector) {
|
|
227
446
|
if (key === '$and' || key === '$or') {
|
|
228
447
|
conditions.push(parseLogicalOperator(key, selector[key], columnName));
|
|
448
|
+
} else if (key === '$nor') {
|
|
449
|
+
// "none of these match" is the negation of the $or.
|
|
450
|
+
conditions.push(negate(parseLogicalOperator('$or', selector[key], columnName)));
|
|
451
|
+
} else if (key === '$not') {
|
|
452
|
+
conditions.push(negate(mangoSelectorToSQL(selector[key], columnName)));
|
|
229
453
|
} else {
|
|
230
454
|
conditions.push(parseCondition(key, selector[key], columnName));
|
|
231
455
|
}
|
|
@@ -266,23 +490,33 @@ var makeSortClause = function makeSortClause(mangoSortBy) {
|
|
|
266
490
|
|
|
267
491
|
|
|
268
492
|
return mangoSortBy.map(function (sort) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
493
|
+
// A bare string is the shorthand for ascending on that field; Object.keys
|
|
494
|
+
// would otherwise read its first index and sort on the JSON path "$.0".
|
|
495
|
+
var entry = typeof sort === 'string' ? (0, _defineProperty2.default)({}, sort, 'asc') : sort;
|
|
496
|
+
|
|
497
|
+
if (!isPlainObject(entry)) {
|
|
498
|
+
throw new _errors.UnsupportedMangoSelectorError("Cannot sort on ".concat(JSON.stringify(sort)));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
var attribute = Object.keys(entry)[0]; // ASC/DESC are bare SQL keywords, so the direction cannot be quoted the
|
|
502
|
+
// way a value would be - it has to be whitelisted instead.
|
|
503
|
+
|
|
504
|
+
var order = String(entry[attribute]).toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
|
505
|
+
return "".concat(transformMangoFieldInJSONSQL(attribute), " ").concat(order);
|
|
272
506
|
}).join(', ');
|
|
273
507
|
};
|
|
274
508
|
|
|
275
509
|
exports.makeSortClause = makeSortClause;
|
|
276
510
|
|
|
277
|
-
var makeSQLQueryFromMango = function makeSQLQueryFromMango(
|
|
278
|
-
var selector =
|
|
279
|
-
sort =
|
|
280
|
-
indexName =
|
|
281
|
-
partialFilter =
|
|
282
|
-
|
|
283
|
-
limit =
|
|
284
|
-
|
|
285
|
-
skip =
|
|
511
|
+
var makeSQLQueryFromMango = function makeSQLQueryFromMango(_ref3) {
|
|
512
|
+
var selector = _ref3.selector,
|
|
513
|
+
sort = _ref3.sort,
|
|
514
|
+
indexName = _ref3.indexName,
|
|
515
|
+
partialFilter = _ref3.partialFilter,
|
|
516
|
+
_ref3$limit = _ref3.limit,
|
|
517
|
+
limit = _ref3$limit === void 0 ? -1 : _ref3$limit,
|
|
518
|
+
_ref3$skip = _ref3.skip,
|
|
519
|
+
skip = _ref3$skip === void 0 ? 0 : _ref3$skip;
|
|
286
520
|
var whereClause = makeWhereClause(selector); // Restate the partial filter in the query itself. Relying on the partial index
|
|
287
521
|
// alone is unsafe: CREATE INDEX IF NOT EXISTS never rewrites an index built
|
|
288
522
|
// under an older definition, so a stale unfiltered index would silently return
|
|
@@ -303,15 +537,19 @@ var makeSQLQueryFromMango = function makeSQLQueryFromMango(_ref2) {
|
|
|
303
537
|
|
|
304
538
|
var sql = ["SELECT 'by-sequence'.json AS data, 'by-sequence'.doc_id, 'by-sequence'.rev", "FROM 'by-sequence' INDEXED BY ".concat(indexName, ", 'document-store'"), "WHERE 'by-sequence'.seq = 'document-store'.winningseq AND ".concat(whereClause)].join(' ');
|
|
305
539
|
|
|
306
|
-
if (skip > 0) {
|
|
307
|
-
sql += " OFFSET ".concat(skip);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
540
|
if (sortClause) {
|
|
311
541
|
sql += " ORDER BY ".concat(sortClause);
|
|
542
|
+
} // OFFSET is a modifier of LIMIT, not a clause of its own: SQL requires it last
|
|
543
|
+
// and rejects it without a LIMIT. `null` parses but fails at step time with a
|
|
544
|
+
// datatype mismatch, and the `limit = -1` default only catches `undefined`.
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
sql += " LIMIT ".concat(limit == null ? -1 : limit);
|
|
548
|
+
|
|
549
|
+
if (skip > 0) {
|
|
550
|
+
sql += " OFFSET ".concat(skip);
|
|
312
551
|
}
|
|
313
552
|
|
|
314
|
-
sql += " LIMIT ".concat(limit);
|
|
315
553
|
sql += ';';
|
|
316
554
|
return sql;
|
|
317
555
|
};
|
|
@@ -340,13 +578,13 @@ var makeSQLQueryForIds = function makeSQLQueryForIds(ids) {
|
|
|
340
578
|
exports.makeSQLQueryForIds = makeSQLQueryForIds;
|
|
341
579
|
|
|
342
580
|
var makeSQLQueryAll = function makeSQLQueryAll() {
|
|
343
|
-
var
|
|
344
|
-
|
|
345
|
-
limit =
|
|
346
|
-
|
|
347
|
-
skip =
|
|
581
|
+
var _ref4 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
|
|
582
|
+
_ref4$limit = _ref4.limit,
|
|
583
|
+
limit = _ref4$limit === void 0 ? -1 : _ref4$limit,
|
|
584
|
+
_ref4$skip = _ref4.skip,
|
|
585
|
+
skip = _ref4$skip === void 0 ? 0 : _ref4$skip;
|
|
348
586
|
|
|
349
|
-
var sql = ["SELECT 'by-sequence'.json AS data, 'by-sequence'.doc_id, 'by-sequence'.rev", "FROM 'document-store', 'by-sequence'", "WHERE 'by-sequence'.seq = 'document-store'.winningseq AND 'by-sequence'.deleted=0", "LIMIT ".concat(limit)].join(' ');
|
|
587
|
+
var sql = ["SELECT 'by-sequence'.json AS data, 'by-sequence'.doc_id, 'by-sequence'.rev", "FROM 'document-store', 'by-sequence'", "WHERE 'by-sequence'.seq = 'document-store'.winningseq AND 'by-sequence'.deleted=0", "LIMIT ".concat(limit == null ? -1 : limit)].join(' ');
|
|
350
588
|
|
|
351
589
|
if (skip > 0) {
|
|
352
590
|
sql += " OFFSET ".concat(skip);
|
|
@@ -364,10 +602,16 @@ var makeSQLDropIndex = function makeSQLDropIndex(indexName) {
|
|
|
364
602
|
|
|
365
603
|
exports.makeSQLDropIndex = makeSQLDropIndex;
|
|
366
604
|
|
|
367
|
-
var makeSQLCreateMangoIndex = function makeSQLCreateMangoIndex(indexName, fieldsToIndex,
|
|
368
|
-
var partialFilter =
|
|
605
|
+
var makeSQLCreateMangoIndex = function makeSQLCreateMangoIndex(indexName, fieldsToIndex, _ref5) {
|
|
606
|
+
var partialFilter = _ref5.partialFilter;
|
|
607
|
+
// Escaped like the where clause's paths, so an index covers exactly the
|
|
608
|
+
// expression makeWhereClause emits for the same field.
|
|
609
|
+
// XXX - indexName itself is still interpolated raw here, in makeSQLDropIndex
|
|
610
|
+
// and in makeSQLQueryFromMango's INDEXED BY. The three must agree, so they are
|
|
611
|
+
// left alone together: a field name containing a quote would break index
|
|
612
|
+
// resolution rather than the where clause.
|
|
369
613
|
var jsonAttributes = fieldsToIndex.map(function (field) {
|
|
370
|
-
return "json_extract(json, '$.".concat(field, "')");
|
|
614
|
+
return "json_extract(json, '$.".concat(escapeSQLString(field), "')");
|
|
371
615
|
});
|
|
372
616
|
var jsonIndex = jsonAttributes.join(',');
|
|
373
617
|
var sql = "\n CREATE INDEX IF NOT EXISTS '".concat(indexName, "'\n ON 'by-sequence'\n (").concat(jsonIndex, ")\n ");
|
|
@@ -406,13 +650,13 @@ var makeSQLCreateDeletedIndex = function makeSQLCreateDeletedIndex() {
|
|
|
406
650
|
exports.makeSQLCreateDeletedIndex = makeSQLCreateDeletedIndex;
|
|
407
651
|
|
|
408
652
|
var createMangoIndex = /*#__PURE__*/function () {
|
|
409
|
-
var
|
|
653
|
+
var _ref7 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(db, indexName, fieldsToIndex, _ref6) {
|
|
410
654
|
var partialFilter, sql, result;
|
|
411
655
|
return _regenerator.default.wrap(function _callee$(_context) {
|
|
412
656
|
while (1) {
|
|
413
657
|
switch (_context.prev = _context.next) {
|
|
414
658
|
case 0:
|
|
415
|
-
partialFilter =
|
|
659
|
+
partialFilter = _ref6.partialFilter;
|
|
416
660
|
sql = makeSQLCreateMangoIndex(indexName, fieldsToIndex, {
|
|
417
661
|
partialFilter: partialFilter
|
|
418
662
|
});
|
|
@@ -432,14 +676,14 @@ var createMangoIndex = /*#__PURE__*/function () {
|
|
|
432
676
|
}));
|
|
433
677
|
|
|
434
678
|
return function createMangoIndex(_x, _x2, _x3, _x4) {
|
|
435
|
-
return
|
|
679
|
+
return _ref7.apply(this, arguments);
|
|
436
680
|
};
|
|
437
681
|
}();
|
|
438
682
|
|
|
439
683
|
exports.createMangoIndex = createMangoIndex;
|
|
440
684
|
|
|
441
685
|
var deleteIndex = /*#__PURE__*/function () {
|
|
442
|
-
var
|
|
686
|
+
var _ref8 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(db, indexName) {
|
|
443
687
|
var sql;
|
|
444
688
|
return _regenerator.default.wrap(function _callee2$(_context2) {
|
|
445
689
|
while (1) {
|
|
@@ -458,19 +702,62 @@ var deleteIndex = /*#__PURE__*/function () {
|
|
|
458
702
|
}));
|
|
459
703
|
|
|
460
704
|
return function deleteIndex(_x5, _x6) {
|
|
461
|
-
return
|
|
705
|
+
return _ref8.apply(this, arguments);
|
|
462
706
|
};
|
|
463
707
|
}();
|
|
708
|
+
/**
|
|
709
|
+
* op-sqlite answers with positional rows (`rawRows`) and the column names beside
|
|
710
|
+
* them, while older releases exposed a WebSQL-style `rows` accessor. parseResults
|
|
711
|
+
* reads `rows.length` and `rows.item(i)`, so every shape is brought back to that
|
|
712
|
+
* contract: otherwise `rows` is undefined and each query dies reading `length`.
|
|
713
|
+
*
|
|
714
|
+
* @param {object} result - A raw op-sqlite result
|
|
715
|
+
* @returns {object} The same result, with a WebSQL-style `rows` accessor
|
|
716
|
+
*/
|
|
717
|
+
|
|
464
718
|
|
|
465
719
|
exports.deleteIndex = deleteIndex;
|
|
466
720
|
|
|
721
|
+
var toWebSQLResult = function toWebSQLResult(result) {
|
|
722
|
+
if (!result) {
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
var rows = result.rows;
|
|
727
|
+
|
|
728
|
+
if (rows && typeof rows.item === 'function') {
|
|
729
|
+
return result;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
var list = Array.isArray(rows) ? rows : (result.rawRows || []).map(function (row) {
|
|
733
|
+
var columnNames = result.columnNames || [];
|
|
734
|
+
var record = {};
|
|
735
|
+
|
|
736
|
+
for (var i = 0; i < columnNames.length; i++) {
|
|
737
|
+
record[columnNames[i]] = row[i];
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return record;
|
|
741
|
+
});
|
|
742
|
+
return _objectSpread(_objectSpread({}, result), {}, {
|
|
743
|
+
rows: {
|
|
744
|
+
length: list.length,
|
|
745
|
+
item: function item(i) {
|
|
746
|
+
return list[i];
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
exports.toWebSQLResult = toWebSQLResult;
|
|
753
|
+
|
|
467
754
|
var executeSQL = /*#__PURE__*/function () {
|
|
468
|
-
var
|
|
755
|
+
var _ref9 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(db, sql) {
|
|
469
756
|
return _regenerator.default.wrap(function _callee3$(_context3) {
|
|
470
757
|
while (1) {
|
|
471
758
|
switch (_context3.prev = _context3.next) {
|
|
472
759
|
case 0:
|
|
473
|
-
return _context3.abrupt("return", db.executeAsync(sql).catch(function (err) {
|
|
760
|
+
return _context3.abrupt("return", db.executeAsync(sql).then(toWebSQLResult).catch(function (err) {
|
|
474
761
|
var message = err && err.message || String(err); // Before the adapter has created the by-sequence table, the earliest
|
|
475
762
|
// queries hit a missing table. That is transient (the table appears once
|
|
476
763
|
// replication first writes), so return empty instead of rejecting uncaught.
|
|
@@ -500,7 +787,7 @@ var executeSQL = /*#__PURE__*/function () {
|
|
|
500
787
|
}));
|
|
501
788
|
|
|
502
789
|
return function executeSQL(_x7, _x8) {
|
|
503
|
-
return
|
|
790
|
+
return _ref9.apply(this, arguments);
|
|
504
791
|
};
|
|
505
792
|
}();
|
|
506
793
|
|