cozy-pouch-link 60.30.2 → 60.30.4

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,70 +146,249 @@ 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
+ });
145
204
  };
146
205
 
147
- var parseCondition = function parseCondition(field, condition, columnName) {
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 (typeof condition === 'object' && !Array.isArray(condition)) {
152
- for (var operator in condition) {
153
- var sqlOp = MANGO_TO_SQL_OP[operator];
154
-
155
- if (operator === '$in' || operator === '$nin') {
156
- var list = condition[operator] || [];
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
- 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');
162
- } 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];
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
- if (value) {
170
- sqlOp += ' NOT NULL';
171
- } else {
172
- sqlOp += ' NULL';
173
- }
264
+ return conditions.join(' AND ');
265
+ }
174
266
 
175
- conditions.push("".concat(sqlField, " ").concat(sqlOp));
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
- if (operator === '$gt' && condition[operator] === null) {
178
- // Special case for $gt: null conditions
179
- conditions.push("".concat(sqlField, " IS NOT NULL"));
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
- conditions.push("".concat(sqlField, " ").concat(sqlOp, " ").concat(quoteSQLValue(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.
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
- var sqlOperator = operator === '$and' ? 'AND' : 'OR';
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, columnName).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, " "));
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 document-store
215
- // and the column needs qualifying; `json` is the CREATE INDEX context, which
216
- // is scoped to by-sequence alone and must stay unqualified.
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
- return "json_extract(".concat(columnName, ", '$.").concat(field, "')");
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
- var attribute = Object.keys(sort)[0];
270
- var order = String(sort[attribute]).toUpperCase();
271
- 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);
272
506
  }).join(', ');
273
507
  };
274
508
 
275
509
  exports.makeSortClause = makeSortClause;
276
510
 
277
- var makeSQLQueryFromMango = function makeSQLQueryFromMango(_ref2) {
278
- var selector = _ref2.selector,
279
- sort = _ref2.sort,
280
- indexName = _ref2.indexName,
281
- partialFilter = _ref2.partialFilter,
282
- _ref2$limit = _ref2.limit,
283
- limit = _ref2$limit === void 0 ? -1 : _ref2$limit,
284
- _ref2$skip = _ref2.skip,
285
- skip = _ref2$skip === void 0 ? 0 : _ref2$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 _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
344
- _ref3$limit = _ref3.limit,
345
- limit = _ref3$limit === void 0 ? -1 : _ref3$limit,
346
- _ref3$skip = _ref3.skip,
347
- 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;
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, _ref4) {
368
- 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.
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 _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) {
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 = _ref5.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 _ref6.apply(this, arguments);
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 _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) {
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 _ref7.apply(this, arguments);
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 _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) {
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 _ref8.apply(this, arguments);
790
+ return _ref9.apply(this, arguments);
504
791
  };
505
792
  }();
506
793