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.
@@ -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
- $in: 'IN',
26
- $nin: 'NOT IN',
27
- $exists: 'IS'
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
- }; // Quote a value for inline SQL. String single quotes are doubled ('' ) so a
134
- // value like "l'ete.txt" cannot break out of the literal (SQL error) or inject.
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.replace(/'/g, "''"), "'");
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 (typeof condition === 'object' && !Array.isArray(condition)) {
152
- for (var operator in condition) {
153
- var sqlOp = MANGO_TO_SQL_OP[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
+ }
154
263
 
155
- if (operator === '$in' || operator === '$nin') {
156
- var list = condition[operator] || [];
264
+ return conditions.join(' AND ');
265
+ }
157
266
 
158
- if (list.length === 0) {
159
- // "IN ()" is a syntax error. $in [] matches nothing (0 = false),
160
- // $nin [] matches everything (1 = true).
161
- conditions.push(operator === '$in' ? '0' : '1');
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
- var values = list.map(quoteSQLValue).join(', ');
164
- conditions.push("".concat(sqlField, " ").concat(sqlOp, " (").concat(values, ")"));
165
- }
166
- } else if (operator === '$exists') {
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
- conditions.push("".concat(sqlField, " ").concat(sqlOp));
176
- } else {
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
- var sqlOperator = operator === '$and' ? 'AND' : 'OR';
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 "(".concat(mangoSelectorToSQL(cond).replace(/^WHERE /, ''), ")");
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
- return "json_extract(".concat(columnName, ", '$.").concat(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.
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
- var attribute = Object.keys(sort)[0];
252
- var order = String(sort[attribute]).toUpperCase();
253
- return "json_extract(data, '$.".concat(attribute, "') ").concat(order);
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(_ref2) {
260
- var selector = _ref2.selector,
261
- sort = _ref2.sort,
262
- indexName = _ref2.indexName,
263
- _ref2$limit = _ref2.limit,
264
- limit = _ref2$limit === void 0 ? -1 : _ref2$limit,
265
- _ref2$skip = _ref2.skip,
266
- skip = _ref2$skip === void 0 ? 0 : _ref2$skip;
267
- var whereClause = makeWhereClause(selector);
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 _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
313
- _ref3$limit = _ref3.limit,
314
- limit = _ref3$limit === void 0 ? -1 : _ref3$limit,
315
- _ref3$skip = _ref3.skip,
316
- skip = _ref3$skip === void 0 ? 0 : _ref3$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, _ref4) {
337
- var partialFilter = _ref4.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
- var whereClause = makeWhereClause(partialFilter);
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 _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(db, indexName, fieldsToIndex, _ref5) {
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 = _ref5.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 _ref6.apply(this, arguments);
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 _ref7 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(db, indexName) {
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 _ref7.apply(this, arguments);
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 _ref8 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(db, sql) {
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 _ref8.apply(this, arguments);
790
+ return _ref9.apply(this, arguments);
470
791
  };
471
792
  }();
472
793