bun-sqlite-for-rxdb 1.1.3 → 1.3.1

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/index.js CHANGED
@@ -10341,80 +10341,6 @@ var require_dist = __commonJS((exports) => {
10341
10341
  exports.default = PQueue;
10342
10342
  });
10343
10343
 
10344
- // node_modules/fast-stable-stringify/index.js
10345
- var require_fast_stable_stringify = __commonJS((exports, module) => {
10346
- var objToString = Object.prototype.toString;
10347
- var objKeys = Object.keys || function(obj) {
10348
- var keys = [];
10349
- for (var name in obj) {
10350
- keys.push(name);
10351
- }
10352
- return keys;
10353
- };
10354
- function stringify(val, isArrayProp) {
10355
- var i, max, str, keys, key, propVal, toStr;
10356
- if (val === true) {
10357
- return "true";
10358
- }
10359
- if (val === false) {
10360
- return "false";
10361
- }
10362
- switch (typeof val) {
10363
- case "object":
10364
- if (val === null) {
10365
- return null;
10366
- } else if (val.toJSON && typeof val.toJSON === "function") {
10367
- return stringify(val.toJSON(), isArrayProp);
10368
- } else {
10369
- toStr = objToString.call(val);
10370
- if (toStr === "[object Array]") {
10371
- str = "[";
10372
- max = val.length - 1;
10373
- for (i = 0;i < max; i++) {
10374
- str += stringify(val[i], true) + ",";
10375
- }
10376
- if (max > -1) {
10377
- str += stringify(val[i], true);
10378
- }
10379
- return str + "]";
10380
- } else if (toStr === "[object Object]") {
10381
- keys = objKeys(val).sort();
10382
- max = keys.length;
10383
- str = "";
10384
- i = 0;
10385
- while (i < max) {
10386
- key = keys[i];
10387
- propVal = stringify(val[key], false);
10388
- if (propVal !== undefined) {
10389
- if (str) {
10390
- str += ",";
10391
- }
10392
- str += JSON.stringify(key) + ":" + propVal;
10393
- }
10394
- i++;
10395
- }
10396
- return "{" + str + "}";
10397
- } else {
10398
- return JSON.stringify(val);
10399
- }
10400
- }
10401
- case "function":
10402
- case "undefined":
10403
- return isArrayProp ? null : undefined;
10404
- case "string":
10405
- return JSON.stringify(val);
10406
- default:
10407
- return isFinite(val) ? val : null;
10408
- }
10409
- }
10410
- module.exports = function(val) {
10411
- var returnVal = stringify(val, false);
10412
- if (returnVal !== undefined) {
10413
- return "" + returnVal;
10414
- }
10415
- };
10416
- });
10417
-
10418
10344
  // node_modules/rxdb/dist/esm/rx-storage-multiinstance.js
10419
10345
  var import_rxjs = __toESM(require_cjs(), 1);
10420
10346
  var import_operators = __toESM(require_operators(), 1);
