@bolvrk/engine-sqlite 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,615 @@
1
+ // src/metas/index.ts
2
+ var SL_RULE_CORPUS_VERSION = "0.4.0";
3
+ var sl001Meta = {
4
+ id: "SL001",
5
+ version: "1.0.0",
6
+ name: "ADD COLUMN NOT NULL without a default",
7
+ tier: 1,
8
+ defaultSeverity: "critical",
9
+ rationale: 'SQLite refuses to add a NOT NULL column unless it has a default other than NULL — the statement fails with "Cannot add a NOT NULL column with default value NULL", so the migration stops here in every environment, including the one that matters.',
10
+ example: "ALTER TABLE orders ADD COLUMN region TEXT NOT NULL;",
11
+ fix: {
12
+ description: "Give the column a non-NULL default, or add it nullable, backfill, and enforce NOT NULL by rebuilding the table later. In SQLite a default on ADD COLUMN is free: existing rows are not rewritten, the default is read from the schema.",
13
+ example: "ALTER TABLE orders ADD COLUMN region TEXT NOT NULL DEFAULT 'eu';"
14
+ },
15
+ verified: true
16
+ };
17
+ var sl002Meta = {
18
+ id: "SL002",
19
+ version: "1.0.0",
20
+ name: "ADD COLUMN with PRIMARY KEY or UNIQUE",
21
+ tier: 1,
22
+ defaultSeverity: "critical",
23
+ rationale: 'ALTER TABLE ADD COLUMN cannot carry a PRIMARY KEY or UNIQUE constraint — SQLite rejects the statement outright ("Cannot add a PRIMARY KEY column", "Cannot add a UNIQUE column"). The migration fails, and the constraint it wanted still does not exist.',
24
+ example: "ALTER TABLE orders ADD COLUMN external_id TEXT UNIQUE;",
25
+ fix: {
26
+ description: "Add the column plain, then create the unique index as its own statement — that is what UNIQUE would have built anyway. A new primary key needs the table rebuilt: create the new table, copy, drop, rename.",
27
+ example: `ALTER TABLE orders ADD COLUMN external_id TEXT;
28
+ CREATE UNIQUE INDEX orders_external_id ON orders (external_id);`
29
+ },
30
+ verified: true
31
+ };
32
+ var sl003Meta = {
33
+ id: "SL003",
34
+ version: "1.0.0",
35
+ name: "ADD COLUMN with a non-constant default",
36
+ tier: 1,
37
+ defaultSeverity: "critical",
38
+ rationale: 'A column added with ALTER TABLE may not default to CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP or a parenthesised expression: SQLite rejects the statement ("Cannot add a column with non-constant default"). The pattern is common in migrations ported from Postgres, where it works.',
39
+ example: "ALTER TABLE orders ADD COLUMN created_at TEXT DEFAULT CURRENT_TIMESTAMP;",
40
+ fix: {
41
+ description: "Add the column with a constant default (or none), and set the timestamp from application code or a trigger. If the default must be an expression, the column has to be part of a table rebuild.",
42
+ example: `ALTER TABLE orders ADD COLUMN created_at TEXT;
43
+ -- then, from the application or a trigger:
44
+ -- UPDATE orders SET created_at = CURRENT_TIMESTAMP WHERE created_at IS NULL;`
45
+ },
46
+ verified: true
47
+ };
48
+ var sl004Meta = {
49
+ id: "SL004",
50
+ version: "1.0.0",
51
+ name: "ADD COLUMN GENERATED ... STORED",
52
+ tier: 1,
53
+ defaultSeverity: "critical",
54
+ rationale: 'A STORED generated column cannot be added with ALTER TABLE — SQLite only allows VIRTUAL generated columns there ("cannot add a STORED column"). The statement fails; nothing is added.',
55
+ example: "ALTER TABLE orders ADD COLUMN total_cents INTEGER GENERATED ALWAYS AS (qty * unit_cents) STORED;",
56
+ fix: {
57
+ description: "Declare the generated column VIRTUAL, which ALTER TABLE accepts and which costs nothing at write time. A STORED column belongs in CREATE TABLE — for an existing table that means a rebuild.",
58
+ example: "ALTER TABLE orders ADD COLUMN total_cents INTEGER GENERATED ALWAYS AS (qty * unit_cents) VIRTUAL;"
59
+ },
60
+ verified: true
61
+ };
62
+ var sl005Meta = {
63
+ id: "SL005",
64
+ version: "1.0.0",
65
+ name: "ADD COLUMN REFERENCES with a non-NULL default",
66
+ tier: 1,
67
+ defaultSeverity: "warning",
68
+ rationale: 'When foreign keys are enforced (PRAGMA foreign_keys=ON, which most applications set on every connection), a column added with a REFERENCES clause must default to NULL — anything else fails with "Cannot add a REFERENCES column with non-NULL default value". Whether it fails depends on a per-connection pragma, so the same file passes in one runner and stops in another.',
69
+ example: "ALTER TABLE orders ADD COLUMN warehouse_id INTEGER REFERENCES warehouses(id) DEFAULT 1;",
70
+ fix: {
71
+ description: "Add the referencing column with a NULL default, backfill the rows that need a value, and let the application set it from then on.",
72
+ example: `ALTER TABLE orders ADD COLUMN warehouse_id INTEGER REFERENCES warehouses(id);
73
+ -- backfill from the application, in batches`
74
+ },
75
+ verified: true
76
+ };
77
+ var sl006Meta = {
78
+ id: "SL006",
79
+ version: "1.0.0",
80
+ name: "DROP COLUMN rewrites the table",
81
+ tier: 1,
82
+ defaultSeverity: "warning",
83
+ rationale: "DROP COLUMN edits the schema and then rewrites every row of the table to purge the column's values — one write transaction that holds the database's single write lock for the length of the copy. It also fails outright if anything else in the schema names the column (an index, a foreign key, a view, a trigger, a generated column) and is unsupported before SQLite 3.35.",
84
+ example: "ALTER TABLE orders DROP COLUMN legacy_flag;",
85
+ fix: {
86
+ description: "Stop reading the column in code first, let a release soak, and drop it in a scheduled migration — SQLite's write lock means every other writer waits for the rewrite. If the column is indexed or referenced, drop those objects first or the statement fails.",
87
+ example: `-- release N: remove every code reference to legacy_flag
88
+ -- release N+1, scheduled:
89
+ -- DROP INDEX IF EXISTS orders_legacy_flag;
90
+ -- ALTER TABLE orders DROP COLUMN legacy_flag;`
91
+ },
92
+ verified: true
93
+ };
94
+ var sl007Meta = {
95
+ id: "SL007",
96
+ version: "1.0.0",
97
+ name: "RENAME COLUMN or RENAME TABLE during a deploy",
98
+ tier: 1,
99
+ defaultSeverity: "warning",
100
+ rationale: "A rename is instant in SQLite, and that is the problem: the moment it commits, every process still running the previous release fails on the old name. Views and triggers are rewritten to the new name, but application SQL is not.",
101
+ example: "ALTER TABLE orders RENAME COLUMN qty TO quantity;",
102
+ fix: {
103
+ description: "Expand and contract: add the new column, dual-write from the application, backfill, switch readers, then drop the old column in a later release. For a table, create the new one, copy, and swap only when no deployed code reads the old name.",
104
+ example: `ALTER TABLE orders ADD COLUMN quantity INTEGER;
105
+ -- dual-write from the application; backfill in batches
106
+ -- later release, after readers moved:
107
+ -- ALTER TABLE orders DROP COLUMN qty;`
108
+ },
109
+ verified: true
110
+ };
111
+ var sl008Meta = {
112
+ id: "SL008",
113
+ version: "1.0.0",
114
+ name: "ADD COLUMN with a CHECK constraint scans the table",
115
+ tier: 1,
116
+ defaultSeverity: "note",
117
+ rationale: "Adding a column is normally a schema-only change in SQLite. With a CHECK constraint (or NOT NULL on a generated column) the whole table is scanned to verify existing rows first — under the write lock, proportional to the table's size.",
118
+ example: "ALTER TABLE orders ADD COLUMN priority INTEGER DEFAULT 0 CHECK (priority BETWEEN 0 AND 9);",
119
+ fix: {
120
+ description: "Add the column without the CHECK when the table is large and the constraint is guaranteed by the default and the application; enforce it in a rebuild later, or accept the scan in a scheduled window.",
121
+ example: "ALTER TABLE orders ADD COLUMN priority INTEGER DEFAULT 0;"
122
+ },
123
+ verified: true
124
+ };
125
+ var sl009Meta = {
126
+ id: "SL009",
127
+ version: "1.0.0",
128
+ name: "Table rebuild with foreign keys still enforced",
129
+ tier: 1,
130
+ defaultSeverity: "warning",
131
+ rationale: "This migration rebuilds a table the documented way — create the new table, copy, drop the old, rename — but never turns foreign keys off first. With PRAGMA foreign_keys=ON the DROP TABLE performs an implicit DELETE FROM: rows referencing the table fail the migration with a constraint error, or ON DELETE CASCADE quietly removes them.",
132
+ example: `CREATE TABLE orders_new (id INTEGER PRIMARY KEY, qty INTEGER NOT NULL);
133
+ INSERT INTO orders_new SELECT id, qty FROM orders;
134
+ DROP TABLE orders;
135
+ ALTER TABLE orders_new RENAME TO orders;`,
136
+ fix: {
137
+ description: "Follow the twelve steps from the SQLite manual: PRAGMA foreign_keys=OFF outside the transaction, rebuild inside it, PRAGMA foreign_key_check before COMMIT, and PRAGMA foreign_keys=ON after.",
138
+ example: `PRAGMA foreign_keys = OFF;
139
+ BEGIN;
140
+ CREATE TABLE orders_new (id INTEGER PRIMARY KEY, qty INTEGER NOT NULL);
141
+ INSERT INTO orders_new SELECT id, qty FROM orders;
142
+ DROP TABLE orders;
143
+ ALTER TABLE orders_new RENAME TO orders;
144
+ PRAGMA foreign_key_check;
145
+ COMMIT;
146
+ PRAGMA foreign_keys = ON;`
147
+ },
148
+ verified: true
149
+ };
150
+ var sl010Meta = {
151
+ id: "SL010",
152
+ version: "1.0.0",
153
+ name: "PRAGMA that is a no-op inside a transaction",
154
+ tier: 1,
155
+ defaultSeverity: "warning",
156
+ rationale: "PRAGMA foreign_keys and PRAGMA journal_mode do nothing while a transaction is open — SQLite does not error, it silently leaves the setting as it was. A rebuild that switches foreign keys off after BEGIN runs with them on, and everything SL009 warns about applies.",
157
+ example: `BEGIN;
158
+ PRAGMA foreign_keys = OFF;
159
+ DROP TABLE orders;
160
+ COMMIT;`,
161
+ fix: {
162
+ description: "Issue the pragma before BEGIN (and re-enable after COMMIT). If the migration runner wraps every file in a transaction, the pragma has to be set by the runner itself.",
163
+ example: `PRAGMA foreign_keys = OFF;
164
+ BEGIN;
165
+ DROP TABLE orders;
166
+ COMMIT;
167
+ PRAGMA foreign_keys = ON;`
168
+ },
169
+ verified: true
170
+ };
171
+ var sl011Meta = {
172
+ id: "SL011",
173
+ version: "1.0.0",
174
+ name: "Foreign keys switched off and not back on",
175
+ tier: 1,
176
+ defaultSeverity: "warning",
177
+ rationale: "PRAGMA foreign_keys=OFF is per connection and does not reset at COMMIT. A migration that turns enforcement off for a rebuild and never turns it on leaves the runner's connection accepting orphans for as long as it lives — every later statement in the run, and any backfill sharing the connection, goes unchecked.",
178
+ example: `PRAGMA foreign_keys = OFF;
179
+ DROP TABLE orders;`,
180
+ fix: {
181
+ description: "End the file with PRAGMA foreign_keys=ON, after the transaction that needed it off.",
182
+ example: `PRAGMA foreign_keys = OFF;
183
+ BEGIN;
184
+ DROP TABLE orders;
185
+ COMMIT;
186
+ PRAGMA foreign_keys = ON;`
187
+ },
188
+ verified: true
189
+ };
190
+ var sl012Meta = {
191
+ id: "SL012",
192
+ version: "1.0.0",
193
+ name: "Foreign keys re-enabled without a foreign_key_check",
194
+ tier: 1,
195
+ defaultSeverity: "note",
196
+ rationale: "Turning foreign keys back on does not validate what happened while they were off. Step 10 of the SQLite rebuild recipe is PRAGMA foreign_key_check before COMMIT — without it, an orphaned row created during the rebuild is discovered by the first user query that joins on it.",
197
+ example: `PRAGMA foreign_keys = OFF;
198
+ BEGIN;
199
+ DROP TABLE orders;
200
+ ALTER TABLE orders_new RENAME TO orders;
201
+ COMMIT;
202
+ PRAGMA foreign_keys = ON;`,
203
+ fix: {
204
+ description: "Run PRAGMA foreign_key_check inside the transaction, before COMMIT, so a violation can still be rolled back.",
205
+ example: `PRAGMA foreign_keys = OFF;
206
+ BEGIN;
207
+ DROP TABLE orders;
208
+ ALTER TABLE orders_new RENAME TO orders;
209
+ PRAGMA foreign_key_check;
210
+ COMMIT;
211
+ PRAGMA foreign_keys = ON;`
212
+ },
213
+ verified: true
214
+ };
215
+ var sl013Meta = {
216
+ id: "SL013",
217
+ version: "1.0.0",
218
+ name: "Unbounded UPDATE or DELETE",
219
+ tier: 1,
220
+ defaultSeverity: "warning",
221
+ rationale: "An UPDATE or DELETE with no WHERE touches every row in one write transaction. SQLite has one writer at a time: the whole application queues behind it (or hits SQLITE_BUSY) for as long as the statement runs, and the rollback journal or WAL grows by the size of the change.",
222
+ example: "UPDATE orders SET status = 'archived';",
223
+ fix: {
224
+ description: "State the predicate, and batch by rowid range from an operational job so each chunk commits on its own and the write lock is released between batches.",
225
+ example: `-- batched job, not a migration statement:
226
+ -- UPDATE orders SET status = 'archived'
227
+ -- WHERE status = 'closed' AND rowid BETWEEN ? AND ?;`
228
+ },
229
+ verified: true
230
+ };
231
+ var sl014Meta = {
232
+ id: "SL014",
233
+ version: "1.0.0",
234
+ name: "VACUUM in a migration",
235
+ tier: 1,
236
+ defaultSeverity: "warning",
237
+ rationale: "VACUUM rewrites the entire database file into a temporary copy and swaps it in: it needs up to twice the database's size in free disk space, holds the write lock for the whole rewrite, and fails outright if a transaction is open on the connection — so inside a migration runner's transaction it is an error, and outside one it is an outage-sized pause.",
238
+ example: "VACUUM;",
239
+ fix: {
240
+ description: "Vacuum from an operational job in a maintenance window, never from a migration. If space reclaim must be continuous, PRAGMA auto_vacuum=INCREMENTAL with incremental_vacuum in small steps.",
241
+ example: `-- operational runbook, not a migration:
242
+ -- sqlite3 app.db 'VACUUM;'`
243
+ },
244
+ verified: true
245
+ };
246
+ var sl015Meta = {
247
+ id: "SL015",
248
+ version: "1.0.0",
249
+ name: "REINDEX of everything",
250
+ tier: 1,
251
+ defaultSeverity: "warning",
252
+ rationale: "REINDEX with no argument rebuilds every index in every attached database in one write transaction — the write lock is held for the total, not per table. In a migration it is almost always broader than intended.",
253
+ example: "REINDEX;",
254
+ fix: {
255
+ description: "Name the index or table that actually needs rebuilding — REINDEX is only needed after a collation change.",
256
+ example: "REINDEX orders_status_idx;"
257
+ },
258
+ verified: true
259
+ };
260
+ var sl016Meta = {
261
+ id: "SL016",
262
+ version: "1.0.0",
263
+ name: "Index build on an existing table",
264
+ tier: 1,
265
+ defaultSeverity: "note",
266
+ rationale: "SQLite has no CONCURRENTLY: CREATE INDEX scans the table and writes the index under the database's write lock, and every other writer waits (or gets SQLITE_BUSY) for the duration. Fine on a small table; on a large one this is the moment the application stalls.",
267
+ example: "CREATE INDEX orders_status_idx ON orders (status);",
268
+ fix: {
269
+ description: "Build large indexes in a window, with busy_timeout set on the application's connections so a wait is a wait and not an error.",
270
+ example: `-- in the maintenance window, with PRAGMA busy_timeout set on application connections:
271
+ -- CREATE INDEX orders_status_idx ON orders (status);`
272
+ },
273
+ verified: true
274
+ };
275
+ var sl017Meta = {
276
+ id: "SL017",
277
+ version: "1.0.0",
278
+ name: "INT PRIMARY KEY is not a rowid alias",
279
+ tier: 1,
280
+ defaultSeverity: "note",
281
+ rationale: "Only a column declared exactly INTEGER PRIMARY KEY becomes an alias for the rowid. INT PRIMARY KEY (or BIGINT, or INTEGER PRIMARY KEY DESC) is an ordinary column with a separate unique index: every lookup by primary key is an extra B-tree hop, the value is not the rowid, and VACUUM may renumber the real rowid underneath it.",
282
+ example: "CREATE TABLE orders (id INT PRIMARY KEY, qty INTEGER);",
283
+ fix: {
284
+ description: "Spell the type INTEGER, exactly, on the primary key column of a rowid table.",
285
+ example: "CREATE TABLE orders (id INTEGER PRIMARY KEY, qty INTEGER);"
286
+ },
287
+ verified: true
288
+ };
289
+ var sl018Meta = {
290
+ id: "SL018",
291
+ version: "1.0.0",
292
+ name: "AUTOINCREMENT where a plain INTEGER PRIMARY KEY would do",
293
+ tier: 1,
294
+ defaultSeverity: "note",
295
+ rationale: "AUTOINCREMENT makes every insert also update the sqlite_sequence table, and the SQLite manual itself says it should be avoided unless strictly needed. Its one guarantee — a deleted rowid is never reused — is rarely what a migration author meant.",
296
+ example: "CREATE TABLE orders (id INTEGER PRIMARY KEY AUTOINCREMENT, qty INTEGER);",
297
+ fix: {
298
+ description: "Drop the keyword; INTEGER PRIMARY KEY already assigns increasing rowids. Keep AUTOINCREMENT only when reuse of a deleted id would be a correctness bug.",
299
+ example: "CREATE TABLE orders (id INTEGER PRIMARY KEY, qty INTEGER);"
300
+ },
301
+ verified: true
302
+ };
303
+ var sl019Meta = {
304
+ id: "SL019",
305
+ version: "1.0.0",
306
+ name: "Table rebuild that drops the old table before copying its rows",
307
+ tier: 1,
308
+ defaultSeverity: "critical",
309
+ rationale: "The rebuild recipe is create, copy, drop, rename. This migration creates the new table, drops the old one and renames the new one onto its name, but never copies the rows across before the DROP. DROP TABLE discards them; the rename brings back the name with an empty table, and nothing in SQLite fails to say so.",
310
+ example: `CREATE TABLE orders_new (id INTEGER PRIMARY KEY, qty INTEGER NOT NULL);
311
+ DROP TABLE orders;
312
+ ALTER TABLE orders_new RENAME TO orders;`,
313
+ fix: {
314
+ description: "Copy before dropping: INSERT INTO the new table SELECT from the old, inside the same transaction, so a failed copy rolls the whole rebuild back. If the table is meant to come back empty, suppress the rule with a reason that says so.",
315
+ example: `BEGIN;
316
+ CREATE TABLE orders_new (id INTEGER PRIMARY KEY, qty INTEGER NOT NULL);
317
+ INSERT INTO orders_new SELECT id, qty FROM orders;
318
+ DROP TABLE orders;
319
+ ALTER TABLE orders_new RENAME TO orders;
320
+ COMMIT;`
321
+ },
322
+ verified: true
323
+ };
324
+ var sl020Meta = {
325
+ id: "SL020",
326
+ version: "1.0.0",
327
+ name: "CREATE TABLE without STRICT",
328
+ tier: 1,
329
+ defaultSeverity: "note",
330
+ rationale: "Without STRICT, SQLite's column types are suggestions: a TEXT value goes into an INTEGER column, a blob into a date, and nothing complains until the application reads it back. STRICT (3.37+) makes the declared type an actual constraint, and it only exists as a CREATE TABLE option — it cannot be added later without a rebuild.",
331
+ example: "CREATE TABLE orders (id INTEGER PRIMARY KEY, qty INTEGER, note TEXT);",
332
+ fix: {
333
+ description: "Add STRICT to every new table. Column types are then limited to INT, INTEGER, REAL, TEXT, BLOB and ANY, and a value of the wrong type is rejected at insert time instead of surfacing later.",
334
+ example: "CREATE TABLE orders (id INTEGER PRIMARY KEY, qty INTEGER, note TEXT) STRICT;"
335
+ },
336
+ verified: true
337
+ };
338
+ var sl021Meta = {
339
+ id: "SL021",
340
+ version: "1.0.0",
341
+ name: "Foreign key column without an index",
342
+ tier: 1,
343
+ defaultSeverity: "note",
344
+ rationale: "SQLite indexes the parent side of a foreign key (the referenced key must be unique) but never the child side. Every DELETE or UPDATE of a parent row then scans the whole child table to look for references — with foreign keys on, deleting one user scans every order. The manual's advice is an index on every child key column.",
345
+ example: "CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER REFERENCES users(id));",
346
+ fix: {
347
+ description: "Create an index on the referencing column (or columns, in the foreign key's order) in the same migration.",
348
+ example: `CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER REFERENCES users(id));
349
+ CREATE INDEX orders_user_id ON orders (user_id);`
350
+ },
351
+ verified: true
352
+ };
353
+ var sl022Meta = {
354
+ id: "SL022",
355
+ version: "1.0.0",
356
+ name: "Rename with legacy_alter_table on",
357
+ tier: 1,
358
+ defaultSeverity: "warning",
359
+ rationale: 'With PRAGMA legacy_alter_table=ON, RENAME TABLE and RENAME COLUMN go back to the pre-3.26 behaviour: references inside views and triggers are not rewritten. The rename succeeds, and the first query through a view that named the old table fails with "no such table".',
360
+ example: `PRAGMA legacy_alter_table = ON;
361
+ ALTER TABLE orders RENAME TO purchases;`,
362
+ fix: {
363
+ description: "Leave legacy_alter_table off (the default) so SQLite rewrites views and triggers with the rename. If the pragma is on for a specific rebuild step, switch it off before any rename.",
364
+ example: `PRAGMA legacy_alter_table = OFF;
365
+ ALTER TABLE orders RENAME TO purchases;`
366
+ },
367
+ verified: true
368
+ };
369
+ var sl023Meta = {
370
+ id: "SL023",
371
+ version: "1.0.0",
372
+ name: "Dropping what a view or trigger in this migration depends on",
373
+ tier: 1,
374
+ defaultSeverity: "critical",
375
+ rationale: 'This migration creates a view or trigger and then drops the table or column it depends on. SQLite does not stop it: the view fails with "no such column" the first time it is queried, and a trigger that names the column makes every later write to its table fail. Older releases refuse the DROP COLUMN instead. Either way the failure is guaranteed by the file itself, not by what production holds.',
376
+ example: `CREATE VIEW open_orders AS SELECT id, status FROM orders WHERE status = 'open';
377
+ ALTER TABLE orders DROP COLUMN status;`,
378
+ fix: {
379
+ description: "Drop or redefine the view or trigger first, then drop the column or table, then recreate the view without the dependency.",
380
+ example: `DROP VIEW IF EXISTS open_orders;
381
+ ALTER TABLE orders DROP COLUMN status;
382
+ CREATE VIEW open_orders AS SELECT id FROM orders;`
383
+ },
384
+ verified: true
385
+ };
386
+ var sl024Meta = {
387
+ id: "SL024",
388
+ version: "1.0.0",
389
+ name: "PRIMARY KEY column that accepts NULL",
390
+ tier: 1,
391
+ defaultSeverity: "note",
392
+ rationale: "In a rowid table, a PRIMARY KEY column that is not INTEGER does not imply NOT NULL — a long-standing SQLite bug kept for compatibility — so NULL keys are accepted and the uniqueness the key promised is gone (every NULL is distinct). In a WITHOUT ROWID table the same NULL is refused at insert time instead, so the schema behaves differently depending on one table option.",
393
+ example: "CREATE TABLE orders (ref TEXT PRIMARY KEY, qty INTEGER);",
394
+ fix: {
395
+ description: "Spell NOT NULL on every non-INTEGER primary key column so the key is a key.",
396
+ example: "CREATE TABLE orders (ref TEXT PRIMARY KEY NOT NULL, qty INTEGER);"
397
+ },
398
+ verified: true
399
+ };
400
+ var sl025Meta = {
401
+ id: "SL025",
402
+ version: "1.0.0",
403
+ name: "Write migration in a DEFERRED transaction",
404
+ tier: 1,
405
+ defaultSeverity: "note",
406
+ rationale: "A plain BEGIN is DEFERRED: it takes no lock until the first write, then tries to upgrade a read lock to a write lock. If another connection holds the write lock at that moment the upgrade fails immediately with SQLITE_BUSY — busy_timeout does not apply to a lock upgrade — and the migration aborts part way through its reads.",
407
+ example: `BEGIN;
408
+ ALTER TABLE orders ADD COLUMN region TEXT;
409
+ COMMIT;`,
410
+ fix: {
411
+ description: "Start a migration that writes with BEGIN IMMEDIATE: the write lock is taken up front, busy_timeout applies, and there is no upgrade to fail.",
412
+ example: `BEGIN IMMEDIATE;
413
+ ALTER TABLE orders ADD COLUMN region TEXT;
414
+ COMMIT;`
415
+ },
416
+ verified: true
417
+ };
418
+ var sl026Meta = {
419
+ id: "SL026",
420
+ version: "1.0.0",
421
+ name: "BEGIN inside a transaction, or COMMIT without one",
422
+ tier: 1,
423
+ defaultSeverity: "critical",
424
+ rationale: 'SQLite has no nested transactions: a second BEGIN fails with "cannot start a transaction within a transaction", and COMMIT or ROLLBACK with nothing open fails with "no transaction is active". Either error stops the migration where it happens — usually because a runner already wraps the file.',
425
+ example: `BEGIN;
426
+ BEGIN;
427
+ ALTER TABLE orders ADD COLUMN region TEXT;
428
+ COMMIT;`,
429
+ fix: {
430
+ description: "One BEGIN, one COMMIT, in order — or none at all if the migration runner manages the transaction. Use SAVEPOINT for a nested scope.",
431
+ example: `BEGIN;
432
+ ALTER TABLE orders ADD COLUMN region TEXT;
433
+ COMMIT;`
434
+ },
435
+ verified: true
436
+ };
437
+ var sl027Meta = {
438
+ id: "SL027",
439
+ version: "1.0.0",
440
+ name: "Unbounded DELETE that cascades to child tables",
441
+ tier: 1,
442
+ defaultSeverity: "warning",
443
+ rationale: "A DELETE with no WHERE on a table that other tables reference with ON DELETE CASCADE empties those tables too, in the same statement, with foreign keys on — and does nothing to them with foreign keys off. The blast radius depends on a per-connection pragma, and the statement itself names only one table.",
444
+ example: `CREATE TABLE lines (id INTEGER PRIMARY KEY, order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE);
445
+ DELETE FROM orders;`,
446
+ fix: {
447
+ description: "State the predicate and batch the delete; if the intent is to clear the parent and its children, delete the children explicitly first so the migration reads the way it behaves.",
448
+ example: `-- batched job, children first, explicit:
449
+ -- DELETE FROM lines WHERE order_id IN (SELECT id FROM orders WHERE status = 'closed' AND rowid BETWEEN ? AND ?);
450
+ -- DELETE FROM orders WHERE status = 'closed' AND rowid BETWEEN ? AND ?;`
451
+ },
452
+ verified: true
453
+ };
454
+ var sl028Meta = {
455
+ id: "SL028",
456
+ version: "1.0.0",
457
+ name: "Index on a non-deterministic expression",
458
+ tier: 1,
459
+ defaultSeverity: "critical",
460
+ rationale: 'An index expression must give the same answer for the same row every time, so SQLite refuses random(), changes(), last_insert_rowid() and their kin in CREATE INDEX: "non-deterministic functions prohibited in index expressions". The statement fails and the migration stops.',
461
+ example: "CREATE INDEX orders_shuffle ON orders (id + random());",
462
+ fix: {
463
+ description: "Index a deterministic expression — lower(), a column arithmetic, a JSON path — or store the value in a column and index that.",
464
+ example: "CREATE INDEX orders_email_ci ON orders (lower(email));"
465
+ },
466
+ verified: true
467
+ };
468
+ var sl029Meta = {
469
+ id: "SL029",
470
+ version: "1.0.0",
471
+ name: "AUTOINCREMENT on a WITHOUT ROWID table",
472
+ tier: 1,
473
+ defaultSeverity: "critical",
474
+ rationale: 'AUTOINCREMENT only exists for the rowid, and a WITHOUT ROWID table has none: SQLite refuses the CREATE TABLE with "AUTOINCREMENT not allowed on WITHOUT ROWID tables". The table is not created and the migration stops.',
475
+ example: "CREATE TABLE sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, token TEXT) WITHOUT ROWID;",
476
+ fix: {
477
+ description: "Drop the keyword — or drop WITHOUT ROWID if a monotonically increasing integer key is the point of the table.",
478
+ example: "CREATE TABLE sessions (id INTEGER PRIMARY KEY, token TEXT);"
479
+ },
480
+ verified: true
481
+ };
482
+ var sl030Meta = {
483
+ id: "SL030",
484
+ version: "1.0.0",
485
+ name: "ATTACH DATABASE in a migration",
486
+ tier: 1,
487
+ defaultSeverity: "warning",
488
+ rationale: "ATTACH opens a second database file on the runner's connection, by a path that only makes sense on one machine, for the life of that connection. In a migration it is almost always an import script that got committed: it fails on any other host, and while it works it can read from or write to a file the migration never declared.",
489
+ example: `ATTACH DATABASE '/var/backups/legacy.db' AS legacy;
490
+ INSERT INTO orders SELECT * FROM legacy.orders;`,
491
+ fix: {
492
+ description: "Keep imports out of migrations: run them as an operational job with the file path as a parameter, and leave the migration to the schema.",
493
+ example: `-- operational import job, not a migration:
494
+ -- sqlite3 app.db "ATTACH '/path/legacy.db' AS legacy; INSERT INTO orders SELECT * FROM legacy.orders; DETACH legacy;"`
495
+ },
496
+ verified: true
497
+ };
498
+ var sl031Meta = {
499
+ id: "SL031",
500
+ version: "1.0.0",
501
+ name: "Redundant index",
502
+ tier: 1,
503
+ defaultSeverity: "note",
504
+ rationale: "An index whose columns are a leading prefix of another index on the same table, a duplicate of one, or an index on the INTEGER PRIMARY KEY (the rowid, which is the table's own B-tree) changes no query plan — SQLite already answers those lookups from the wider index or the table itself. It still costs a write on every insert, update and delete, and space in the file.",
505
+ example: `CREATE INDEX orders_user ON orders (user_id);
506
+ CREATE INDEX orders_user_created ON orders (user_id, created_at);`,
507
+ fix: {
508
+ description: "Keep the wider index and drop the prefix; never index the INTEGER PRIMARY KEY column or a column that is already UNIQUE.",
509
+ example: "CREATE INDEX orders_user_created ON orders (user_id, created_at);"
510
+ },
511
+ verified: true
512
+ };
513
+ var sl032Meta = {
514
+ id: "SL032",
515
+ version: "1.0.0",
516
+ name: "Indexes built before the bulk copy in a rebuild",
517
+ tier: 1,
518
+ defaultSeverity: "note",
519
+ rationale: "In a table rebuild the copy is the expensive step. Creating the new table's indexes before INSERT ... SELECT makes SQLite maintain every index row by row during the copy — random B-tree inserts for each — instead of one sorted build per index afterwards. The manual's own recipe creates the indexes after the copy.",
520
+ example: `CREATE TABLE orders_new (id INTEGER PRIMARY KEY, user_id INTEGER, qty INTEGER) STRICT;
521
+ CREATE INDEX orders_new_user ON orders_new (user_id);
522
+ INSERT INTO orders_new SELECT id, user_id, qty FROM orders;`,
523
+ fix: {
524
+ description: "Copy first, then create the indexes on the populated table.",
525
+ example: `CREATE TABLE orders_new (id INTEGER PRIMARY KEY, user_id INTEGER, qty INTEGER) STRICT;
526
+ INSERT INTO orders_new SELECT id, user_id, qty FROM orders;
527
+ CREATE INDEX orders_new_user ON orders_new (user_id);`
528
+ },
529
+ verified: true
530
+ };
531
+ var sl033Meta = {
532
+ id: "SL033",
533
+ version: "1.0.0",
534
+ name: "Text or blob primary key on a rowid table",
535
+ tier: 1,
536
+ defaultSeverity: "note",
537
+ rationale: "On a rowid table a TEXT or BLOB primary key is not the table's key: rows live in a B-tree keyed by the hidden rowid, and the primary key is a separate unique index. Every lookup by key walks the index and then the table, and every row is stored twice over (key in the index, key in the row). WITHOUT ROWID makes the primary key the table's own B-tree — the manual recommends it exactly for this shape.",
538
+ example: "CREATE TABLE sessions (token TEXT PRIMARY KEY NOT NULL, user_id INTEGER) STRICT;",
539
+ fix: {
540
+ description: "Declare the table WITHOUT ROWID when the primary key is not an integer and rows are small (under about a twentieth of a page). Keep a rowid table for wide rows or when code depends on rowid.",
541
+ example: "CREATE TABLE sessions (token TEXT PRIMARY KEY NOT NULL, user_id INTEGER) WITHOUT ROWID, STRICT;"
542
+ },
543
+ verified: true
544
+ };
545
+ var sl034Meta = {
546
+ id: "SL034",
547
+ version: "1.0.0",
548
+ name: "Journal mode set to something other than WAL",
549
+ tier: 1,
550
+ defaultSeverity: "note",
551
+ rationale: "journal_mode is stored in the database file: a migration that sets DELETE, TRUNCATE or PERSIST changes every future connection, not just the runner's. In those modes a writer blocks readers and readers block the writer for the length of each transaction; WAL lets readers proceed during a write and is the mode the application almost certainly wants.",
552
+ example: "PRAGMA journal_mode = DELETE;",
553
+ fix: {
554
+ description: "Set WAL once, from the application's connection setup or a deliberate operational step — and if a migration must set it, set WAL.",
555
+ example: "PRAGMA journal_mode = WAL;"
556
+ },
557
+ verified: true
558
+ };
559
+ var sl035Meta = {
560
+ id: "SL035",
561
+ version: "1.0.0",
562
+ name: "Connection-scoped pragma in a migration",
563
+ tier: 1,
564
+ defaultSeverity: "note",
565
+ rationale: "cache_size, synchronous, temp_store, mmap_size and busy_timeout live on the connection that set them. In a migration they tune the runner's connection for a few seconds and reach the application never — and synchronous=OFF removes durability for exactly the connection doing the schema change.",
566
+ example: `PRAGMA synchronous = OFF;
567
+ PRAGMA mmap_size = 268435456;
568
+ ALTER TABLE orders ADD COLUMN region TEXT;`,
569
+ fix: {
570
+ description: "Set connection pragmas where connections are opened, in the application, so every connection gets them. Leave migrations to the schema.",
571
+ example: `ALTER TABLE orders ADD COLUMN region TEXT;
572
+ -- application connection setup: PRAGMA synchronous = NORMAL; PRAGMA mmap_size = 268435456;`
573
+ },
574
+ verified: true
575
+ };
576
+ var sqliteMetas = [
577
+ sl001Meta,
578
+ sl002Meta,
579
+ sl003Meta,
580
+ sl004Meta,
581
+ sl005Meta,
582
+ sl006Meta,
583
+ sl007Meta,
584
+ sl008Meta,
585
+ sl009Meta,
586
+ sl010Meta,
587
+ sl011Meta,
588
+ sl012Meta,
589
+ sl013Meta,
590
+ sl014Meta,
591
+ sl015Meta,
592
+ sl016Meta,
593
+ sl017Meta,
594
+ sl018Meta,
595
+ sl019Meta,
596
+ sl020Meta,
597
+ sl021Meta,
598
+ sl022Meta,
599
+ sl023Meta,
600
+ sl024Meta,
601
+ sl025Meta,
602
+ sl026Meta,
603
+ sl027Meta,
604
+ sl028Meta,
605
+ sl029Meta,
606
+ sl030Meta,
607
+ sl031Meta,
608
+ sl032Meta,
609
+ sl033Meta,
610
+ sl034Meta,
611
+ sl035Meta
612
+ ];
613
+ var SQLITE_RULE_IDS = new Set(sqliteMetas.map((meta) => meta.id));
614
+
615
+ export { SL_RULE_CORPUS_VERSION, sl001Meta, sl002Meta, sl003Meta, sl004Meta, sl005Meta, sl006Meta, sl007Meta, sl008Meta, sl009Meta, sl010Meta, sl011Meta, sl012Meta, sl013Meta, sl014Meta, sl015Meta, sl016Meta, sl017Meta, sl018Meta, sl019Meta, sl020Meta, sl021Meta, sl022Meta, sl023Meta, sl024Meta, sl025Meta, sl026Meta, sl027Meta, sl028Meta, sl029Meta, sl030Meta, sl031Meta, sl032Meta, sl033Meta, sl034Meta, sl035Meta, sqliteMetas, SQLITE_RULE_IDS };
@@ -0,0 +1,4 @@
1
+ export { checkSqliteMigration, CREDENTIAL_RULE_IDS, sqliteStatementStartLines, type SqliteCheckOptions } from "./engine";
2
+ export { parseSqliteMigration, SqliteMigrationParseError, type ParsedSqliteMigration, type ParsedSqliteStatement } from "./parser";
3
+ export { finding, type SlRule, type SlRuleContext } from "./rule";
4
+ export { allSqliteRules, SL_RULE_CORPUS_VERSION, SQLITE_RULE_IDS, sqliteMetas } from "./rules/index";