@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.
- package/LICENSE.md +105 -0
- package/README.md +26 -0
- package/dist/engine.d.ts +24 -0
- package/dist/helpers.d.ts +77 -0
- package/dist/index-m4sztshc.js +615 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1421 -0
- package/dist/metas/index.d.ts +53 -0
- package/dist/metas/index.js +80 -0
- package/dist/parser.d.ts +33 -0
- package/dist/rule.d.ts +17 -0
- package/dist/rules/add-column.d.ts +13 -0
- package/dist/rules/dml-and-maintenance.d.ts +7 -0
- package/dist/rules/index.d.ts +5 -0
- package/dist/rules/indexes-and-types.d.ts +7 -0
- package/dist/rules/performance.d.ts +11 -0
- package/dist/rules/rebuild-pragmas.d.ts +11 -0
- package/dist/rules/schema-and-transactions.d.ts +23 -0
- package/dist/rules/table-shape.d.ts +5 -0
- package/package.json +45 -0
- package/src/engine.ts +89 -0
- package/src/helpers.ts +254 -0
- package/src/index.ts +5 -0
- package/src/metas/LICENSE +202 -0
- package/src/metas/index.ts +598 -0
- package/src/parser.ts +68 -0
- package/src/rule.ts +36 -0
- package/src/rules/LICENSE +202 -0
- package/src/rules/add-column.ts +180 -0
- package/src/rules/dml-and-maintenance.ts +75 -0
- package/src/rules/index.ts +18 -0
- package/src/rules/indexes-and-types.ts +99 -0
- package/src/rules/performance.ts +207 -0
- package/src/rules/rebuild-pragmas.ts +142 -0
- package/src/rules/schema-and-transactions.ts +446 -0
- package/src/rules/table-shape.ts +76 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@bolvrk/engine-sqlite/metas` — RuleMeta objects ONLY. Nothing in this
|
|
3
|
+
* module may carry a `check` function or import the parser: the website
|
|
4
|
+
* bundles this entry into the browser to render the rule pages and the
|
|
5
|
+
* database filter. The implementations live in ../rules and import their
|
|
6
|
+
* meta from here, never the other way around. A test asserts no exported
|
|
7
|
+
* object has a `check` property.
|
|
8
|
+
*
|
|
9
|
+
* Every danger claim below is backed by the SQLite documentation named in
|
|
10
|
+
* the comment next to `verified`, and the "fails outright" claims are
|
|
11
|
+
* re-proven against a real SQLite in test/claims.test.ts.
|
|
12
|
+
*/
|
|
13
|
+
import type { RuleMeta } from "@bolvrk/engine";
|
|
14
|
+
/** The SL corpus version. Bump on any rule addition, removal, or behavior change. */
|
|
15
|
+
export declare const SL_RULE_CORPUS_VERSION = "0.4.0";
|
|
16
|
+
export declare const sl001Meta: RuleMeta;
|
|
17
|
+
export declare const sl002Meta: RuleMeta;
|
|
18
|
+
export declare const sl003Meta: RuleMeta;
|
|
19
|
+
export declare const sl004Meta: RuleMeta;
|
|
20
|
+
export declare const sl005Meta: RuleMeta;
|
|
21
|
+
export declare const sl006Meta: RuleMeta;
|
|
22
|
+
export declare const sl007Meta: RuleMeta;
|
|
23
|
+
export declare const sl008Meta: RuleMeta;
|
|
24
|
+
export declare const sl009Meta: RuleMeta;
|
|
25
|
+
export declare const sl010Meta: RuleMeta;
|
|
26
|
+
export declare const sl011Meta: RuleMeta;
|
|
27
|
+
export declare const sl012Meta: RuleMeta;
|
|
28
|
+
export declare const sl013Meta: RuleMeta;
|
|
29
|
+
export declare const sl014Meta: RuleMeta;
|
|
30
|
+
export declare const sl015Meta: RuleMeta;
|
|
31
|
+
export declare const sl016Meta: RuleMeta;
|
|
32
|
+
export declare const sl017Meta: RuleMeta;
|
|
33
|
+
export declare const sl018Meta: RuleMeta;
|
|
34
|
+
export declare const sl019Meta: RuleMeta;
|
|
35
|
+
export declare const sl020Meta: RuleMeta;
|
|
36
|
+
export declare const sl021Meta: RuleMeta;
|
|
37
|
+
export declare const sl022Meta: RuleMeta;
|
|
38
|
+
export declare const sl023Meta: RuleMeta;
|
|
39
|
+
export declare const sl024Meta: RuleMeta;
|
|
40
|
+
export declare const sl025Meta: RuleMeta;
|
|
41
|
+
export declare const sl026Meta: RuleMeta;
|
|
42
|
+
export declare const sl027Meta: RuleMeta;
|
|
43
|
+
export declare const sl028Meta: RuleMeta;
|
|
44
|
+
export declare const sl029Meta: RuleMeta;
|
|
45
|
+
export declare const sl030Meta: RuleMeta;
|
|
46
|
+
export declare const sl031Meta: RuleMeta;
|
|
47
|
+
export declare const sl032Meta: RuleMeta;
|
|
48
|
+
export declare const sl033Meta: RuleMeta;
|
|
49
|
+
export declare const sl034Meta: RuleMeta;
|
|
50
|
+
export declare const sl035Meta: RuleMeta;
|
|
51
|
+
/** Every SL meta, in corpus order. */
|
|
52
|
+
export declare const sqliteMetas: readonly RuleMeta[];
|
|
53
|
+
export declare const SQLITE_RULE_IDS: ReadonlySet<string>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SL_RULE_CORPUS_VERSION,
|
|
3
|
+
SQLITE_RULE_IDS,
|
|
4
|
+
sl001Meta,
|
|
5
|
+
sl002Meta,
|
|
6
|
+
sl003Meta,
|
|
7
|
+
sl004Meta,
|
|
8
|
+
sl005Meta,
|
|
9
|
+
sl006Meta,
|
|
10
|
+
sl007Meta,
|
|
11
|
+
sl008Meta,
|
|
12
|
+
sl009Meta,
|
|
13
|
+
sl010Meta,
|
|
14
|
+
sl011Meta,
|
|
15
|
+
sl012Meta,
|
|
16
|
+
sl013Meta,
|
|
17
|
+
sl014Meta,
|
|
18
|
+
sl015Meta,
|
|
19
|
+
sl016Meta,
|
|
20
|
+
sl017Meta,
|
|
21
|
+
sl018Meta,
|
|
22
|
+
sl019Meta,
|
|
23
|
+
sl020Meta,
|
|
24
|
+
sl021Meta,
|
|
25
|
+
sl022Meta,
|
|
26
|
+
sl023Meta,
|
|
27
|
+
sl024Meta,
|
|
28
|
+
sl025Meta,
|
|
29
|
+
sl026Meta,
|
|
30
|
+
sl027Meta,
|
|
31
|
+
sl028Meta,
|
|
32
|
+
sl029Meta,
|
|
33
|
+
sl030Meta,
|
|
34
|
+
sl031Meta,
|
|
35
|
+
sl032Meta,
|
|
36
|
+
sl033Meta,
|
|
37
|
+
sl034Meta,
|
|
38
|
+
sl035Meta,
|
|
39
|
+
sqliteMetas
|
|
40
|
+
} from "../index-m4sztshc.js";
|
|
41
|
+
export {
|
|
42
|
+
SL_RULE_CORPUS_VERSION,
|
|
43
|
+
SQLITE_RULE_IDS,
|
|
44
|
+
sl001Meta,
|
|
45
|
+
sl002Meta,
|
|
46
|
+
sl003Meta,
|
|
47
|
+
sl004Meta,
|
|
48
|
+
sl005Meta,
|
|
49
|
+
sl006Meta,
|
|
50
|
+
sl007Meta,
|
|
51
|
+
sl008Meta,
|
|
52
|
+
sl009Meta,
|
|
53
|
+
sl010Meta,
|
|
54
|
+
sl011Meta,
|
|
55
|
+
sl012Meta,
|
|
56
|
+
sl013Meta,
|
|
57
|
+
sl014Meta,
|
|
58
|
+
sl015Meta,
|
|
59
|
+
sl016Meta,
|
|
60
|
+
sl017Meta,
|
|
61
|
+
sl018Meta,
|
|
62
|
+
sl019Meta,
|
|
63
|
+
sl020Meta,
|
|
64
|
+
sl021Meta,
|
|
65
|
+
sl022Meta,
|
|
66
|
+
sl023Meta,
|
|
67
|
+
sl024Meta,
|
|
68
|
+
sl025Meta,
|
|
69
|
+
sl026Meta,
|
|
70
|
+
sl027Meta,
|
|
71
|
+
sl028Meta,
|
|
72
|
+
sl029Meta,
|
|
73
|
+
sl030Meta,
|
|
74
|
+
sl031Meta,
|
|
75
|
+
sl032Meta,
|
|
76
|
+
sl033Meta,
|
|
77
|
+
sl034Meta,
|
|
78
|
+
sl035Meta,
|
|
79
|
+
sqliteMetas
|
|
80
|
+
};
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite parse layer: sql-parser-cst (MIT, pure TypeScript, a real
|
|
3
|
+
* dialect-aware grammar with source ranges). The only place SQLite SQL is
|
|
4
|
+
* turned into structure — rules walk the CST, never regex over SQL. Anything
|
|
5
|
+
* the grammar rejects fails the whole check: a partial check could hide a
|
|
6
|
+
* danger, so the engine reports "not checked" rather than guessing.
|
|
7
|
+
*/
|
|
8
|
+
import { type Node, type Statement } from "sql-parser-cst";
|
|
9
|
+
export type { Node, Statement };
|
|
10
|
+
export declare class SqliteMigrationParseError extends Error {
|
|
11
|
+
readonly cause?: unknown | undefined;
|
|
12
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
13
|
+
}
|
|
14
|
+
export interface ParsedSqliteStatement {
|
|
15
|
+
/** 0-based position in the migration file (empty statements — a stray `;` — are skipped). */
|
|
16
|
+
index: number;
|
|
17
|
+
/** CST node type, e.g. "alter_table_stmt", "pragma_stmt". */
|
|
18
|
+
type: Statement["type"];
|
|
19
|
+
/** The statement node; rules descend from here. */
|
|
20
|
+
node: Statement;
|
|
21
|
+
/** Verbatim SQL text of this statement, trimmed. */
|
|
22
|
+
sql: string;
|
|
23
|
+
/** Character offset of the statement's start in the migration text. */
|
|
24
|
+
location: number;
|
|
25
|
+
/** Character offset one past the statement's end. */
|
|
26
|
+
end: number;
|
|
27
|
+
}
|
|
28
|
+
export interface ParsedSqliteMigration {
|
|
29
|
+
statements: ParsedSqliteStatement[];
|
|
30
|
+
/** sql-parser-cst version the tree came from. */
|
|
31
|
+
parserVersion: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function parseSqliteMigration(sql: string): ParsedSqliteMigration;
|
package/dist/rule.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SL rule interface — the same shape as the Postgres engine's Rule
|
|
3
|
+
* (packages/engine/src/rule.ts) over the SQLite parse tree. Deliberately NOT
|
|
4
|
+
* shared code: the only shared abstractions between engines are the findings
|
|
5
|
+
* contract, the RuleMeta shape and the severity taxonomy.
|
|
6
|
+
*/
|
|
7
|
+
import type { Finding, RuleMeta } from "@bolvrk/engine";
|
|
8
|
+
import type { ParsedSqliteMigration, ParsedSqliteStatement } from "./parser";
|
|
9
|
+
export interface SlRuleContext {
|
|
10
|
+
migration: ParsedSqliteMigration;
|
|
11
|
+
}
|
|
12
|
+
export interface SlRule {
|
|
13
|
+
meta: RuleMeta;
|
|
14
|
+
check(ctx: SlRuleContext): Finding[];
|
|
15
|
+
}
|
|
16
|
+
/** Helper for building a finding with the rule's identity filled in. */
|
|
17
|
+
export declare function finding(rule: SlRule, stmt: ParsedSqliteStatement, props: Omit<Finding, "ruleId" | "ruleVersion" | "tier" | "statementIndex" | "sql" | "severity"> & Partial<Pick<Finding, "severity">>): Finding;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SlRule } from "../rule";
|
|
2
|
+
/** SL001: NOT NULL with no default (or DEFAULT NULL) — the statement fails. */
|
|
3
|
+
export declare const sl001: SlRule;
|
|
4
|
+
/** SL002: PRIMARY KEY or UNIQUE on an added column — the statement fails. */
|
|
5
|
+
export declare const sl002: SlRule;
|
|
6
|
+
/** SL003: DEFAULT CURRENT_* or a parenthesised expression — the statement fails. */
|
|
7
|
+
export declare const sl003: SlRule;
|
|
8
|
+
/** SL004: GENERATED ... STORED on an added column — the statement fails. */
|
|
9
|
+
export declare const sl004: SlRule;
|
|
10
|
+
/** SL005: REFERENCES with a non-NULL default — fails whenever foreign keys are enforced. */
|
|
11
|
+
export declare const sl005: SlRule;
|
|
12
|
+
/** SL008: a CHECK constraint on an added column makes SQLite scan the whole table. */
|
|
13
|
+
export declare const sl008: SlRule;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SlRule } from "../rule";
|
|
2
|
+
/** SL013: UPDATE / DELETE with no WHERE. */
|
|
3
|
+
export declare const sl013: SlRule;
|
|
4
|
+
/** SL014: VACUUM — an error inside a transaction, a whole-file rewrite outside one. */
|
|
5
|
+
export declare const sl014: SlRule;
|
|
6
|
+
/** SL015: REINDEX with no target. */
|
|
7
|
+
export declare const sl015: SlRule;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { SlRule } from "../rule";
|
|
2
|
+
export { SL_RULE_CORPUS_VERSION, SQLITE_RULE_IDS, sqliteMetas } from "../metas/index";
|
|
3
|
+
export type { SlRule, SlRuleContext } from "../rule";
|
|
4
|
+
/** The SL corpus, in id order. */
|
|
5
|
+
export declare const allSqliteRules: readonly SlRule[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SlRule } from "../rule";
|
|
2
|
+
/** SL016: CREATE INDEX on a table that already exists — built under the write lock, no CONCURRENTLY. */
|
|
3
|
+
export declare const sl016: SlRule;
|
|
4
|
+
/** SL017: a PRIMARY KEY column that is not exactly `INTEGER` (or is `INTEGER ... DESC`) in a rowid table. */
|
|
5
|
+
export declare const sl017: SlRule;
|
|
6
|
+
/** SL018: AUTOINCREMENT. */
|
|
7
|
+
export declare const sl018: SlRule;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SlRule } from "../rule";
|
|
2
|
+
/** SL031: an index the planner will never prefer over what already exists. */
|
|
3
|
+
export declare const sl031: SlRule;
|
|
4
|
+
/** SL032: CREATE INDEX on a new table before the INSERT ... SELECT that fills it. */
|
|
5
|
+
export declare const sl032: SlRule;
|
|
6
|
+
/** SL033: TEXT/BLOB primary key on a rowid table. */
|
|
7
|
+
export declare const sl033: SlRule;
|
|
8
|
+
/** SL034: journal_mode set to a rollback mode. */
|
|
9
|
+
export declare const sl034: SlRule;
|
|
10
|
+
/** SL035: a pragma that only affects the runner's connection. */
|
|
11
|
+
export declare const sl035: SlRule;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SlRule } from "../rule";
|
|
2
|
+
/** SL009: the rebuild recipe without PRAGMA foreign_keys=OFF before it. */
|
|
3
|
+
export declare const sl009: SlRule;
|
|
4
|
+
/** SL010: PRAGMA foreign_keys / journal_mode after BEGIN — silently ignored. */
|
|
5
|
+
export declare const sl010: SlRule;
|
|
6
|
+
/** SL011: foreign keys switched off and never back on in this file. */
|
|
7
|
+
export declare const sl011: SlRule;
|
|
8
|
+
/** SL012: foreign keys re-enabled without PRAGMA foreign_key_check in between. */
|
|
9
|
+
export declare const sl012: SlRule;
|
|
10
|
+
/** SL019: the rebuild recipe with the copy step missing — the old table is dropped before its rows reach the new one. */
|
|
11
|
+
export declare const sl019: SlRule;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type SlRule } from "../rule";
|
|
2
|
+
/** SL020: a new table without STRICT. */
|
|
3
|
+
export declare const sl020: SlRule;
|
|
4
|
+
/** SL021: a child key without an index on its columns. */
|
|
5
|
+
export declare const sl021: SlRule;
|
|
6
|
+
/** SL022: RENAME while legacy_alter_table is on. */
|
|
7
|
+
export declare const sl022: SlRule;
|
|
8
|
+
/** SL023: DROP TABLE / DROP COLUMN of something a view or trigger created here still references. */
|
|
9
|
+
export declare const sl023: SlRule;
|
|
10
|
+
/** SL024: a non-INTEGER PRIMARY KEY column in a rowid table without NOT NULL. */
|
|
11
|
+
export declare const sl024: SlRule;
|
|
12
|
+
/** SL025: a plain (deferred) BEGIN followed by writes. */
|
|
13
|
+
export declare const sl025: SlRule;
|
|
14
|
+
/** SL026: BEGIN inside an open transaction, or COMMIT/ROLLBACK with none open. */
|
|
15
|
+
export declare const sl026: SlRule;
|
|
16
|
+
/** SL027: DELETE without WHERE on a parent whose children (declared here) cascade. */
|
|
17
|
+
export declare const sl027: SlRule;
|
|
18
|
+
/** SL028: CREATE INDEX over an expression that calls a non-deterministic function. */
|
|
19
|
+
export declare const sl028: SlRule;
|
|
20
|
+
/** SL029: AUTOINCREMENT on a WITHOUT ROWID table — the CREATE fails. */
|
|
21
|
+
export declare const sl029: SlRule;
|
|
22
|
+
/** SL030: ATTACH DATABASE inside a migration. */
|
|
23
|
+
export declare const sl030: SlRule;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type SlRule } from "../rule";
|
|
2
|
+
/** SL006: DROP COLUMN rewrites the table (and fails if the column is referenced). */
|
|
3
|
+
export declare const sl006: SlRule;
|
|
4
|
+
/** SL007: RENAME COLUMN / RENAME TO breaks the previous release the moment it commits. */
|
|
5
|
+
export declare const sl007: SlRule;
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bolvrk/engine-sqlite",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Bolvrk SQLite migration danger rules (SL###) over a real SQLite grammar. ALPHA — static rules only, behind --engine=sqlite in the CLI.",
|
|
5
|
+
"license": "(FSL-1.1-ALv2 AND Apache-2.0)",
|
|
6
|
+
"homepage": "https://bolvrk.com",
|
|
7
|
+
"repository": "github:bolvrk/bolvrk",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"bun": "./src/index.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./metas": {
|
|
16
|
+
"types": "./dist/metas/index.d.ts",
|
|
17
|
+
"bun": "./src/metas/index.ts",
|
|
18
|
+
"import": "./dist/metas/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src",
|
|
24
|
+
"LICENSE.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"build": "rm -rf dist && bun build src/index.ts src/metas/index.ts --outdir dist --target node --external @bolvrk/engine --external sql-parser-cst --splitting && tsc -p tsconfig.build.json",
|
|
29
|
+
"docs": "bun scripts/gen-rule-docs.ts"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"sql-parser-cst": "0.42.1"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@bolvrk/engine": "^0.1.4"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"sqlite",
|
|
40
|
+
"migrations",
|
|
41
|
+
"sql",
|
|
42
|
+
"lint",
|
|
43
|
+
"ddl"
|
|
44
|
+
]
|
|
45
|
+
}
|
package/src/engine.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
import {
|
|
3
|
+
applySuppressions,
|
|
4
|
+
collectSuppressions,
|
|
5
|
+
CONTRACT_VERSION,
|
|
6
|
+
scanSecrets,
|
|
7
|
+
sortFindings,
|
|
8
|
+
type CheckReport,
|
|
9
|
+
type Finding,
|
|
10
|
+
type ParsedMigration,
|
|
11
|
+
} from "@bolvrk/engine";
|
|
12
|
+
import packageJson from "../package.json";
|
|
13
|
+
import { parseSqliteMigration } from "./parser";
|
|
14
|
+
import type { SlRule } from "./rule";
|
|
15
|
+
import { allSqliteRules, SL_RULE_CORPUS_VERSION, SQLITE_RULE_IDS } from "./rules/index";
|
|
16
|
+
|
|
17
|
+
export interface SqliteCheckOptions {
|
|
18
|
+
/** Override the rule set (used by tests); defaults to the full SL corpus. */
|
|
19
|
+
rules?: readonly SlRule[];
|
|
20
|
+
/** Skip the credential rules (BC003, BC004) that `check` runs on every engine. Default: run them. */
|
|
21
|
+
skipCredentials?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The credential rules `bolvrk check` runs on Postgres and, through here, on SQLite too — text-level, engine-neutral. */
|
|
25
|
+
export const CREDENTIAL_RULE_IDS: ReadonlySet<string> = new Set(["BC003", "BC004"]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Credential findings come from the engine's line scanner. Re-anchor each to
|
|
29
|
+
* the SQLite statement that contains its line so the finding follows the
|
|
30
|
+
* statement contract (statementIndex, statement SQL) — with the secret
|
|
31
|
+
* masked, as the scanner masked it.
|
|
32
|
+
*/
|
|
33
|
+
function credentialFindings(sql: string, migration: ReturnType<typeof parseSqliteMigration>): Finding[] {
|
|
34
|
+
const report = scanSecrets(sql);
|
|
35
|
+
if (report.findings.length === 0) return [];
|
|
36
|
+
const lineStarts: number[] = [0];
|
|
37
|
+
for (let i = 0; i < sql.length; i++) if (sql.charCodeAt(i) === 10) lineStarts.push(i + 1);
|
|
38
|
+
const lines = sql.split(/\r?\n/);
|
|
39
|
+
const out: Finding[] = [];
|
|
40
|
+
for (const hit of report.findings) {
|
|
41
|
+
if (!CREDENTIAL_RULE_IDS.has(hit.ruleId)) continue;
|
|
42
|
+
const offset = lineStarts[hit.statementIndex] ?? -1;
|
|
43
|
+
const stmt = migration.statements.find((s) => offset >= s.location && offset < s.end);
|
|
44
|
+
if (!stmt) continue;
|
|
45
|
+
const original = lines[hit.statementIndex] ?? "";
|
|
46
|
+
out.push({ ...hit, statementIndex: stmt.index, sql: original ? stmt.sql.replace(original.trim(), hit.sql.trim()) : stmt.sql });
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run a SQLite migration through the SL corpus. Same findings contract as
|
|
53
|
+
* the Postgres engine, same determinism guarantee: identical input produces
|
|
54
|
+
* byte-identical report JSON. Static (tier 1) only — SQLite has no server
|
|
55
|
+
* to introspect, and the rules that would want a live file (table sizes)
|
|
56
|
+
* say so in their message instead.
|
|
57
|
+
*
|
|
58
|
+
* Inline suppressions (`-- bolvrk-ignore SL001: reason`) work exactly as on
|
|
59
|
+
* Postgres: the engine's own collector reads the comment lines between
|
|
60
|
+
* statements as the parser located them.
|
|
61
|
+
*/
|
|
62
|
+
export function checkSqliteMigration(sql: string, options: SqliteCheckOptions = {}): CheckReport {
|
|
63
|
+
const migration = parseSqliteMigration(sql);
|
|
64
|
+
const rules = options.rules ?? allSqliteRules;
|
|
65
|
+
const raw = [...rules.flatMap((rule) => rule.check({ migration })), ...(options.skipCredentials ? [] : credentialFindings(sql, migration))];
|
|
66
|
+
// The suppression collector only reads index/location/end of each statement.
|
|
67
|
+
const suppressions = collectSuppressions(sql, migration as unknown as ParsedMigration, new Set([...SQLITE_RULE_IDS, ...CREDENTIAL_RULE_IDS]));
|
|
68
|
+
const findings = sortFindings(applySuppressions(raw, suppressions));
|
|
69
|
+
return {
|
|
70
|
+
contractVersion: CONTRACT_VERSION,
|
|
71
|
+
engine: "sqlite",
|
|
72
|
+
engineVersion: packageJson.version,
|
|
73
|
+
ruleCorpusVersion: SL_RULE_CORPUS_VERSION,
|
|
74
|
+
schemaInspected: false,
|
|
75
|
+
statementCount: migration.statements.length,
|
|
76
|
+
findings,
|
|
77
|
+
...(suppressions.length > 0 ? { suppressions } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 1-based start line of each statement — for SARIF locations. */
|
|
82
|
+
export function sqliteStatementStartLines(sql: string): number[] {
|
|
83
|
+
const migration = parseSqliteMigration(sql);
|
|
84
|
+
return migration.statements.map((stmt) => {
|
|
85
|
+
let line = 1;
|
|
86
|
+
for (let i = 0; i < stmt.location; i++) if (sql.charCodeAt(i) === 10) line++;
|
|
87
|
+
return line;
|
|
88
|
+
});
|
|
89
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* CST helpers the SL rules are written against: one implementation of
|
|
4
|
+
* "which table does this statement touch", "is this statement inside an
|
|
5
|
+
* explicit transaction", and the shape of a column definition.
|
|
6
|
+
*/
|
|
7
|
+
import type { Node } from "sql-parser-cst";
|
|
8
|
+
import type { ParsedSqliteMigration, ParsedSqliteStatement } from "./parser";
|
|
9
|
+
|
|
10
|
+
type AnyNode = Record<string, any>;
|
|
11
|
+
|
|
12
|
+
/** Unquoted name of an identifier or a `schema.name` member expression. */
|
|
13
|
+
export function nameOf(node: AnyNode | undefined | null): string | undefined {
|
|
14
|
+
if (!node) return undefined;
|
|
15
|
+
if (node.type === "identifier") return node.name as string;
|
|
16
|
+
if (node.type === "member_expr") return nameOf(node.property);
|
|
17
|
+
if (node.type === "keyword") return String(node.name).toLowerCase();
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Items of a `list_expr`, or the single node when the parser did not wrap one. */
|
|
22
|
+
export function items(node: AnyNode | undefined | null): AnyNode[] {
|
|
23
|
+
if (!node) return [];
|
|
24
|
+
if (node.type === "list_expr") return node.items ?? [];
|
|
25
|
+
if (node.type === "paren_expr") return items(node.expr);
|
|
26
|
+
return [node];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The ALTER TABLE actions of a statement (empty for anything else). */
|
|
30
|
+
export function alterActions(stmt: ParsedSqliteStatement): AnyNode[] {
|
|
31
|
+
if (stmt.node.type !== "alter_table_stmt") return [];
|
|
32
|
+
return items((stmt.node as AnyNode).actions);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Column definitions added by `ALTER TABLE ... ADD [COLUMN]` in this statement. */
|
|
36
|
+
export function addedColumns(stmt: ParsedSqliteStatement): AnyNode[] {
|
|
37
|
+
return alterActions(stmt)
|
|
38
|
+
.filter((action) => action.type === "alter_action_add_column")
|
|
39
|
+
.map((action) => action.column)
|
|
40
|
+
.filter((column) => column?.type === "column_definition");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Column definitions of a CREATE TABLE statement (constraints declared at table level excluded). */
|
|
44
|
+
export function createdColumns(stmt: ParsedSqliteStatement): AnyNode[] {
|
|
45
|
+
if (stmt.node.type !== "create_table_stmt") return [];
|
|
46
|
+
return items((stmt.node as AnyNode).columns).filter((item) => item.type === "column_definition");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Table-level constraints of a CREATE TABLE statement. */
|
|
50
|
+
export function tableConstraints(stmt: ParsedSqliteStatement): AnyNode[] {
|
|
51
|
+
if (stmt.node.type !== "create_table_stmt") return [];
|
|
52
|
+
return items((stmt.node as AnyNode).columns).filter((item) => item.type !== "column_definition");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A column definition's constraint of the given CST type, if any. */
|
|
56
|
+
export function constraintOf(column: AnyNode, type: string): AnyNode | undefined {
|
|
57
|
+
return (column.constraints ?? []).find((c: AnyNode) => c.type === type || c.constraint?.type === type);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Keyword name(s) of a data type node, uppercased and joined — `INT`, `INTEGER`, `VARCHAR`. */
|
|
61
|
+
export function dataTypeName(column: AnyNode): string {
|
|
62
|
+
const name = column.dataType?.name;
|
|
63
|
+
if (!name) return "";
|
|
64
|
+
const parts = Array.isArray(name) ? name : [name];
|
|
65
|
+
return parts.map((part: AnyNode) => String(part?.name ?? part?.text ?? "")).join(" ").toUpperCase();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Table options of CREATE TABLE (`WITHOUT ROWID`, `STRICT`), uppercased. */
|
|
69
|
+
export function tableOptions(stmt: ParsedSqliteStatement): string[] {
|
|
70
|
+
if (stmt.node.type !== "create_table_stmt") return [];
|
|
71
|
+
return items((stmt.node as AnyNode).options)
|
|
72
|
+
.filter((option) => option.type === "table_option")
|
|
73
|
+
.map((option) => (Array.isArray(option.name) ? option.name : [option.name]).map((kw: AnyNode) => String(kw?.name ?? "")).join(" ").toUpperCase());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The primary table a statement targets, if any. */
|
|
77
|
+
export function tableOf(stmt: ParsedSqliteStatement): string | undefined {
|
|
78
|
+
const node = stmt.node as AnyNode;
|
|
79
|
+
switch (node.type) {
|
|
80
|
+
case "alter_table_stmt":
|
|
81
|
+
case "create_index_stmt":
|
|
82
|
+
return nameOf(node.table);
|
|
83
|
+
case "create_table_stmt":
|
|
84
|
+
case "create_view_stmt":
|
|
85
|
+
return nameOf(node.name);
|
|
86
|
+
case "drop_table_stmt":
|
|
87
|
+
return nameOf(items(node.tables)[0]);
|
|
88
|
+
case "insert_stmt":
|
|
89
|
+
return nameOf(node.clauses?.find((c: AnyNode) => c.type === "insert_clause")?.table);
|
|
90
|
+
case "update_stmt":
|
|
91
|
+
return nameOf(items(node.clauses?.find((c: AnyNode) => c.type === "update_clause")?.tables)[0]);
|
|
92
|
+
case "delete_stmt":
|
|
93
|
+
return nameOf(items(node.clauses?.find((c: AnyNode) => c.type === "delete_clause")?.tables)[0]);
|
|
94
|
+
default:
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Tables dropped by a DROP TABLE statement. */
|
|
100
|
+
export function droppedTables(stmt: ParsedSqliteStatement): string[] {
|
|
101
|
+
if (stmt.node.type !== "drop_table_stmt") return [];
|
|
102
|
+
return items((stmt.node as AnyNode).tables).map(nameOf).filter((n): n is string => !!n);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** True when `table` is created by an earlier statement of this migration. */
|
|
106
|
+
export function tableCreatedInMigration(migration: ParsedSqliteMigration, beforeIndex: number, table: string): boolean {
|
|
107
|
+
const wanted = table.toLowerCase();
|
|
108
|
+
return migration.statements.some(
|
|
109
|
+
(stmt) => stmt.index < beforeIndex && stmt.node.type === "create_table_stmt" && tableOf(stmt)?.toLowerCase() === wanted,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** One "drop, then rename another table onto the name" pair in a migration. */
|
|
114
|
+
export interface TableRebuild {
|
|
115
|
+
/** The DROP TABLE statement. */
|
|
116
|
+
drop: ParsedSqliteStatement;
|
|
117
|
+
/** The ALTER TABLE ... RENAME TO statement that restores the dropped name. */
|
|
118
|
+
rename: ParsedSqliteStatement;
|
|
119
|
+
/** The name being rebuilt. */
|
|
120
|
+
table: string;
|
|
121
|
+
/** The table renamed onto it. */
|
|
122
|
+
replacement: string;
|
|
123
|
+
/** True when the replacement was created earlier in this file — the documented recipe rather than a swap of two live tables. */
|
|
124
|
+
replacementCreatedHere: boolean;
|
|
125
|
+
/** An INSERT into the replacement that reads from the old table, issued before the DROP. */
|
|
126
|
+
copy?: ParsedSqliteStatement;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Every rebuild in the migration: DROP TABLE x followed, later in the file,
|
|
131
|
+
* by ALTER TABLE y RENAME TO x. The interim table's name plays no part —
|
|
132
|
+
* the match is structural, on the dropped name coming back as a rename
|
|
133
|
+
* target. Cross-file rebuilds are invisible; the engine sees one file.
|
|
134
|
+
*/
|
|
135
|
+
export function tableRebuilds(migration: ParsedSqliteMigration): TableRebuild[] {
|
|
136
|
+
const out: TableRebuild[] = [];
|
|
137
|
+
for (const drop of migration.statements) {
|
|
138
|
+
for (const table of droppedTables(drop)) {
|
|
139
|
+
const wanted = table.toLowerCase();
|
|
140
|
+
const rename = migration.statements.find(
|
|
141
|
+
(later) =>
|
|
142
|
+
later.index > drop.index &&
|
|
143
|
+
alterActions(later).some((a) => a.type === "alter_action_rename" && String(nameOf(a.newName) ?? "").toLowerCase() === wanted),
|
|
144
|
+
);
|
|
145
|
+
if (!rename) continue;
|
|
146
|
+
const replacement = tableOf(rename) ?? "";
|
|
147
|
+
const copy = migration.statements.find(
|
|
148
|
+
(stmt) =>
|
|
149
|
+
stmt.index < drop.index &&
|
|
150
|
+
stmt.node.type === "insert_stmt" &&
|
|
151
|
+
tableOf(stmt)?.toLowerCase() === replacement.toLowerCase() &&
|
|
152
|
+
readsFromTable(stmt, table),
|
|
153
|
+
);
|
|
154
|
+
out.push({ drop, rename, table, replacement, replacementCreatedHere: !!replacement && tableCreatedInMigration(migration, rename.index, replacement), copy });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** True when a table name appears anywhere in the statement other than as its own target — a FROM, a join, a subquery. */
|
|
161
|
+
function readsFromTable(stmt: ParsedSqliteStatement, table: string): boolean {
|
|
162
|
+
const wanted = table.toLowerCase();
|
|
163
|
+
const node = stmt.node as AnyNode;
|
|
164
|
+
const sources = (node.clauses ?? []).filter((c: AnyNode) => c.type !== "insert_clause");
|
|
165
|
+
return descendants(sources, (n) => n.type === "identifier" && String(n.name).toLowerCase() === wanted).length > 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The WHERE clause of an UPDATE / DELETE, if present. */
|
|
169
|
+
export function hasWhere(stmt: ParsedSqliteStatement): boolean {
|
|
170
|
+
const clauses: AnyNode[] = (stmt.node as AnyNode).clauses ?? [];
|
|
171
|
+
return clauses.some((c) => c.type === "where_clause");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface PragmaRead {
|
|
175
|
+
name: string;
|
|
176
|
+
/** Assigned value, lowercased (`off`, `on`, `0`, `wal`); undefined for a bare query like `PRAGMA foreign_key_check`. */
|
|
177
|
+
value?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Name and value of a PRAGMA statement; undefined for anything else. */
|
|
181
|
+
export function pragmaOf(stmt: ParsedSqliteStatement): PragmaRead | undefined {
|
|
182
|
+
if (stmt.node.type !== "pragma_stmt") return undefined;
|
|
183
|
+
const pragma = (stmt.node as AnyNode).pragma;
|
|
184
|
+
if (!pragma) return undefined;
|
|
185
|
+
if (pragma.type === "pragma_assignment") return { name: nameOf(pragma.name)!.toLowerCase(), value: literalText(pragma.value) };
|
|
186
|
+
if (pragma.type === "pragma_func_call") {
|
|
187
|
+
const arg = pragma.args?.expr;
|
|
188
|
+
return { name: nameOf(pragma.name)!.toLowerCase(), value: arg ? literalText(arg) : undefined };
|
|
189
|
+
}
|
|
190
|
+
const name = nameOf(pragma);
|
|
191
|
+
return name ? { name: name.toLowerCase() } : undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function literalText(node: AnyNode | undefined): string | undefined {
|
|
195
|
+
if (!node) return undefined;
|
|
196
|
+
// A negative number (`PRAGMA cache_size = -64000`) parses as a prefix operator over the literal.
|
|
197
|
+
if (node.type === "prefix_op_expr" && node.expr) return `${String(node.operator ?? "")}${literalText(node.expr) ?? ""}`;
|
|
198
|
+
if (node.type === "keyword") return String(node.name).toLowerCase();
|
|
199
|
+
if (node.type === "identifier") return String(node.name).toLowerCase();
|
|
200
|
+
if ("value" in node && node.value !== undefined && node.value !== null) return String(node.value).toLowerCase();
|
|
201
|
+
if (node.text) return String(node.text).toLowerCase();
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** SQLite's boolean spellings for a pragma value. */
|
|
206
|
+
export function pragmaIsOff(value: string | undefined): boolean {
|
|
207
|
+
return value !== undefined && ["off", "0", "false", "no"].includes(value);
|
|
208
|
+
}
|
|
209
|
+
export function pragmaIsOn(value: string | undefined): boolean {
|
|
210
|
+
return value !== undefined && ["on", "1", "true", "yes"].includes(value);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* For each statement, whether it runs inside an explicit transaction opened
|
|
215
|
+
* earlier in the file (BEGIN ... COMMIT/END/ROLLBACK). Statements before any
|
|
216
|
+
* BEGIN, or after the matching COMMIT, are "autocommit" from the file's point
|
|
217
|
+
* of view — a migration runner may still wrap the whole file, which is why
|
|
218
|
+
* rules that depend on this say so in their message.
|
|
219
|
+
*/
|
|
220
|
+
export function insideTransaction(migration: ParsedSqliteMigration): boolean[] {
|
|
221
|
+
const out: boolean[] = [];
|
|
222
|
+
let open = false;
|
|
223
|
+
for (const stmt of migration.statements) {
|
|
224
|
+
const type = stmt.node.type;
|
|
225
|
+
if (type === "start_transaction_stmt") {
|
|
226
|
+
out.push(false);
|
|
227
|
+
open = true;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (type === "commit_transaction_stmt" || type === "rollback_transaction_stmt") {
|
|
231
|
+
out.push(open);
|
|
232
|
+
open = false;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
out.push(open);
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Depth-first descendants of a CST node matching a predicate. */
|
|
241
|
+
export function descendants(node: Node | AnyNode | undefined, predicate: (node: AnyNode) => boolean): AnyNode[] {
|
|
242
|
+
const out: AnyNode[] = [];
|
|
243
|
+
const walk = (value: any) => {
|
|
244
|
+
if (!value || typeof value !== "object") return;
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
for (const item of value) walk(item);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (value.type && predicate(value)) out.push(value);
|
|
250
|
+
for (const key of Object.keys(value)) if (key !== "range" && typeof value[key] === "object") walk(value[key]);
|
|
251
|
+
};
|
|
252
|
+
walk(node);
|
|
253
|
+
return out;
|
|
254
|
+
}
|