bun-sqlite-for-rxdb 1.5.2 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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) {
@@ -12294,24 +12471,15 @@ function getColumnInfo(path2, schema) {
12294
12471
  }
12295
12472
 
12296
12473
  // src/query/smart-regex.ts
12297
- var INDEX_CACHE = new Map;
12298
- var MAX_INDEX_CACHE_SIZE = 1000;
12299
12474
  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);
12475
+ const cache = getIndexCache();
12476
+ const cacheKey = `${fieldName}:${JSON.stringify(schema.indexes || [])}`;
12477
+ const cached = cache.get(cacheKey);
12303
12478
  if (cached !== undefined) {
12304
- INDEX_CACHE.delete(cacheKey);
12305
- INDEX_CACHE.set(cacheKey, cached);
12306
12479
  return cached;
12307
12480
  }
12308
12481
  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);
12482
+ cache.set(cacheKey, false);
12315
12483
  return false;
12316
12484
  }
12317
12485
  const hasLowerIndex = schema.indexes.some((idx) => {
@@ -12323,12 +12491,7 @@ function hasExpressionIndex(fieldName, schema) {
12323
12491
  return normalized === `lower(${fieldName})`;
12324
12492
  });
12325
12493
  });
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);
12494
+ cache.set(cacheKey, hasLowerIndex);
12332
12495
  return hasLowerIndex;
12333
12496
  }
12334
12497
  function isValidRegexOptions2(options) {
@@ -12949,29 +13112,47 @@ function _stringify(value, stack) {
12949
13112
  }
12950
13113
 
12951
13114
  // 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) {
13115
+ function buildWhereClause(selector, schema, collectionName, cache) {
12955
13116
  if (!selector || typeof selector !== "object")
12956
13117
  return null;
12957
- const cacheKey = `v${schema.version}_${collectionName}_${stableStringify(selector)}`;
12958
- const cached = cache.get(cacheKey);
13118
+ const actualCache = cache ?? getGlobalCache();
13119
+ const cacheKey = `v${schema.version}_${stableStringify(selector)}`;
13120
+ const cached = actualCache.get(cacheKey);
12959
13121
  if (cached !== undefined) {
12960
- cache.delete(cacheKey);
12961
- cache.set(cacheKey, cached);
12962
13122
  return cached;
12963
13123
  }
12964
13124
  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);
13125
+ actualCache.set(cacheKey, result);
12973
13126
  return result;
12974
13127
  }
