@syntopica/db-quality 0.2.1 → 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 +67 -19
- package/dist/cli.js +612 -215
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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": {
|
|
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
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
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
|