@@ -12284,6 +12210,36 @@ function addRxStorageMultiInstanceSupport(storageName, instanceCreationParams, i
12284
12210
  // src/instance.ts
12285
12211
  var import_rxjs2 = __toESM(require_cjs(), 1);
12286
12212
 
12213
+ // src/query/regex-matcher.ts
12214
+ var REGEX_CACHE = new Map;
12215
+ var MAX_REGEX_CACHE_SIZE = 100;
12216
+ function compileRegex(pattern, options) {
12217
+ const cacheKey = `${pattern}::${options || ""}`;
12218
+ const cached = REGEX_CACHE.get(cacheKey);
12219
+ if (cached) {
12220
+ return cached.regex;
12221
+ }
12222
+ const regex = new RegExp(pattern, options);
12223
+ if (REGEX_CACHE.size >= MAX_REGEX_CACHE_SIZE) {
12224
+ const firstKey = REGEX_CACHE.keys().next().value;
12225
+ if (firstKey) {
12226
+ REGEX_CACHE.delete(firstKey);
12227
+ }
12228
+ }
12229
+ REGEX_CACHE.set(cacheKey, { regex });
12230
+ return regex;
12231
+ }
12232
+ function matchesRegex(value, pattern, options) {
12233
+ const regex = compileRegex(pattern, options);
12234
+ if (typeof value === "string") {
12235
+ return regex.test(value);
12236
+ }
12237
+ if (Array.isArray(value)) {
12238
+ return value.some((v) => typeof v === "string" && regex.test(v));
12239
+ }
12240
+ return false;
12241
+ }
12242
+
12287
12243
  // src/query/schema-mapper.ts
12288
12244
  function getColumnInfo(path2, schema) {
12289
12245
  if (path2 === "_deleted") {
@@ -12302,39 +12258,87 @@ function getColumnInfo(path2, schema) {
12302
12258
  }
12303
12259
 
12304
12260
  // src/query/smart-regex.ts
12305
- function smartRegexToLike(field, pattern, options) {
12306
- const caseInsensitive = options?.includes("i");
12261
+ var INDEX_CACHE = new Map;
12262
+ var MAX_INDEX_CACHE_SIZE = 1000;
12263
+ function hasExpressionIndex(fieldName, schema) {
12264
+ const indexKey = schema.indexes ? JSON.stringify(schema.indexes) : "none";
12265
+ const cacheKey = `${schema.version}_${fieldName}_${indexKey}`;
12266
+ const cached = INDEX_CACHE.get(cacheKey);
12267
+ if (cached !== undefined) {
12268
+ INDEX_CACHE.delete(cacheKey);
12269
+ INDEX_CACHE.set(cacheKey, cached);
12270
+ return cached;
12271
+ }
12272
+ if (!schema.indexes) {
12273
+ if (INDEX_CACHE.size >= MAX_INDEX_CACHE_SIZE) {
12274
+ const firstKey = INDEX_CACHE.keys().next().value;
12275
+ if (firstKey)
12276
+ INDEX_CACHE.delete(firstKey);
12277
+ }
12278
+ INDEX_CACHE.set(cacheKey, false);
12279
+ return false;
12280
+ }
12281
+ const hasLowerIndex = schema.indexes.some((idx) => {
12282
+ const fields = Array.isArray(idx) ? idx : [idx];
12283
+ return fields.some((f) => {
12284
+ if (typeof f !== "string")
12285
+ return false;
12286
+ const normalized = f.toLowerCase().replace(/\s/g, "");
12287
+ return normalized === `lower(${fieldName})`;
12288
+ });
12289
+ });
12290
+ if (INDEX_CACHE.size >= MAX_INDEX_CACHE_SIZE) {
12291
+ const firstKey = INDEX_CACHE.keys().next().value;
12292
+ if (firstKey)
12293
+ INDEX_CACHE.delete(firstKey);
12294
+ }
12295
+ INDEX_CACHE.set(cacheKey, hasLowerIndex);
12296
+ return hasLowerIndex;
12297
+ }
12298
+ function isComplexRegex(pattern) {
12299
+ return /[*+?()[\]{}|]/.test(pattern.replace(/\\\./g, ""));
12300
+ }
12301
+ function escapeForLike(str) {
12302
+ return str.replace(/[\\%_]/g, "\\$&");
12303
+ }
12304
+ function smartRegexToLike(field, pattern, options, schema, fieldName) {
12305
+ if (typeof pattern !== "string")
12306
+ return null;
12307
+ const caseInsensitive = options?.includes("i") ?? false;
12308
+ const hasLowerIndex = hasExpressionIndex(fieldName, schema);
12307
12309
  const startsWithAnchor = pattern.startsWith("^");
12308
12310
  const endsWithAnchor = pattern.endsWith("$");
12309
12311
  let cleanPattern = pattern.replace(/^\^/, "").replace(/\$$/, "");
12310
- if (startsWithAnchor && endsWithAnchor && !/[*+?()[\]{}|]/.test(cleanPattern)) {
12311
- const exact = cleanPattern.replace(/\\\./g, ".");
12312
+ if (isComplexRegex(cleanPattern)) {
12313
+ return null;
12314
+ }
12315
+ const unescaped = cleanPattern.replace(/\\\./g, ".");
12316
+ const escaped = escapeForLike(unescaped);
12317
+ if (startsWithAnchor && endsWithAnchor) {
12312
12318
  if (caseInsensitive) {
12313
- const escaped = exact.replace(/%/g, "\\%").replace(/_/g, "\\_");
12314
- return { sql: `${field} LIKE ? COLLATE NOCASE ESCAPE '\\'`, args: [escaped] };
12319
+ return hasLowerIndex ? { sql: `LOWER(${field}) = ?`, args: [unescaped.toLowerCase()] } : { sql: `${field} = ? COLLATE NOCASE`, args: [unescaped] };
12315
12320
  }
12316
- return { sql: `${field} = ?`, args: [exact] };
12321
+ return { sql: `${field} = ?`, args: [unescaped] };
12317
12322
  }
12318
12323
  if (startsWithAnchor) {
12319
- const prefix = cleanPattern.replace(/\\\./g, ".");
12320
- if (!/[*+?()[\]{}|]/.test(prefix)) {
12321
- const escaped = prefix.replace(/%/g, "\\%").replace(/_/g, "\\_");
12322
- return caseInsensitive ? { sql: `${field} LIKE ? COLLATE NOCASE ESCAPE '\\'`, args: [escaped + "%"] } : { sql: `${field} LIKE ? ESCAPE '\\'`, args: [escaped + "%"] };
12324
+ const suffix = caseInsensitive ? escaped.toLowerCase() : escaped;
12325
+ if (caseInsensitive && hasLowerIndex) {
12326
+ return { sql: `LOWER(${field}) LIKE ? ESCAPE '\\'`, args: [suffix + "%"] };
12323
12327
  }
12328
+ return { sql: `${field} LIKE ?${caseInsensitive ? " COLLATE NOCASE" : ""} ESCAPE '\\'`, args: [suffix + "%"] };
12324
12329
  }
12325
12330
  if (endsWithAnchor) {
12326
- const suffix = cleanPattern.replace(/\\\./g, ".");
12327
- if (!/[*+?()[\]{}|]/.test(suffix)) {
12328
- const escaped = suffix.replace(/%/g, "\\%").replace(/_/g, "\\_");
12329
- return caseInsensitive ? { sql: `${field} LIKE ? COLLATE NOCASE ESCAPE '\\'`, args: ["%" + escaped] } : { sql: `${field} LIKE ? ESCAPE '\\'`, args: ["%" + escaped] };
12331
+ const prefix = caseInsensitive ? escaped.toLowerCase() : escaped;
12332
+ if (caseInsensitive && hasLowerIndex) {
12333
+ return { sql: `LOWER(${field}) LIKE ? ESCAPE '\\'`, args: ["%" + prefix] };
12330
12334
  }
12335
+ return { sql: `${field} LIKE ?${caseInsensitive ? " COLLATE NOCASE" : ""} ESCAPE '\\'`, args: ["%" + prefix] };
12331
12336
  }
12332
- cleanPattern = cleanPattern.replace(/\\\./g, ".");
12333
- if (!/[*+?()[\]{}|^$]/.test(cleanPattern)) {
12334
- const escaped = cleanPattern.replace(/%/g, "\\%").replace(/_/g, "\\_");
12335
- return caseInsensitive ? { sql: `${field} LIKE ? COLLATE NOCASE ESCAPE '\\'`, args: ["%" + escaped + "%"] } : { sql: `${field} LIKE ? ESCAPE '\\'`, args: ["%" + escaped + "%"] };
12337
+ const middle = caseInsensitive ? escaped.toLowerCase() : escaped;
12338
+ if (caseInsensitive && hasLowerIndex) {
12339
+ return { sql: `LOWER(${field}) LIKE ? ESCAPE '\\'`, args: ["%" + middle + "%"] };
12336
12340
  }
12337
- return null;
12341
+ return { sql: `${field} LIKE ?${caseInsensitive ? " COLLATE NOCASE" : ""} ESCAPE '\\'`, args: ["%" + middle + "%"] };
12338
12342
  }
12339
12343
 
12340
12344
  // src/query/operators.ts
@@ -12406,40 +12410,82 @@ function translateExists(field, exists) {
12406
12410
  args: []
12407
12411
  };
12408
12412
  }
12409
- function translateRegex(field, pattern, options) {
12410
- const smartResult = smartRegexToLike(field, pattern, options);
12413
+ function translateRegex(field, pattern, options, schema, fieldName) {
12414
+ const smartResult = smartRegexToLike(field, pattern, options, schema, fieldName);
12411
12415
  if (smartResult)
12412
12416
  return smartResult;
12413
12417
  return null;
12414
12418
  }
12415
- function translateElemMatch(field, criteria) {
12416
- return null;
12417
- }
12418
- function translateNot(field, criteria) {
12419
- const inner = processOperatorValue(field, criteria);
12419
+ function buildElemMatchConditions(criteria) {
12420
+ const conditions = [];
12421
+ const args = [];
12422
+ for (const [key, value] of Object.entries(criteria)) {
12423
+ if (key.startsWith("$")) {
12424
+ const fragment = processOperatorValue("json_each.value", { [key]: value });
12425
+ conditions.push(fragment.sql);
12426
+ args.push(...fragment.args);
12427
+ } else {
12428
+ const propertyField = `json_extract(json_each.value, '$.${key}')`;
12429
+ const fragment = processOperatorValue(propertyField, value);
12430
+ conditions.push(fragment.sql);
12431
+ args.push(...fragment.args);
12432
+ }
12433
+ }
12420
12434
  return {
12421
- sql: `NOT(${inner.sql})`,
12422
- args: inner.args
12435
+ sql: conditions.length > 0 ? conditions.join(" AND ") : "1=1",
12436
+ args
12423
12437
  };
12424
12438
  }
12425
- function translateNor(conditions) {
12426
- if (conditions.length === 0) {
12427
- return { sql: "1=1", args: [] };
12439
+ function translateElemMatch(field, criteria) {
12440
+ if (typeof criteria !== "object" || criteria === null) {
12441
+ return {
12442
+ sql: `EXISTS (SELECT 1 FROM json_each(${field}) WHERE json_each.value = ?)`,
12443
+ args: [criteria]
12444
+ };
12445
+ }
12446
+ if (criteria.$and && Array.isArray(criteria.$and)) {
12447
+ const fragments = criteria.$and.map((cond) => buildElemMatchConditions(cond));
12448
+ const sql = fragments.map((f) => f.sql).join(" AND ");
12449
+ const args = fragments.flatMap((f) => f.args);
12450
+ return {
12451
+ sql: `EXISTS (SELECT 1 FROM json_each(${field}) WHERE ${sql})`,
12452
+ args
12453
+ };
12454
+ }
12455
+ if (criteria.$or && Array.isArray(criteria.$or)) {
12456
+ const fragments = criteria.$or.map((cond) => buildElemMatchConditions(cond));
12457
+ const sql = fragments.map((f) => f.sql).join(" OR ");
12458
+ const args = fragments.flatMap((f) => f.args);
12459
+ return {
12460
+ sql: `EXISTS (SELECT 1 FROM json_each(${field}) WHERE ${sql})`,
12461
+ args
12462
+ };
12463
+ }
12464
+ if (criteria.$nor && Array.isArray(criteria.$nor)) {
12465
+ const fragments = criteria.$nor.map((cond) => buildElemMatchConditions(cond));
12466
+ const sql = fragments.map((f) => f.sql).join(" OR ");
12467
+ const args = fragments.flatMap((f) => f.args);
12468
+ return {
12469
+ sql: `EXISTS (SELECT 1 FROM json_each(${field}) WHERE NOT (${sql}))`,
12470
+ args
12471
+ };
12472
+ }
12473
+ const fragment = buildElemMatchConditions(criteria);
12474
+ if (fragment.sql === "1=1") {
12475
+ return null;
12428
12476
  }
12429
- const fragments = conditions.map((condition) => {
12430
- const [[field, value]] = Object.entries(condition);
12431
- return processOperatorValue(field, value);
12432
- });
12433
- const sql = fragments.map((f) => `(${f.sql})`).join(" OR ");
12434
- const args = fragments.flatMap((f) => f.args);
12435
12477
  return {
12436
- sql: `NOT(${sql})`,
12437
- args
12478
+ sql: `EXISTS (SELECT 1 FROM json_each(${field}) WHERE ${fragment.sql})`,
12479
+ args: fragment.args
12438
12480
  };
12439
12481
  }
12440
12482
  function processOperatorValue(field, value) {
12441
12483
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
12442
12484
  const [[op, opValue]] = Object.entries(value);
12485
+ if (!op.startsWith("$")) {
12486
+ const jsonPath = `json_extract(${field}, '$.${op}')`;
12487
+ return translateEq(jsonPath, opValue);
12488
+ }
12443
12489
  switch (op) {
12444
12490
  case "$eq":
12445
12491
  return translateEq(field, opValue);
@@ -12457,27 +12503,68 @@ function processOperatorValue(field, value) {
12457
12503
  return translateIn(field, opValue);
12458
12504
  case "$nin":
12459
12505
  return translateNin(field, opValue);
12506
+ case "$exists":
12507
+ return translateExists(field, opValue);
12508
+ case "$size":
12509
+ return translateSize(field, opValue);
12510
+ case "$mod": {
12511
+ const result = translateMod(field, opValue);
12512
+ if (!result)
12513
+ return translateEq(field, opValue);
12514
+ return result;
12515
+ }
12516
+ case "$not": {
12517
+ const result = translateNot(field, opValue);
12518
+ if (!result)
12519
+ return translateEq(field, opValue);
12520
+ return result;
12521
+ }
12522
+ case "$and": {
12523
+ if (!Array.isArray(opValue))
12524
+ return translateEq(field, opValue);
12525
+ const fragments = opValue.map((v) => processOperatorValue(field, v));
12526
+ const sql = fragments.map((f) => f.sql).join(" AND ");
12527
+ const args = fragments.flatMap((f) => f.args);
12528
+ return { sql: `(${sql})`, args };
12529
+ }
12530
+ case "$or": {
12531
+ if (!Array.isArray(opValue))
12532
+ return translateEq(field, opValue);
12533
+ const fragments = opValue.map((v) => processOperatorValue(field, v));
12534
+ const sql = fragments.map((f) => f.sql).join(" OR ");
12535
+ const args = fragments.flatMap((f) => f.args);
12536
+ return { sql: `(${sql})`, args };
12537
+ }
12460
12538
  default:
12461
12539
  return translateEq(field, opValue);
12462
12540
  }
12463
12541
  }
12464
12542
  return translateEq(field, value);
12465
12543
  }
12466
- function translateType(field, type6) {
12544
+ function translateNot(field, criteria) {
12545
+ if (!criteria || typeof criteria === "object" && Object.keys(criteria).length === 0)
12546
+ return null;
12547
+ const inner = processOperatorValue(field, criteria);
12548
+ return {
12549
+ sql: `NOT(${inner.sql})`,
12550
+ args: inner.args
12551
+ };
12552
+ }
12553
+ function translateType(jsonColumn, fieldName, type6) {
12554
+ const jsonPath = `$.${fieldName}`;
12467
12555
  switch (type6) {
12468
- case "number":
12469
- return {
12470
- sql: `(typeof(${field}) = 'integer' OR typeof(${field}) = 'real')`,
12471
- args: []
12472
- };
12473
- case "string":
12474
- return { sql: `typeof(${field}) = 'text'`, args: [] };
12475
12556
  case "null":
12476
- return { sql: `typeof(${field}) = 'null'`, args: [] };
12557
+ return { sql: `json_type(${jsonColumn}, '${jsonPath}') = 'null'`, args: [] };
12477
12558
  case "boolean":
12559
+ return { sql: `json_type(${jsonColumn}, '${jsonPath}') IN ('true', 'false')`, args: [] };
12560
+ case "number":
12561
+ return { sql: `json_type(${jsonColumn}, '${jsonPath}') IN ('integer', 'real')`, args: [] };
12562
+ case "string":
12563
+ return { sql: `json_type(${jsonColumn}, '${jsonPath}') = 'text'`, args: [] };
12478
12564
  case "array":
12565
+ return { sql: `json_type(${jsonColumn}, '${jsonPath}') = 'array'`, args: [] };
12479
12566
  case "object":
12480
- case "date":
12567
+ return { sql: `json_type(${jsonColumn}, '${jsonPath}') = 'object'`, args: [] };
12481
12568
  default:
12482
12569
  return null;
12483
12570
  }
@@ -12488,19 +12575,120 @@ function translateSize(field, size) {
12488
12575
  args: [size]
12489
12576
  };
12490
12577
  }
12491
- function translateMod(field, [divisor, remainder]) {
12578
+ function translateMod(field, value) {
12579
+ if (!Array.isArray(value) || value.length !== 2)
12580
+ return null;
12581
+ const [divisor, remainder] = value;
12492
12582
  return {
12493
12583
  sql: `${field} % ? = ?`,
12494
12584
  args: [divisor, remainder]
12495
12585
  };
12496
12586
  }
12497
12587
 
12588
+ // src/utils/stable-stringify.ts
12589
+ var strEscapeRegex = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/;
12590
+ function strEscape(str) {
12591
+ if (str.length < 5000 && !strEscapeRegex.test(str)) {
12592
+ return `"${str}"`;
12593
+ }
12594
+ return JSON.stringify(str);
12595
+ }
12596
+ function callSafe(method, thisArg) {
12597
+ try {
12598
+ return method.call(thisArg);
12599
+ } catch (error) {
12600
+ return `[Error: ${error instanceof Error ? error.message : String(error)}]`;
12601
+ }
12602
+ }
12603
+ function sortKeys(keys) {
12604
+ if (keys.length > 100) {
12605
+ return keys.sort();
12606
+ }
12607
+ for (let i = 1;i < keys.length; i++) {
12608
+ const current = keys[i];
12609
+ let pos = i;
12610
+ while (pos !== 0 && keys[pos - 1] > current) {
12611
+ keys[pos] = keys[pos - 1];
12612
+ pos--;
12613
+ }
12614
+ keys[pos] = current;
12615
+ }
12616
+ return keys;
12617
+ }
12618
+ function stableStringify(value) {
12619
+ return _stringify(value, []);
12620
+ }
12621
+ function _stringify(value, stack) {
12622
+ if (value === null)
12623
+ return "null";
12624
+ if (value === true)
12625
+ return "true";
12626
+ if (value === false)
12627
+ return "false";
12628
+ const type6 = typeof value;
12629
+ if (type6 === "string")
12630
+ return strEscape(value);
12631
+ if (type6 === "number")
12632
+ return isFinite(value) ? String(value) : "null";
12633
+ if (type6 === "undefined")
12634
+ return "null";
12635
+ if (type6 === "bigint")
12636
+ return String(value);
12637
+ if (Array.isArray(value)) {
12638
+ if (stack.indexOf(value) !== -1)
12639
+ return '"[Circular]"';
12640
+ stack.push(value);
12641
+ if (value.length === 0) {
12642
+ stack.pop();
12643
+ return "[]";
12644
+ }
12645
+ let res = "[" + _stringify(value[0], stack);
12646
+ for (let i = 1;i < value.length; i++) {
12647
+ res += "," + _stringify(value[i], stack);
12648
+ }
12649
+ stack.pop();
12650
+ return res + "]";
12651
+ }
12652
+ if (type6 === "object") {
12653
+ const obj = value;
12654
+ if (stack.indexOf(value) !== -1)
12655
+ return '"[Circular]"';
12656
+ stack.push(value);
12657
+ if ("toJSON" in obj && typeof obj.toJSON === "function") {
12658
+ const result = _stringify(callSafe(obj.toJSON, obj), stack);
12659
+ stack.pop();
12660
+ return result;
12661
+ }
12662
+ const objType = Object.prototype.toString.call(obj);
12663
+ if (objType !== "[object Object]") {
12664
+ const result = JSON.stringify(obj);
12665
+ stack.pop();
12666
+ return result;
12667
+ }
12668
+ const keys = sortKeys(Object.keys(obj));
12669
+ let res = "{";
12670
+ let separator = "";
12671
+ for (let i = 0;i < keys.length; i++) {
12672
+ const key = keys[i];
12673
+ const val = obj[key];
12674
+ if (val === undefined)
12675
+ continue;
12676
+ res += separator + strEscape(key) + ":" + _stringify(val, stack);
12677
+ separator = ",";
12678
+ }
12679
+ stack.pop();
12680
+ return res + "}";
12681
+ }
12682
+ return "null";
12683
+ }
12684
+
12498
12685
  // src/query/builder.ts
12499
- var import_fast_stable_stringify = __toESM(require_fast_stable_stringify(), 1);
12500
12686
  var QUERY_CACHE = new Map;
12501
- var MAX_CACHE_SIZE = 500;
12502
- function buildWhereClause(selector, schema) {
12503
- const cacheKey = `v${schema.version}_${import_fast_stable_stringify.default(selector)}`;
12687
+ var MAX_CACHE_SIZE = 1000;
12688
+ function buildWhereClause(selector, schema, collectionName) {
12689
+ if (!selector || typeof selector !== "object")
12690
+ return null;
12691
+ const cacheKey = `v${schema.version}_${collectionName}_${stableStringify(selector)}`;
12504
12692
  const cached = QUERY_CACHE.get(cacheKey);
12505
12693
  if (cached) {
12506
12694
  QUERY_CACHE.delete(cacheKey);
@@ -12508,6 +12696,8 @@ function buildWhereClause(selector, schema) {
12508
12696
  return cached;
12509
12697
  }
12510
12698
  const result = processSelector(selector, schema, 0);
12699
+ if (!result)
12700
+ return null;
12511
12701
  if (QUERY_CACHE.size >= MAX_CACHE_SIZE) {
12512
12702
  const firstKey = QUERY_CACHE.keys().next().value;
12513
12703
  if (firstKey)
@@ -12516,12 +12706,27 @@ function buildWhereClause(selector, schema) {
12516
12706
  QUERY_CACHE.set(cacheKey, result);
12517
12707
  return result;
12518
12708
  }
12709
+ function buildLogicalOperator(operator, conditions, schema, logicalDepth) {
12710
+ if (conditions.length === 0) {
12711
+ return { sql: "1=1", args: [] };
12712
+ }
12713
+ const fragments = conditions.map((subSelector) => processSelector(subSelector, schema, logicalDepth + 1));
12714
+ if (fragments.some((f) => f === null))
12715
+ return null;
12716
+ const sql = fragments.map((f) => f.sql).join(" OR ");
12717
+ const args = fragments.flatMap((f) => f.args);
12718
+ return operator === "nor" ? { sql: `NOT(${sql})`, args } : { sql, args };
12719
+ }
12519
12720
  function processSelector(selector, schema, logicalDepth) {
12721
+ if (!selector || typeof selector !== "object")
12722
+ return null;
12520
12723
  const conditions = [];
12521
12724
  const args = [];
12522
12725
  for (const [field, value] of Object.entries(selector)) {
12523
12726
  if (field === "$and" && Array.isArray(value)) {
12524
- const andFragments = value.map((subSelector) => processSelector(subSelector, schema, logicalDepth));
12727
+ const andFragments = value.map((subSelector) => processSelector(subSelector, schema, logicalDepth + 1));
12728
+ if (andFragments.some((f) => f === null))
12729
+ return null;
12525
12730
  const andConditions = andFragments.map((f) => f.sql);
12526
12731
  const needsParens = logicalDepth > 0 && andConditions.length > 1;
12527
12732
  const joined = andConditions.join(" AND ");
@@ -12530,22 +12735,25 @@ function processSelector(selector, schema, logicalDepth) {
12530
12735
  continue;
12531
12736
  }
12532
12737
  if (field === "$or" && Array.isArray(value)) {
12533
- const orFragments = value.map((subSelector) => processSelector(subSelector, schema, logicalDepth + 1));
12534
- const orConditions = orFragments.map((f) => f.sql);
12535
- const needsParens = logicalDepth > 0 && orConditions.length > 1;
12536
- const joined = orConditions.join(" OR ");
12537
- conditions.push(needsParens ? `(${joined})` : joined);
12538
- orFragments.forEach((f) => args.push(...f.args));
12738
+ const orFragment = buildLogicalOperator("or", value, schema, logicalDepth);
12739
+ if (!orFragment)
12740
+ return null;
12741
+ const needsParens = logicalDepth > 0;
12742
+ conditions.push(needsParens ? `(${orFragment.sql})` : orFragment.sql);
12743
+ args.push(...orFragment.args);
12539
12744
  continue;
12540
12745
  }
12541
12746
  if (field === "$nor" && Array.isArray(value)) {
12542
- const norFragment = translateNor(value);
12747
+ const norFragment = buildLogicalOperator("nor", value, schema, logicalDepth);
12748
+ if (!norFragment)
12749
+ return null;
12543
12750
  conditions.push(norFragment.sql);
12544
12751
  args.push(...norFragment.args);
12545
12752
  continue;
12546
12753
  }
12547
12754
  const columnInfo = getColumnInfo(field, schema);
12548
12755
  const fieldName = columnInfo.column || `json_extract(data, '${columnInfo.jsonPath}')`;
12756
+ const actualFieldName = columnInfo.jsonPath?.replace(/^\$\./, "") || columnInfo.column || field;
12549
12757
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
12550
12758
  for (const [op, opValue] of Object.entries(value)) {
12551
12759
  let fragment;
@@ -12579,32 +12787,40 @@ function processSelector(selector, schema, logicalDepth) {
12579
12787
  break;
12580
12788
  case "$regex":
12581
12789
  const options = value.$options;
12582
- const regexFragment = translateRegex(fieldName, opValue, options);
12790
+ const regexFragment = translateRegex(fieldName, opValue, options, schema, actualFieldName);
12583
12791
  if (!regexFragment)
12584
- continue;
12792
+ return null;
12585
12793
  fragment = regexFragment;
12586
12794
  break;
12587
12795
  case "$elemMatch":
12588
12796
  const elemMatchFragment = translateElemMatch(fieldName, opValue);
12589
12797
  if (!elemMatchFragment)
12590
- continue;
12798
+ return null;
12591
12799
  fragment = elemMatchFragment;
12592
12800
  break;
12593
- case "$not":
12594
- fragment = translateNot(fieldName, opValue);
12801
+ case "$not": {
12802
+ const notResult = translateNot(fieldName, opValue);
12803
+ if (!notResult)
12804
+ return null;
12805
+ fragment = notResult;
12595
12806
  break;
12807
+ }
12596
12808
  case "$type":
12597
- const typeFragment = translateType(fieldName, opValue);
12809
+ const typeFragment = translateType("data", actualFieldName, opValue);
12598
12810
  if (!typeFragment)
12599
- continue;
12811
+ return null;
12600
12812
  fragment = typeFragment;
12601
12813
  break;
12602
12814
  case "$size":
12603
12815
  fragment = translateSize(fieldName, opValue);
12604
12816
  break;
12605
- case "$mod":
12606
- fragment = translateMod(fieldName, opValue);
12817
+ case "$mod": {
12818
+ const modResult = translateMod(fieldName, opValue);
12819
+ if (!modResult)
12820
+ return null;
12821
+ fragment = modResult;
12607
12822
  break;
12823
+ }
12608
12824
  default:
12609
12825
  continue;
12610
12826
  }
@@ -12828,32 +13044,64 @@ function ensureRxStorageInstanceParamsAreCorrect(params) {
12828
13044
  class StatementManager {
12829
13045
  db;
12830
13046
  staticStatements = new Map;
13047
+ static MAX_STATEMENTS = 500;
13048
+ closed = false;
12831
13049
  constructor(db) {
12832
13050
  this.db = db;
12833
13051
  }
13052
+ checkClosed() {
13053
+ if (this.closed) {
13054
+ throw new Error("StatementManager is closed");
13055
+ }
13056
+ }
13057
+ evictOldest() {
13058
+ if (this.staticStatements.size >= StatementManager.MAX_STATEMENTS) {
13059
+ const firstKey = this.staticStatements.keys().next().value;
13060
+ if (firstKey) {
13061
+ const stmt = this.staticStatements.get(firstKey);
13062
+ stmt?.finalize();
13063
+ this.staticStatements.delete(firstKey);
13064
+ }
13065
+ }
13066
+ }
12834
13067
  all(queryWithParams) {
13068
+ this.checkClosed();
12835
13069
  const { query, params } = queryWithParams;
12836
- if (this.isStaticSQL(query)) {
12837
- let stmt = this.staticStatements.get(query);
12838
- if (!stmt) {
12839
- stmt = this.db.query(query);
12840
- this.staticStatements.set(query, stmt);
12841
- }
12842
- return stmt.all(...params);
13070
+ let stmt = this.staticStatements.get(query);
13071
+ if (stmt) {
13072
+ this.staticStatements.delete(query);
13073
+ this.staticStatements.set(query, stmt);
12843
13074
  } else {
12844
- const stmt = this.db.prepare(query);
12845
- try {
12846
- return stmt.all(...params);
12847
- } finally {
12848
- stmt.finalize();
12849
- }
13075
+ this.evictOldest();
13076
+ stmt = this.db.query(query);
13077
+ this.staticStatements.set(query, stmt);
13078
+ }
13079
+ return stmt.all(...params);
13080
+ }
13081
+ get(queryWithParams) {
13082
+ this.checkClosed();
13083
+ const { query, params } = queryWithParams;
13084
+ let stmt = this.staticStatements.get(query);
13085
+ if (stmt) {
13086
+ this.staticStatements.delete(query);
13087
+ this.staticStatements.set(query, stmt);
13088
+ } else {
13089
+ this.evictOldest();
13090
+ stmt = this.db.query(query);
13091
+ this.staticStatements.set(query, stmt);
12850
13092
  }
13093
+ return stmt.get(...params);
12851
13094
  }
12852
13095
  run(queryWithParams) {
13096
+ this.checkClosed();
12853
13097
  const { query, params } = queryWithParams;
12854
13098
  if (this.isStaticSQL(query)) {
12855
13099
  let stmt = this.staticStatements.get(query);
12856
- if (!stmt) {
13100
+ if (stmt) {
13101
+ this.staticStatements.delete(query);
13102
+ this.staticStatements.set(query, stmt);
13103
+ } else {
13104
+ this.evictOldest();
12857
13105
  stmt = this.db.query(query);
12858
13106
  this.staticStatements.set(query, stmt);
12859
13107
  }
@@ -12872,6 +13120,7 @@ class StatementManager {
12872
13120
  stmt.finalize();
12873
13121
  }
12874
13122
  this.staticStatements.clear();
13123
+ this.closed = true;
12875
13124
  }
12876
13125
  isStaticSQL(query) {
12877
13126
  if (query.includes("WHERE (")) {
@@ -12912,6 +13161,28 @@ function releaseDatabase(databaseName) {
12912
13161
  }
12913
13162
  }
12914
13163
 
13164
+ // src/transaction-queue.ts
13165
+ var TX_QUEUE_BY_DATABASE = new WeakMap;
13166
+ async function sqliteTransaction(database, handler) {
13167
+ let queue = TX_QUEUE_BY_DATABASE.get(database);
13168
+ if (!queue) {
13169
+ queue = Promise.resolve();
13170
+ }
13171
+ const result = queue.then(async () => {
13172
+ database.run("BEGIN IMMEDIATE");
13173
+ try {
13174
+ const handlerResult = await handler();
13175
+ database.run("COMMIT");
13176
+ return handlerResult;
13177
+ } catch (error) {
13178
+ database.run("ROLLBACK");
13179
+ throw error;
13180
+ }
13181
+ });
13182
+ TX_QUEUE_BY_DATABASE.set(database, result.then(() => {}).catch(() => {}));
13183
+ return result;
13184
+ }
13185
+
12915
13186
  // src/instance.ts
12916
13187
  class BunSQLiteStorageInstance {
12917
13188
  db;
@@ -12947,6 +13218,15 @@ class BunSQLiteStorageInstance {
12947
13218
  if (filename !== ":memory:") {
12948
13219
  this.db.run("PRAGMA journal_mode = WAL");
12949
13220
  this.db.run("PRAGMA synchronous = NORMAL");
13221
+ this.db.run("PRAGMA wal_autocheckpoint = 1000");
13222
+ this.db.run("PRAGMA cache_size = -32000");
13223
+ this.db.run("PRAGMA analysis_limit = 400");
13224
+ const mmapSize = this.options.mmapSize ?? 268435456;
13225
+ if (mmapSize > 0) {
13226
+ this.db.run(`PRAGMA mmap_size = ${mmapSize}`);
13227
+ }
13228
+ this.db.run("PRAGMA temp_store = MEMORY");
13229
+ this.db.run("PRAGMA locking_mode = NORMAL");
12950
13230
  }
12951
13231
  this.db.run(`
12952
13232
  CREATE TABLE IF NOT EXISTS "${this.tableName}" (
@@ -12962,8 +13242,17 @@ class BunSQLiteStorageInstance {
12962
13242
  if (this.schema.indexes) {
12963
13243
  for (const index of this.schema.indexes) {
12964
13244
  const fields = Array.isArray(index) ? index : [index];
12965
- const indexName = `idx_${this.tableName}_${fields.join("_")}`;
12966
- const columns = fields.map((field) => `json_extract(data, '$.${field}')`).join(", ");
13245
+ const indexName = `idx_${this.tableName}_${fields.join("_").replace(/[()]/g, "_")}`;
13246
+ const columns = fields.map((field) => {
13247
+ if (typeof field !== "string")
13248
+ return `json_extract(data, '$.${field}')`;
13249
+ const funcMatch = field.match(/^(\w+)\((.+)\)$/);
13250
+ if (funcMatch) {
13251
+ const [, func, fieldName] = funcMatch;
13252
+ return `${func}(json_extract(data, '$.${fieldName}'))`;
13253
+ }
13254
+ return `json_extract(data, '$.${field}')`;
13255
+ }).join(", ");
12967
13256
  this.db.run(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${this.tableName}"(${columns})`);
12968
13257
  }
12969
13258
  }
@@ -12976,69 +13265,85 @@ class BunSQLiteStorageInstance {
12976
13265
  `);
12977
13266
  }
12978
13267
  async bulkWrite(documentWrites, context) {
12979
- if (documentWrites.length === 0) {
12980
- return { error: [] };
12981
- }
12982
- const ids = documentWrites.map((w) => w.document[this.primaryPath]);
12983
- const docsInDb = await this.findDocumentsById(ids, true);
12984
- const docsInDbMap = new Map(docsInDb.map((d) => [d[this.primaryPath], d]));
12985
- const categorized = categorizeBulkWriteRows(this, this.primaryPath, docsInDbMap, documentWrites, context);
12986
- const insertQuery = `INSERT INTO "${this.tableName}" (id, data, deleted, rev, mtime_ms) VALUES (?, jsonb(?), ?, ?, ?)`;
12987
- const updateQuery = `UPDATE "${this.tableName}" SET data = jsonb(?), deleted = ?, rev = ?, mtime_ms = ? WHERE id = ?`;
12988
- for (const row of categorized.bulkInsertDocs) {
12989
- const doc = row.document;
12990
- const id = doc[this.primaryPath];
12991
- try {
12992
- this.stmtManager.run({ query: insertQuery, params: [id, JSON.stringify(doc), doc._deleted ? 1 : 0, doc._rev, doc._meta.lwt] });
12993
- } catch (err) {
12994
- if (err.code === "SQLITE_CONSTRAINT_PRIMARYKEY" || err.code === "SQLITE_CONSTRAINT_UNIQUE") {
12995
- const documentInDb = docsInDbMap.get(id);
12996
- categorized.errors.push({
12997
- isError: true,
12998
- status: 409,
12999
- documentId: id,
13000
- writeRow: row,
13001
- documentInDb: documentInDb || doc
13002
- });
13003
- } else {
13004
- throw err;
13268
+ return sqliteTransaction(this.db, async () => {
13269
+ if (documentWrites.length === 0) {
13270
+ return { error: [] };
13271
+ }
13272
+ const ids = documentWrites.map((w) => w.document[this.primaryPath]);
13273
+ const docsInDb = await this.findDocumentsById(ids, true);
13274
+ const docsInDbMap = new Map(docsInDb.map((d) => [d[this.primaryPath], d]));
13275
+ const categorized = categorizeBulkWriteRows(this, this.primaryPath, docsInDbMap, documentWrites, context);
13276
+ const updateQuery = `UPDATE "${this.tableName}" SET data = jsonb(?), deleted = ?, rev = ?, mtime_ms = ? WHERE id = ?`;
13277
+ const BATCH_SIZE = 100;
13278
+ for (let i = 0;i < categorized.bulkInsertDocs.length; i += BATCH_SIZE) {
13279
+ const batch = categorized.bulkInsertDocs.slice(i, i + BATCH_SIZE);
13280
+ const placeholders = batch.map(() => "(?, jsonb(?), ?, ?, ?)").join(", ");
13281
+ const insertQuery = `INSERT INTO "${this.tableName}" (id, data, deleted, rev, mtime_ms) VALUES ${placeholders}`;
13282
+ const params = [];
13283
+ for (const row of batch) {
13284
+ const doc = row.document;
13285
+ const id = doc[this.primaryPath];
13286
+ params.push(id, JSON.stringify(doc), doc._deleted ? 1 : 0, doc._rev, doc._meta.lwt);
13287
+ }
13288
+ try {
13289
+ this.stmtManager.run({ query: insertQuery, params });
13290
+ } catch (err) {
13291
+ if (err && typeof err === "object" && "code" in err && (err.code === "SQLITE_CONSTRAINT_PRIMARYKEY" || err.code === "SQLITE_CONSTRAINT_UNIQUE")) {
13292
+ for (const row of batch) {
13293
+ const doc = row.document;
13294
+ const id = doc[this.primaryPath];
13295
+ const documentInDb = docsInDbMap.get(id);
13296
+ categorized.errors.push({
13297
+ isError: true,
13298
+ status: 409,
13299
+ documentId: id,
13300
+ writeRow: row,
13301
+ documentInDb: documentInDb || doc
13302
+ });
13303
+ }
13304
+ } else {
13305
+ throw err;
13306
+ }
13005
13307
  }
13006
13308
  }
13007
- }
13008
- for (const row of categorized.bulkUpdateDocs) {
13009
- const doc = row.document;
13010
- const id = doc[this.primaryPath];
13011
- this.stmtManager.run({ query: updateQuery, params: [JSON.stringify(doc), doc._deleted ? 1 : 0, doc._rev, doc._meta.lwt, id] });
13012
- }
13013
- const insertAttQuery = `INSERT OR REPLACE INTO "${this.tableName}_attachments" (id, data, digest) VALUES (?, ?, ?)`;
13014
- const deleteAttQuery = `DELETE FROM "${this.tableName}_attachments" WHERE id = ?`;
13015
- for (const att of [...categorized.attachmentsAdd, ...categorized.attachmentsUpdate]) {
13016
- this.stmtManager.run({
13017
- query: insertAttQuery,
13018
- params: [
13019
- this.attachmentMapKey(att.documentId, att.attachmentId),
13020
- att.attachmentData.data,
13021
- att.digest
13022
- ]
13023
- });
13024
- }
13025
- for (const att of categorized.attachmentsRemove) {
13026
- this.stmtManager.run({
13027
- query: deleteAttQuery,
13028
- params: [this.attachmentMapKey(att.documentId, att.attachmentId)]
13029
- });
13030
- }
13031
- const failedDocIds = new Set(categorized.errors.map((e) => e.documentId));
13032
- categorized.eventBulk.events = categorized.eventBulk.events.filter((event) => !failedDocIds.has(event.documentId));
13033
- if (categorized.eventBulk.events.length > 0 && categorized.newestRow) {
13034
- const lastState = categorized.newestRow.document;
13035
- categorized.eventBulk.checkpoint = {
13036
- id: lastState[this.primaryPath],
13037
- lwt: lastState._meta.lwt
13038
- };
13039
- this.changeStream$.next(categorized.eventBulk);
13040
- }
13041
- return { error: categorized.errors };
13309
+ for (let i = 0;i < categorized.bulkUpdateDocs.length; i += BATCH_SIZE) {
13310
+ const batch = categorized.bulkUpdateDocs.slice(i, i + BATCH_SIZE);
13311
+ for (const row of batch) {
13312
+ const doc = row.document;
13313
+ const id = doc[this.primaryPath];
13314
+ this.stmtManager.run({ query: updateQuery, params: [JSON.stringify(doc), doc._deleted ? 1 : 0, doc._rev, doc._meta.lwt, id] });
13315
+ }
13316
+ }
13317
+ const insertAttQuery = `INSERT OR REPLACE INTO "${this.tableName}_attachments" (id, data, digest) VALUES (?, ?, ?)`;
13318
+ const deleteAttQuery = `DELETE FROM "${this.tableName}_attachments" WHERE id = ?`;
13319
+ for (const att of [...categorized.attachmentsAdd, ...categorized.attachmentsUpdate]) {
13320
+ this.stmtManager.run({
13321
+ query: insertAttQuery,
13322
+ params: [
13323
+ this.attachmentMapKey(att.documentId, att.attachmentId),
13324
+ att.attachmentData.data,
13325
+ att.digest
13326
+ ]
13327
+ });
13328
+ }
13329
+ for (const att of categorized.attachmentsRemove) {
13330
+ this.stmtManager.run({
13331
+ query: deleteAttQuery,
13332
+ params: [this.attachmentMapKey(att.documentId, att.attachmentId)]
13333
+ });
13334
+ }
13335
+ const failedDocIds = new Set(categorized.errors.map((e) => e.documentId));
13336
+ categorized.eventBulk.events = categorized.eventBulk.events.filter((event) => !failedDocIds.has(event.documentId));
13337
+ if (categorized.eventBulk.events.length > 0 && categorized.newestRow) {
13338
+ const lastState = categorized.newestRow.document;
13339
+ categorized.eventBulk.checkpoint = {
13340
+ id: lastState[this.primaryPath],
13341
+ lwt: lastState._meta.lwt
13342
+ };
13343
+ this.changeStream$.next(categorized.eventBulk);
13344
+ }
13345
+ return { error: categorized.errors };
13346
+ });
13042
13347
  }
13043
13348
  async findDocumentsById(ids, withDeleted) {
13044
13349
  if (ids.length === 0)
@@ -13050,47 +13355,39 @@ class BunSQLiteStorageInstance {
13050
13355
  return rows.map((row) => JSON.parse(row.data));
13051
13356
  }
13052
13357
  async query(preparedQuery) {
13053
- try {
13054
- const { sql: whereClause, args } = buildWhereClause(preparedQuery.query.selector, this.schema);
13055
- const sql = `
13056
- SELECT json(data) as data FROM "${this.tableName}"
13057
- WHERE (${whereClause})
13058
- `;
13059
- if (process.env.DEBUG_QUERIES) {
13060
- const explainSql = `EXPLAIN QUERY PLAN ${sql}`;
13061
- const plan = this.stmtManager.all({ query: explainSql, params: args });
13062
- console.log("[DEBUG_QUERIES] Query plan:", JSON.stringify(plan, null, 2));
13063
- console.log("[DEBUG_QUERIES] SQL:", sql);
13064
- console.log("[DEBUG_QUERIES] Args:", args);
13065
- }
13066
- const rows = this.stmtManager.all({ query: sql, params: args });
13067
- let documents = rows.map((row) => JSON.parse(row.data));
13068
- if (preparedQuery.query.sort && preparedQuery.query.sort.length > 0) {
13069
- documents = this.sortDocuments(documents, preparedQuery.query.sort);
13070
- }
13071
- if (preparedQuery.query.skip) {
13072
- documents = documents.slice(preparedQuery.query.skip);
13073
- }
13074
- if (preparedQuery.query.limit) {
13075
- documents = documents.slice(0, preparedQuery.query.limit);
13076
- }
13077
- return { documents };
13078
- } catch (err) {
13079
- const query = `SELECT json(data) as data FROM "${this.tableName}"`;
13080
- const rows = this.stmtManager.all({ query, params: [] });
13081
- let documents = rows.map((row) => JSON.parse(row.data));
13082
- documents = documents.filter((doc) => this.matchesSelector(doc, preparedQuery.query.selector));
13083
- if (preparedQuery.query.sort && preparedQuery.query.sort.length > 0) {
13084
- documents = this.sortDocuments(documents, preparedQuery.query.sort);
13085
- }
13086
- if (preparedQuery.query.skip) {
13087
- documents = documents.slice(preparedQuery.query.skip);
13088
- }
13089
- if (preparedQuery.query.limit) {
13090
- documents = documents.slice(0, preparedQuery.query.limit);
13091
- }
13092
- return { documents };
13093
- }
13358
+ const whereResult = buildWhereClause(preparedQuery.query.selector, this.schema, this.collectionName);
13359
+ if (!whereResult) {
13360
+ return this.queryWithOurMemory(preparedQuery);
13361
+ }
13362
+ const { sql: whereClause, args } = whereResult;
13363
+ let sql = `SELECT json(data) as data FROM "${this.tableName}" WHERE (${whereClause})`;
13364
+ const queryArgs = [...args];
13365
+ if (preparedQuery.query.sort && preparedQuery.query.sort.length > 0) {
13366
+ const orderBy = preparedQuery.query.sort.map((sortField) => {
13367
+ const [field, direction] = Object.entries(sortField)[0];
13368
+ const dir = direction === "asc" ? "ASC" : "DESC";
13369
+ return `json_extract(data, '$.${field}') ${dir}`;
13370
+ }).join(", ");
13371
+ sql += ` ORDER BY ${orderBy}`;
13372
+ }
13373
+ if (preparedQuery.query.limit) {
13374
+ sql += ` LIMIT ?`;
13375
+ queryArgs.push(preparedQuery.query.limit);
13376
+ }
13377
+ if (preparedQuery.query.skip) {
13378
+ sql += ` OFFSET ?`;
13379
+ queryArgs.push(preparedQuery.query.skip);
13380
+ }
13381
+ if (process.env.DEBUG_QUERIES) {
13382
+ const explainSql = `EXPLAIN QUERY PLAN ${sql}`;
13383
+ const plan = this.stmtManager.all({ query: explainSql, params: queryArgs });
13384
+ console.log("[DEBUG_QUERIES] Query plan:", JSON.stringify(plan, null, 2));
13385
+ console.log("[DEBUG_QUERIES] SQL:", sql);
13386
+ console.log("[DEBUG_QUERIES] Args:", queryArgs);
13387
+ }
13388
+ const rows = this.stmtManager.all({ query: sql, params: queryArgs });
13389
+ const documents = rows.map((row) => JSON.parse(row.data));
13390
+ return { documents };
13094
13391
  }
13095
13392
  matchesSelector(doc, selector) {
13096
13393
  for (const [key, value] of Object.entries(selector)) {
@@ -13135,9 +13432,18 @@ class BunSQLiteStorageInstance {
13135
13432
  return path2.split(".").reduce((current, key) => current?.[key], obj);
13136
13433
  }
13137
13434
  async count(preparedQuery) {
13138
- const result = await this.query(preparedQuery);
13435
+ const whereResult = buildWhereClause(preparedQuery.query.selector, this.schema, this.collectionName);
13436
+ if (!whereResult) {
13437
+ const allDocs = await this.queryWithOurMemory(preparedQuery);
13438
+ return {
13439
+ count: allDocs.documents.length,
13440
+ mode: "fast"
13441
+ };
13442
+ }
13443
+ const { sql, args } = whereResult;
13444
+ const result = this.db.query(`SELECT COUNT(*) as count FROM "${this.tableName}" WHERE (${sql})`).get(...args);
13139
13445
  return {
13140
- count: result.documents.length,
13446
+ count: result?.count ?? 0,
13141
13447
  mode: "fast"
13142
13448
  };
13143
13449
  }
@@ -13198,9 +13504,43 @@ class BunSQLiteStorageInstance {
13198
13504
  const rows = this.stmtManager.all({ query: sql, params: [checkpointLwt, checkpointLwt, checkpointId, limit] });
13199
13505
  const documents = rows.map((row) => JSON.parse(row.data));
13200
13506
  const lastDoc = documents[documents.length - 1];
13201
- const newCheckpoint = lastDoc ? { id: lastDoc[this.primaryPath], lwt: lastDoc._meta.lwt } : checkpoint ?? null;
13507
+ const newCheckpoint = lastDoc ? {
13508
+ id: String(lastDoc[this.primaryPath]),
13509
+ lwt: lastDoc._meta.lwt
13510
+ } : checkpoint ?? null;
13202
13511
  return { documents, checkpoint: newCheckpoint };
13203
13512
  }
13513
+ queryWithOurMemory(preparedQuery) {
13514
+ const query = `SELECT json(data) as data FROM "${this.tableName}"`;
13515
+ const rows = this.stmtManager.all({ query, params: [] });
13516
+ let documents = rows.map((row) => JSON.parse(row.data));
13517
+ documents = documents.filter((doc) => this.matchesRegexSelector(doc, preparedQuery.query.selector));
13518
+ if (preparedQuery.query.sort && preparedQuery.query.sort.length > 0) {
13519
+ documents = this.sortDocuments(documents, preparedQuery.query.sort);
13520
+ }
13521
+ if (preparedQuery.query.skip) {
13522
+ documents = documents.slice(preparedQuery.query.skip);
13523
+ }
13524
+ if (preparedQuery.query.limit) {
13525
+ documents = documents.slice(0, preparedQuery.query.limit);
13526
+ }
13527
+ return { documents };
13528
+ }
13529
+ matchesRegexSelector(doc, selector) {
13530
+ for (const [field, value] of Object.entries(selector)) {
13531
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
13532
+ const ops = value;
13533
+ if (ops.$regex) {
13534
+ const fieldValue = this.getNestedValue(doc, field);
13535
+ const pattern = ops.$regex;
13536
+ const options = ops.$options;
13537
+ if (!matchesRegex(fieldValue, pattern, options))
13538
+ return false;
13539
+ }
13540
+ }
13541
+ }
13542
+ return true;
13543
+ }
13204
13544
  }
13205
13545
 
13206
13546
  // src/storage.ts