bun-sqlite-for-rxdb 1.5.3 → 1.5.6

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
@@ -12228,9 +12228,191 @@ function addRxStorageMultiInstanceSupport(storageName, instanceCreationParams, i
12228
12228
  // src/instance.ts
12229
12229
  var import_rxjs2 = __toESM(require_cjs(), 1);
12230
12230
 
12231
- // src/query/regex-matcher.ts
12232
- var REGEX_CACHE = new Map;
12231
+ // src/query/sieve-cache.ts
12232
+ class SieveCache {
12233
+ #capacity;
12234
+ #map;
12235
+ #keys;
12236
+ #values;
12237
+ #visited;
12238
+ #newer;
12239
+ #older;
12240
+ #head = 0;
12241
+ #tail = 0;
12242
+ #hand = 0;
12243
+ #freeHead = 0;
12244
+ #nextFreeIndex = 1;
12245
+ constructor(capacity) {
12246
+ if (capacity < 1)
12247
+ throw new RangeError("Capacity must be at least 1");
12248
+ const arraySize = capacity + 1;
12249
+ this.#capacity = capacity;
12250
+ this.#map = new Map;
12251
+ this.#keys = new Array(arraySize);
12252
+ this.#values = new Array(arraySize);
12253
+ this.#visited = new Uint8Array(arraySize);
12254
+ this.#newer = new Uint32Array(arraySize);
12255
+ this.#older = new Uint32Array(arraySize);
12256
+ }
12257
+ get size() {
12258
+ return this.#map.size;
12259
+ }
12260
+ has(key) {
12261
+ return this.#map.has(key);
12262
+ }
12263
+ get(key) {
12264
+ const index = this.#map.get(key);
12265
+ if (index !== undefined) {
12266
+ this.#visited[index] = 1;
12267
+ return this.#values[index];
12268
+ }
12269
+ return;
12270
+ }
12271
+ set(key, value) {
12272
+ let index = this.#map.get(key);
12273
+ if (index !== undefined) {
12274
+ this.#values[index] = value;
12275
+ this.#visited[index] = 1;
12276
+ return this;
12277
+ }
12278
+ index = this.#getFreeIndex();
12279
+ this.#keys[index] = key;
12280
+ this.#values[index] = value;
12281
+ this.#visited[index] = 0;
12282
+ this.#map.set(key, index);
12283
+ if (this.#head === 0) {
12284
+ this.#head = this.#tail = index;
12285
+ } else {
12286
+ this.#newer[this.#head] = index;
12287
+ this.#older[index] = this.#head;
12288
+ this.#head = index;
12289
+ }
12290
+ return this;
12291
+ }
12292
+ delete(key) {
12293
+ const index = this.#map.get(key);
12294
+ if (index !== undefined) {
12295
+ this.#map.delete(key);
12296
+ this.#removeNode(index);
12297
+ this.#keys[index] = undefined;
12298
+ this.#values[index] = undefined;
12299
+ this.#newer[index] = this.#freeHead;
12300
+ this.#freeHead = index;
12301
+ return true;
12302
+ }
12303
+ return false;
12304
+ }
12305
+ clear() {
12306
+ this.#map.clear();
12307
+ this.#head = 0;
12308
+ this.#tail = 0;
12309
+ this.#hand = 0;
12310
+ this.#freeHead = 0;
12311
+ this.#nextFreeIndex = 1;
12312
+ this.#keys.fill(undefined);
12313
+ this.#values.fill(undefined);
12314
+ this.#newer.fill(0);
12315
+ this.#older.fill(0);
12316
+ this.#visited.fill(0);
12317
+ }
12318
+ #getFreeIndex() {
12319
+ if (this.#freeHead !== 0) {
12320
+ const index = this.#freeHead;
12321
+ this.#freeHead = this.#newer[index];
12322
+ this.#newer[index] = 0;
12323
+ return index;
12324
+ }
12325
+ if (this.#nextFreeIndex <= this.#capacity) {
12326
+ return this.#nextFreeIndex++;
12327
+ }
12328
+ return this.#evict();
12329
+ }
12330
+ #evict() {
12331
+ let hand = this.#hand === 0 ? this.#tail : this.#hand;
12332
+ while (this.#visited[hand] === 1) {
12333
+ this.#visited[hand] = 0;
12334
+ hand = this.#newer[hand] === 0 ? this.#tail : this.#newer[hand];
12335
+ }
12336
+ this.#hand = this.#newer[hand];
12337
+ const victimIndex = hand;
12338
+ this.#map.delete(this.#keys[victimIndex]);
12339
+ this.#removeNode(victimIndex);
12340
+ return victimIndex;
12341
+ }
12342
+ #removeNode(index) {
12343
+ const newer = this.#newer[index];
12344
+ const older = this.#older[index];
12345
+ if (newer !== 0) {
12346
+ this.#older[newer] = older;
12347
+ } else {
12348
+ this.#head = older;
12349
+ }
12350
+ if (older !== 0) {
12351
+ this.#newer[older] = newer;
12352
+ } else {
12353
+ this.#tail = newer;
12354
+ }
12355
+ if (this.#hand === index) {
12356
+ this.#hand = newer;
12357
+ }
12358
+ this.#newer[index] = 0;
12359
+ this.#older[index] = 0;
12360
+ }
12361
+ forEach(callbackfn, thisArg) {
12362
+ this.#map.forEach((index, key) => {
12363
+ callbackfn.call(thisArg, this.#values[index], key, this);
12364
+ });
12365
+ }
12366
+ *entries() {
12367
+ for (const [key, index] of this.#map.entries()) {
12368
+ yield [key, this.#values[index]];
12369
+ }
12370
+ }
12371
+ *keys() {
12372
+ for (const key of this.#map.keys()) {
12373
+ yield key;
12374
+ }
12375
+ }
12376
+ *values() {
12377
+ for (const index of this.#map.values()) {
12378
+ yield this.#values[index];
12379
+ }
12380
+ }
12381
+ [Symbol.iterator]() {
12382
+ return this.entries();
12383
+ }
12384
+ get [Symbol.toStringTag]() {
12385
+ return `SieveCache(${this.#capacity})`;
12386
+ }
12387
+ }
12388
+
12389
+ // src/query/cache.ts
12390
+ var MAX_QUERY_CACHE_SIZE = 5000;
12233
12391
  var MAX_REGEX_CACHE_SIZE = 100;