13128
+ function buildWhereClauseWithFallback(selector, schema, collectionName, cache) {
13129
+ if (!selector || typeof selector !== "object") {
13130
+ return { sqlWhere: null, jsSelector: null };
13131
+ }
13132
+ const sqlResult = buildWhereClause(selector, schema, collectionName, cache);
13133
+ if (sqlResult) {
13134
+ return { sqlWhere: sqlResult, jsSelector: null };
13135
+ }
13136
+ const splitResult = splitSelector(selector, schema);
13137
+ return splitResult;
13138
+ }
13139
+ function splitSelector(selector, schema) {
13140
+ const sqlConditions = [];
13141
+ const jsConditions = [];
13142
+ const entries = Object.entries(selector);
13143
+ for (const [field, value] of entries) {
13144
+ const testSelector = { [field]: value };
13145
+ const sqlFragment = processSelector(testSelector, schema, 0);
13146
+ if (sqlFragment) {
13147
+ sqlConditions.push(testSelector);
13148
+ } else {
13149
+ jsConditions.push(testSelector);
13150
+ }
13151
+ }
13152
+ const sqlWhere = sqlConditions.length > 0 ? processSelector({ $and: sqlConditions }, schema, 0) : null;
13153
+ const jsSelector = jsConditions.length > 0 ? jsConditions.length === 1 ? jsConditions[0] : { $and: jsConditions } : null;
13154
+ return { sqlWhere, jsSelector };
13155
+ }
12975
13156
  function buildLogicalOperator(operator, conditions, schema, logicalDepth) {
12976
13157
  if (conditions.length === 0) {
12977
13158
  return { sql: operator === "or" ? "1=0" : "1=1", args: [] };
@@ -13016,6 +13197,14 @@ function processSelector(selector, schema, logicalDepth) {
13016
13197
  args.push(...norFragment.args);
13017
13198
  continue;
13018
13199
  }
13200
+ if (field === "$not" && typeof value === "object" && value !== null && !Array.isArray(value)) {
13201
+ const innerFragment = processSelector(value, schema, logicalDepth + 1);
13202
+ if (!innerFragment)
13203
+ return null;
13204
+ conditions.push(`NOT (${innerFragment.sql})`);
13205
+ args.push(...innerFragment.args);
13206
+ continue;
13207
+ }
13019
13208
  const columnInfo = getColumnInfo(field, schema);
13020
13209
  const fieldName = columnInfo.column || `json_extract(data, '${columnInfo.jsonPath}')`;
13021
13210
  const actualFieldName = columnInfo.jsonPath?.replace(/^\$\./, "") || columnInfo.column || field;
@@ -13529,9 +13718,6 @@ class StatementManager {
13529
13718
  this.closed = true;
13530
13719
  }
13531
13720
  isStaticSQL(query) {
13532
- if (query.includes("WHERE (")) {
13533
- return false;
13534
- }
13535
13721
  return true;
13536
13722
  }
13537
13723
  }
@@ -13594,7 +13780,7 @@ class BunSQLiteStorageInstance {
13594
13780
  db;
13595
13781
  stmtManager;
13596
13782
  changeStream$ = new import_rxjs2.Subject;
13597
- queryCache = new Map;
13783
+ queryCache;
13598
13784
  databaseName;
13599
13785
  collectionName;
13600
13786
  schema;
@@ -13602,6 +13788,7 @@ class BunSQLiteStorageInstance {
13602
13788
  options;
13603
13789
  primaryPath;
13604
13790
  tableName;
13791
+ useStoredColumns;
13605
13792
  closed;
13606
13793
  constructor(params, settings = {}) {
13607
13794
  ensureRxStorageInstanceParamsAreCorrect(params);
@@ -13612,9 +13799,11 @@ class BunSQLiteStorageInstance {
13612
13799
  const primaryKey = params.schema.primaryKey;
13613
13800
  this.primaryPath = typeof primaryKey === "string" ? primaryKey : primaryKey.key;
13614
13801
  this.tableName = `${params.collectionName}_v${params.schema.version}`;
13802
+ this.useStoredColumns = this.options?.useStoredColumns ?? "virtual";
13615
13803
  const filename = settings.filename || ":memory:";
13616
13804
  this.db = getDatabase(this.databaseName, filename);
13617
13805
  this.stmtManager = new StatementManager(this.db);
13806
+ this.queryCache = getQueryCache(this.db);
13618
13807
  this.internals = {
13619
13808
  db: this.db,
13620
13809
  primaryPath: this.primaryPath
@@ -13635,15 +13824,37 @@ class BunSQLiteStorageInstance {
13635
13824
  this.db.run("PRAGMA temp_store = MEMORY");
13636
13825
  this.db.run("PRAGMA locking_mode = NORMAL");
13637
13826
  }
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
- `);
13827
+ if (this.useStoredColumns === "virtual") {
13828
+ this.db.run(`
13829
+ CREATE TABLE IF NOT EXISTS "${this.tableName}" (
13830
+ id TEXT PRIMARY KEY NOT NULL,
13831
+ data BLOB NOT NULL,
13832
+ deleted INTEGER GENERATED ALWAYS AS (json_extract(data, '$._deleted')) VIRTUAL,
13833
+ rev TEXT GENERATED ALWAYS AS (json_extract(data, '$._rev')) VIRTUAL,
13834
+ mtime_ms REAL GENERATED ALWAYS AS (json_extract(data, '$._meta.lwt')) VIRTUAL
13835
+ )
13836
+ `);
13837
+ } else if (this.useStoredColumns === "stored") {
13838
+ this.db.run(`
13839
+ CREATE TABLE IF NOT EXISTS "${this.tableName}" (
13840
+ id TEXT PRIMARY KEY NOT NULL,
13841
+ data BLOB NOT NULL,
13842
+ deleted INTEGER GENERATED ALWAYS AS (json_extract(data, '$._deleted')) STORED,
13843
+ rev TEXT GENERATED ALWAYS AS (json_extract(data, '$._rev')) STORED,
13844
+ mtime_ms REAL GENERATED ALWAYS AS (json_extract(data, '$._meta.lwt')) STORED
13845
+ )
13846
+ `);
13847
+ } else {
13848
+ this.db.run(`
13849
+ CREATE TABLE IF NOT EXISTS "${this.tableName}" (
13850
+ id TEXT PRIMARY KEY NOT NULL,
13851
+ data BLOB NOT NULL,
13852
+ deleted INTEGER NOT NULL DEFAULT 0,
13853
+ rev TEXT NOT NULL,
13854
+ mtime_ms REAL NOT NULL
13855
+ )
13856
+ `);
13857
+ }
13647
13858
  this.db.run(`CREATE INDEX IF NOT EXISTS "idx_${this.tableName}_deleted_id" ON "${this.tableName}"(deleted, id)`);
13648
13859
  this.db.run(`CREATE INDEX IF NOT EXISTS "idx_${this.tableName}_mtime_ms_id" ON "${this.tableName}"(mtime_ms, id)`);
13649
13860
  if (this.schema.indexes) {
@@ -13677,49 +13888,70 @@ class BunSQLiteStorageInstance {
13677
13888
  return { error: [] };
13678
13889
  }
13679
13890
  const ids = documentWrites.map((w) => w.document[this.primaryPath]);
13680
- const docsInDb = await this.findDocumentsById(ids, true);
13891
+ const LOOKUP_BATCH_SIZE = 500;
13892
+ const docsInDb = [];
13893
+ for (let i = 0;i < ids.length; i += LOOKUP_BATCH_SIZE) {
13894
+ const batch = ids.slice(i, i + LOOKUP_BATCH_SIZE);
13895
+ const batchDocs = await this.findDocumentsById(batch, true);
13896
+ docsInDb.push(...batchDocs);
13897
+ }
13681
13898
  const docsInDbMap = new Map(docsInDb.map((d) => [d[this.primaryPath], d]));
13682
13899
  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
- });
13900
+ const CHUNK_SIZE = 50;
13901
+ 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 = ?`);
13902
+ const insertBatch = this.db.transaction((docs) => {
13903
+ for (let i = 0;i < docs.length; i += CHUNK_SIZE) {
13904
+ const chunk = docs.slice(i, i + CHUNK_SIZE);
13905
+ const placeholders = this.useStoredColumns ? chunk.map(() => "(?, jsonb(?))").join(", ") : chunk.map(() => "(?, jsonb(?), ?, ?, ?)").join(", ");
13906
+ const insertQuery = this.useStoredColumns ? `INSERT INTO "${this.tableName}" (id, data) VALUES ${placeholders}` : `INSERT INTO "${this.tableName}" (id, data, deleted, rev, mtime_ms) VALUES ${placeholders}`;
13907
+ const params = [];
13908
+ for (const row of chunk) {
13909
+ const doc = row.document;
13910
+ const id = doc[this.primaryPath];
13911
+ if (this.useStoredColumns) {
13912
+ params.push(id, JSON.stringify(doc));
13913
+ } else {
13914
+ params.push(id, JSON.stringify(doc), doc._deleted ? 1 : 0, doc._rev, doc._meta.lwt);
13915
+ }
13916
+ }
13917
+ try {
13918
+ this.stmtManager.run({ query: insertQuery, params });
13919
+ } catch (err) {
13920
+ if (err && typeof err === "object" && "code" in err && (err.code === "SQLITE_CONSTRAINT_PRIMARYKEY" || err.code === "SQLITE_CONSTRAINT_UNIQUE")) {
13921
+ for (const row of chunk) {
13922
+ const doc = row.document;
13923
+ const id = doc[this.primaryPath];
13924
+ const documentInDb = docsInDbMap.get(id);
13925
+ categorized.errors.push({
13926
+ isError: true,
13927
+ status: 409,
13928
+ documentId: id,
13929
+ writeRow: row,
13930
+ documentInDb: documentInDb || doc
13931
+ });
13932
+ }
13933
+ } else {
13934
+ throw err;
13710
13935
  }
13711
- } else {
13712
- throw err;
13713
13936
  }
13714
13937
  }
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) {
13938
+ });
13939
+ const updateBatch = this.db.transaction((docs) => {
13940
+ for (const row of docs) {
13719
13941
  const doc = row.document;
13720
13942
  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] });
13943
+ if (this.useStoredColumns) {
13944
+ updateStmt.run(JSON.stringify(doc), id);
13945
+ } else {
13946
+ updateStmt.run(JSON.stringify(doc), doc._deleted ? 1 : 0, doc._rev, doc._meta.lwt, id);
13947
+ }
13722
13948
  }
13949
+ });
13950
+ if (categorized.bulkInsertDocs.length > 0) {
13951
+ insertBatch(categorized.bulkInsertDocs);
13952
+ }
13953
+ if (categorized.bulkUpdateDocs.length > 0) {
13954
+ updateBatch(categorized.bulkUpdateDocs);
13723
13955
  }
13724
13956
  const insertAttQuery = `INSERT OR REPLACE INTO "${this.tableName}_attachments" (id, data, digest) VALUES (?, ?, ?)`;
13725
13957
  const deleteAttQuery = `DELETE FROM "${this.tableName}_attachments" WHERE id = ?`;
@@ -13762,31 +13994,37 @@ class BunSQLiteStorageInstance {
13762
13994
  return rows.map((row) => JSON.parse(row.data));
13763
13995
  }
13764
13996
  async query(preparedQuery) {
13765
- const whereResult = buildWhereClause(preparedQuery.query.selector, this.schema, this.collectionName, this.queryCache);
13766
- if (!whereResult) {
13767
- return this.queryWithOurMemory(preparedQuery);
13997
+ const { sqlWhere, jsSelector } = buildWhereClauseWithFallback(preparedQuery.query.selector, this.schema, this.collectionName, this.queryCache);
13998
+ let sql = `SELECT json(data) as data FROM "${this.tableName}"`;
13999
+ const queryArgs = [];
14000
+ if (sqlWhere) {
14001
+ sql += ` WHERE (${sqlWhere.sql})`;
14002
+ queryArgs.push(...sqlWhere.args);
13768
14003
  }
13769
- const { sql: whereClause, args } = whereResult;
13770
- let sql = `SELECT json(data) as data FROM "${this.tableName}" WHERE (${whereClause})`;
13771
- const queryArgs = [...args];
13772
14004
  if (preparedQuery.query.sort && preparedQuery.query.sort.length > 0) {
13773
14005
  const orderBy = preparedQuery.query.sort.map((sortField) => {
13774
14006
  const [field, direction] = Object.entries(sortField)[0];
13775
14007
  const dir = direction === "asc" ? "ASC" : "DESC";
13776
- return `json_extract(data, '$.${field}') ${dir}`;
14008
+ const colInfo = getColumnInfo(field, this.schema);
14009
+ const colName = colInfo.column || `json_extract(data, '${colInfo.jsonPath}')`;
14010
+ return `${colName} ${dir}`;
13777
14011
  }).join(", ");
13778
14012
  sql += ` ORDER BY ${orderBy}`;
13779
14013
  }
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`;
14014
+ const skip = preparedQuery.query.skip || 0;
14015
+ const limit = preparedQuery.query.limit;
14016
+ if (!jsSelector) {
14017
+ if (limit !== undefined) {
14018
+ sql += ` LIMIT ?`;
14019
+ queryArgs.push(limit);
14020
+ }
14021
+ if (skip > 0) {
14022
+ if (limit === undefined) {
14023
+ sql += ` LIMIT -1`;
14024
+ }
14025
+ sql += ` OFFSET ?`;
14026
+ queryArgs.push(skip);
13787
14027
  }
13788
- sql += ` OFFSET ?`;
13789
- queryArgs.push(preparedQuery.query.skip);
13790
14028
  }
13791
14029
  if (process.env.DEBUG_QUERIES) {
13792
14030
  const explainSql = `EXPLAIN QUERY PLAN ${sql}`;
@@ -13795,9 +14033,29 @@ class BunSQLiteStorageInstance {
13795
14033
  console.log("[DEBUG_QUERIES] SQL:", sql);
13796
14034
  console.log("[DEBUG_QUERIES] Args:", queryArgs);
13797
14035
  }
13798
- const rows = this.stmtManager.all({ query: sql, params: queryArgs });
13799
- const documents = rows.map((row) => JSON.parse(row.data));
13800
- return { documents };
14036
+ if (jsSelector) {
14037
+ const stmt = this.db.prepare(sql);
14038
+ const documents = [];
14039
+ let skipped = 0;
14040
+ for (const row of stmt.all(...queryArgs)) {
14041
+ const doc = JSON.parse(row.data);
14042
+ if (matchesSelector(doc, jsSelector)) {
14043
+ if (skip > 0 && skipped < skip) {
14044
+ skipped++;
14045
+ continue;
14046
+ }
14047
+ documents.push(doc);
14048
+ if (limit !== undefined && documents.length >= limit) {
14049
+ break;
14050
+ }
14051
+ }
14052
+ }
14053
+ return { documents };
14054
+ } else {
14055
+ const rows = this.stmtManager.all({ query: sql, params: queryArgs });
14056
+ const documents = rows.map((row) => JSON.parse(row.data));
14057
+ return { documents };
14058
+ }
13801
14059
  }
13802
14060
  sortDocuments(docs, sort) {
13803
14061
  return docs.sort((a, b) => {
@@ -13819,7 +14077,7 @@ class BunSQLiteStorageInstance {
13819
14077
  async count(preparedQuery) {
13820
14078
  const whereResult = buildWhereClause(preparedQuery.query.selector, this.schema, this.collectionName, this.queryCache);
13821
14079
  if (!whereResult) {
13822
- const allDocs = await this.queryWithOurMemory(preparedQuery);
14080
+ const allDocs = this.queryWithOurMemory(preparedQuery);
13823
14081
  return {
13824
14082
  count: allDocs.documents.length,
13825
14083
  mode: "fast"
@@ -13852,7 +14110,6 @@ class BunSQLiteStorageInstance {
13852
14110
  if (this.closed)
13853
14111
  return this.closed;
13854
14112
  this.closed = (async () => {
13855
- this.queryCache.clear();
13856
14113
  this.changeStream$.complete();
13857
14114
  this.stmtManager.close();
13858
14115
  releaseDatabase(this.databaseName);
@@ -13900,21 +14157,16 @@ class BunSQLiteStorageInstance {
13900
14157
  return { documents, checkpoint: newCheckpoint };
13901
14158
  }
13902
14159
  queryWithOurMemory(preparedQuery) {
13903
- const query = `SELECT json(data) as data FROM "${this.tableName}"`;
14160
+ let query = `SELECT json(data) as data FROM "${this.tableName}"`;
13904
14161
  const selector = preparedQuery.query.selector;
13905
14162
  const hasSort = preparedQuery.query.sort && preparedQuery.query.sort.length > 0;
13906
14163
  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 };
14164
+ const orderBy = preparedQuery.query.sort.map((sortField) => {
14165
+ const [field, direction] = Object.entries(sortField)[0];
14166
+ const dir = direction === "asc" ? "ASC" : "DESC";
14167
+ return `json_extract(data, '$.${field}') ${dir}`;
14168
+ }).join(", ");
14169
+ query += ` ORDER BY ${orderBy}`;
13918
14170
  }
13919
14171
  const stmt = this.db.prepare(query);
13920
14172
  const documents = [];
@@ -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
@@ -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.2",
3
+ "version": "1.5.4",
4
4
  "author": "adam2am",
5
5
  "repository": {
6
6
  "type": "git",