@crvouga/sqlite-mem 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,10 +6,11 @@ Pure TypeScript, completely in-memory SQLite implementation aiming for **full SQ
6
6
  - **Zero** WASM, native bindings, workers, or filesystem dependencies
7
7
  - Entire database stored in memory
8
8
  - **Synchronous** ESM-only API (no Promises, no `require`)
9
- - **Verified against SQLite 3.51.0** (`bun:sqlite`) via differential contracts + fail-closed gate
9
+ - **SQL dialect verified** against SQLite 3.51.0 / 3.53.0 (`bun:sqlite`) via differential contracts + fail-closed gate
10
+ - **Not** a drop-in for `sql.js` / `sqlite-wasm` APIs, on-disk `.sqlite` files, or user-defined functions
10
11
  - Intentional differences: deterministic `random()` / `'now'` by default, and a custom snapshot format (not `.sqlite` files)
11
12
 
12
- See [COMPATIBILITY.md](COMPATIBILITY.md) for the matrix and [COMPATIBILITY-AUDIT.md](COMPATIBILITY-AUDIT.md) for the audit report. Agents contributing to this repo: start with [AGENTS.md](AGENTS.md).
13
+ See [COMPATIBILITY.md](COMPATIBILITY.md) for the matrix, [docs/DROP-IN-CONTRACT.md](docs/DROP-IN-CONTRACT.md) for the falsifiable claim, and [docs/GAP-ANALYSIS.md](docs/GAP-ANALYSIS.md) for what is still unproven. Agents: [AGENTS.md](AGENTS.md).
13
14
 
14
15
  ## Documentation
15
16
 
@@ -19,6 +20,11 @@ See [COMPATIBILITY.md](COMPATIBILITY.md) for the matrix and [COMPATIBILITY-AUDIT
19
20
  | [AGENTS.md](AGENTS.md) | Architecture, how to change code, test/compat gates |
20
21
  | [COMPATIBILITY.md](COMPATIBILITY.md) | Feature matrix + verify commands |
21
22
  | [COMPATIBILITY-AUDIT.md](COMPATIBILITY-AUDIT.md) | Audit evidence |
23
+ | [docs/DROP-IN-CONTRACT.md](docs/DROP-IN-CONTRACT.md) | Falsifiable drop-in claim (what “same” means) |
24
+ | [docs/PROOF.md](docs/PROOF.md) | Evidence argument + what is not proven |
25
+ | [docs/GAP-ANALYSIS.md](docs/GAP-ANALYSIS.md) | Phase 0 gap analysis vs full drop-in catalog |
26
+ | [docs/GAP-CATALOG.md](docs/GAP-CATALOG.md) | Current unproven / thin / intentional inventory |
27
+ | [DIVERGENCES.md](DIVERGENCES.md) | Auto-generated intentional divergences |
22
28
  | [docs/SECRETS.md](docs/SECRETS.md) | npm / CI publish setup |
23
29
  | [benchmarks/PERFORMANCE.md](benchmarks/PERFORMANCE.md) | Performance notes |
24
30
 
@@ -208,9 +214,11 @@ The exports of the main entry (`@crvouga/sqlite-mem`) are **frozen**:
208
214
 
209
215
  ## Compatibility notes for integrators
210
216
 
211
- Goal: drop-in SQL behavior vs SQLite **3.51.0**. Full matrix: [COMPATIBILITY.md](COMPATIBILITY.md).
217
+ Goal: **SQL dialect** behavioral parity vs SQLite **3.51.0** / **3.53.0** for the `@crvouga/sqlite-mem` sync API. Full matrix: [COMPATIBILITY.md](COMPATIBILITY.md). Contract: [docs/DROP-IN-CONTRACT.md](docs/DROP-IN-CONTRACT.md).
212
218
 
213
- **Intentional differences:** custom `SQLM` snapshots; seeded `random()` / fixed `'now'` by default (`{ random: "os" }` / `{ now: "system" }` match SQLite entropy and wall clock); no C API / on-disk DB / VFS.
219
+ This is **not** a drop-in replacement for `sql.js`, `@sqlite.org/sqlite-wasm`, or better-sqlite3’s full Node API. There is no `.sqlite` file codec, no `create_function` / custom collations, no `stmt.step()` / `iterate()`, and `ATTACH 'file'` opens an empty in-memory schema.
220
+
221
+ **Intentional differences:** custom `SQLM` snapshots; seeded `random()` / fixed `'now'` by default (`{ random: "os" }` / `{ now: "system" }` match SQLite entropy and wall clock); no C API / on-disk DB / VFS. Machine-readable list: [DIVERGENCES.md](DIVERGENCES.md).
214
222
 
215
223
  **Know these thin or partial areas** (do not assume full oracle fidelity):
216
224
 
@@ -48,7 +48,7 @@
48
48
  "scope": "select",
49
49
  "predicate": "INDEXED BY / NOT INDEXED is accepted and ignored",
50
50
  "specifiedBehavior": "Query results match the unhinted plan; missing-index errors are not raised.",
51
- "pinnedBy": ["COMPATIBILITY.md"]
51
+ "pinnedBy": ["indexes/indexed-by.test.ts", "COMPATIBILITY.md"]
52
52
  },
