@crvouga/sqlite-mem 1.13.0 → 1.14.0
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/AGENTS.md +2 -0
- package/dist/executor/env.d.ts +4 -0
- package/dist/index.js +268 -55
- package/dist/index.js.map +3 -3
- package/dist/indexes/index.d.ts +2 -0
- package/dist/runtime/assert.d.ts +11 -0
- package/dist/runtime/catch.d.ts +3 -0
- package/dist/unstable.js +133 -30
- package/dist/unstable.js.map +3 -3
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -67,6 +67,8 @@ Hot / large files: `parser/parser.ts`, `executor/select.ts`, `executor/dml.ts`.
|
|
|
67
67
|
- **`Database`** (API) vs **`DatabaseState`** (engine storage).
|
|
68
68
|
- Throw **`SqliteError`** with an `ErrorCategory`. Missing SQL must fail loud via `unsupported()` — the inventory gate fails if the oracle exposes an unimplemented builtin/module.
|
|
69
69
|
- Fast-path helpers (`tryExecuteSimpleSelect`, `tryFastInsert`, `tryIndexedTableRows`, …): return `null` → fall through. Update **both** paths when semantics change.
|
|
70
|
+
- **Dual-path invariant:** residual `WHERE` filtering is skipped on an index/hash access path only when `whereFullyCovered(where, coveredColumns, …)` is true (see [`src/planner/access.ts`](src/planner/access.ts)). Any change to WHERE/join/insert semantics requires updating both fast and full paths **and** the fast≡full property tests under `tests/fuzz/fast-path.test.ts`.
|
|
71
|
+
- Internal invariants use always-on asserts in [`src/runtime/assert.ts`](src/runtime/assert.ts) (`assert`, `assertUnreachable`, `assertRowShape`).
|
|
70
72
|
- TypeScript: `strict` + `noUncheckedIndexedAccess`. Imports use `.ts` extensions. Biome: 2-space, double quotes, 120 columns.
|
|
71
73
|
- IEEE `-0` is canonicalized to `+0` on bind, affinity, and arithmetic. Keep determinism invariants (see README).
|
|
72
74
|
|
package/dist/executor/env.d.ts
CHANGED
|
@@ -49,6 +49,10 @@ export declare class ExecutionEnv {
|
|
|
49
49
|
triggerScope: ScopeRow | null;
|
|
50
50
|
/** Cap SELECT output rows (used by Statement.get). */
|
|
51
51
|
maxRows: number;
|
|
52
|
+
/** When true, skip simple-select / simple-join fast paths (tests only). */
|
|
53
|
+
forceFullSelect: boolean;
|
|
54
|
+
/** When true, skip tryFastInsert (tests only). */
|
|
55
|
+
forceFullInsert: boolean;
|
|
52
56
|
includeNamedRows: boolean;
|
|
53
57
|
includeValues: boolean;
|
|
54
58
|
/** Source text of the statement currently executing (for sqlite_master.sql). */
|
package/dist/index.js
CHANGED
|
@@ -2765,6 +2765,7 @@ var IndexStore = class _IndexStore {
|
|
|
2765
2765
|
entries;
|
|
2766
2766
|
keyValues;
|
|
2767
2767
|
sortedKeys;
|
|
2768
|
+
mapsShared = false;
|
|
2768
2769
|
frozen = false;
|
|
2769
2770
|
constructor(name = "index", entries, keyValues) {
|
|
2770
2771
|
this.name = name;
|
|
@@ -2818,10 +2819,22 @@ var IndexStore = class _IndexStore {
|
|
|
2818
2819
|
if (values.some((value) => value === null)) return [];
|
|
2819
2820
|
const prefix = serializeIndexEntry(values);
|
|
2820
2821
|
const exact = this.entries.get(prefix);
|
|
2821
|
-
const needle = `${prefix}|`;
|
|
2822
2822
|
const rowids = exact ? [...exact] : [];
|
|
2823
|
-
|
|
2824
|
-
|
|
2823
|
+
const keys = this.orderedKeys();
|
|
2824
|
+
let start = 0;
|
|
2825
|
+
let end = keys.length;
|
|
2826
|
+
while (start < end) {
|
|
2827
|
+
const mid = start + end >>> 1;
|
|
2828
|
+
const kv = this.keyValues.get(keys[mid]);
|
|
2829
|
+
if (!kv || prefixKeyLess(kv, values)) start = mid + 1;
|
|
2830
|
+
else end = mid;
|
|
2831
|
+
}
|
|
2832
|
+
for (let i = start; i < keys.length; i++) {
|
|
2833
|
+
const kv = this.keyValues.get(keys[i]);
|
|
2834
|
+
if (!kv || !prefixKeyMatches(kv, values)) break;
|
|
2835
|
+
const key = keys[i];
|
|
2836
|
+
if (key === prefix) continue;
|
|
2837
|
+
rowids.push(...this.entries.get(key) ?? []);
|
|
2825
2838
|
}
|
|
2826
2839
|
return rowids;
|
|
2827
2840
|
}
|
|
@@ -2830,9 +2843,12 @@ var IndexStore = class _IndexStore {
|
|
|
2830
2843
|
* `op` applies to the first key component.
|
|
2831
2844
|
*/
|
|
2832
2845
|
rangeLookup(op, bound, bound2) {
|
|
2833
|
-
const rowids = [];
|
|
2834
2846
|
const keys = this.orderedKeys();
|
|
2835
|
-
|
|
2847
|
+
const rowids = [];
|
|
2848
|
+
const start = lowerBoundKeyValues(keys, this.keyValues, bound, op === ">" ? "gt" : op === ">=" ? "ge" : "any");
|
|
2849
|
+
const end = op === "<" || op === "<=" ? upperBoundKeyValues(keys, this.keyValues, bound, op === "<" ? "lt" : "le") : op === "between" && bound2 !== void 0 ? upperBoundKeyValues(keys, this.keyValues, bound2, "le") : keys.length;
|
|
2850
|
+
for (let i = start; i < end; i++) {
|
|
2851
|
+
const key = keys[i];
|
|
2836
2852
|
const values = this.keyValues.get(key);
|
|
2837
2853
|
if (!values || values[0] === void 0 || values[0] === null) continue;
|
|
2838
2854
|
const cmp = compareSerializedOrder(values[0], bound);
|
|
@@ -2889,7 +2905,12 @@ var IndexStore = class _IndexStore {
|
|
|
2889
2905
|
this.sortedKeys = null;
|
|
2890
2906
|
}
|
|
2891
2907
|
clone() {
|
|
2892
|
-
|
|
2908
|
+
const copy = new _IndexStore(this.name);
|
|
2909
|
+
copy.entries = this.entries;
|
|
2910
|
+
copy.keyValues = this.keyValues;
|
|
2911
|
+
copy.sortedKeys = this.sortedKeys;
|
|
2912
|
+
copy.mapsShared = true;
|
|
2913
|
+
return copy;
|
|
2893
2914
|
}
|
|
2894
2915
|
freeze() {
|
|
2895
2916
|
this.frozen = true;
|
|
@@ -2945,6 +2966,18 @@ var IndexStore = class _IndexStore {
|
|
|
2945
2966
|
}
|
|
2946
2967
|
assertMutable() {
|
|
2947
2968
|
if (this.frozen) throw new SqliteError("internal: cannot mutate a frozen index", "other");
|
|
2969
|
+
this.forkMaps();
|
|
2970
|
+
}
|
|
2971
|
+
forkMaps() {
|
|
2972
|
+
if (!this.mapsShared) return;
|
|
2973
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2974
|
+
for (const [key, rowids] of this.entries) entries.set(key, [...rowids]);
|
|
2975
|
+
const keyValues = /* @__PURE__ */ new Map();
|
|
2976
|
+
for (const [key, values] of this.keyValues) keyValues.set(key, [...values]);
|
|
2977
|
+
this.entries = entries;
|
|
2978
|
+
this.keyValues = keyValues;
|
|
2979
|
+
this.mapsShared = false;
|
|
2980
|
+
this.sortedKeys = null;
|
|
2948
2981
|
}
|
|
2949
2982
|
};
|
|
2950
2983
|
function serializeIndexKey(values) {
|
|
@@ -2983,6 +3016,53 @@ function serializeValue(value) {
|
|
|
2983
3016
|
function compareSerializedOrder(left, right) {
|
|
2984
3017
|
return compareSql(left, right) ?? 0;
|
|
2985
3018
|
}
|
|
3019
|
+
function prefixKeyLess(keyValues, prefix) {
|
|
3020
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
3021
|
+
const cmp = compareSerializedOrder(keyValues[i] ?? null, prefix[i]);
|
|
3022
|
+
if (cmp !== 0) return cmp < 0;
|
|
3023
|
+
}
|
|
3024
|
+
return false;
|
|
3025
|
+
}
|
|
3026
|
+
function prefixKeyMatches(keyValues, prefix) {
|
|
3027
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
3028
|
+
if (compareSerializedOrder(keyValues[i] ?? null, prefix[i]) !== 0) return false;
|
|
3029
|
+
}
|
|
3030
|
+
return true;
|
|
3031
|
+
}
|
|
3032
|
+
function lowerBoundKeyValues(keys, keyValues, bound, mode) {
|
|
3033
|
+
let lo = 0;
|
|
3034
|
+
let hi = keys.length;
|
|
3035
|
+
while (lo < hi) {
|
|
3036
|
+
const mid = lo + hi >>> 1;
|
|
3037
|
+
const value = keyValues.get(keys[mid])?.[0];
|
|
3038
|
+
if (value === void 0 || value === null) {
|
|
3039
|
+
lo = mid + 1;
|
|
3040
|
+
continue;
|
|
3041
|
+
}
|
|
3042
|
+
const cmp = compareSerializedOrder(value, bound);
|
|
3043
|
+
const before = mode === "gt" ? cmp <= 0 : mode === "ge" ? cmp < 0 : false;
|
|
3044
|
+
if (before) lo = mid + 1;
|
|
3045
|
+
else hi = mid;
|
|
3046
|
+
}
|
|
3047
|
+
return lo;
|
|
3048
|
+
}
|
|
3049
|
+
function upperBoundKeyValues(keys, keyValues, bound, mode) {
|
|
3050
|
+
let lo = 0;
|
|
3051
|
+
let hi = keys.length;
|
|
3052
|
+
while (lo < hi) {
|
|
3053
|
+
const mid = lo + hi >>> 1;
|
|
3054
|
+
const value = keyValues.get(keys[mid])?.[0];
|
|
3055
|
+
if (value === void 0 || value === null) {
|
|
3056
|
+
lo = mid + 1;
|
|
3057
|
+
continue;
|
|
3058
|
+
}
|
|
3059
|
+
const cmp = compareSerializedOrder(value, bound);
|
|
3060
|
+
const after = mode === "lt" ? cmp >= 0 : cmp > 0;
|
|
3061
|
+
if (after) hi = mid;
|
|
3062
|
+
else lo = mid + 1;
|
|
3063
|
+
}
|
|
3064
|
+
return lo;
|
|
3065
|
+
}
|
|
2986
3066
|
function sameRowid(left, right) {
|
|
2987
3067
|
return typeof left === "bigint" || typeof right === "bigint" ? BigInt(left) === BigInt(right) : left === right;
|
|
2988
3068
|
}
|
|
@@ -3366,6 +3446,9 @@ function parseFts5Query(input) {
|
|
|
3366
3446
|
}
|
|
3367
3447
|
if (p.kind === "TERM") {
|
|
3368
3448
|
next();
|
|
3449
|
+
if (p.prefix && (p.value === "" || p.value.includes("*"))) {
|
|
3450
|
+
throw new SqliteError(`unknown special query: ${p.value}`, "other");
|
|
3451
|
+
}
|
|
3369
3452
|
return { type: "term", value: p.value, prefix: p.prefix, column: null, columns: null };
|
|
3370
3453
|
}
|
|
3371
3454
|
throw new SqliteError(`fts5: syntax error near "${displayTok(p)}"`, "syntax");
|
|
@@ -3532,17 +3615,7 @@ function displayTok(t) {
|
|
|
3532
3615
|
}
|
|
3533
3616
|
}
|
|
3534
3617
|
function parseFts3Query(input) {
|
|
3535
|
-
|
|
3536
|
-
return parseFts5Query(input);
|
|
3537
|
-
} catch {
|
|
3538
|
-
const terms = input.split(/\s+/).filter(Boolean).filter((t) => !/^(AND|OR|NOT|NEAR)$/i.test(t));
|
|
3539
|
-
if (terms.length === 0) return { type: "true" };
|
|
3540
|
-
const nodes = terms.map((t) => {
|
|
3541
|
-
const prefix = t.endsWith("*");
|
|
3542
|
-
return { type: "term", value: prefix ? t.slice(0, -1) : t, prefix, column: null, columns: null };
|
|
3543
|
-
});
|
|
3544
|
-
return nodes.length === 1 ? nodes[0] : { type: "and", children: nodes };
|
|
3545
|
-
}
|
|
3618
|
+
return parseFts5Query(input);
|
|
3546
3619
|
}
|
|
3547
3620
|
|
|
3548
3621
|
// src/vtable/fts/porter.ts
|
|
@@ -4593,6 +4666,26 @@ var FtsVocabVirtualTable = class _FtsVocabVirtualTable {
|
|
|
4593
4666
|
}
|
|
4594
4667
|
};
|
|
4595
4668
|
|
|
4669
|
+
// src/runtime/assert.ts
|
|
4670
|
+
function assert(condition, message) {
|
|
4671
|
+
if (!condition) {
|
|
4672
|
+
throw new SqliteError(`internal assertion failed: ${message}`, "other");
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
function assertUnreachable(value, message = "unreachable") {
|
|
4676
|
+
throw new SqliteError(`internal assertion failed: ${message} (${String(value)})`, "other");
|
|
4677
|
+
}
|
|
4678
|
+
function assertRowShape(table, row) {
|
|
4679
|
+
assert(row.values.length === table.columns.length, `row.values.length !== table.columns.length for ${table.name}`);
|
|
4680
|
+
}
|
|
4681
|
+
var SQLITE_MAX_LENGTH = 2147483647;
|
|
4682
|
+
function assertBlobLength(length, _feature) {
|
|
4683
|
+
if (!Number.isFinite(length) || length < 0) return;
|
|
4684
|
+
if (length >= SQLITE_MAX_LENGTH) {
|
|
4685
|
+
throw new SqliteError(`string or blob too big`, "other", "SQLITE_TOOBIG");
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4596
4689
|
// src/types/collation.ts
|
|
4597
4690
|
function normalizeForCollation(value, name) {
|
|
4598
4691
|
const collation = builtinCollation(name);
|
|
@@ -4769,6 +4862,7 @@ var Table = class _Table {
|
|
|
4769
4862
|
this.scanCache = null;
|
|
4770
4863
|
}
|
|
4771
4864
|
commitRow(row) {
|
|
4865
|
+
assertRowShape(this, row);
|
|
4772
4866
|
this.indexEquality(row);
|
|
4773
4867
|
this.invalidateScan();
|
|
4774
4868
|
}
|
|
@@ -4991,16 +5085,11 @@ var Table = class _Table {
|
|
|
4991
5085
|
copy.nextRowid = this.nextRowid;
|
|
4992
5086
|
copy.maximumRowid = this.maximumRowid;
|
|
4993
5087
|
if (this.slab) {
|
|
4994
|
-
|
|
4995
|
-
for (const row of this.slab.scan()) {
|
|
4996
|
-
copy.rows.set(row.rowid, cloneRow(row));
|
|
4997
|
-
if (max === null || compareRowids(row.rowid, max) > 0) max = row.rowid;
|
|
4998
|
-
}
|
|
4999
|
-
copy.maximumRowid = max ?? void 0;
|
|
5088
|
+
for (const row of this.slab.scan()) copy.rows.set(row.rowid, row);
|
|
5000
5089
|
} else {
|
|
5001
|
-
for (const [rowid, row] of this.rows) copy.rows.set(rowid,
|
|
5090
|
+
for (const [rowid, row] of this.rows) copy.rows.set(rowid, row);
|
|
5002
5091
|
}
|
|
5003
|
-
for (const [clusterKey, row] of this.clusteredRows) copy.clusteredRows.set(clusterKey,
|
|
5092
|
+
for (const [clusterKey, row] of this.clusteredRows) copy.clusteredRows.set(clusterKey, row);
|
|
5004
5093
|
return copy;
|
|
5005
5094
|
}
|
|
5006
5095
|
freeze() {
|
|
@@ -9297,6 +9386,7 @@ var scalarFunctions = {
|
|
|
9297
9386
|
requireArgs3("randomblob", args, 1);
|
|
9298
9387
|
if (args[0] === null) return null;
|
|
9299
9388
|
const length = Math.max(0, Math.trunc(numeric(args[0])));
|
|
9389
|
+
assertBlobLength(length, "randomblob");
|
|
9300
9390
|
const out = new Uint8Array(length);
|
|
9301
9391
|
let offset = 0;
|
|
9302
9392
|
while (offset < length) {
|
|
@@ -9311,6 +9401,7 @@ var scalarFunctions = {
|
|
|
9311
9401
|
requireArgs3("zeroblob", args, 1);
|
|
9312
9402
|
if (args[0] === null) return null;
|
|
9313
9403
|
const length = Math.max(0, Math.trunc(numeric(args[0])));
|
|
9404
|
+
assertBlobLength(length, "zeroblob");
|
|
9314
9405
|
return new Uint8Array(length);
|
|
9315
9406
|
},
|
|
9316
9407
|
hex(args) {
|
|
@@ -9676,6 +9767,10 @@ var ExecutionEnv = class {
|
|
|
9676
9767
|
triggerScope = null;
|
|
9677
9768
|
/** Cap SELECT output rows (used by Statement.get). */
|
|
9678
9769
|
maxRows = Number.POSITIVE_INFINITY;
|
|
9770
|
+
/** When true, skip simple-select / simple-join fast paths (tests only). */
|
|
9771
|
+
forceFullSelect = false;
|
|
9772
|
+
/** When true, skip tryFastInsert (tests only). */
|
|
9773
|
+
forceFullInsert = false;
|
|
9679
9774
|
includeNamedRows = true;
|
|
9680
9775
|
includeValues = true;
|
|
9681
9776
|
/** Source text of the statement currently executing (for sqlite_master.sql). */
|
|
@@ -9696,6 +9791,8 @@ var ExecutionEnv = class {
|
|
|
9696
9791
|
this.triggerDepth = 0;
|
|
9697
9792
|
this.triggerScope = null;
|
|
9698
9793
|
this.maxRows = Number.POSITIVE_INFINITY;
|
|
9794
|
+
this.forceFullSelect = false;
|
|
9795
|
+
this.forceFullInsert = false;
|
|
9699
9796
|
this.includeNamedRows = true;
|
|
9700
9797
|
this.includeValues = true;
|
|
9701
9798
|
this.statementSql = null;
|
|
@@ -9987,10 +10084,19 @@ function evalBinary(op, leftExpr, rightExpr, ctx) {
|
|
|
9987
10084
|
if (left === null || right === null) return null;
|
|
9988
10085
|
switch (op) {
|
|
9989
10086
|
case "+":
|
|
10087
|
+
if (storageClassOf(left) === "integer" && storageClassOf(right) === "integer") {
|
|
10088
|
+
return integerArithmetic("+", left, right);
|
|
10089
|
+
}
|
|
9990
10090
|
return asNumber(numberValue(left) + numberValue(right));
|
|
9991
10091
|
case "-":
|
|
10092
|
+
if (storageClassOf(left) === "integer" && storageClassOf(right) === "integer") {
|
|
10093
|
+
return integerArithmetic("-", left, right);
|
|
10094
|
+
}
|
|
9992
10095
|
return asNumber(numberValue(left) - numberValue(right));
|
|
9993
10096
|
case "*":
|
|
10097
|
+
if (storageClassOf(left) === "integer" && storageClassOf(right) === "integer") {
|
|
10098
|
+
return integerArithmetic("*", left, right);
|
|
10099
|
+
}
|
|
9994
10100
|
return asNumber(numberValue(left) * numberValue(right));
|
|
9995
10101
|
case "/": {
|
|
9996
10102
|
const divisor = numberValue(right);
|
|
@@ -10027,6 +10133,17 @@ function evalBinary(op, leftExpr, rightExpr, ctx) {
|
|
|
10027
10133
|
function safeIntegerResult(value) {
|
|
10028
10134
|
return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(Number.MIN_SAFE_INTEGER) ? Number(value) : value;
|
|
10029
10135
|
}
|
|
10136
|
+
var I64_MIN = -(2n ** 63n);
|
|
10137
|
+
var I64_MAX = 2n ** 63n - 1n;
|
|
10138
|
+
function integerArithmetic(op, left, right) {
|
|
10139
|
+
const a = integerValue(left);
|
|
10140
|
+
const b = integerValue(right);
|
|
10141
|
+
const result = op === "+" ? a + b : op === "-" ? a - b : a * b;
|
|
10142
|
+
if (result < I64_MIN || result > I64_MAX) {
|
|
10143
|
+
return asSqlReal(Number(result));
|
|
10144
|
+
}
|
|
10145
|
+
return safeIntegerResult(result);
|
|
10146
|
+
}
|
|
10030
10147
|
function compareRowValues(op, leftExprs, rightExprs, ctx) {
|
|
10031
10148
|
if (leftExprs.length !== rightExprs.length) {
|
|
10032
10149
|
throw new SqliteError("row value misused", "misuse");
|
|
@@ -11255,6 +11372,11 @@ function valuesEqual(left, right) {
|
|
|
11255
11372
|
return false;
|
|
11256
11373
|
}
|
|
11257
11374
|
|
|
11375
|
+
// src/runtime/catch.ts
|
|
11376
|
+
function isExpectedFastPathMiss(error, categories = ["no_such_table"]) {
|
|
11377
|
+
return error instanceof SqliteError && categories.includes(error.category);
|
|
11378
|
+
}
|
|
11379
|
+
|
|
11258
11380
|
// src/planner/access.ts
|
|
11259
11381
|
function conjunctions(expr) {
|
|
11260
11382
|
if (expr.type === "binary" && expr.op === "AND") {
|
|
@@ -11318,7 +11440,8 @@ function lookupTableRows(from, where, env) {
|
|
|
11318
11440
|
let table;
|
|
11319
11441
|
try {
|
|
11320
11442
|
table = db.getTable(from.name);
|
|
11321
|
-
} catch {
|
|
11443
|
+
} catch (error) {
|
|
11444
|
+
if (!isExpectedFastPathMiss(error)) throw error;
|
|
11322
11445
|
return null;
|
|
11323
11446
|
}
|
|
11324
11447
|
const parts = conjunctions(where);
|
|
@@ -11326,8 +11449,9 @@ function lookupTableRows(from, where, env) {
|
|
|
11326
11449
|
const evalConst = (expr) => {
|
|
11327
11450
|
try {
|
|
11328
11451
|
return evalExpr(expr, env.createEvalContext(null));
|
|
11329
|
-
} catch {
|
|
11330
|
-
return void 0;
|
|
11452
|
+
} catch (error) {
|
|
11453
|
+
if (error instanceof SqliteError) return void 0;
|
|
11454
|
+
throw error;
|
|
11331
11455
|
}
|
|
11332
11456
|
};
|
|
11333
11457
|
const resolved = [];
|
|
@@ -11474,7 +11598,8 @@ function tryIndexedOrder(from, order, env, limit) {
|
|
|
11474
11598
|
let table;
|
|
11475
11599
|
try {
|
|
11476
11600
|
table = db.getTable(from.name);
|
|
11477
|
-
} catch {
|
|
11601
|
+
} catch (error) {
|
|
11602
|
+
if (!isExpectedFastPathMiss(error)) throw error;
|
|
11478
11603
|
return null;
|
|
11479
11604
|
}
|
|
11480
11605
|
const col = order.expr.name.toLowerCase();
|
|
@@ -11708,7 +11833,8 @@ function buildSqliteMaster(state) {
|
|
|
11708
11833
|
function tryExecuteSimpleSelect(stmt, env) {
|
|
11709
11834
|
if (stmt.with || stmt.compound || stmt.distinct || stmt.groupBy.length > 0 || stmt.having || stmt.windows.length > 0)
|
|
11710
11835
|
return null;
|
|
11711
|
-
if (stmt.orderBy.length >
|
|
11836
|
+
if (stmt.orderBy.length > 1) return null;
|
|
11837
|
+
if (stmt.orderBy.length === 1 && stmt.where) return null;
|
|
11712
11838
|
if (stmt.from?.type !== "table") return null;
|
|
11713
11839
|
for (const column of stmt.columns) {
|
|
11714
11840
|
if (column.type === "star") continue;
|
|
@@ -11724,7 +11850,8 @@ function tryExecuteSimpleSelect(stmt, env) {
|
|
|
11724
11850
|
let table;
|
|
11725
11851
|
try {
|
|
11726
11852
|
table = db.getTable(from.name);
|
|
11727
|
-
} catch {
|
|
11853
|
+
} catch (error) {
|
|
11854
|
+
if (!isExpectedFastPathMiss(error)) throw error;
|
|
11728
11855
|
return null;
|
|
11729
11856
|
}
|
|
11730
11857
|
if (table.withoutRowid) return null;
|
|
@@ -11749,8 +11876,9 @@ function tryExecuteSimpleSelect(stmt, env) {
|
|
|
11749
11876
|
let value;
|
|
11750
11877
|
try {
|
|
11751
11878
|
value = evalIndependent(eq.valueExpr, env);
|
|
11752
|
-
} catch {
|
|
11753
|
-
return null;
|
|
11879
|
+
} catch (error) {
|
|
11880
|
+
if (error instanceof SqliteError) return null;
|
|
11881
|
+
throw error;
|
|
11754
11882
|
}
|
|
11755
11883
|
const column = eq.column.toLowerCase();
|
|
11756
11884
|
const affinity = isRowidName2(column) ? "INTEGER" : table.columns.find((item) => (item.nameLower ?? item.name.toLowerCase()) === column)?.affinity;
|
|
@@ -11767,19 +11895,26 @@ function tryExecuteSimpleSelect(stmt, env) {
|
|
|
11767
11895
|
offset = Math.max(0, Number(offsetValue));
|
|
11768
11896
|
const n = Number(limitValue);
|
|
11769
11897
|
if (n >= 0) limit = Math.min(limit, n);
|
|
11770
|
-
} catch {
|
|
11771
|
-
return null;
|
|
11898
|
+
} catch (error) {
|
|
11899
|
+
if (error instanceof SqliteError) return null;
|
|
11900
|
+
throw error;
|
|
11772
11901
|
}
|
|
11773
11902
|
}
|
|
11774
11903
|
const columns = projectNames(stmt.columns, table);
|
|
11775
11904
|
if (limit <= 0) return pack(env, columns, []);
|
|
11776
11905
|
let source = table.scan();
|
|
11777
11906
|
let alreadyFiltered = false;
|
|
11778
|
-
if (stmt.
|
|
11907
|
+
if (stmt.orderBy.length === 1 && stmt.orderBy[0]) {
|
|
11908
|
+
const take = limit < Number.MAX_SAFE_INTEGER ? offset + limit : void 0;
|
|
11909
|
+
const ordered = tryIndexedOrder(from, { expr: stmt.orderBy[0].expr, dir: stmt.orderBy[0].dir ?? "ASC" }, env, take);
|
|
11910
|
+
if (!ordered) return null;
|
|
11911
|
+
source = ordered.rows;
|
|
11912
|
+
alreadyFiltered = true;
|
|
11913
|
+
} else if (stmt.where) {
|
|
11779
11914
|
const indexed = lookupTableRows(from, stmt.where, env);
|
|
11780
11915
|
if (indexed) {
|
|
11781
11916
|
source = indexed.rows;
|
|
11782
|
-
alreadyFiltered =
|
|
11917
|
+
alreadyFiltered = whereFullyCovered(stmt.where, indexed.coveredColumns, alias, table.name);
|
|
11783
11918
|
}
|
|
11784
11919
|
}
|
|
11785
11920
|
const projectors = projectionFns(stmt.columns, table);
|
|
@@ -11825,7 +11960,8 @@ function tryExecuteSimpleJoin(stmt, env) {
|
|
|
11825
11960
|
try {
|
|
11826
11961
|
leftTable = leftDb.getTable(leftRef.name);
|
|
11827
11962
|
rightTable = rightDb.getTable(rightRef.name);
|
|
11828
|
-
} catch {
|
|
11963
|
+
} catch (error) {
|
|
11964
|
+
if (!isExpectedFastPathMiss(error)) throw error;
|
|
11829
11965
|
return null;
|
|
11830
11966
|
}
|
|
11831
11967
|
if (leftTable.withoutRowid || rightTable.withoutRowid) return null;
|
|
@@ -11910,7 +12046,7 @@ function evalIndependent(expr, env) {
|
|
|
11910
12046
|
if (expr.type === "null") return null;
|
|
11911
12047
|
if (expr.type === "parameter") return env.getBoundParameter(expr.name);
|
|
11912
12048
|
if (expr.type === "unary" && expr.op === "-" && expr.expr.type === "literal" && typeof expr.expr.value === "number") {
|
|
11913
|
-
return -expr.expr.value;
|
|
12049
|
+
return canonicalizeNumber(-expr.expr.value);
|
|
11914
12050
|
}
|
|
11915
12051
|
throw new Error("not independent");
|
|
11916
12052
|
}
|
|
@@ -11930,7 +12066,7 @@ function applyAffinityLocal(value, affinity) {
|
|
|
11930
12066
|
}
|
|
11931
12067
|
function executeSelect2(stmt, env, parent) {
|
|
11932
12068
|
env.selectRunner = executeSelect2;
|
|
11933
|
-
if (!parent && !stmt.with && !stmt.compound) {
|
|
12069
|
+
if (!parent && !stmt.with && !stmt.compound && !env.forceFullSelect) {
|
|
11934
12070
|
const simple = tryExecuteSimpleSelect(stmt, env) ?? tryExecuteSimpleJoin(stmt, env);
|
|
11935
12071
|
if (simple) return simple;
|
|
11936
12072
|
}
|
|
@@ -13524,7 +13660,7 @@ function executeInsertCore(stmt, env) {
|
|
|
13524
13660
|
const totalBefore = env.state.totalChanges;
|
|
13525
13661
|
const view = writableView(stmt.table, "INSERT", env);
|
|
13526
13662
|
if (view) return executeViewInsert(stmt, view, env, totalBefore);
|
|
13527
|
-
const fast = tryFastInsert(stmt, env);
|
|
13663
|
+
const fast = env.forceFullInsert ? null : tryFastInsert(stmt, env);
|
|
13528
13664
|
if (fast) return fast;
|
|
13529
13665
|
let table = env.state.getWritableTable(stmt.table);
|
|
13530
13666
|
const columnNames = stmt.columns ?? table.columns.map((column) => column.name);
|
|
@@ -13727,7 +13863,8 @@ function tryFastInsert(stmt, env) {
|
|
|
13727
13863
|
let table;
|
|
13728
13864
|
try {
|
|
13729
13865
|
table = env.state.getWritableTable(plan.tableName);
|
|
13730
|
-
} catch {
|
|
13866
|
+
} catch (error) {
|
|
13867
|
+
if (!isExpectedFastPathMiss(error)) throw error;
|
|
13731
13868
|
return null;
|
|
13732
13869
|
}
|
|
13733
13870
|
if (env.state.databaseForTable(table).triggers.size > 0) return null;
|
|
@@ -13742,7 +13879,8 @@ function buildFastInsertPlan(stmt, env) {
|
|
|
13742
13879
|
let table;
|
|
13743
13880
|
try {
|
|
13744
13881
|
table = env.state.getTable(stmt.table);
|
|
13745
|
-
} catch {
|
|
13882
|
+
} catch (error) {
|
|
13883
|
+
if (!isExpectedFastPathMiss(error)) throw error;
|
|
13746
13884
|
return null;
|
|
13747
13885
|
}
|
|
13748
13886
|
if (!table.isUnconstrained()) return null;
|
|
@@ -14330,16 +14468,91 @@ function assertForeignKeyValues(values, constraint, env, excludeParent) {
|
|
|
14330
14468
|
if (values.some((value) => value === null)) return;
|
|
14331
14469
|
const parent = env.state.getTable(constraint.refTable);
|
|
14332
14470
|
const parentColumns = constraint.refColumns ?? parent.columns.filter((column) => column.primaryKey).map((column) => column.name);
|
|
14333
|
-
if (
|
|
14334
|
-
if (excludeParent && candidate.rowid === excludeParent.rowid) return false;
|
|
14335
|
-
return values.every((value, index) => {
|
|
14336
|
-
const parentColumn = parentColumns[index];
|
|
14337
|
-
return parentColumn !== void 0 && compareSql(value, parent.cell(candidate, normalizeColumnName(parentColumn))) === 0;
|
|
14338
|
-
});
|
|
14339
|
-
})) {
|
|
14471
|
+
if (findParentRowsForFk(parent, parentColumns, values, env, excludeParent).length === 0) {
|
|
14340
14472
|
throw new SqliteError("FOREIGN KEY constraint failed", "constraint_foreign", "SQLITE_CONSTRAINT_FOREIGNKEY");
|
|
14341
14473
|
}
|
|
14342
14474
|
}
|
|
14475
|
+
function parentRowMatchesFk(parent, parentColumns, values, excludeParent, candidate) {
|
|
14476
|
+
if (excludeParent && candidate.rowid === excludeParent.rowid) return false;
|
|
14477
|
+
return values.every((value, index) => {
|
|
14478
|
+
const parentColumn = parentColumns[index];
|
|
14479
|
+
return parentColumn !== void 0 && compareSql(value, parent.cell(candidate, normalizeColumnName(parentColumn))) === 0;
|
|
14480
|
+
});
|
|
14481
|
+
}
|
|
14482
|
+
function findParentRowsForFk(parent, parentColumns, values, env, excludeParent) {
|
|
14483
|
+
const db = env.state.databaseForTable(parent);
|
|
14484
|
+
const pk = parent.integerPkColumn();
|
|
14485
|
+
if (parentColumns.length === 1) {
|
|
14486
|
+
const col = parentColumns[0].toLowerCase();
|
|
14487
|
+
if (col === "rowid" || col === "_rowid_" || col === "oid" || pk && col === (pk.nameLower ?? pk.name.toLowerCase())) {
|
|
14488
|
+
const row = parent.getByKey(values[0]);
|
|
14489
|
+
if (row && parentRowMatchesFk(parent, parentColumns, values, excludeParent, row)) return [row];
|
|
14490
|
+
return [];
|
|
14491
|
+
}
|
|
14492
|
+
}
|
|
14493
|
+
const normalizedValues = values.map((value, index) => {
|
|
14494
|
+
const colName = parentColumns[index];
|
|
14495
|
+
const column = parent.columns.find((item) => (item.nameLower ?? item.name.toLowerCase()) === colName.toLowerCase());
|
|
14496
|
+
return normalizeForCollation(value, column?.collate ?? "BINARY");
|
|
14497
|
+
});
|
|
14498
|
+
for (const indexName of parent.indexes) {
|
|
14499
|
+
const index = db.indexes.get(indexName.toLowerCase());
|
|
14500
|
+
if (!index?.unique || index.where || index.columns.some((column) => column.expr)) continue;
|
|
14501
|
+
if (index.columns.length !== parentColumns.length) continue;
|
|
14502
|
+
if (!index.columns.every((column, idx) => column.name.toLowerCase() === parentColumns[idx].toLowerCase()))
|
|
14503
|
+
continue;
|
|
14504
|
+
const rows = [];
|
|
14505
|
+
for (const rowid of index.store.lookup(normalizedValues)) {
|
|
14506
|
+
const candidate = parent.get(rowid);
|
|
14507
|
+
if (candidate && parentRowMatchesFk(parent, parentColumns, values, excludeParent, candidate))
|
|
14508
|
+
rows.push(candidate);
|
|
14509
|
+
}
|
|
14510
|
+
return rows;
|
|
14511
|
+
}
|
|
14512
|
+
return [...parent.scan()].filter(
|
|
14513
|
+
(candidate) => parentRowMatchesFk(parent, parentColumns, values, excludeParent, candidate)
|
|
14514
|
+
);
|
|
14515
|
+
}
|
|
14516
|
+
function findChildRowsForFk(child, childColumns, parentColumns, parent, parentRow, env) {
|
|
14517
|
+
const refValues = parentColumns.map((name) => parent.cell(parentRow, normalizeColumnName(name)));
|
|
14518
|
+
if (refValues.some((value) => value === null)) return [];
|
|
14519
|
+
if (childColumns.length === 1) {
|
|
14520
|
+
const col = childColumns[0].toLowerCase();
|
|
14521
|
+
const matches = child.lookupEquality(col, refValues[0]);
|
|
14522
|
+
if (matches) {
|
|
14523
|
+
return matches.filter(
|
|
14524
|
+
(candidate) => foreignKeyMatches(childColumns, candidate, parentColumns, parentRow, child, parent)
|
|
14525
|
+
);
|
|
14526
|
+
}
|
|
14527
|
+
}
|
|
14528
|
+
const db = env.state.databaseForTable(child);
|
|
14529
|
+
const normalizedValues = refValues.map((value, index) => {
|
|
14530
|
+
const colName = childColumns[index];
|
|
14531
|
+
const column = child.columns.find((item) => (item.nameLower ?? item.name.toLowerCase()) === colName.toLowerCase());
|
|
14532
|
+
return normalizeForCollation(value, column?.collate ?? "BINARY");
|
|
14533
|
+
});
|
|
14534
|
+
for (const indexName of child.indexes) {
|
|
14535
|
+
const index = db.indexes.get(indexName.toLowerCase());
|
|
14536
|
+
if (!index || index.where || index.columns.some((column) => column.expr)) continue;
|
|
14537
|
+
if (index.columns.length < childColumns.length) continue;
|
|
14538
|
+
if (!childColumns.every((name, idx) => index.columns[idx]?.name.toLowerCase() === name.toLowerCase())) {
|
|
14539
|
+
continue;
|
|
14540
|
+
}
|
|
14541
|
+
const lookupValues = normalizedValues.slice(0, index.columns.length);
|
|
14542
|
+
const rowids = lookupValues.length === index.columns.length ? index.store.lookup(lookupValues) : index.store.lookupPrefix(lookupValues);
|
|
14543
|
+
const rows = [];
|
|
14544
|
+
for (const rowid of rowids) {
|
|
14545
|
+
const candidate = child.get(rowid);
|
|
14546
|
+
if (candidate && foreignKeyMatches(childColumns, candidate, parentColumns, parentRow, child, parent)) {
|
|
14547
|
+
rows.push(candidate);
|
|
14548
|
+
}
|
|
14549
|
+
}
|
|
14550
|
+
return rows;
|
|
14551
|
+
}
|
|
14552
|
+
return [...child.scan()].filter(
|
|
14553
|
+
(candidate) => foreignKeyMatches(childColumns, candidate, parentColumns, parentRow, child, parent)
|
|
14554
|
+
);
|
|
14555
|
+
}
|
|
14343
14556
|
function checkDeferredForeignKeys(env) {
|
|
14344
14557
|
if (!env.state.foreignKeysEnabled) return;
|
|
14345
14558
|
for (const table of env.state.tables.values()) {
|
|
@@ -14362,8 +14575,8 @@ function applyReferentialDelete(parent, row, env) {
|
|
|
14362
14575
|
child = env.state.ensureWritableTable(child);
|
|
14363
14576
|
const target = child;
|
|
14364
14577
|
const referenced = constraint.refColumns ?? parentPk;
|
|
14365
|
-
const matches =
|
|
14366
|
-
(candidate) => !(target === parent && candidate.rowid === row.rowid)
|
|
14578
|
+
const matches = findChildRowsForFk(target, constraint.columns, referenced, parent, row, env).filter(
|
|
14579
|
+
(candidate) => !(target === parent && candidate.rowid === row.rowid)
|
|
14367
14580
|
);
|
|
14368
14581
|
for (const candidate of matches) {
|
|
14369
14582
|
if (constraint.onDelete === "CASCADE") {
|
|
@@ -14409,9 +14622,7 @@ function applyReferentialUpdate(parent, before, after, env) {
|
|
|
14409
14622
|
const oldValues = referenced.map((name) => parent.cell(before, normalizeColumnName(name)));
|
|
14410
14623
|
const newValues = referenced.map((name) => parent.cell(after, normalizeColumnName(name)));
|
|
14411
14624
|
if (oldValues.every((value, index) => compareSql(value, newValues[index] ?? null) === 0)) continue;
|
|
14412
|
-
const matches =
|
|
14413
|
-
(candidate) => foreignKeyMatches(constraint.columns, candidate, referenced, before, target, parent)
|
|
14414
|
-
);
|
|
14625
|
+
const matches = findChildRowsForFk(target, constraint.columns, referenced, parent, before, env);
|
|
14415
14626
|
for (const candidate of matches) {
|
|
14416
14627
|
if (constraint.onUpdate === "CASCADE") {
|
|
14417
14628
|
const updated = updateOne(
|
|
@@ -14801,6 +15012,8 @@ function executeStatement(stmt, env) {
|
|
|
14801
15012
|
case "vacuum":
|
|
14802
15013
|
env.state.recordChange(0);
|
|
14803
15014
|
return emptyResult(0, env.state.lastInsertRowid);
|
|
15015
|
+
default:
|
|
15016
|
+
return assertUnreachable(stmt);
|
|
14804
15017
|
}
|
|
14805
15018
|
}
|
|
14806
15019
|
function executeReindex(stmt, env) {
|