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 +491 -175
- package/dist/src/debug-array-index.d.ts +2 -0
- package/dist/src/instance.d.ts +1 -0
- package/dist/src/query/builder.d.ts +8 -3
- package/dist/src/query/cache.d.ts +18 -0
- package/dist/src/query/no-op-cache.d.ts +10 -0
- package/dist/src/query/operators.d.ts +2 -1
- package/dist/src/query/sieve-cache.d.ts +17 -0
- package/dist/src/query/smart-regex.d.ts +0 -1
- package/dist/src/types.d.ts +10 -0
- package/package.json +2 -1
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/
|
|
12232
|
-
|
|
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
|
|
12244
|
-
const
|
|
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:
|
|
12432
|
+
throw new Error(`Invalid regex options: ${options}`);
|
|
12250
12433
|
}
|
|
12251
12434
|
const regex = new RegExp(pattern, options);
|
|
12252
|
-
|
|
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
|
-
|
|
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
|
|
12301
|
-
const cacheKey = `${
|
|
12302
|
-
const cached =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
12539
|
+
return { sql: `LOWER(${field}) = LOWER(?)`, args: [unescaped] };
|
|
12367
12540
|
}
|
|
12368
12541
|
return { sql: `${field} = ?`, args: [unescaped] };
|
|
12369
12542
|
}
|
|
12370
12543
|
if (startsWithAnchor) {
|
|
12371
|
-
|
|
12372
|
-
|
|
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
|
|
12547
|
+
return { sql: `${field} LIKE ? ESCAPE '\\'`, args: [escaped + "%"] };
|
|
12376
12548
|
}
|
|
12377
12549
|
if (endsWithAnchor) {
|
|
12378
|
-
|
|
12379
|
-
|
|
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
|
|
12553
|
+
return { sql: `${field} LIKE ? ESCAPE '\\'`, args: ["%" + escaped] };
|
|
12383
12554
|
}
|
|
12384
|
-
|
|
12385
|
-
|
|
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
|
|
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, '
|
|
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
|
-
|
|
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
|
|
13039
|
+
return null;
|
|
12828
13040
|
}
|
|
12829
13041
|
}
|
|
12830
|
-
function translateSize(
|
|
13042
|
+
function translateSize(jsonColumn, jsonPath, size, isDirectPath = false) {
|
|
13043
|
+
const path2 = isDirectPath ? jsonPath : `$.${jsonPath}`;
|
|
12831
13044
|
return {
|
|
12832
|
-
sql: `json_array_length(${
|
|
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
|
-
|
|
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
|
|
12958
|
-
const
|
|
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
|
-
|
|
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) =>
|
|
13216
|
+
const sql = fragments.map((f) => f.sql).join(operator === "and" ? " AND " : " OR ");
|
|
12983
13217
|
const args = fragments.flatMap((f) => f.args);
|
|
12984
|
-
|
|
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, '${
|
|
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
|
|
13032
|
-
if (!
|
|
13276
|
+
const neFrag = translateLeafOperator("$ne", fieldName, opValue, schema, actualFieldName);
|
|
13277
|
+
if (!neFrag)
|
|
13033
13278
|
return null;
|
|
13034
|
-
fragment =
|
|
13279
|
+
fragment = neFrag;
|
|
13035
13280
|
} else if (opValue instanceof Date) {
|
|
13036
|
-
const
|
|
13037
|
-
if (!
|
|
13281
|
+
const neFrag = translateLeafOperator("$ne", fieldName, opValue, schema, actualFieldName);
|
|
13282
|
+
if (!neFrag)
|
|
13038
13283
|
return null;
|
|
13039
|
-
fragment =
|
|
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
|
|
13081
|
-
if (!
|
|
13325
|
+
const neFrag = translateLeafOperator("$ne", fieldName, opValueObj, schema, actualFieldName);
|
|
13326
|
+
if (!neFrag)
|
|
13082
13327
|
return null;
|
|
13083
|
-
fragment =
|
|
13328
|
+
fragment = neFrag;
|
|
13084
13329
|
} else {
|
|
13085
13330
|
const [[innerOp, innerVal]] = Object.entries(opValueObj);
|
|
13086
|
-
|
|
13087
|
-
|
|
13088
|
-
|
|
13089
|
-
|
|
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}, '
|
|
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
|
|
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.
|
|
13639
|
-
|
|
13640
|
-
|
|
13641
|
-
|
|
13642
|
-
|
|
13643
|
-
|
|
13644
|
-
|
|
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
|
|
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
|
|
13684
|
-
const
|
|
13685
|
-
|
|
13686
|
-
|
|
13687
|
-
|
|
13688
|
-
|
|
13689
|
-
|
|
13690
|
-
|
|
13691
|
-
const
|
|
13692
|
-
|
|
13693
|
-
|
|
13694
|
-
|
|
13695
|
-
|
|
13696
|
-
|
|
13697
|
-
|
|
13698
|
-
|
|
13699
|
-
|
|
13700
|
-
|
|
13701
|
-
|
|
13702
|
-
|
|
13703
|
-
|
|
13704
|
-
|
|
13705
|
-
|
|
13706
|
-
|
|
13707
|
-
|
|
13708
|
-
|
|
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
|
-
|
|
13717
|
-
|
|
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.
|
|
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
|
|
13766
|
-
|
|
13767
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13781
|
-
|
|
13782
|
-
|
|
13783
|
-
|
|
13784
|
-
|
|
13785
|
-
|
|
13786
|
-
|
|
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
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
13908
|
-
|
|
13909
|
-
|
|
13910
|
-
|
|
13911
|
-
|
|
13912
|
-
|
|
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 = [];
|
package/dist/src/instance.d.ts
CHANGED
|
@@ -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
|
-
|
|
4
|
-
export
|
|
5
|
-
export
|
|
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(
|
|
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
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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
|
+
"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",
|