53
53
  {
54
54
  "id": "materialized-hint-ignored",
@@ -31,6 +31,7 @@ export declare class Statement {
31
31
  private namedPlan;
32
32
  private env;
33
33
  private statements;
34
+ private statementSqls;
34
35
  private schemaVersion;
35
36
  private constructor();
36
37
  /**
@@ -51,6 +51,8 @@ export declare class ExecutionEnv {
51
51
  maxRows: number;
52
52
  includeNamedRows: boolean;
53
53
  includeValues: boolean;
54
+ /** Source text of the statement currently executing (for sqlite_master.sql). */
55
+ statementSql: string | null;
54
56
  constructor(state: DatabaseState, transactions: TransactionManager, params?: readonly unknown[], functions?: FunctionRegistry, hooks?: ExecutionHooks);
55
57
  reset(params: readonly unknown[]): void;
56
58
  getBoundParameter(name: string | number): SqlValue;
package/dist/index.js CHANGED
@@ -640,10 +640,12 @@ var PREC = {
640
640
  JSON_ARROW: 80
641
641
  };
642
642
  var Parser = class {
643
- constructor(tokens) {
643
+ constructor(tokens, source = "") {
644
644
  this.tokens = tokens;
645
+ this.source = source;
645
646
  }
646
647
  tokens;
648
+ source;
647
649
  pos = 0;
648
650
  current() {
649
651
  return this.tokens[this.pos] ?? this.tokens[this.tokens.length - 1];
@@ -727,13 +729,21 @@ var Parser = class {
727
729
  }
728
730
  // ── Statements ──────────────────────────────────────────────────────────
729
731
  parseStatements() {
730
- const stmts = [];
732
+ return this.parseUnits().map((unit) => unit.statement);
733
+ }
734
+ /** Parse statements with per-statement source slices for catalog `sql` text. */
735
+ parseUnits() {
736
+ const units = [];
731
737
  while (!this.at("EOF")) {
732
738
  if (this.match("SEMI")) continue;
733
- stmts.push(this.parseStatement());
739
+ const start = this.current().start;
740
+ const statement = this.parseStatement();
741
+ const end = this.pos > 0 ? this.tokens[this.pos - 1].end : this.current().end;
734
742
  this.match("SEMI");
743
+ const sql = this.source ? this.source.slice(start, end).trimEnd() : "";
744
+ units.push({ statement, sql });
735
745
  }
736
- return stmts;
746
+ return units;
737
747
  }
738
748
  parseStatement() {
739
749
  if (this.match("EXPLAIN")) {
@@ -2359,14 +2369,13 @@ var Parser = class {
2359
2369
  return { kind: "following", expr };
2360
2370
  }
2361
2371
  };
2362
- function parseTokens(tokens) {
2363
- return new Parser(tokens).parseStatements();
2372
+ function parseTokenUnits(tokens, source) {
2373
+ return new Parser(tokens, source).parseUnits();
2364
2374
  }
2365
2375
 
2366
2376
  // src/parser/index.ts
2367
- function parse(sql) {
2368
- const tokens = tokenize(sql);
2369
- return parseTokens(tokens);
2377
+ function parseUnits(sql) {
2378
+ return parseTokenUnits(tokenize(sql), sql);
2370
2379
  }
2371
2380
 
2372
2381
  // src/runtime/clock.ts
@@ -9160,6 +9169,8 @@ var ExecutionEnv = class {
9160
9169
  maxRows = Number.POSITIVE_INFINITY;
9161
9170
  includeNamedRows = true;
9162
9171
  includeValues = true;
9172
+ /** Source text of the statement currently executing (for sqlite_master.sql). */
9173
+ statementSql = null;
9163
9174
  constructor(state, transactions, params = [], functions = defaultFunctionRegistry, hooks = {}) {
9164
9175
  this.state = state;
9165
9176
  this.transactions = transactions;
@@ -9178,6 +9189,7 @@ var ExecutionEnv = class {
9178
9189
  this.maxRows = Number.POSITIVE_INFINITY;
9179
9190
  this.includeNamedRows = true;
9180
9191
  this.includeValues = true;
9192
+ this.statementSql = null;
9181
9193
  }
9182
9194
  getBoundParameter(name) {
9183
9195
  if (typeof name === "number") {
@@ -9410,6 +9422,78 @@ function executeDetach(stmt, env) {
9410
9422
  return emptyResult(0, env.state.lastInsertRowid);
9411
9423
  }
9412
9424
 
9425
+ // src/schema/master-sql.ts
9426
+ function normalizeMasterSql(sql) {
9427
+ const trimmed = sql.trim().replace(/;+\s*$/u, "").trim();
9428
+ if (!trimmed) return trimmed;
9429
+ const tokens = tokenize(trimmed);
9430
+ if (tokens.length === 0 || tokens[0].kind === "EOF") return trimmed;
9431
+ let i = 0;
9432
+ const at = (offset, ...kinds) => {
9433
+ const tok = tokens[i + offset];
9434
+ return tok !== void 0 && kinds.includes(tok.kind);
9435
+ };
9436
+ if (!at(0, "CREATE")) return trimmed;
9437
+ i++;
9438
+ if (at(0, "TEMP", "TEMPORARY")) i++;
9439
+ let headKind = "table";
9440
+ if (at(0, "UNIQUE") && at(1, "INDEX")) {
9441
+ headKind = "unique_index";
9442
+ i += 2;
9443
+ } else if (at(0, "VIRTUAL") && at(1, "TABLE")) {
9444
+ headKind = "vtable";
9445
+ i += 2;
9446
+ } else if (at(0, "TABLE")) {
9447
+ headKind = "table";
9448
+ i++;
9449
+ } else if (at(0, "VIEW")) {
9450
+ headKind = "view";
9451
+ i++;
9452
+ } else if (at(0, "INDEX")) {
9453
+ headKind = "index";
9454
+ i++;
9455
+ } else if (at(0, "TRIGGER")) {
9456
+ headKind = "trigger";
9457
+ i++;
9458
+ } else {
9459
+ return trimmed;
9460
+ }
9461
+ if (at(0, "IF") && at(1, "NOT") && at(2, "EXISTS")) i += 3;
9462
+ let nameText = "";
9463
+ let nameEnd = 0;
9464
+ if (tokens[i] && tokens[i].kind !== "EOF" && at(1, "DOT") && tokens[i + 2] && tokens[i + 2].kind !== "EOF") {
9465
+ nameText = trimmed.slice(tokens[i + 2].start, tokens[i + 2].end);
9466
+ nameEnd = tokens[i + 2].end;
9467
+ i += 3;
9468
+ } else if (tokens[i] && tokens[i].kind !== "EOF") {
9469
+ nameText = trimmed.slice(tokens[i].start, tokens[i].end);
9470
+ nameEnd = tokens[i].end;
9471
+ i++;
9472
+ }
9473
+ const body = nameEnd < trimmed.length ? trimmed.slice(nameEnd) : "";
9474
+ const needsSpace = body.length > 0 && !/^[\s(]/u.test(body);
9475
+ const head = headKind === "unique_index" ? "CREATE UNIQUE INDEX" : headKind === "vtable" ? "CREATE VIRTUAL TABLE" : headKind === "view" ? "CREATE VIEW" : headKind === "index" ? "CREATE INDEX" : headKind === "trigger" ? "CREATE TRIGGER" : "CREATE TABLE";
9476
+ if (body.length === 0) return `${head} ${nameText}`;
9477
+ return needsSpace ? `${head} ${nameText} ${body}` : `${head} ${nameText}${body}`;
9478
+ }
9479
+ function appendAddColumnToMasterSql(originalSql, alterSql) {
9480
+ if (!originalSql || !alterSql) return originalSql;
9481
+ const match = alterSql.match(/\bADD\s+(?:COLUMN\s+)?(.+)$/iu);
9482
+ if (!match?.[1]) return originalSql;
9483
+ const colDef = match[1].trim().replace(/;+\s*$/u, "").trim();
9484
+ const close = originalSql.lastIndexOf(")");
9485
+ if (close < 0) return originalSql;
9486
+ return `${originalSql.slice(0, close)}, ${colDef}${originalSql.slice(close)}`;
9487
+ }
9488
+ function synthesizeCtasMasterSql(tableName, columnNames) {
9489
+ const cols = columnNames.map(quoteIdentIfNeeded).join(",");
9490
+ return `CREATE TABLE ${quoteIdentIfNeeded(tableName)}(${cols})`;
9491
+ }
9492
+ function quoteIdentIfNeeded(name) {
9493
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) return name;
9494
+ return `"${name.replaceAll('"', '""')}"`;
9495
+ }
9496
+
9413
9497
  // src/functions/window.ts
9414
9498
  function rowsEqual(left, right) {
9415
9499
  if (left.length !== right.length) return false;
@@ -11991,14 +12075,17 @@ function scanFtsVocab(db, alias, vocab) {
11991
12075
 
11992
12076
  // src/executor/ddl.ts
11993
12077
  function executeCreateTable(stmt, env) {
12078
+ const sourceSql = env.statementSql ? normalizeMasterSql(env.statementSql) : null;
11994
12079
  if (!stmt.asSelect) {
11995
- env.state.createTable(stmt);
12080
+ env.state.createTable(stmt, sourceSql);
11996
12081
  env.state.recordChange(0);
11997
12082
  return emptyResult(0, env.state.lastInsertRowid);
11998
12083
  }
11999
12084
  const result = executeSelect2(stmt.asSelect, env);
12000
12085
  const columns = result.columns.map((name) => ({ name, typeName: null, constraints: [] }));
12001
- const table = env.state.createTable({ ...stmt, columns, constraints: [], asSelect: null });
12086
+ const bareName = stmt.name.includes(".") ? stmt.name.split(".").pop() ?? stmt.name : stmt.name;
12087
+ const ctasSql = synthesizeCtasMasterSql(bareName, result.columns);
12088
+ const table = env.state.createTable({ ...stmt, columns, constraints: [], asSelect: null }, ctasSql);
12002
12089
  for (const values of resultValues(result)) {
12003
12090
  table.insert(
12004
12091
  new Map(table.columns.map((column, index) => [normalizeColumnName(column.name), values[index] ?? null]))
@@ -12023,7 +12110,7 @@ function executeCreateTable(stmt, env) {
12023
12110
  return emptyResult(0, env.state.lastInsertRowid);
12024
12111
  }
12025
12112
  function executeCreateIndex(stmt, env) {
12026
- const index = env.state.createIndex(stmt);
12113
+ const index = env.state.createIndex(stmt, env.statementSql ? normalizeMasterSql(env.statementSql) : null);
12027
12114
  const table = env.state.getTable(stmt.table);
12028
12115
  try {
12029
12116
  for (const row of table.scan()) {
@@ -12103,6 +12190,7 @@ function executeAlterTable(stmt, env) {
12103
12190
  throw new SqliteError("Cannot add a NOT NULL column with default value NULL", "other");
12104
12191
  table.columns.push(column);
12105
12192
  for (const row of table.rows.values()) row.values.set(normalizeColumnName(column.name), defaultValue);
12193
+ table.originalSql = appendAddColumnToMasterSql(table.originalSql, env.statementSql);
12106
12194
  table.clearEqualityHashes();
12107
12195
  env.state.schemaVersion++;
12108
12196
  } else {
@@ -12150,7 +12238,7 @@ function executeDropIndex(stmt, env) {
12150
12238
  return emptyResult(0, env.state.lastInsertRowid);
12151
12239
  }
12152
12240
  function executeCreateView(stmt, env) {
12153
- env.state.createView(stmt);
12241
+ env.state.createView(stmt, env.statementSql ? normalizeMasterSql(env.statementSql) : null);
12154
12242
  return emptyResult(0, env.state.lastInsertRowid);
12155
12243
  }
12156
12244
  function executeDropView(stmt, env) {
@@ -12243,7 +12331,7 @@ function executeCreateTrigger(stmt, env) {
12243
12331
  forEachRow: stmt.forEachRow,
12244
12332
  body: stmt.body,
12245
12333
  updateColumns: stmt.updateColumns,
12246
- originalSql: null
12334
+ originalSql: env.statementSql ? normalizeMasterSql(env.statementSql) : null
12247
12335
  });
12248
12336
  return emptyResult(0, env.state.lastInsertRowid);
12249
12337
  }
@@ -12340,7 +12428,7 @@ function triggerScope(table, oldRow, newValues) {
12340
12428
 
12341
12429
  // src/executor/vtable.ts
12342
12430
  function executeCreateVirtualTable(stmt, env) {
12343
- env.state.createVirtualTable(stmt);
12431
+ env.state.createVirtualTable(stmt, env.statementSql ? normalizeMasterSql(env.statementSql) : null);
12344
12432
  env.state.recordChange(0);
12345
12433
  return emptyResult(0, env.state.lastInsertRowid);
12346
12434
  }
@@ -13642,10 +13730,11 @@ function executeAnalyze(stmt, env) {
13642
13730
 
13643
13731
  // src/api/statement.ts
13644
13732
  var Statement = class _Statement {
13645
- constructor(database, sql, statements) {
13733
+ constructor(database, sql, statements, statementSqls) {
13646
13734
  this.database = database;
13647
13735
  this.sql = sql;
13648
13736
  this.statements = statements;
13737
+ this.statementSqls = statementSqls;
13649
13738
  this.schemaVersion = database.state.schemaVersion;
13650
13739
  }
13651
13740
  database;
@@ -13653,13 +13742,24 @@ var Statement = class _Statement {
13653
13742
  namedPlan = null;
13654
13743
  env = null;
13655
13744
  statements;
13745
+ statementSqls;
13656
13746
  schemaVersion;
13657
13747
  /**
13658
13748
  * Construct a {@link Statement} for {@link Database.prepare} / {@link Database.exec}.
13659
13749
  * @internal
13660
13750
  */
13661
- static create(database, sql, statements) {
13662
- return new _Statement(database, sql, statements);
13751
+ static create(database, sql, statements, statementSqls) {
13752
+ return new _Statement(database, sql, statements, statementSqls ?? statements.map(() => sql));
13753
+ }
13754
+ /** @internal Build from {@link parseUnits}. */
13755
+ static createFromSql(database, sql) {
13756
+ const units = parseUnits(sql);
13757
+ return new _Statement(
13758
+ database,
13759
+ sql,
13760
+ units.map((u) => u.statement),
13761
+ units.map((u) => u.sql)
13762
+ );
13663
13763
  }
13664
13764
  /**
13665
13765
  * Execute for side effects (INSERT / UPDATE / DELETE / DDL).
@@ -13722,9 +13822,11 @@ var Statement = class _Statement {
13722
13822
  env.includeValues = true;
13723
13823
  this.bindNamed(env, params);
13724
13824
  let result;
13725
- for (const statement of this.statements) {
13726
- result = executeStatement(statement, env);
13825
+ for (let i = 0; i < this.statements.length; i++) {
13826
+ env.statementSql = this.statementSqls[i] ?? this.sql;
13827
+ result = executeStatement(this.statements[i], env);
13727
13828
  }
13829
+ env.statementSql = null;
13728
13830
  return result;
13729
13831
  }
13730
13832
  obtainEnv(params) {
@@ -13744,7 +13846,9 @@ var Statement = class _Statement {
13744
13846
  }
13745
13847
  reprepareIfSchemaChanged() {
13746
13848
  if (this.schemaVersion === this.database.state.schemaVersion) return;
13747
- this.statements = parse(this.sql);
13849
+ const units = parseUnits(this.sql);
13850
+ this.statements = units.map((u) => u.statement);
13851
+ this.statementSqls = units.map((u) => u.sql);
13748
13852
  this.env = null;
13749
13853
  this.namedPlan = null;
13750
13854
  this.schemaVersion = this.database.state.schemaVersion;
@@ -13835,7 +13939,7 @@ var Database = class {
13835
13939
  if (arguments.length > 1) {
13836
13940
  throw new SqliteError("exec() does not accept parameters; use prepare() or query()", "misuse");
13837
13941
  }
13838
- Statement.create(this, sql, parse(sql)).run();
13942
+ Statement.createFromSql(this, sql).run();
13839
13943
  }
13840
13944
  /**
13841
13945
  * Execute a single-statement query and return all rows as objects keyed by column name.
@@ -13985,14 +14089,19 @@ var Database = class {
13985
14089
  if (this.closed) throw new SqliteError("Database is closed", "misuse");
13986
14090
  }
13987
14091
  prepareSingle(sql) {
13988
- const statements = parse(sql);
13989
- if (statements.length === 0) {
14092
+ const units = parseUnits(sql);
14093
+ if (units.length === 0) {
13990
14094
  throw new SqliteError("empty statement", "misuse");
13991
14095
  }
13992
- if (statements.length > 1) {
14096
+ if (units.length > 1) {
13993
14097
  throw new SqliteError("query()/prepare() accept a single statement only; use exec() for scripts", "misuse");
13994
14098
  }
13995
- return Statement.create(this, sql, statements);
14099
+ return Statement.create(
14100
+ this,
14101
+ sql,
14102
+ units.map((u) => u.statement),
14103
+ units.map((u) => u.sql)
14104
+ );
13996
14105
  }
13997
14106
  };
13998
14107
  var disposeKey = Symbol.dispose;