@syntopica/db-quality 0.2.0 → 0.3.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/README.md CHANGED
@@ -51,7 +51,14 @@ a dependency:
51
51
  "supabase": { "migrations": "supabase/migrations" },
52
52
  "prisma": { "schema": "prisma/schema.prisma" },
53
53
  "drizzle": { "roots": ["src"], "objectNames": ["db", "tx"] },
54
- "sqlite": { "files": ["data/app.db"] },
54
+ "sqlite": {
55
+ "files": ["data/app.db"],
56
+ "queries": {
57
+ "paths": ["src/sql"],
58
+ "database": "~/dev/app-copy.db",
59
+ "minRows": 10000
60
+ }
61
+ },
55
62
  "postgrest": { "roots": ["src", "app"] },
56
63
  "audit": { "inGate": true, "bloatThreshold": 5, "soda": "db-quality/soda" },
57
64
  "perf": {
@@ -80,27 +87,29 @@ Every section is optional. `audit.soda` names a directory holding a Soda Core
80
87
  database password. `postgrest.roots` names the directories scanned for `.ts` and
81
88
  `.tsx` files. The `perf` values above are the built-in defaults except `inGate`,
82
89
  which `init` always writes as `false` — the gate measures nothing until a
83
- project reaches phase 4.
90
+ project reaches phase 4. `sqlite.queries` is optional and never written by
91
+ `init`; see [SQLite query files](#sqlite-query-files).
84
92
 
85
93
  ## Findings
86
94
 
87
- | Code | Source | Severity | What it means |
88
- | ------------------ | --------------------------- | ----------- | --------------------------------------------------------------------------------------------------- |
89
- | `BDB001` | permissive-policy | warn | a policy uses `using (true)` or `with check (true)` |
90
- | `BDB002` | rls-enabled-no-policy | warn | RLS on, no policy in any migration: service role only |
91
- | `BDB003` | table-without-rls | warn | a `public` table never enables row level security |
92
- | `BDB004` | auth-uid-not-wrapped | warn | `auth.uid()` in a policy without `(select ...)`: evaluated per row |
93
- | `BDB005` | definer-without-search-path | warn | `SECURITY DEFINER` function without `set search_path` |
94
- | `BDB100/<rule>` | squawk | as squawk | migration lock and schema hazards, Supabase profile |
95
- | `BDB200/<rule>` | prisma-lint | warn | relation field without an index |
96
- | `BDB300/<rule>` | eslint-plugin-drizzle | error | `delete` or `update` without `.where()` |
97
- | `BDB401`-`BDB403` | sqlite3 | error/warn | integrity, dangling foreign keys, table without primary key |
98
- | `BDB500/<name>` | Supabase advisors | as Supabase | splinter security and performance lints on the live project |
99
- | `BDB601`, `BDB602` | Supabase inspect | warn | never-scanned index, table bloat over `audit.bloatThreshold` |
100
- | `BDB700/<check>` | Soda Core | error/warn | a failed or warned data check from `<audit.soda>/checks.yml` |
101
- | `BDB801`-`BDB805` | PostgREST rules | warn | query-chain rules on `.ts`/`.tsx` under `postgrest.roots`; see [PostgREST rules](#postgrest-rules) |
102
- | `BDB901`-`BDB904` | perf diff | error/warn | live regressions, slow queries, sequential scans, temp spill; see [Performance](#performance) |
103
- | `BDB911`-`BDB913` | perf bench | error/warn | bench query regressions, plan degradation, stale planner estimates; see [Performance](#performance) |
95
+ | Code | Source | Severity | What it means |
96
+ | ------------------ | --------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------- |
97
+ | `BDB001` | permissive-policy | warn | a policy uses `using (true)` or `with check (true)` |
98
+ | `BDB002` | rls-enabled-no-policy | warn | RLS on, no policy in any migration: service role only |
99
+ | `BDB003` | table-without-rls | warn | a `public` table never enables row level security |
100
+ | `BDB004` | auth-uid-not-wrapped | warn | `auth.uid()` in a policy without `(select ...)`: evaluated per row |
101
+ | `BDB005` | definer-without-search-path | warn | `SECURITY DEFINER` function without `set search_path` |
102
+ | `BDB100/<rule>` | squawk | as squawk | migration lock and schema hazards, Supabase profile |
103
+ | `BDB200/<rule>` | prisma-lint | warn | relation field without an index |
104
+ | `BDB300/<rule>` | eslint-plugin-drizzle | error | `delete` or `update` without `.where()` |
105
+ | `BDB401`-`BDB403` | sqlite3 | error/warn | integrity, dangling foreign keys, table without primary key |
106
+ | `BDB404`-`BDB406` | SQLite query files | warn | optional-parameter guard, comma-list `instr()`, full scan of a large table; see [SQLite query files](#sqlite-query-files) |
107
+ | `BDB500/<name>` | Supabase advisors | as Supabase | splinter security and performance lints on the live project |
108
+ | `BDB601`, `BDB602` | Supabase inspect | warn | never-scanned index, table bloat over `audit.bloatThreshold` |
109
+ | `BDB700/<check>` | Soda Core | error/warn | a failed or warned data check from `<audit.soda>/checks.yml` |
110
+ | `BDB801`-`BDB805` | PostgREST rules | warn | query-chain rules on `.ts`/`.tsx` under `postgrest.roots`; see [PostgREST rules](#postgrest-rules) |
111
+ | `BDB901`-`BDB904` | perf diff | error/warn | live regressions, slow queries, sequential scans, temp spill; see [Performance](#performance) |
112
+ | `BDB911`-`BDB913` | perf bench | error/warn | bench query regressions, plan degradation, stale planner estimates; see [Performance](#performance) |
104
113
 
105
114
  Disable a code for a project with an object naming the reason:
106
115
 
@@ -167,6 +176,45 @@ named method like `.eq()`, and anything about a view or a table the migrations
167
176
  do not define — the index knowledge `BDB802` and `BDB803` use comes only from
168
177
  `create table`, `create index` and constraint statements.
169
178
 
179
+ ## SQLite query files
180
+
181
+ An application that keeps its SQLite queries in `.sql` files (loaded with
182
+ `include_str!`, `readFileSync` or the like) can have `check` and `gate` read
183
+ them. `sqlite.queries.paths` names directories, relative to the project root,
184
+ walked recursively for `*.sql`; each file is split into statements, comments
185
+ removed, and only statements starting with `SELECT`, `WITH`, `UPDATE`, `DELETE`,
186
+ `INSERT` or `REPLACE` are considered. Findings carry the line of the match.
187
+
188
+ | Code | Rule | What it proves |
189
+ | -------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
190
+ | `BDB404` | optional-parameter-guard | `?1 IS NULL OR ...` (or `... OR ?1 IS NULL`, with `?`, `?NNN`, `:name`, `@name`, `$name`): SQLite plans at prepare time, before the value is bound, so the guarded column's index is out of reach |
191
+ | `BDB405` | comma-list-membership | `instr(',' \|\| ?1 \|\| ',', ',' \|\| col \|\| ',')`: list membership no index can answer; bind a JSON array and use `col IN (SELECT value FROM json_each(?1))` |
192
+ | `BDB406` | full-scan | with `sqlite.queries.database`, `EXPLAIN QUERY PLAN` shows `SCAN <table>` or `SCAN <table> USING [COVERING] INDEX` on a table of at least `minRows` rows |
193
+
194
+ `BDB404` and `BDB405` need nothing but the files. `BDB406` needs
195
+ `sqlite.queries.database`: a SQLite file whose schema matches the queries,
196
+ usually a local copy of the application's store. It is resolved against the
197
+ project root; a leading `~/` is the home directory; an absolute path is used as
198
+ is. It is opened with `sqlite3 -readonly`. Every statement is planned with its
199
+ parameters left unbound, which is the plan the application gets at prepare time;
200
+ binding a literal would let SQLite fold a `?1 IS NULL OR` guard away and show a
201
+ plan the application never runs. A `SCAN` of a CTE, a subquery, a constant row
202
+ or a virtual table is not reported; an alias is resolved to its table through
203
+ the statement's `FROM`/`JOIN`. Rows are counted once per table per run, the
204
+ default `minRows` is 10000, and there is at most one finding per file, table and
205
+ scan kind. When the same plan also sorts the rows in a temporary B-tree for
206
+ `ORDER BY`, the message says so.
207
+
208
+ A statement `sqlite3` cannot plan is skipped rather than failed: a table the
209
+ application creates at run time, a module the shell lacks, syntax the shell
210
+ rejects. A function the application registers itself (`vexa_strip_digits`) does
211
+ not stop `EXPLAIN QUERY PLAN` in current shells, so those statements are still
212
+ planned. A configured database that does not exist or is not readable skips
213
+ `BDB406` with one line on stderr naming the path, and the static rules still
214
+ run, so a CI runner without the local copy stays green on what it can check. A
215
+ scan that is intended (a batch job, a garbage collector) is carried in the
216
+ baseline or the rule is disabled with a reason.
217
+
170
218
  ## Performance
171
219
 
172
220
  Three commands measure a live Supabase project, and the gate's `perf` stage runs