12392
+ var MAX_INDEX_CACHE_SIZE = 1000;
12393
+ var queryCacheByDatabase = new WeakMap;
12394
+ var GLOBAL_QUERY_CACHE = new SieveCache(MAX_QUERY_CACHE_SIZE);
12395
+ function getQueryCache(database) {
12396
+ let cache = queryCacheByDatabase.get(database);
12397
+ if (!cache) {
12398
+ cache = new SieveCache(MAX_QUERY_CACHE_SIZE);
12399
+ queryCacheByDatabase.set(database, cache);
12400
+ }
12401
+ return cache;
12402
+ }
12403
+ function getGlobalCache() {
12404
+ return GLOBAL_QUERY_CACHE;
12405
+ }
12406
+ var REGEX_CACHE = new SieveCache(MAX_REGEX_CACHE_SIZE);
12407
+ function getRegexCache() {
12408
+ return REGEX_CACHE;
12409
+ }
12410
+ var INDEX_CACHE = new SieveCache(MAX_INDEX_CACHE_SIZE);
12411
+ function getIndexCache() {
12412
+ return INDEX_CACHE;
12413
+ }
12414
+
12415
+ // src/query/regex-matcher.ts
12234
12416
  function isValidRegexOptions(options) {
12235
12417
  for (let i = 0;i < options.length; i++) {
12236
12418
  const c = options[i];
@@ -12240,22 +12422,17 @@ function isValidRegexOptions(options) {
12240
12422
  return true;
12241
12423
  }
12242
12424
  function compileRegex(pattern, options) {
12243
- const cacheKey = `${pattern}::${options || ""}`;
12244
- const cached = REGEX_CACHE.get(cacheKey);
12425
+ const cache = getRegexCache();
12426
+ const cacheKey = `${pattern}:${options || ""}`;
12427
+ const cached = cache.get(cacheKey);
12245
12428
  if (cached) {
12246
12429
  return cached.regex;
12247
12430
  }
12248
12431
  if (options && !isValidRegexOptions(options)) {
12249
- throw new Error(`Invalid regex options: "${options}". Valid options are: i, m, s, x, u`);
12432
+ throw new Error(`Invalid regex options: ${options}`);
12250
12433
  }
12251
12434
  const regex = new RegExp(pattern, options);
12252
- if (REGEX_CACHE.size >= MAX_REGEX_CACHE_SIZE) {
12253
- const firstKey = REGEX_CACHE.keys().next().value;
12254
- if (firstKey) {
12255
- REGEX_CACHE.delete(firstKey);
12256
- }
12257
- }
12258
- REGEX_CACHE.set(cacheKey, { regex });
12435
+ cache.set(cacheKey, { regex });
12259
12436
  return regex;
12260
12437
  }
12261
12438
  function matchesRegex(value, pattern, options) {
@@ -12286,32 +12463,33 @@ function getColumnInfo(path2, schema) {
12286
12463
  const properties = schema.properties;
12287
12464
  const fieldSchema = properties?.[path2];
12288
12465
  if (fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema) {
12289
- if (fieldSchema.type === "array") {
12466
+ const schemaType = fieldSchema.type;
12467
+ if (schemaType === "array") {
12290
12468
  return { jsonPath: `$.${path2}`, type: "array" };
12291
12469
  }
12470
+ if (schemaType === "string") {
12471
+ return { jsonPath: `$.${path2}`, type: "string" };
12472
+ }
12473
+ if (schemaType === "number") {
12474
+ return { jsonPath: `$.${path2}`, type: "number" };
12475
+ }
12476
+ if (schemaType === "boolean") {
12477
+ return { jsonPath: `$.${path2}`, type: "boolean" };
12478
+ }
12292
12479
  }
12293
12480
  return { jsonPath: `$.${path2}`, type: "unknown" };
12294
12481
  }
12295
12482
 
12296
12483
  // src/query/smart-regex.ts
12297
- var INDEX_CACHE = new Map;
12298
- var MAX_INDEX_CACHE_SIZE = 1000;
12299
12484
  function hasExpressionIndex(fieldName, schema) {
12300
- const indexKey = schema.indexes ? JSON.stringify(schema.indexes) : "none";
12301
- const cacheKey = `${schema.version}_${fieldName}_${indexKey}`;
12302
- const cached = INDEX_CACHE.get(cacheKey);
12485
+ const cache = getIndexCache();
12486
+ const cacheKey = `${fieldName}:${JSON.stringify(schema.indexes || [])}`;
12487
+ const cached = cache.get(cacheKey);
12303
12488
  if (cached !== undefined) {
12304
- INDEX_CACHE.delete(cacheKey);
12305
- INDEX_CACHE.set(cacheKey, cached);
12306
12489
  return cached;
12307
12490
  }
12308
12491
  if (!schema.indexes) {
12309
- if (INDEX_CACHE.size >= MAX_INDEX_CACHE_SIZE) {
12310
- const firstKey = INDEX_CACHE.keys().next().value;
12311
- if (firstKey)
12312
- INDEX_CACHE.delete(firstKey);
12313
- }
12314
- INDEX_CACHE.set(cacheKey, false);
12492
+ cache.set(cacheKey, false);
12315
12493
  return false;
12316
12494
  }
12317
12495
  const hasLowerIndex = schema.indexes.some((idx) => {
@@ -12323,12 +12501,7 @@ function hasExpressionIndex(fieldName, schema) {
12323
12501
  return normalized === `lower(${fieldName})`;
12324
12502
  });
12325
12503
  });
12326
- if (INDEX_CACHE.size >= MAX_INDEX_CACHE_SIZE) {
12327
- const firstKey = INDEX_CACHE.keys().next().value;
12328
- if (firstKey)
12329
- INDEX_CACHE.delete(firstKey);
12330
- }
12331
- INDEX_CACHE.set(cacheKey, hasLowerIndex);
12504
+ cache.set(cacheKey, hasLowerIndex);
12332
12505
  return hasLowerIndex;
12333
12506
  }
12334
12507
  function isValidRegexOptions2(options) {
@@ -12363,32 +12536,41 @@ function smartRegexToLike(field, pattern, options, schema, fieldName) {
12363
12536
  const escaped = escapeForLike(unescaped);
12364
12537
  if (startsWithAnchor && endsWithAnchor) {
12365
12538
  if (caseInsensitive) {
12366
- return hasLowerIndex ? { sql: `LOWER(${field}) = ?`, args: [unescaped.toLowerCase()] } : { sql: `${field} = ? COLLATE NOCASE`, args: [unescaped] };
12539
+ return { sql: `LOWER(${field}) = LOWER(?)`, args: [unescaped] };
12367
12540
  }
12368
12541
  return { sql: `${field} = ?`, args: [unescaped] };
12369
12542
  }
12370
12543
  if (startsWithAnchor) {
12371
- const suffix = caseInsensitive ? escaped.toLowerCase() : escaped;
12372
- if (caseInsensitive && hasLowerIndex) {
12373
- return { sql: `LOWER(${field}) LIKE ? ESCAPE '\\'`, args: [suffix + "%"] };
12544
+ if (caseInsensitive) {
12545
+ return { sql: `LOWER(${field}) LIKE LOWER(?) ESCAPE '\\'`, args: [escaped + "%"] };
12374
12546
  }
12375
- return { sql: `${field} LIKE ?${caseInsensitive ? " COLLATE NOCASE" : ""} ESCAPE '\\'`, args: [suffix + "%"] };
12547
+ return { sql: `${field} LIKE ? ESCAPE '\\'`, args: [escaped + "%"] };
12376
12548
  }
12377
12549
  if (endsWithAnchor) {
12378
- const prefix = caseInsensitive ? escaped.toLowerCase() : escaped;
12379
- if (caseInsensitive && hasLowerIndex) {
12380
- return { sql: `LOWER(${field}) LIKE ? ESCAPE '\\'`, args: ["%" + prefix] };
12550
+ if (caseInsensitive) {
12551
+ return { sql: `LOWER(${field}) LIKE LOWER(?) ESCAPE '\\'`, args: ["%" + escaped] };
12381
12552
  }
12382
- return { sql: `${field} LIKE ?${caseInsensitive ? " COLLATE NOCASE" : ""} ESCAPE '\\'`, args: ["%" + prefix] };
12553
+ return { sql: `${field} LIKE ? ESCAPE '\\'`, args: ["%" + escaped] };
12383
12554
  }
12384
- const middle = caseInsensitive ? escaped.toLowerCase() : escaped;
12385
- if (caseInsensitive && hasLowerIndex) {
12386
- return { sql: `LOWER(${field}) LIKE ? ESCAPE '\\'`, args: ["%" + middle + "%"] };
12555
+ if (caseInsensitive) {
12556
+ return { sql: `LOWER(${field}) LIKE LOWER(?) ESCAPE '\\'`, args: ["%" + escaped + "%"] };
12387
12557
  }
12388
- return { sql: `${field} LIKE ?${caseInsensitive ? " COLLATE NOCASE" : ""} ESCAPE '\\'`, args: ["%" + middle + "%"] };
12558
+ return { sql: `${field} LIKE ? ESCAPE '\\'`, args: ["%" + escaped + "%"] };
12389
12559
  }
12390
12560
 
12391
12561
  // src/query/operators.ts
12562
+ function buildJsonPath(fieldName) {
12563
+ const segments = fieldName.split(".");
12564
+ let path2 = "$";
12565
+ for (const segment of segments) {
12566
+ if (/^\d+$/.test(segment)) {
12567
+ path2 += `[${segment}]`;
12568
+ } else {
12569
+ path2 += `.${segment}`;
12570
+ }
12571
+ }
12572
+ return path2;
12573
+ }
12392
12574
  function normalizeValueForSQLite(value) {
12393
12575
  if (value instanceof Date) {
12394
12576
  return value.toISOString();
@@ -12552,6 +12734,18 @@ function translateExists(field, exists) {
12552
12734
  };
12553
12735
  }
12554
12736
  function translateRegex(field, pattern, options, schema, fieldName) {
12737
+ const columnInfo = getColumnInfo(fieldName, schema);
12738
+ const isArray = field !== "value" && columnInfo.type === "array";
12739
+ if (isArray) {
12740
+ const smartResult2 = smartRegexToLike("value", pattern, options, schema, fieldName);
12741
+ if (smartResult2) {
12742
+ return {
12743
+ sql: `EXISTS (SELECT 1 FROM jsonb_each(${field}) WHERE ${smartResult2.sql})`,
12744
+ args: smartResult2.args
12745
+ };
12746
+ }
12747
+ return null;
12748
+ }
12555
12749
  const smartResult = smartRegexToLike(field, pattern, options, schema, fieldName);
12556
12750
  if (smartResult)
12557
12751
  return smartResult;
@@ -12594,7 +12788,7 @@ function handleLogicalOperator(operator, value, schema, baseFieldName) {
12594
12788
  };
12595
12789
  }
12596
12790
  function handleFieldCondition(fieldName, value, schema, baseFieldName) {
12597
- const propertyField = `json_extract(value, '$.${fieldName}')`;
12791
+ const propertyField = `json_extract(value, '${buildJsonPath(fieldName)}')`;
12598
12792
  const nestedFieldName = `${baseFieldName}.${fieldName}`;
12599
12793
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
12600
12794
  if (value instanceof RegExp) {
@@ -12668,17 +12862,14 @@ function translateElemMatch(field, criteria, schema, actualFieldName) {
12668
12862
  if (typeof criteria === "object" && criteria !== null && !Array.isArray(criteria) && Object.keys(criteria).length === 0) {
12669
12863
  return { sql: "1=0", args: [] };
12670
12864
  }
12671
- if (typeof criteria !== "object" || criteria === null) {
12672
- return {
12673
- sql: `EXISTS (SELECT 1 FROM jsonb_each(${field}) WHERE value = ?)`,
12674
- args: [criteria]
12675
- };
12865
+ if (typeof criteria !== "object" || criteria === null || Array.isArray(criteria)) {
12866
+ return null;
12676
12867
  }
12677
12868
  if (criteria.$and && Array.isArray(criteria.$and)) {
12678
12869
  const fragments = criteria.$and.map((cond) => buildElemMatchConditions(cond, schema, actualFieldName));
12679
12870
  if (fragments.some((f) => f === null))
12680
12871
  return null;
12681
- const sql = fragments.map((f) => f.sql).join(" AND ");
12872
+ const sql = fragments.map((f) => `COALESCE((${f.sql}), 0)`).join(" AND ");
12682
12873
  const args = fragments.flatMap((f) => f.args);
12683
12874
  return {
12684
12875
  sql: `EXISTS (SELECT 1 FROM jsonb_each(${field}) WHERE ${sql})`,
@@ -12689,7 +12880,7 @@ function translateElemMatch(field, criteria, schema, actualFieldName) {
12689
12880
  const fragments = criteria.$or.map((cond) => buildElemMatchConditions(cond, schema, actualFieldName));
12690
12881
  if (fragments.some((f) => f === null))
12691
12882
  return null;
12692
- const sql = fragments.map((f) => f.sql).join(" OR ");
12883
+ const sql = fragments.map((f) => `COALESCE((${f.sql}), 0)`).join(" OR ");
12693
12884
  const args = fragments.flatMap((f) => f.args);
12694
12885
  return {
12695
12886
  sql: `EXISTS (SELECT 1 FROM jsonb_each(${field}) WHERE ${sql})`,
@@ -12700,7 +12891,7 @@ function translateElemMatch(field, criteria, schema, actualFieldName) {
12700
12891
  const fragments = criteria.$nor.map((cond) => buildElemMatchConditions(cond, schema, actualFieldName));
12701
12892
  if (fragments.some((f) => f === null))
12702
12893
  return null;
12703
- const sql = fragments.map((f) => f.sql).join(" OR ");
12894
+ const sql = fragments.map((f) => `COALESCE((${f.sql}), 0)`).join(" OR ");
12704
12895
  const args = fragments.flatMap((f) => f.args);
12705
12896
  return {
12706
12897
  sql: `EXISTS (SELECT 1 FROM jsonb_each(${field}) WHERE NOT (${sql}))`,
@@ -12711,7 +12902,7 @@ function translateElemMatch(field, criteria, schema, actualFieldName) {
12711
12902
  if (!fragment)
12712
12903
  return null;
12713
12904
  return {
12714
- sql: `EXISTS (SELECT 1 FROM jsonb_each(${field}) WHERE ${fragment.sql})`,
12905
+ sql: `EXISTS (SELECT 1 FROM jsonb_each(${field}) WHERE ${asBoolean(fragment.sql)})`,
12715
12906
  args: fragment.args
12716
12907
  };
12717
12908
  }
@@ -12735,8 +12926,21 @@ function translateLeafOperator(op, field, value, schema, actualFieldName) {
12735
12926
  return translateNin(field, value, schema, actualFieldName);
12736
12927
  case "$exists":
12737
12928
  return translateExists(field, value);
12738
- case "$size":
12739
- return translateSize(field, value);
12929
+ case "$size": {
12930
+ const columnInfo = getColumnInfo(actualFieldName, schema);
12931
+ if (columnInfo.type !== "array" && columnInfo.type !== "unknown") {
12932
+ return { sql: "1=0", args: [] };
12933
+ }
12934
+ let jsonColumn = "data";
12935
+ let jsonPath = actualFieldName;
12936
+ let isDirectPath = false;
12937
+ if (field === "value") {
12938
+ jsonColumn = "value";
12939
+ jsonPath = "";
12940
+ isDirectPath = true;
12941
+ }
12942
+ return translateSize(jsonColumn, jsonPath, value, isDirectPath);
12943
+ }
12740
12944
  case "$mod": {
12741
12945
  const result = translateMod(field, value);
12742
12946
  if (!result)
@@ -12802,9 +13006,12 @@ function translateLeafOperator(op, field, value, schema, actualFieldName) {
12802
13006
  return translateEq(field, value, schema, actualFieldName);
12803
13007
  }
12804
13008
  }
13009
+ function asBoolean(sql) {
13010
+ return `COALESCE((${sql}), 0)`;
13011
+ }
12805
13012
  function wrapWithNot(innerFragment) {
12806
13013
  return {
12807
- sql: `NOT (${innerFragment.sql})`,
13014
+ sql: `NOT (${asBoolean(innerFragment.sql)})`,
12808
13015
  args: innerFragment.args
12809
13016
  };
12810
13017
  }
@@ -12814,22 +13021,28 @@ function translateType(jsonColumn, fieldName, type6, isDirectPath = false) {
12814
13021
  case "null":
12815
13022
  return { sql: `json_type(${jsonColumn}, '${jsonPath}') = 'null'`, args: [] };
12816
13023
  case "boolean":
13024
+ case "bool":
12817
13025
  return { sql: `json_type(${jsonColumn}, '${jsonPath}') IN ('true', 'false')`, args: [] };
12818
13026
  case "number":
13027
+ case "int":
13028
+ case "long":
13029
+ case "double":
13030
+ case "decimal":
12819
13031
  return { sql: `json_type(${jsonColumn}, '${jsonPath}') IN ('integer', 'real')`, args: [] };
12820
13032
  case "string":
12821
- return { sql: `json_type(${jsonColumn}, '${jsonPath}') = 'text'`, args: [] };
13033
+ return { sql: `COALESCE(json_type(${jsonColumn}, '${jsonPath}') = 'text', 0)`, args: [] };
12822
13034
  case "array":
12823
13035
  return { sql: `json_type(${jsonColumn}, '${jsonPath}') = 'array'`, args: [] };
12824
13036
  case "object":
12825
13037
  return { sql: `json_type(${jsonColumn}, '${jsonPath}') = 'object'`, args: [] };
12826
13038
  default:
12827
- return { sql: "1=0", args: [] };
13039
+ return null;
12828
13040
  }
12829
13041
  }
12830
- function translateSize(field, size) {
13042
+ function translateSize(jsonColumn, jsonPath, size, isDirectPath = false) {
13043
+ const path2 = isDirectPath ? jsonPath : `$.${jsonPath}`;
12831
13044
  return {
12832
- sql: `json_array_length(${field}) = ?`,
13045
+ sql: `json_array_length(${jsonColumn}, '${path2}') = ?`,
12833
13046
  args: [size]
12834
13047
  };
12835
13048
  }
@@ -12949,29 +13162,50 @@ function _stringify(value, stack) {
12949
13162
  }
12950
13163
 
12951
13164
  // src/query/builder.ts
12952
- var MAX_CACHE_SIZE = 1000;
12953
- var GLOBAL_CACHE = new Map;
12954
- function buildWhereClause(selector, schema, collectionName, cache = GLOBAL_CACHE) {
13165
+ function buildWhereClause(selector, schema, collectionName, cache) {
12955
13166
  if (!selector || typeof selector !== "object")
12956
13167
  return null;
12957
- const cacheKey = `v${schema.version}_${collectionName}_${stableStringify(selector)}`;
12958
- const cached = cache.get(cacheKey);
13168
+ const actualCache = cache ?? getGlobalCache();
13169
+ const cacheKey = `v3_${schema.version}_${stableStringify(selector)}`;
13170
+ const cached = actualCache.get(cacheKey);
12959
13171
  if (cached !== undefined) {
12960
- cache.delete(cacheKey);
12961
- cache.set(cacheKey, cached);
12962
13172
  return cached;
12963
13173
  }
12964
13174
  const result = processSelector(selector, schema, 0);
12965
- if (!result)
12966
- return null;
12967
- if (cache.size >= MAX_CACHE_SIZE) {
12968
- const firstKey = cache.keys().next().value;
12969
- if (firstKey)
12970
- cache.delete(firstKey);
12971
- }
12972
- cache.set(cacheKey, result);
13175
+ actualCache.set(cacheKey, result);
12973
13176
  return result;
12974
13177
  }
13178
+ function buildWhereClauseWithFallback(selector, schema, collectionName, cache) {
13179
+ if (!selector || typeof selector !== "object") {
13180
+ return { sqlWhere: null, jsSelector: null };
13181
+ }
13182
+ const sqlResult = buildWhereClause(selector, schema, collectionName, cache);
13183
+ if (sqlResult) {
13184
+ return { sqlWhere: sqlResult, jsSelector: null };
13185
+ }
13186
+ const splitResult = splitSelector(selector, schema);
13187
+ return splitResult;
13188
+ }
13189
+ function splitSelector(selector, schema) {
13190
+ const sqlConditions = [];
13191
+ const jsConditions = [];
13192
+ const entries = Object.entries(selector);
13193
+ for (const [field, value] of entries) {
13194
+ const testSelector = { [field]: value };
13195
+ const sqlFragment = processSelector(testSelector, schema, 0);
13196
+ if (sqlFragment) {
13197
+ sqlConditions.push(testSelector);
13198
+ } else {
13199
+ jsConditions.push(testSelector);
13200
+ }
13201
+ }
13202
+ const sqlWhere = sqlConditions.length > 0 ? processSelector({ $and: sqlConditions }, schema, 0) : null;
13203
+ const jsSelector = jsConditions.length > 0 ? jsConditions.length === 1 ? jsConditions[0] : { $and: jsConditions } : null;
13204
+ return { sqlWhere, jsSelector };
13205
+ }
13206
+ function coerceToBoolean(sql) {
13207
+ return `COALESCE((${sql}), 0)`;
13208
+ }
12975
13209
  function buildLogicalOperator(operator, conditions, schema, logicalDepth) {
12976
13210
  if (conditions.length === 0) {
12977
13211
  return { sql: operator === "or" ? "1=0" : "1=1", args: [] };
@@ -12979,9 +13213,12 @@ function buildLogicalOperator(operator, conditions, schema, logicalDepth) {
12979
13213
  const fragments = conditions.map((subSelector) => processSelector(subSelector, schema, logicalDepth + 1));
12980
13214
  if (fragments.some((f) => f === null))
12981
13215
  return null;
12982
- const sql = fragments.map((f) => `(${f.sql})`).join(" OR ");
13216
+ const sql = fragments.map((f) => f.sql).join(operator === "and" ? " AND " : " OR ");
12983
13217
  const args = fragments.flatMap((f) => f.args);
12984
- return operator === "nor" ? { sql: `NOT(${sql})`, args } : { sql, args };
13218
+ if (operator === "nor") {
13219
+ return { sql: `NOT(${coerceToBoolean(sql)})`, args };
13220
+ }
13221
+ return { sql, args };
12985
13222
  }
12986
13223
  function processSelector(selector, schema, logicalDepth) {
12987
13224
  if (!selector || typeof selector !== "object")
@@ -13016,8 +13253,16 @@ function processSelector(selector, schema, logicalDepth) {
13016
13253
  args.push(...norFragment.args);
13017
13254
  continue;
13018
13255
  }
13256
+ if (field === "$not" && typeof value === "object" && value !== null && !Array.isArray(value)) {
13257
+ const innerFragment = processSelector(value, schema, logicalDepth + 1);
13258
+ if (!innerFragment)
13259
+ return null;
13260
+ conditions.push(`NOT (${innerFragment.sql})`);
13261
+ args.push(...innerFragment.args);
13262
+ continue;
13263
+ }
13019
13264
  const columnInfo = getColumnInfo(field, schema);
13020
- const fieldName = columnInfo.column || `json_extract(data, '${columnInfo.jsonPath}')`;
13265
+ const fieldName = columnInfo.column || `json_extract(data, '${buildJsonPath(field)}')`;
13021
13266
  const actualFieldName = columnInfo.jsonPath?.replace(/^\$\./, "") || columnInfo.column || field;
13022
13267
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
13023
13268
  if (Object.keys(value).length === 0) {
@@ -13028,15 +13273,15 @@ function processSelector(selector, schema, logicalDepth) {
13028
13273
  let fragment;
13029
13274
  if (op === "$not") {
13030
13275
  if (typeof opValue !== "object" || opValue === null || Array.isArray(opValue)) {
13031
- const eqFrag = translateLeafOperator("$eq", fieldName, opValue, schema, actualFieldName);
13032
- if (!eqFrag)
13276
+ const neFrag = translateLeafOperator("$ne", fieldName, opValue, schema, actualFieldName);
13277
+ if (!neFrag)
13033
13278
  return null;
13034
- fragment = wrapWithNot(eqFrag);
13279
+ fragment = neFrag;
13035
13280
  } else if (opValue instanceof Date) {
13036
- const eqFrag = translateLeafOperator("$eq", fieldName, opValue, schema, actualFieldName);
13037
- if (!eqFrag)
13281
+ const neFrag = translateLeafOperator("$ne", fieldName, opValue, schema, actualFieldName);
13282
+ if (!neFrag)
13038
13283
  return null;
13039
- fragment = wrapWithNot(eqFrag);
13284
+ fragment = neFrag;
13040
13285
  } else if (opValue instanceof RegExp) {
13041
13286
  const regexFrag = translateLeafOperator("$regex", fieldName, opValue, schema, actualFieldName);
13042
13287
  if (!regexFrag)
@@ -13077,16 +13322,23 @@ function processSelector(selector, schema, logicalDepth) {
13077
13322
  } else {
13078
13323
  const hasOperators = innerKeys.some((k) => k.startsWith("$"));
13079
13324
  if (!hasOperators) {
13080
- const eqFrag = translateLeafOperator("$eq", fieldName, opValueObj, schema, actualFieldName);
13081
- if (!eqFrag)
13325
+ const neFrag = translateLeafOperator("$ne", fieldName, opValueObj, schema, actualFieldName);
13326
+ if (!neFrag)
13082
13327
  return null;
13083
- fragment = wrapWithNot(eqFrag);
13328
+ fragment = neFrag;
13084
13329
  } else {
13085
13330
  const [[innerOp, innerVal]] = Object.entries(opValueObj);
13086
- const innerFrag = translateLeafOperator(innerOp, fieldName, innerVal, schema, actualFieldName);
13087
- if (!innerFrag)
13088
- return null;
13089
- fragment = wrapWithNot(innerFrag);
13331
+ if (innerOp === "$eq") {
13332
+ const neFrag = translateLeafOperator("$ne", fieldName, innerVal, schema, actualFieldName);
13333
+ if (!neFrag)
13334
+ return null;
13335
+ fragment = neFrag;
13336
+ } else {
13337
+ const innerFrag = translateLeafOperator(innerOp, fieldName, innerVal, schema, actualFieldName);
13338
+ if (!innerFrag)
13339
+ return null;
13340
+ fragment = wrapWithNot(innerFrag);
13341
+ }
13090
13342
  }
13091
13343
  }
13092
13344
  }
@@ -13111,7 +13363,7 @@ function processSelector(selector, schema, logicalDepth) {
13111
13363
  } else if (op === "$options") {
13112
13364
  continue;
13113
13365
  } else if (!op.startsWith("$")) {
13114
- const jsonPath = `json_extract(${fieldName}, '$.${op}')`;
13366
+ const jsonPath = `json_extract(${fieldName}, '${buildJsonPath(op)}')`;
13115
13367
  const nestedFieldName = `${actualFieldName}.${op}`;
13116
13368
  const leafFrag = translateLeafOperator("$eq", jsonPath, opValue, schema, nestedFieldName);
13117
13369
  if (!leafFrag)
@@ -13529,9 +13781,6 @@ class StatementManager {
13529
13781
  this.closed = true;
13530
13782
  }
13531
13783
  isStaticSQL(query) {
13532
- if (query.includes("WHERE (")) {
13533
- return false;
13534
- }
13535
13784
  return true;
13536
13785
  }
13537
13786
  }
@@ -13594,7 +13843,7 @@ class BunSQLiteStorageInstance {
13594
13843
  db;
13595
13844
  stmtManager;
13596
13845
  changeStream$ = new import_rxjs2.Subject;
13597
- queryCache = new Map;
13846
+ queryCache;
13598
13847
  databaseName;
13599
13848
  collectionName;
13600
13849
  schema;
@@ -13602,6 +13851,7 @@ class BunSQLiteStorageInstance {
13602
13851
  options;
13603
13852
  primaryPath;
13604
13853
  tableName;
13854
+ useStoredColumns;
13605
13855
  closed;
13606
13856
  constructor(params, settings = {}) {
13607
13857
  ensureRxStorageInstanceParamsAreCorrect(params);
@@ -13612,9 +13862,11 @@ class BunSQLiteStorageInstance {
13612
13862
  const primaryKey = params.schema.primaryKey;
13613
13863
  this.primaryPath = typeof primaryKey === "string" ? primaryKey : primaryKey.key;
13614
13864
  this.tableName = `${params.collectionName}_v${params.schema.version}`;
13865
+ this.useStoredColumns = this.options?.useStoredColumns ?? "virtual";
13615
13866
  const filename = settings.filename || ":memory:";
13616
13867
  this.db = getDatabase(this.databaseName, filename);
13617
13868
  this.stmtManager = new StatementManager(this.db);
13869
+ this.queryCache = getQueryCache(this.db);
13618
13870
  this.internals = {
13619
13871
  db: this.db,
13620
13872
  primaryPath: this.primaryPath
@@ -13622,6 +13874,7 @@ class BunSQLiteStorageInstance {
13622
13874
  this.initTable(filename);
13623
13875
  }
13624
13876
  initTable(filename) {
13877
+ this.db.run("PRAGMA case_sensitive_like = ON");
13625
13878
  if (filename !== ":memory:") {
13626
13879
  this.db.run("PRAGMA journal_mode = WAL");
13627
13880
  this.db.run("PRAGMA synchronous = NORMAL");
@@ -13635,15 +13888,37 @@ class BunSQLiteStorageInstance {
13635
13888
  this.db.run("PRAGMA temp_store = MEMORY");
13636
13889
  this.db.run("PRAGMA locking_mode = NORMAL");
13637
13890
  }
13638
- this.db.run(`
13639
- CREATE TABLE IF NOT EXISTS "${this.tableName}" (
13640
- id TEXT PRIMARY KEY NOT NULL,
13641
- data BLOB NOT NULL,
13642
- deleted INTEGER NOT NULL DEFAULT 0,
13643
- rev TEXT NOT NULL,
13644
- mtime_ms REAL NOT NULL
13645
- )
13646
- `);
13891
+ if (this.useStoredColumns === "virtual") {
13892
+ this.db.run(`
13893
+ CREATE TABLE IF NOT EXISTS "${this.tableName}" (
13894
+ id TEXT PRIMARY KEY NOT NULL,
13895
+ data BLOB NOT NULL,
13896
+ deleted INTEGER GENERATED ALWAYS AS (json_extract(data, '$._deleted')) VIRTUAL,
13897
+ rev TEXT GENERATED ALWAYS AS (json_extract(data, '$._rev')) VIRTUAL,
13898
+ mtime_ms REAL GENERATED ALWAYS AS (json_extract(data, '$._meta.lwt')) VIRTUAL
13899
+ )
13900
+ `);
13901
+ } else if (this.useStoredColumns === "stored") {
13902
+ this.db.run(`
13903
+ CREATE TABLE IF NOT EXISTS "${this.tableName}" (
13904
+ id TEXT PRIMARY KEY NOT NULL,
13905
+ data BLOB NOT NULL,
13906
+ deleted INTEGER GENERATED ALWAYS AS (json_extract(data, '$._deleted')) STORED,
13907
+ rev TEXT GENERATED ALWAYS AS (json_extract(data, '$._rev')) STORED,
13908
+ mtime_ms REAL GENERATED ALWAYS AS (json_extract(data, '$._meta.lwt')) STORED
13909
+ )
13910
+ `);
13911
+ } else {
13912
+ this.db.run(`
13913
+ CREATE TABLE IF NOT EXISTS "${this.tableName}" (
13914
+ id TEXT PRIMARY KEY NOT NULL,
13915
+ data BLOB NOT NULL,
13916
+ deleted INTEGER NOT NULL DEFAULT 0,
13917
+ rev TEXT NOT NULL,
13918
+ mtime_ms REAL NOT NULL
13919
+ )
13920
+ `);
13921
+ }
13647
13922
  this.db.run(`CREATE INDEX IF NOT EXISTS "idx_${this.tableName}_deleted_id" ON "${this.tableName}"(deleted, id)`);
13648
13923
  this.db.run(`CREATE INDEX IF NOT EXISTS "idx_${this.tableName}_mtime_ms_id" ON "${this.tableName}"(mtime_ms, id)`);
13649
13924
  if (this.schema.indexes) {
@@ -13677,49 +13952,70 @@ class BunSQLiteStorageInstance {
13677
13952
  return { error: [] };
13678
13953
  }
13679
13954
  const ids = documentWrites.map((w) => w.document[this.primaryPath]);
13680
- const docsInDb = await this.findDocumentsById(ids, true);
13955
+ const LOOKUP_BATCH_SIZE = 500;
13956
+ const docsInDb = [];
13957
+ for (let i = 0;i < ids.length; i += LOOKUP_BATCH_SIZE) {
13958
+ const batch = ids.slice(i, i + LOOKUP_BATCH_SIZE);
13959
+ const batchDocs = await this.findDocumentsById(batch, true);
13960
+ docsInDb.push(...batchDocs);
13961
+ }
13681
13962
  const docsInDbMap = new Map(docsInDb.map((d) => [d[this.primaryPath], d]));
13682
13963
  const categorized = categorizeBulkWriteRows(this, this.primaryPath, docsInDbMap, documentWrites, context);
13683
- const updateQuery = `UPDATE "${this.tableName}" SET data = jsonb(?), deleted = ?, rev = ?, mtime_ms = ? WHERE id = ?`;
13684
- const BATCH_SIZE = 100;
13685
- for (let i = 0;i < categorized.bulkInsertDocs.length; i += BATCH_SIZE) {
13686
- const batch = categorized.bulkInsertDocs.slice(i, i + BATCH_SIZE);
13687
- const placeholders = batch.map(() => "(?, jsonb(?), ?, ?, ?)").join(", ");
13688
- const insertQuery = `INSERT INTO "${this.tableName}" (id, data, deleted, rev, mtime_ms) VALUES ${placeholders}`;
13689
- const params = [];
13690
- for (const row of batch) {
13691
- const doc = row.document;
13692
- const id = doc[this.primaryPath];
13693
- params.push(id, JSON.stringify(doc), doc._deleted ? 1 : 0, doc._rev, doc._meta.lwt);
13694
- }
13695
- try {
13696
- this.stmtManager.run({ query: insertQuery, params });
13697
- } catch (err) {
13698
- if (err && typeof err === "object" && "code" in err && (err.code === "SQLITE_CONSTRAINT_PRIMARYKEY" || err.code === "SQLITE_CONSTRAINT_UNIQUE")) {
13699
- for (const row of batch) {
13700
- const doc = row.document;
13701
- const id = doc[this.primaryPath];
13702
- const documentInDb = docsInDbMap.get(id);
13703
- categorized.errors.push({
13704
- isError: true,
13705
- status: 409,
13706
- documentId: id,
13707
- writeRow: row,
13708
- documentInDb: documentInDb || doc
13709
- });
13964
+ const CHUNK_SIZE = 50;
13965
+ const updateStmt = this.useStoredColumns ? this.db.prepare(`UPDATE "${this.tableName}" SET data = jsonb(?) WHERE id = ?`) : this.db.prepare(`UPDATE "${this.tableName}" SET data = jsonb(?), deleted = ?, rev = ?, mtime_ms = ? WHERE id = ?`);
13966
+ const insertBatch = this.db.transaction((docs) => {
13967
+ for (let i = 0;i < docs.length; i += CHUNK_SIZE) {
13968
+ const chunk = docs.slice(i, i + CHUNK_SIZE);
13969
+ const placeholders = this.useStoredColumns ? chunk.map(() => "(?, jsonb(?))").join(", ") : chunk.map(() => "(?, jsonb(?), ?, ?, ?)").join(", ");
13970
+ const insertQuery = this.useStoredColumns ? `INSERT INTO "${this.tableName}" (id, data) VALUES ${placeholders}` : `INSERT INTO "${this.tableName}" (id, data, deleted, rev, mtime_ms) VALUES ${placeholders}`;
13971
+ const params = [];
13972
+ for (const row of chunk) {
13973
+ const doc = row.document;
13974
+ const id = doc[this.primaryPath];
13975
+ if (this.useStoredColumns) {
13976
+ params.push(id, JSON.stringify(doc));
13977
+ } else {
13978
+ params.push(id, JSON.stringify(doc), doc._deleted ? 1 : 0, doc._rev, doc._meta.lwt);
13979
+ }
13980
+ }
13981
+ try {
13982
+ this.stmtManager.run({ query: insertQuery, params });
13983
+ } catch (err) {
13984
+ if (err && typeof err === "object" && "code" in err && (err.code === "SQLITE_CONSTRAINT_PRIMARYKEY" || err.code === "SQLITE_CONSTRAINT_UNIQUE")) {
13985
+ for (const row of chunk) {
13986
+ const doc = row.document;
13987
+ const id = doc[this.primaryPath];
13988
+ const documentInDb = docsInDbMap.get(id);
13989
+ categorized.errors.push({
13990
+ isError: true,
13991
+ status: 409,
13992
+ documentId: id,
13993
+ writeRow: row,
13994
+ documentInDb: documentInDb || doc
13995
+ });
13996
+ }
13997
+ } else {
13998
+ throw err;
13710
13999
  }
13711
- } else {
13712
- throw err;
13713
14000
  }
13714
14001
  }
13715
- }
13716
- for (let i = 0;i < categorized.bulkUpdateDocs.length; i += BATCH_SIZE) {
13717
- const batch = categorized.bulkUpdateDocs.slice(i, i + BATCH_SIZE);
13718
- for (const row of batch) {
14002
+ });
14003
+ const updateBatch = this.db.transaction((docs) => {
14004
+ for (const row of docs) {
13719
14005
  const doc = row.document;
13720
14006
  const id = doc[this.primaryPath];
13721
- this.stmtManager.run({ query: updateQuery, params: [JSON.stringify(doc), doc._deleted ? 1 : 0, doc._rev, doc._meta.lwt, id] });
14007
+ if (this.useStoredColumns) {
14008
+ updateStmt.run(JSON.stringify(doc), id);
14009
+ } else {
14010
+ updateStmt.run(JSON.stringify(doc), doc._deleted ? 1 : 0, doc._rev, doc._meta.lwt, id);
14011
+ }
13722
14012
  }
14013
+ });
14014
+ if (categorized.bulkInsertDocs.length > 0) {
14015
+ insertBatch(categorized.bulkInsertDocs);
14016
+ }
14017
+ if (categorized.bulkUpdateDocs.length > 0) {
14018
+ updateBatch(categorized.bulkUpdateDocs);
13723
14019
  }
13724
14020
  const insertAttQuery = `INSERT OR REPLACE INTO "${this.tableName}_attachments" (id, data, digest) VALUES (?, ?, ?)`;
13725
14021
  const deleteAttQuery = `DELETE FROM "${this.tableName}_attachments" WHERE id = ?`;
@@ -13762,31 +14058,37 @@ class BunSQLiteStorageInstance {
13762
14058
  return rows.map((row) => JSON.parse(row.data));
13763
14059
  }
13764
14060
  async query(preparedQuery) {
13765
- const whereResult = buildWhereClause(preparedQuery.query.selector, this.schema, this.collectionName, this.queryCache);
13766
- if (!whereResult) {
13767
- return this.queryWithOurMemory(preparedQuery);
14061
+ const { sqlWhere, jsSelector } = buildWhereClauseWithFallback(preparedQuery.query.selector, this.schema, this.collectionName, this.queryCache);
14062
+ let sql = `SELECT json(data) as data FROM "${this.tableName}"`;
14063
+ const queryArgs = [];
14064
+ if (sqlWhere) {
14065
+ sql += ` WHERE (${sqlWhere.sql})`;
14066
+ queryArgs.push(...sqlWhere.args);
13768
14067
  }
13769
- const { sql: whereClause, args } = whereResult;
13770
- let sql = `SELECT json(data) as data FROM "${this.tableName}" WHERE (${whereClause})`;
13771
- const queryArgs = [...args];
13772
14068
  if (preparedQuery.query.sort && preparedQuery.query.sort.length > 0) {
13773
14069
  const orderBy = preparedQuery.query.sort.map((sortField) => {
13774
14070
  const [field, direction] = Object.entries(sortField)[0];
13775
14071
  const dir = direction === "asc" ? "ASC" : "DESC";
13776
- return `json_extract(data, '$.${field}') ${dir}`;
14072
+ const colInfo = getColumnInfo(field, this.schema);
14073
+ const colName = colInfo.column || `json_extract(data, '${colInfo.jsonPath}')`;
14074
+ return `${colName} ${dir}`;
13777
14075
  }).join(", ");
13778
14076
  sql += ` ORDER BY ${orderBy}`;
13779
14077
  }
13780
- if (preparedQuery.query.limit) {
13781
- sql += ` LIMIT ?`;
13782
- queryArgs.push(preparedQuery.query.limit);
13783
- }
13784
- if (preparedQuery.query.skip) {
13785
- if (!preparedQuery.query.limit) {
13786
- sql += ` LIMIT -1`;
14078
+ const skip = preparedQuery.query.skip || 0;
14079
+ const limit = preparedQuery.query.limit;
14080
+ if (!jsSelector) {
14081
+ if (limit !== undefined) {
14082
+ sql += ` LIMIT ?`;
14083
+ queryArgs.push(limit);
14084
+ }
14085
+ if (skip > 0) {
14086
+ if (limit === undefined) {
14087
+ sql += ` LIMIT -1`;
14088
+ }
14089
+ sql += ` OFFSET ?`;
14090
+ queryArgs.push(skip);
13787
14091
  }
13788
- sql += ` OFFSET ?`;
13789
- queryArgs.push(preparedQuery.query.skip);
13790
14092
  }
13791
14093
  if (process.env.DEBUG_QUERIES) {
13792
14094
  const explainSql = `EXPLAIN QUERY PLAN ${sql}`;
@@ -13795,9 +14097,29 @@ class BunSQLiteStorageInstance {
13795
14097
  console.log("[DEBUG_QUERIES] SQL:", sql);
13796
14098
  console.log("[DEBUG_QUERIES] Args:", queryArgs);
13797
14099
  }
13798
- const rows = this.stmtManager.all({ query: sql, params: queryArgs });
13799
- const documents = rows.map((row) => JSON.parse(row.data));
13800
- return { documents };
14100
+ if (jsSelector) {
14101
+ const stmt = this.db.prepare(sql);
14102
+ const documents = [];
14103
+ let skipped = 0;
14104
+ for (const row of stmt.all(...queryArgs)) {
14105
+ const doc = JSON.parse(row.data);
14106
+ if (matchesSelector(doc, jsSelector)) {
14107
+ if (skip > 0 && skipped < skip) {
14108
+ skipped++;
14109
+ continue;
14110
+ }
14111
+ documents.push(doc);
14112
+ if (limit !== undefined && documents.length >= limit) {
14113
+ break;
14114
+ }
14115
+ }
14116
+ }
14117
+ return { documents };
14118
+ } else {
14119
+ const rows = this.stmtManager.all({ query: sql, params: queryArgs });
14120
+ const documents = rows.map((row) => JSON.parse(row.data));
14121
+ return { documents };
14122
+ }
13801
14123
  }
13802
14124
  sortDocuments(docs, sort) {
13803
14125
  return docs.sort((a, b) => {
@@ -13819,7 +14141,7 @@ class BunSQLiteStorageInstance {
13819
14141
  async count(preparedQuery) {
13820
14142
  const whereResult = buildWhereClause(preparedQuery.query.selector, this.schema, this.collectionName, this.queryCache);
13821
14143
  if (!whereResult) {
13822
- const allDocs = await this.queryWithOurMemory(preparedQuery);
14144
+ const allDocs = this.queryWithOurMemory(preparedQuery);
13823
14145
  return {
13824
14146
  count: allDocs.documents.length,
13825
14147
  mode: "fast"
@@ -13852,7 +14174,6 @@ class BunSQLiteStorageInstance {
13852
14174
  if (this.closed)
13853
14175
  return this.closed;
13854
14176
  this.closed = (async () => {
13855
- this.queryCache.clear();
13856
14177
  this.changeStream$.complete();
13857
14178
  this.stmtManager.close();
13858
14179
  releaseDatabase(this.databaseName);
@@ -13900,21 +14221,16 @@ class BunSQLiteStorageInstance {
13900
14221
  return { documents, checkpoint: newCheckpoint };
13901
14222
  }
13902
14223
  queryWithOurMemory(preparedQuery) {
13903
- const query = `SELECT json(data) as data FROM "${this.tableName}"`;
14224
+ let query = `SELECT json(data) as data FROM "${this.tableName}"`;
13904
14225
  const selector = preparedQuery.query.selector;
13905
14226
  const hasSort = preparedQuery.query.sort && preparedQuery.query.sort.length > 0;
13906
14227
  if (hasSort) {
13907
- const rows = this.stmtManager.all({ query, params: [] });
13908
- let documents2 = rows.map((row) => JSON.parse(row.data));
13909
- documents2 = documents2.filter((doc) => matchesSelector(doc, selector));
13910
- documents2 = this.sortDocuments(documents2, preparedQuery.query.sort);
13911
- if (preparedQuery.query.skip) {
13912
- documents2 = documents2.slice(preparedQuery.query.skip);
13913
- }
13914
- if (preparedQuery.query.limit) {
13915
- documents2 = documents2.slice(0, preparedQuery.query.limit);
13916
- }
13917
- return { documents: documents2 };
14228
+ const orderBy = preparedQuery.query.sort.map((sortField) => {
14229
+ const [field, direction] = Object.entries(sortField)[0];
14230
+ const dir = direction === "asc" ? "ASC" : "DESC";
14231
+ return `json_extract(data, '$.${field}') ${dir}`;
14232
+ }).join(", ");
14233
+ query += ` ORDER BY ${orderBy}`;
13918
14234
  }
13919
14235
  const stmt = this.db.prepare(query);
13920
14236
  const documents = [];
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=debug-array-index.d.ts.map
@@ -13,6 +13,7 @@ export declare class BunSQLiteStorageInstance<RxDocType> implements RxStorageIns
13
13
  readonly options: Readonly<BunSQLiteStorageSettings>;
14
14
  private primaryPath;
15
15
  private tableName;
16
+ private useStoredColumns;
16
17
  closed?: Promise<void>;
17
18
  constructor(params: RxStorageInstanceCreationParams<RxDocType, BunSQLiteStorageSettings>, settings?: BunSQLiteStorageSettings);
18
19
  private initTable;
@@ -1,7 +1,12 @@
1
1
  import type { RxJsonSchema, MangoQuerySelector, RxDocumentData } from 'rxdb';
2
2
  import type { SqlFragment } from './operators';
3
- export declare function getCacheSize(): number;
4
- export declare function clearCache(): void;
5
- export declare function buildWhereClause<RxDocType>(selector: MangoQuerySelector<RxDocumentData<RxDocType>>, schema: RxJsonSchema<RxDocumentData<RxDocType>>, collectionName: string, cache?: Map<string, SqlFragment | null>): SqlFragment | null;
3
+ import type { SieveCache } from './sieve-cache';
4
+ export { getCacheSize, clearCache } from './cache';
5
+ export interface BipartiteQuery<RxDocType> {
6
+ sqlWhere: SqlFragment | null;
7
+ jsSelector: MangoQuerySelector<RxDocumentData<RxDocType>> | null;
8
+ }
9
+ export declare function buildWhereClause<RxDocType>(selector: MangoQuerySelector<RxDocumentData<RxDocType>>, schema: RxJsonSchema<RxDocumentData<RxDocType>>, collectionName: string, cache?: Map<string, SqlFragment | null> | SieveCache<string, SqlFragment | null>): SqlFragment | null;
10
+ export declare function buildWhereClauseWithFallback<RxDocType>(selector: MangoQuerySelector<RxDocumentData<RxDocType>>, schema: RxJsonSchema<RxDocumentData<RxDocType>>, collectionName: string, cache?: Map<string, SqlFragment | null> | SieveCache<string, SqlFragment | null>): BipartiteQuery<RxDocType>;
6
11
  export declare function buildLogicalOperator<RxDocType>(operator: 'or' | 'nor' | 'and', conditions: MangoQuerySelector<RxDocumentData<RxDocType>>[], schema: RxJsonSchema<RxDocumentData<RxDocType>>, logicalDepth: number): SqlFragment | null;
7
12
  //# sourceMappingURL=builder.d.ts.map
@@ -0,0 +1,18 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type { SqlFragment } from './operators';
3
+ import { SieveCache } from './sieve-cache';
4
+ export declare const MAX_QUERY_CACHE_SIZE = 5000;
5
+ export declare const MAX_REGEX_CACHE_SIZE = 100;
6
+ export declare const MAX_INDEX_CACHE_SIZE = 1000;
7
+ export declare function getQueryCache(database: Database): SieveCache<string, SqlFragment | null>;
8
+ export declare function getGlobalCache(): SieveCache<string, SqlFragment | null>;
9
+ export declare function getCacheSize(): number;
10
+ export declare function clearCache(): void;
11
+ export interface RegexCacheEntry {
12
+ regex: RegExp;
13
+ }
14
+ export declare function getRegexCache(): SieveCache<string, RegexCacheEntry>;
15
+ export declare function clearRegexCache(): void;
16
+ export declare function getIndexCache(): SieveCache<string, boolean>;
17
+ export declare function clearIndexCache(): void;
18
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1,10 @@
1
+ import type { SqlFragment } from './operators';
2
+ export declare class NoOpCache<K, V> {
3
+ get size(): number;
4
+ has(_key: K): boolean;
5
+ get(_key: K): V | undefined;
6
+ set(_key: K, _value: V): this;
7
+ clear(): void;
8
+ }
9
+ export declare const NO_CACHE: NoOpCache<string, SqlFragment | null>;
10
+ //# sourceMappingURL=no-op-cache.d.ts.map
@@ -8,6 +8,7 @@ type OperatorExpression = {
8
8
  [key: string]: unknown;
9
9
  };
10
10
  export type ElemMatchCriteria = QueryValue | OperatorExpression;
11
+ export declare function buildJsonPath(fieldName: string): string;
11
12
  export declare function translateEq<RxDocType>(field: string, value: unknown, schema?: RxJsonSchema<RxDocumentData<RxDocType>>, actualFieldName?: string): SqlFragment;
12
13
  export declare function translateNe<RxDocType>(field: string, value: unknown, schema?: RxJsonSchema<RxDocumentData<RxDocType>>, actualFieldName?: string): SqlFragment;
13
14
  export declare function translateGt<RxDocType>(field: string, value: unknown, schema?: RxJsonSchema<RxDocumentData<RxDocType>>, actualFieldName?: string): SqlFragment;
@@ -22,7 +23,7 @@ export declare function translateElemMatch<RxDocType>(field: string, criteria: E
22
23
  export declare function translateLeafOperator<RxDocType>(op: string, field: string, value: unknown, schema: RxJsonSchema<RxDocumentData<RxDocType>>, actualFieldName: string): SqlFragment | null;
23
24
  export declare function wrapWithNot(innerFragment: SqlFragment): SqlFragment;
24
25
  export declare function translateType(jsonColumn: string, fieldName: string, type: string, isDirectPath?: boolean): SqlFragment | null;
25
- export declare function translateSize(field: string, size: number): SqlFragment;
26
+ export declare function translateSize(jsonColumn: string, jsonPath: string, size: number, isDirectPath?: boolean): SqlFragment;
26
27
  export declare function translateMod(field: string, value: unknown): SqlFragment | null;
27
28
  export {};
28
29
  //# sourceMappingURL=operators.d.ts.map
@@ -0,0 +1,17 @@
1
+ export declare class SieveCache<K, V> {
2
+ #private;
3
+ constructor(capacity: number);
4
+ get size(): number;
5
+ has(key: K): boolean;
6
+ get(key: K): V | undefined;
7
+ set(key: K, value: V): this;
8
+ delete(key: K): boolean;
9
+ clear(): void;
10
+ forEach(callbackfn: (value: V, key: K, map: SieveCache<K, V>) => void, thisArg?: any): void;
11
+ entries(): IterableIterator<[K, V]>;
12
+ keys(): IterableIterator<K>;
13
+ values(): IterableIterator<V>;
14
+ [Symbol.iterator](): IterableIterator<[K, V]>;
15
+ get [Symbol.toStringTag](): string;
16
+ }
17
+ //# sourceMappingURL=sieve-cache.d.ts.map
@@ -3,6 +3,5 @@ export interface SqlFragment {
3
3
  sql: string;
4
4
  args: (string | number | boolean)[];
5
5
  }
6
- export declare function clearRegexCache(): void;
7
6
  export declare function smartRegexToLike<RxDocType>(field: string, pattern: string, options: string | undefined, schema: RxJsonSchema<RxDocumentData<RxDocType>>, fieldName: string): SqlFragment | null;
8
7
  //# sourceMappingURL=smart-regex.d.ts.map
@@ -12,6 +12,16 @@ export interface BunSQLiteStorageSettings {
12
12
  * @default 268435456 (256MB)
13
13
  */
14
14
  mmapSize?: number;
15
+ /**
16
+ * Use generated columns for _deleted and _meta.lwt fields.
17
+ * - false: Regular columns with manual extraction (baseline)
18
+ * - 'virtual': VIRTUAL generated columns (computed on-the-fly, no storage overhead) - RECOMMENDED
19
+ * - 'stored': STORED generated columns (pre-computed, +11% storage, 58% faster queries)
20
+ * Requires SQLite 3.31.0+ (Bun 1.0+ includes SQLite 3.42+).
21
+ * @default 'virtual'
22
+ * @experimental Alpha feature - opt-in for testing
23
+ */
24
+ useStoredColumns?: false | 'virtual' | 'stored';
15
25
  }
16
26
  export interface BunSQLiteInternals {
17
27
  db: Database;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-sqlite-for-rxdb",
3
- "version": "1.5.3",
3
+ "version": "1.5.6",
4
4
  "author": "adam2am",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,6 +14,7 @@
14
14
  "main": "dist/index.js",
15
15
  "dependencies": {},
16
16
  "devDependencies": {
17
+ "@msgpack/msgpack": "^3.1.3",
17
18
  "@types/better-sqlite3": "^7.6.13",
18
19
  "@types/bun": "latest",
19
20
  "better-sqlite3": "^12.6.2",