cozy-pouch-link 60.30.1 → 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 +402 -81
- package/dist/db/sqlite/sql.mango.spec.js +225 -25
- package/dist/db/sqlite/sql.spec.js +114 -5
- package/dist/db/sqlite/sqliteDb.native.js +90 -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 +5 -3
- 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,86 +146,312 @@ 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
|
+
});
|
|
204
|
+
};
|
|
205
|
+
|
|
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);
|
|
145
242
|
};
|
|
146
243
|
|
|
147
244
|
var parseCondition = function parseCondition(field, condition) {
|
|
245
|
+
var columnName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'data';
|
|
148
246
|
var conditions = [];
|
|
149
|
-
var sqlField = transformMangoFieldInJSONSQL(field);
|
|
247
|
+
var sqlField = transformMangoFieldInJSONSQL(field, columnName);
|
|
248
|
+
|
|
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
|
+
}
|
|
150
256
|
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
263
|
|
|
155
|
-
|
|
156
|
-
|
|
264
|
+
return conditions.join(' AND ');
|
|
265
|
+
}
|
|
157
266
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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');
|
|
294
|
+
} else {
|
|
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'));
|
|
162
315
|
} else {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
var value = condition[operator];
|
|
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.
|
|
168
320
|
|
|
169
|
-
if (value) {
|
|
170
|
-
sqlOp += ' NOT NULL';
|
|
171
|
-
} else {
|
|
172
|
-
sqlOp += ' NULL';
|
|
173
|
-
}
|
|
174
321
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (operator === '$gt' && condition[operator] === null) {
|
|
178
|
-
// Special case for $gt: null conditions
|
|
179
|
-
conditions.push("".concat(sqlField, " IS NOT NULL"));
|
|
180
|
-
} else {
|
|
181
|
-
conditions.push("".concat(sqlField, " ").concat(sqlOp, " ").concat(quoteSQLValue(condition[operator])));
|
|
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
|
-
var parseLogicalOperator = function parseLogicalOperator(operator, conditionsArray) {
|
|
193
|
-
|
|
374
|
+
var parseLogicalOperator = function parseLogicalOperator(operator, conditionsArray, columnName) {
|
|
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, " "));
|
|
393
|
+
}; // PouchDB keeps a document's id and rev in by-sequence COLUMNS and strips them
|
|
394
|
+
// from the stored JSON, so json_extract(json, '$._id') is NULL on every row. A
|
|
395
|
+
// selector on `_id` would therefore match nothing it is meant to match, and a
|
|
396
|
+
// `$nin` on it would filter nothing at all.
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
var PHYSICAL_COLUMNS = {
|
|
400
|
+
_id: 'doc_id',
|
|
401
|
+
_rev: 'rev'
|
|
198
402
|
};
|
|
199
403
|
|
|
200
404
|
var transformMangoFieldInJSONSQL = function transformMangoFieldInJSONSQL(field) {
|
|
201
405
|
var columnName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'data';
|
|
202
|
-
|
|
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.
|
|
408
|
+
|
|
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.
|
|
413
|
+
return columnName === 'json' ? physicalColumn : "'by-sequence'.".concat(physicalColumn);
|
|
414
|
+
}
|
|
415
|
+
|
|
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), "')");
|
|
203
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
|
+
|
|
204
435
|
|
|
205
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
|
+
}
|
|
442
|
+
|
|
206
443
|
var conditions = [];
|
|
207
444
|
|
|
208
445
|
for (var key in selector) {
|
|
209
446
|
if (key === '$and' || key === '$or') {
|
|
210
|
-
conditions.push(parseLogicalOperator(key, selector[key]));
|
|
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)));
|
|
211
453
|
} else {
|
|
212
|
-
conditions.push(parseCondition(key, selector[key]));
|
|
454
|
+
conditions.push(parseCondition(key, selector[key], columnName));
|
|
213
455
|
}
|
|
214
456
|
}
|
|
215
457
|
|
|
@@ -218,14 +460,14 @@ var mangoSelectorToSQL = function mangoSelectorToSQL(selector) {
|
|
|
218
460
|
|
|
219
461
|
exports.mangoSelectorToSQL = mangoSelectorToSQL;
|
|
220
462
|
|
|
221
|
-
var makeWhereClause = function makeWhereClause(selector) {
|
|
463
|
+
var makeWhereClause = function makeWhereClause(selector, columnName) {
|
|
222
464
|
var baseWhere = 'DELETED = 0';
|
|
223
465
|
|
|
224
466
|
if (!selector) {
|
|
225
467
|
return baseWhere;
|
|
226
468
|
}
|
|
227
469
|
|
|
228
|
-
var mangoWhere = mangoSelectorToSQL(selector);
|
|
470
|
+
var mangoWhere = mangoSelectorToSQL(selector, columnName);
|
|
229
471
|
|
|
230
472
|
if (!mangoWhere) {
|
|
231
473
|
return baseWhere;
|
|
@@ -248,23 +490,46 @@ var makeSortClause = function makeSortClause(mangoSortBy) {
|
|
|
248
490
|
|
|
249
491
|
|
|
250
492
|
return mangoSortBy.map(function (sort) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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);
|
|
254
506
|
}).join(', ');
|
|
255
507
|
};
|
|
256
508
|
|
|
257
509
|
exports.makeSortClause = makeSortClause;
|
|
258
510
|
|
|
259
|
-
var makeSQLQueryFromMango = function makeSQLQueryFromMango(
|
|
260
|
-
var selector =
|
|
261
|
-
sort =
|
|
262
|
-
indexName =
|
|
263
|
-
|
|
264
|
-
limit =
|
|
265
|
-
|
|
266
|
-
skip =
|
|
267
|
-
|
|
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;
|
|
520
|
+
var whereClause = makeWhereClause(selector); // Restate the partial filter in the query itself. Relying on the partial index
|
|
521
|
+
// alone is unsafe: CREATE INDEX IF NOT EXISTS never rewrites an index built
|
|
522
|
+
// under an older definition, so a stale unfiltered index would silently return
|
|
523
|
+
// the rows the filter is meant to hide.
|
|
524
|
+
|
|
525
|
+
if (partialFilter) {
|
|
526
|
+
var partialWhere = mangoSelectorToSQL(partialFilter);
|
|
527
|
+
|
|
528
|
+
if (partialWhere) {
|
|
529
|
+
whereClause += " AND (".concat(partialWhere, ")");
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
268
533
|
var sortClause = makeSortClause(sort); // Scan by-sequence via the mango index, then join document-store on its
|
|
269
534
|
// winningseq so only WINNING revisions survive (matches CouchDB mango, which
|
|
270
535
|
// queries the winning rev). `data` aliases 'by-sequence'.json; `deleted` lives
|
|
@@ -272,15 +537,19 @@ var makeSQLQueryFromMango = function makeSQLQueryFromMango(_ref2) {
|
|
|
272
537
|
|
|
273
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(' ');
|
|
274
539
|
|
|
275
|
-
if (skip > 0) {
|
|
276
|
-
sql += " OFFSET ".concat(skip);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
540
|
if (sortClause) {
|
|
280
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);
|
|
281
551
|
}
|
|
282
552
|
|
|
283
|
-
sql += " LIMIT ".concat(limit);
|
|
284
553
|
sql += ';';
|
|
285
554
|
return sql;
|
|
286
555
|
};
|
|
@@ -309,13 +578,13 @@ var makeSQLQueryForIds = function makeSQLQueryForIds(ids) {
|
|
|
309
578
|
exports.makeSQLQueryForIds = makeSQLQueryForIds;
|
|
310
579
|
|
|
311
580
|
var makeSQLQueryAll = function makeSQLQueryAll() {
|
|
312
|
-
var
|
|
313
|
-
|
|
314
|
-
limit =
|
|
315
|
-
|
|
316
|
-
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;
|
|
317
586
|
|
|
318
|
-
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(' ');
|
|
319
588
|
|
|
320
589
|
if (skip > 0) {
|
|
321
590
|
sql += " OFFSET ".concat(skip);
|
|
@@ -333,16 +602,25 @@ var makeSQLDropIndex = function makeSQLDropIndex(indexName) {
|
|
|
333
602
|
|
|
334
603
|
exports.makeSQLDropIndex = makeSQLDropIndex;
|
|
335
604
|
|
|
336
|
-
var makeSQLCreateMangoIndex = function makeSQLCreateMangoIndex(indexName, fieldsToIndex,
|
|
337
|
-
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.
|
|
338
613
|
var jsonAttributes = fieldsToIndex.map(function (field) {
|
|
339
|
-
return "json_extract(json, '$.".concat(field, "')");
|
|
614
|
+
return "json_extract(json, '$.".concat(escapeSQLString(field), "')");
|
|
340
615
|
});
|
|
341
616
|
var jsonIndex = jsonAttributes.join(',');
|
|
342
617
|
var sql = "\n CREATE INDEX IF NOT EXISTS '".concat(indexName, "'\n ON 'by-sequence'\n (").concat(jsonIndex, ")\n ");
|
|
343
618
|
|
|
344
619
|
if (partialFilter) {
|
|
345
|
-
|
|
620
|
+
// `data` only exists as a SELECT alias of 'by-sequence'.json; inside CREATE
|
|
621
|
+
// INDEX the column is `json`. Emitting the alias made every partial index
|
|
622
|
+
// fail with "no such column: data", so the filter was never enforced.
|
|
623
|
+
var whereClause = makeWhereClause(partialFilter, 'json');
|
|
346
624
|
sql += " WHERE ".concat(whereClause);
|
|
347
625
|
}
|
|
348
626
|
|
|
@@ -372,13 +650,13 @@ var makeSQLCreateDeletedIndex = function makeSQLCreateDeletedIndex() {
|
|
|
372
650
|
exports.makeSQLCreateDeletedIndex = makeSQLCreateDeletedIndex;
|
|
373
651
|
|
|
374
652
|
var createMangoIndex = /*#__PURE__*/function () {
|
|
375
|
-
var
|
|
653
|
+
var _ref7 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(db, indexName, fieldsToIndex, _ref6) {
|
|
376
654
|
var partialFilter, sql, result;
|
|
377
655
|
return _regenerator.default.wrap(function _callee$(_context) {
|
|
378
656
|
while (1) {
|
|
379
657
|
switch (_context.prev = _context.next) {
|
|
380
658
|
case 0:
|
|
381
|
-
partialFilter =
|
|
659
|
+
partialFilter = _ref6.partialFilter;
|
|
382
660
|
sql = makeSQLCreateMangoIndex(indexName, fieldsToIndex, {
|
|
383
661
|
partialFilter: partialFilter
|
|
384
662
|
});
|
|
@@ -398,14 +676,14 @@ var createMangoIndex = /*#__PURE__*/function () {
|
|
|
398
676
|
}));
|
|
399
677
|
|
|
400
678
|
return function createMangoIndex(_x, _x2, _x3, _x4) {
|
|
401
|
-
return
|
|
679
|
+
return _ref7.apply(this, arguments);
|
|
402
680
|
};
|
|
403
681
|
}();
|
|
404
682
|
|
|
405
683
|
exports.createMangoIndex = createMangoIndex;
|
|
406
684
|
|
|
407
685
|
var deleteIndex = /*#__PURE__*/function () {
|
|
408
|
-
var
|
|
686
|
+
var _ref8 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(db, indexName) {
|
|
409
687
|
var sql;
|
|
410
688
|
return _regenerator.default.wrap(function _callee2$(_context2) {
|
|
411
689
|
while (1) {
|
|
@@ -424,19 +702,62 @@ var deleteIndex = /*#__PURE__*/function () {
|
|
|
424
702
|
}));
|
|
425
703
|
|
|
426
704
|
return function deleteIndex(_x5, _x6) {
|
|
427
|
-
return
|
|
705
|
+
return _ref8.apply(this, arguments);
|
|
428
706
|
};
|
|
429
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
|
+
|
|
430
718
|
|
|
431
719
|
exports.deleteIndex = deleteIndex;
|
|
432
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
|
+
|
|
433
754
|
var executeSQL = /*#__PURE__*/function () {
|
|
434
|
-
var
|
|
755
|
+
var _ref9 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(db, sql) {
|
|
435
756
|
return _regenerator.default.wrap(function _callee3$(_context3) {
|
|
436
757
|
while (1) {
|
|
437
758
|
switch (_context3.prev = _context3.next) {
|
|
438
759
|
case 0:
|
|
439
|
-
return _context3.abrupt("return", db.executeAsync(sql).catch(function (err) {
|
|
760
|
+
return _context3.abrupt("return", db.executeAsync(sql).then(toWebSQLResult).catch(function (err) {
|
|
440
761
|
var message = err && err.message || String(err); // Before the adapter has created the by-sequence table, the earliest
|
|
441
762
|
// queries hit a missing table. That is transient (the table appears once
|
|
442
763
|
// replication first writes), so return empty instead of rejecting uncaught.
|
|
@@ -466,7 +787,7 @@ var executeSQL = /*#__PURE__*/function () {
|
|
|
466
787
|
}));
|
|
467
788
|
|
|
468
789
|
return function executeSQL(_x7, _x8) {
|
|
469
|
-
return
|
|
790
|
+
return _ref9.apply(this, arguments);
|
|
470
791
|
};
|
|
471
792
|
}();
|
|
472
793
|
|