supaschema 0.1.0-rc.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/.agents/skills/supaschema/SKILL.md +61 -0
- package/.claude/hooks/block-generated-migration-edits.mjs +32 -0
- package/.claude/rules/supaschema.md +22 -0
- package/.claude/settings.json +16 -0
- package/.claude/skills/supaschema/SKILL.md +61 -0
- package/.codex/hooks/supaschema-tool-gate.mjs +73 -0
- package/.codex/hooks.json +16 -0
- package/.codex/rules/supaschema.rules +22 -0
- package/AGENTS.md +40 -0
- package/LICENSE +661 -0
- package/LICENSE-COMMERCIAL.md +35 -0
- package/README.md +249 -0
- package/benchmarks/README.md +104 -0
- package/benchmarks/compare.js +489 -0
- package/benchmarks/fixtures/additive/from.sql +8 -0
- package/benchmarks/fixtures/additive/manifest.json +1 -0
- package/benchmarks/fixtures/additive/to.sql +9 -0
- package/benchmarks/fixtures/functions-policies/from.sql +24 -0
- package/benchmarks/fixtures/functions-policies/manifest.json +5 -0
- package/benchmarks/fixtures/functions-policies/to.sql +24 -0
- package/benchmarks/plot-lib.js +234 -0
- package/benchmarks/plot-svg.js +339 -0
- package/benchmarks/plot.js +154 -0
- package/benchmarks/tools/bench-all.sh +49 -0
- package/benchmarks/tools/build-project-fixture.mjs +245 -0
- package/benchmarks/tools/compare-db.mjs +101 -0
- package/benchmarks/tools/compare-fixtures.mjs +84 -0
- package/benchmarks/tools/compare-report.mjs +90 -0
- package/benchmarks/tools/compare-supabase.mjs +67 -0
- package/benchmarks/tools/registry.js +266 -0
- package/benchmarks/tools/run-workflow.mjs +77 -0
- package/bin/postinstall.mjs +26 -0
- package/bin/supaschema +2 -0
- package/config-schema.json +208 -0
- package/corpus/supabase-style/corpus.json +6 -0
- package/corpus/supabase-style/migrations/20260101000000_init.sql +28 -0
- package/corpus/supabase-style/migrations/20260102000000_noise.sql +13 -0
- package/corpus/supabase-style/migrations/20260103000000_churn.sql +4 -0
- package/corpus/supabase-style/migrations/20260104000000_triggers.sql +17 -0
- package/corpus/supabase-style/roles.sql +13 -0
- package/corpus/supabase-style/tree/functions.sql +26 -0
- package/corpus/supabase-style/tree/policies.sql +4 -0
- package/corpus/supabase-style/tree/schema.sql +4 -0
- package/corpus/supabase-style/tree/tables.sql +20 -0
- package/corpus/supabase-style/tree/triggers.sql +3 -0
- package/corpus/supabase-style/tree/types.sql +2 -0
- package/corpus/supabase-style/tree/views.sql +6 -0
- package/dist/audit.d.ts +20 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +68 -0
- package/dist/benchmark-db.d.ts +5 -0
- package/dist/benchmark-db.d.ts.map +1 -0
- package/dist/benchmark-db.js +71 -0
- package/dist/benchmark-fixtures.d.ts +10 -0
- package/dist/benchmark-fixtures.d.ts.map +1 -0
- package/dist/benchmark-fixtures.js +201 -0
- package/dist/benchmark.d.ts +2 -0
- package/dist/benchmark.d.ts.map +1 -0
- package/dist/benchmark.js +308 -0
- package/dist/catalog-comments.d.ts +9 -0
- package/dist/catalog-comments.d.ts.map +1 -0
- package/dist/catalog-comments.js +194 -0
- package/dist/catalog-extras.d.ts +12 -0
- package/dist/catalog-extras.d.ts.map +1 -0
- package/dist/catalog-extras.js +408 -0
- package/dist/catalog-foreign.d.ts +15 -0
- package/dist/catalog-foreign.d.ts.map +1 -0
- package/dist/catalog-foreign.js +114 -0
- package/dist/catalog-tables.d.ts +9 -0
- package/dist/catalog-tables.d.ts.map +1 -0
- package/dist/catalog-tables.js +114 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +351 -0
- package/dist/check-hazards.d.ts +7 -0
- package/dist/check-hazards.d.ts.map +1 -0
- package/dist/check-hazards.js +83 -0
- package/dist/check-reporters.d.ts +8 -0
- package/dist/check-reporters.d.ts.map +1 -0
- package/dist/check-reporters.js +76 -0
- package/dist/check.d.ts +3 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +229 -0
- package/dist/cli-defaults.d.ts +24 -0
- package/dist/cli-defaults.d.ts.map +1 -0
- package/dist/cli-defaults.js +65 -0
- package/dist/cli-diff.d.ts +13 -0
- package/dist/cli-diff.d.ts.map +1 -0
- package/dist/cli-diff.js +348 -0
- package/dist/cli-reports.d.ts +9 -0
- package/dist/cli-reports.d.ts.map +1 -0
- package/dist/cli-reports.js +90 -0
- package/dist/cli-tools.d.ts +17 -0
- package/dist/cli-tools.d.ts.map +1 -0
- package/dist/cli-tools.js +136 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +239 -0
- package/dist/config-schema-gen.d.ts +2 -0
- package/dist/config-schema-gen.d.ts.map +1 -0
- package/dist/config-schema-gen.js +11 -0
- package/dist/config.d.ts +58 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +132 -0
- package/dist/core.d.ts +115 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1 -0
- package/dist/corpus.d.ts +26 -0
- package/dist/corpus.d.ts.map +1 -0
- package/dist/corpus.js +112 -0
- package/dist/database-url.d.ts +8 -0
- package/dist/database-url.d.ts.map +1 -0
- package/dist/database-url.js +74 -0
- package/dist/db-admin.d.ts +23 -0
- package/dist/db-admin.d.ts.map +1 -0
- package/dist/db-admin.js +147 -0
- package/dist/diagnostics.d.ts +16 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +155 -0
- package/dist/diff-score.d.ts +12 -0
- package/dist/diff-score.d.ts.map +1 -0
- package/dist/diff-score.js +339 -0
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +110 -0
- package/dist/hash.d.ts +7 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +34 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/lineage.d.ts +23 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/lineage.js +61 -0
- package/dist/migrations-status.d.ts +35 -0
- package/dist/migrations-status.d.ts.map +1 -0
- package/dist/migrations-status.js +131 -0
- package/dist/plan-order.d.ts +4 -0
- package/dist/plan-order.d.ts.map +1 -0
- package/dist/plan-order.js +178 -0
- package/dist/planner-replace.d.ts +4 -0
- package/dist/planner-replace.d.ts.map +1 -0
- package/dist/planner-replace.js +76 -0
- package/dist/planner-table.d.ts +3 -0
- package/dist/planner-table.d.ts.map +1 -0
- package/dist/planner-table.js +165 -0
- package/dist/planner.d.ts +5 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +385 -0
- package/dist/render-guards.d.ts +12 -0
- package/dist/render-guards.d.ts.map +1 -0
- package/dist/render-guards.js +159 -0
- package/dist/render.d.ts +7 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +325 -0
- package/dist/selfcheck.d.ts +11 -0
- package/dist/selfcheck.d.ts.map +1 -0
- package/dist/selfcheck.js +43 -0
- package/dist/source-normalize.d.ts +14 -0
- package/dist/source-normalize.d.ts.map +1 -0
- package/dist/source-normalize.js +420 -0
- package/dist/source.d.ts +4 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +233 -0
- package/dist/sql/ast.d.ts +42 -0
- package/dist/sql/ast.d.ts.map +1 -0
- package/dist/sql/ast.js +241 -0
- package/dist/sql/canonical-nodes.d.ts +5 -0
- package/dist/sql/canonical-nodes.d.ts.map +1 -0
- package/dist/sql/canonical-nodes.js +101 -0
- package/dist/sql/extract-helpers.d.ts +18 -0
- package/dist/sql/extract-helpers.d.ts.map +1 -0
- package/dist/sql/extract-helpers.js +127 -0
- package/dist/sql/extract.d.ts +13 -0
- package/dist/sql/extract.d.ts.map +1 -0
- package/dist/sql/extract.js +323 -0
- package/dist/sql/facts.d.ts +34 -0
- package/dist/sql/facts.d.ts.map +1 -0
- package/dist/sql/facts.js +392 -0
- package/dist/sql/identifiers.d.ts +13 -0
- package/dist/sql/identifiers.d.ts.map +1 -0
- package/dist/sql/identifiers.js +83 -0
- package/dist/sql/normalize-deparse.d.ts +25 -0
- package/dist/sql/normalize-deparse.d.ts.map +1 -0
- package/dist/sql/normalize-deparse.js +96 -0
- package/dist/sql/object-hash.d.ts +5 -0
- package/dist/sql/object-hash.d.ts.map +1 -0
- package/dist/sql/object-hash.js +24 -0
- package/dist/sql/parser.d.ts +8 -0
- package/dist/sql/parser.d.ts.map +1 -0
- package/dist/sql/parser.js +89 -0
- package/dist/sql/privileges.d.ts +33 -0
- package/dist/sql/privileges.d.ts.map +1 -0
- package/dist/sql/privileges.js +379 -0
- package/dist/sql/split.d.ts +3 -0
- package/dist/sql/split.d.ts.map +1 -0
- package/dist/sql/split.js +182 -0
- package/dist/sql/statements.d.ts +17 -0
- package/dist/sql/statements.d.ts.map +1 -0
- package/dist/sql/statements.js +284 -0
- package/dist/sql/table-constraints.d.ts +15 -0
- package/dist/sql/table-constraints.d.ts.map +1 -0
- package/dist/sql/table-constraints.js +304 -0
- package/dist/sql/table-shape.d.ts +38 -0
- package/dist/sql/table-shape.d.ts.map +1 -0
- package/dist/sql/table-shape.js +287 -0
- package/dist/sync.d.ts +27 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +86 -0
- package/dist/typegen-model.d.ts +78 -0
- package/dist/typegen-model.d.ts.map +1 -0
- package/dist/typegen-model.js +338 -0
- package/dist/typegen-views.d.ts +7 -0
- package/dist/typegen-views.d.ts.map +1 -0
- package/dist/typegen-views.js +92 -0
- package/dist/typegen-zod.d.ts +3 -0
- package/dist/typegen-zod.d.ts.map +1 -0
- package/dist/typegen-zod.js +149 -0
- package/dist/typegen.d.ts +4 -0
- package/dist/typegen.d.ts.map +1 -0
- package/dist/typegen.js +184 -0
- package/dist/validators.d.ts +3 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +104 -0
- package/dist/verify-environment.d.ts +5 -0
- package/dist/verify-environment.d.ts.map +1 -0
- package/dist/verify-environment.js +92 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +261 -0
- package/docs/benchmarks/additive-correctness.svg +86 -0
- package/docs/benchmarks/additive-latency.svg +60 -0
- package/docs/benchmarks/functions-policies-correctness.svg +86 -0
- package/docs/benchmarks/functions-policies-latency.svg +60 -0
- package/docs/benchmarks/realistic-correctness.svg +86 -0
- package/docs/benchmarks/realistic-latency.svg +60 -0
- package/docs/benchmarks/scaling-latency.svg +106 -0
- package/docs/benchmarks/workflow-latency.svg +98 -0
- package/docs/benchmarks/xl-correctness.svg +86 -0
- package/docs/benchmarks/xl-latency.svg +60 -0
- package/docs/benchmarks/xxl-correctness.svg +86 -0
- package/docs/benchmarks/xxl-latency.svg +66 -0
- package/docs/case-study-anilize.md +51 -0
- package/docs/ci-gate.md +44 -0
- package/docs/ci.md +68 -0
- package/docs/commands.md +35 -0
- package/docs/config.md +72 -0
- package/docs/corpus.md +33 -0
- package/docs/diagnostics.md +77 -0
- package/docs/hints.md +92 -0
- package/docs/release.md +19 -0
- package/docs/support-matrix.md +57 -0
- package/examples/postgres/schemas/001_app.sql +17 -0
- package/examples/supabase/schemas/001_app.sql +13 -0
- package/examples/supabase/schemas-next/001_app.sql +21 -0
- package/examples/supabase/supaschema.config.json +15 -0
- package/package.json +99 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supaschema
|
|
3
|
+
description: Generate, check, and verify replay-safe PostgreSQL/Supabase migrations from declarative SQL tree diffs with supaschema. Use when schema changes are requested, migrations must be created or validated, schema drift needs detection, or a supaschema diagnostic (SUPA_*) blocks a plan.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# supaschema Migration Workflow
|
|
7
|
+
|
|
8
|
+
## Contract
|
|
9
|
+
|
|
10
|
+
This skill is a direct execution contract for producing schema migrations with supaschema. Follow the workflow in order; do not hand-author migration SQL for changes the declarative tree can express, and never edit a generated migration (the `-- supaschema: lineage` marker) by hand.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. **Edit the declarative tree** (`supabase/schemas/**` or the project's configured tree) to express the desired end state. Use schema-qualified object names.
|
|
15
|
+
2. **Generate the migration:**
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
supaschema diff
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Zero-flag defaults (printed to stderr; flags override): `--from` resolves to the database (then `git:HEAD`), `--to` to the config schema tree, and the file lands in `config.migrationsDir` as `<UTC timestamp>_<derived name>.sql`. Pass `--name <snake_case>` to control the name. The write is no-clobber and chain-gated. If it exits 2, read the diagnostic:
|
|
22
|
+
- `SUPA_PLAN_DESTRUCTIVE_HINT_REQUIRED` / `SUPA_PLAN_COLUMN_ALTER_HINT_REQUIRED` / `SUPA_PLAN_VIEW_REPLACE_INCOMPATIBLE` / `SUPA_PLAN_ROUTINE_RETURN_TYPE_CHANGED` — review the rendered `-- BLOCKED` section, then add the exact object key to `hints.destructive` in `supaschema.config.json` and regenerate. Never use `"*"` in committed config.
|
|
23
|
+
- `SUPA_DIFF_LINEAGE_BROKEN` — a pending generated migration exists; diff from the post-migration state instead: `--from database:<db with pending applied>`.
|
|
24
|
+
- `SUPA_DIFF_LINEAGE_DUPLICATE` — the transition is already pending; apply or remove the pending migration instead of regenerating.
|
|
25
|
+
- Renames: declare `{ "from": "<key>", "to": "<key>" }` in `hints.renames`; renames are never inferred.
|
|
26
|
+
|
|
27
|
+
3. **Check replay safety:** `supaschema check` gates every `.sql` in the migrations directory (or name specific files) — must exit 0 for generated and hand-authored migrations alike.
|
|
28
|
+
4. **Verify execution** (when any database is resolvable — the URL auto-resolves from `SUPASCHEMA_DATABASE_URL` or the nearest `supabase/config.toml`):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
supaschema verify
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Defaults to the newest pending migration in the migrations directory with the same from/to defaults as `diff`; pass `--migration <file>` to verify a specific one.
|
|
35
|
+
|
|
36
|
+
Add `--ensure-roles` when the migration grants to roles a bare PostgreSQL server lacks (e.g. `authenticated`). A fingerprint mismatch itemizes the differing objects in the diagnostic hint.
|
|
37
|
+
|
|
38
|
+
5. **Commit** the tree change, the generated migration, and the refreshed types file together. supaschema never stages or applies; the migration runner (e.g. `supabase db push`) owns the database. TypeScript types come from the tree (`supaschema types` creates `database.types.ts`; every later `diff` refreshes it) — never wait for a deploy or run introspection-based typegen to get correct types.
|
|
39
|
+
|
|
40
|
+
## Drift Detection
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
supaschema diff --fail-on-diff --quiet
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Exit 3 means the live database and the tree have diverged; exit 0 means parity. Use this as a CI gate (`docs/ci.md` has the full pipeline recipe).
|
|
47
|
+
|
|
48
|
+
When drift is large or blocked, triage before editing:
|
|
49
|
+
|
|
50
|
+
- `supaschema diff --summary` — operation/diagnostic counts grouped by kind and schema, printed even when the plan is blocked.
|
|
51
|
+
- `supaschema diff --write-hints <file>` — writes the gated destructive object keys as a reviewable `hints.destructive` skeleton (no-clobber).
|
|
52
|
+
- `supaschema audit --from <source> [--json]` — modeled coverage by kind/schema plus every statement outside the contract grouped by diagnostic code.
|
|
53
|
+
- `supaschema selfcheck` — re-extracts a live catalog's rendered SQL and reports any object whose identity diverges (`SUPA_SELFCHECK_*`); zero mismatches proves cross-lane identity parity.
|
|
54
|
+
- `supaschema migrations` — classifies on-disk migrations against a target's applied history: applied, pending, ghost, or out-of-order.
|
|
55
|
+
|
|
56
|
+
## Boundaries
|
|
57
|
+
|
|
58
|
+
- Sources for either side of a diff: `dir:<tree>`, `git:<ref>`, `database:<url|$ENV>`, `dump:<file.sql>`, `catalog:<snapshot.json>`.
|
|
59
|
+
- Data statements (`INSERT`/`UPDATE`/`DELETE`/`DO`) and enum reordering/removal are hand-authored migrations — validate them with `check` and `verify`; the enum recipe is in `docs/hints.md`.
|
|
60
|
+
- Keep `transactionMode: "per-migration"` for transactional runners; `CREATE INDEX CONCURRENTLY` is blocked under `supabase-auto` and splits to a `.concurrent.sql` companion under `adapter: "postgres"`.
|
|
61
|
+
- `supaschema explain <SUPA_CODE>` decodes any diagnostic offline.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
const lineageMarker = "-- supaschema: lineage ";
|
|
5
|
+
const editTools = new Set(["Edit", "MultiEdit", "Write"]);
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const payload = JSON.parse(readFileSync(0, "utf8"));
|
|
9
|
+
const toolName = typeof payload?.tool_name === "string" ? payload.tool_name : "";
|
|
10
|
+
const filePath =
|
|
11
|
+
typeof payload?.tool_input?.file_path === "string" ? payload.tool_input.file_path : "";
|
|
12
|
+
if (!editTools.has(toolName) || !filePath.endsWith(".sql")) {
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
let existing = "";
|
|
16
|
+
try {
|
|
17
|
+
existing = readFileSync(filePath, "utf8");
|
|
18
|
+
} catch {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
if (!existing.includes(lineageMarker)) {
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
process.stderr.write(
|
|
25
|
+
`${filePath} is a supaschema-generated migration (lineage marker present). ` +
|
|
26
|
+
"Do not hand-edit it: change the declarative schema tree, delete this file if it is stale, " +
|
|
27
|
+
"and regenerate with `supaschema diff`. See .claude/rules/supaschema.md.\n",
|
|
28
|
+
);
|
|
29
|
+
process.exit(2);
|
|
30
|
+
} catch {
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generated-migration ownership and supaschema workflow policy for schema changes.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# supaschema Migration Policy
|
|
6
|
+
|
|
7
|
+
## Contract
|
|
8
|
+
|
|
9
|
+
This rule owns how schema migrations are produced and protected in a repo that uses supaschema: migrations are generated from the declarative tree, generated artifacts are never hand-edited, destructive intent is hint-gated, and verification mirrors the migration runner. The repeatable command workflow lives in the `supaschema` skill; write-time enforcement is hook-owned.
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
- Schema intent changes only in the declarative SQL tree (e.g. `supabase/schemas/**`). Render the migration with `supaschema diff`; do not hand-author migration SQL for changes the tree can express.
|
|
14
|
+
- Any `.sql` file containing the `-- supaschema: lineage` marker is a generated artifact. Never edit it; edit the tree and regenerate. The PreToolUse hook blocks such edits in both Claude and Codex runtimes.
|
|
15
|
+
- Never overwrite a migration file. supaschema writes no-clobber (`SUPA_DIFF_OUTPUT_EXISTS`); a stale file is deleted deliberately, not clobbered.
|
|
16
|
+
- Honor the lineage chain gate: `SUPA_DIFF_LINEAGE_BROKEN` means diff from the post-migration state (`--from database:<applied db>`), `SUPA_DIFF_LINEAGE_DUPLICATE` means the change is already pending. `--no-check-chain` requires explicit human approval.
|
|
17
|
+
- Destructive operations (drops, table/type replacements, column drops, column type changes, incompatible view/routine replacements) stay blocked until the exact object key is added to `hints.destructive` after reviewing the rendered SQL. `"*"` never lands in committed config. A hinted table replace is not data-preserving; prefer the column ALTER lane the planner renders.
|
|
18
|
+
- Renames are declared in `hints.renames`, never inferred. Kind changes and cross-schema moves are unsupported by design.
|
|
19
|
+
- Run `supaschema check` on every generated or hand-authored migration. Run `supaschema verify` before merge when a database is reachable; keep `transactionMode: "per-migration"` for transactional runners such as `supabase db push`.
|
|
20
|
+
- `CREATE INDEX CONCURRENTLY` is blocked under `adapter: "supabase-auto"`; under `adapter: "postgres"` it lands in the `.concurrent.sql` companion, which runs outside a transaction through an operational lane.
|
|
21
|
+
- Database URLs resolve flag (`$ENV` indirection supported) > named `config.environments` entry via `--env` > `SUPASCHEMA_DATABASE_URL` > nearest `supabase/config.toml`. Do not hard-code connection strings in scripts, config, or CI.
|
|
22
|
+
- Decode any `SUPA_*` diagnostic with `supaschema explain <CODE>`; recovery procedures live in `docs/hints.md`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-generated-migration-edits.mjs",
|
|
10
|
+
"timeout": 10
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supaschema
|
|
3
|
+
description: Generate, check, and verify replay-safe PostgreSQL/Supabase migrations from declarative SQL tree diffs with supaschema. Use when schema changes are requested, migrations must be created or validated, schema drift needs detection, or a supaschema diagnostic (SUPA_*) blocks a plan.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# supaschema Migration Workflow
|
|
7
|
+
|
|
8
|
+
## Contract
|
|
9
|
+
|
|
10
|
+
This skill is a direct execution contract for producing schema migrations with supaschema. Follow the workflow in order; do not hand-author migration SQL for changes the declarative tree can express, and never edit a generated migration (the `-- supaschema: lineage` marker) by hand.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. **Edit the declarative tree** (`supabase/schemas/**` or the project's configured tree) to express the desired end state. Use schema-qualified object names.
|
|
15
|
+
2. **Generate the migration:**
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
supaschema diff
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Zero-flag defaults (printed to stderr; flags override): `--from` resolves to the database (then `git:HEAD`), `--to` to the config schema tree, and the file lands in `config.migrationsDir` as `<UTC timestamp>_<derived name>.sql`. Pass `--name <snake_case>` to control the name. The write is no-clobber and chain-gated. If it exits 2, read the diagnostic:
|
|
22
|
+
- `SUPA_PLAN_DESTRUCTIVE_HINT_REQUIRED` / `SUPA_PLAN_COLUMN_ALTER_HINT_REQUIRED` / `SUPA_PLAN_VIEW_REPLACE_INCOMPATIBLE` / `SUPA_PLAN_ROUTINE_RETURN_TYPE_CHANGED` — review the rendered `-- BLOCKED` section, then add the exact object key to `hints.destructive` in `supaschema.config.json` and regenerate. Never use `"*"` in committed config.
|
|
23
|
+
- `SUPA_DIFF_LINEAGE_BROKEN` — a pending generated migration exists; diff from the post-migration state instead: `--from database:<db with pending applied>`.
|
|
24
|
+
- `SUPA_DIFF_LINEAGE_DUPLICATE` — the transition is already pending; apply or remove the pending migration instead of regenerating.
|
|
25
|
+
- Renames: declare `{ "from": "<key>", "to": "<key>" }` in `hints.renames`; renames are never inferred.
|
|
26
|
+
|
|
27
|
+
3. **Check replay safety:** `supaschema check` gates every `.sql` in the migrations directory (or name specific files) — must exit 0 for generated and hand-authored migrations alike.
|
|
28
|
+
4. **Verify execution** (when any database is resolvable — the URL auto-resolves from `SUPASCHEMA_DATABASE_URL` or the nearest `supabase/config.toml`):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
supaschema verify
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Defaults to the newest pending migration in the migrations directory with the same from/to defaults as `diff`; pass `--migration <file>` to verify a specific one.
|
|
35
|
+
|
|
36
|
+
Add `--ensure-roles` when the migration grants to roles a bare PostgreSQL server lacks (e.g. `authenticated`). A fingerprint mismatch itemizes the differing objects in the diagnostic hint.
|
|
37
|
+
|
|
38
|
+
5. **Commit** the tree change, the generated migration, and the refreshed types file together. supaschema never stages or applies; the migration runner (e.g. `supabase db push`) owns the database. TypeScript types come from the tree (`supaschema types` creates `database.types.ts`; every later `diff` refreshes it) — never wait for a deploy or run introspection-based typegen to get correct types.
|
|
39
|
+
|
|
40
|
+
## Drift Detection
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
supaschema diff --fail-on-diff --quiet
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Exit 3 means the live database and the tree have diverged; exit 0 means parity. Use this as a CI gate (`docs/ci.md` has the full pipeline recipe).
|
|
47
|
+
|
|
48
|
+
When drift is large or blocked, triage before editing:
|
|
49
|
+
|
|
50
|
+
- `supaschema diff --summary` — operation/diagnostic counts grouped by kind and schema, printed even when the plan is blocked.
|
|
51
|
+
- `supaschema diff --write-hints <file>` — writes the gated destructive object keys as a reviewable `hints.destructive` skeleton (no-clobber).
|
|
52
|
+
- `supaschema audit --from <source> [--json]` — modeled coverage by kind/schema plus every statement outside the contract grouped by diagnostic code.
|
|
53
|
+
- `supaschema selfcheck` — re-extracts a live catalog's rendered SQL and reports any object whose identity diverges (`SUPA_SELFCHECK_*`); zero mismatches proves cross-lane identity parity.
|
|
54
|
+
- `supaschema migrations` — classifies on-disk migrations against a target's applied history: applied, pending, ghost, or out-of-order.
|
|
55
|
+
|
|
56
|
+
## Boundaries
|
|
57
|
+
|
|
58
|
+
- Sources for either side of a diff: `dir:<tree>`, `git:<ref>`, `database:<url|$ENV>`, `dump:<file.sql>`, `catalog:<snapshot.json>`.
|
|
59
|
+
- Data statements (`INSERT`/`UPDATE`/`DELETE`/`DO`) and enum reordering/removal are hand-authored migrations — validate them with `check` and `verify`; the enum recipe is in `docs/hints.md`.
|
|
60
|
+
- Keep `transactionMode: "per-migration"` for transactional runners; `CREATE INDEX CONCURRENTLY` is blocked under `supabase-auto` and splits to a `.concurrent.sql` companion under `adapter: "postgres"`.
|
|
61
|
+
- `supaschema explain <SUPA_CODE>` decodes any diagnostic offline.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const lineageMarker = "-- supaschema: lineage ";
|
|
6
|
+
const updateHeader = "*** Update File: ";
|
|
7
|
+
const deleteHeader = "*** Delete File: ";
|
|
8
|
+
const addHeader = "*** Add File: ";
|
|
9
|
+
|
|
10
|
+
function patchTargets(patchText) {
|
|
11
|
+
const updates = [];
|
|
12
|
+
const deletes = [];
|
|
13
|
+
const adds = new Set();
|
|
14
|
+
for (const line of patchText.split("\n")) {
|
|
15
|
+
if (line.startsWith(updateHeader)) {
|
|
16
|
+
updates.push(resolve(line.slice(updateHeader.length).trim()));
|
|
17
|
+
} else if (line.startsWith(deleteHeader)) {
|
|
18
|
+
deletes.push(resolve(line.slice(deleteHeader.length).trim()));
|
|
19
|
+
} else if (line.startsWith(addHeader)) {
|
|
20
|
+
adds.add(resolve(line.slice(addHeader.length).trim()));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const rewrites = deletes.filter((path) => adds.has(path));
|
|
24
|
+
return [...updates, ...rewrites];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function editTargets(payload) {
|
|
28
|
+
const toolName = typeof payload?.tool_name === "string" ? payload.tool_name : "";
|
|
29
|
+
const input = payload?.tool_input ?? {};
|
|
30
|
+
if (toolName === "apply_patch") {
|
|
31
|
+
const patch = typeof input.patch === "string" ? input.patch : (input.input ?? "");
|
|
32
|
+
return typeof patch === "string" ? patchTargets(patch) : [];
|
|
33
|
+
}
|
|
34
|
+
if (typeof input.file_path === "string" && input.file_path.length > 0) {
|
|
35
|
+
return [input.file_path];
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isGeneratedMigration(path) {
|
|
41
|
+
if (!path.endsWith(".sql")) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return readFileSync(path, "utf8").includes(lineageMarker);
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function emit(result) {
|
|
52
|
+
process.stdout.write(JSON.stringify(result));
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const payload = JSON.parse(readFileSync(0, "utf8"));
|
|
58
|
+
const blocked = editTargets(payload).find((path) => isGeneratedMigration(path));
|
|
59
|
+
if (!blocked) {
|
|
60
|
+
emit({});
|
|
61
|
+
}
|
|
62
|
+
emit({
|
|
63
|
+
hookSpecificOutput: {
|
|
64
|
+
hookEventName: "PreToolUse",
|
|
65
|
+
permissionDecision: "deny",
|
|
66
|
+
permissionDecisionReason: `${blocked} is a supaschema-generated migration (lineage marker present). Do not hand-edit it: change the declarative schema tree, delete this file if it is stale, and regenerate with \`supaschema diff\`. See .claude/rules/supaschema.md.`,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
emit({
|
|
71
|
+
systemMessage: `supaschema-tool-gate hook error (fail-open): ${error instanceof Error ? error.message : String(error)}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/supaschema-tool-gate.mjs\"",
|
|
9
|
+
"timeout": 10,
|
|
10
|
+
"statusMessage": "Checking supaschema generated-migration policy"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# supaschema migration policy (Codex rules surface).
|
|
2
|
+
# Enforcement: write-time denial of hand-edits to generated migrations is
|
|
3
|
+
# owned by .codex/hooks/supaschema-tool-gate.mjs (PreToolUse). This file
|
|
4
|
+
# carries the policy text for review and prompting.
|
|
5
|
+
#
|
|
6
|
+
# - Schema intent changes only in the declarative SQL tree; render migrations
|
|
7
|
+
# with `supaschema diff`, never hand-author SQL the tree can express.
|
|
8
|
+
# - A .sql file containing the `-- supaschema: lineage` marker is a generated
|
|
9
|
+
# artifact: change the tree and regenerate; never hand-edit it.
|
|
10
|
+
# - Migration writes are no-clobber, and the lineage chain gate must not be
|
|
11
|
+
# bypassed with --no-check-chain without explicit human approval.
|
|
12
|
+
# - Destructive changes (drops, column type changes, incompatible view or
|
|
13
|
+
# routine replacements) require the exact object key in hints.destructive
|
|
14
|
+
# after reviewing the rendered SQL; never commit "*".
|
|
15
|
+
# - Run `supaschema check` on every migration; run `supaschema verify` before
|
|
16
|
+
# merge when a database is reachable. Keep transactionMode per-migration
|
|
17
|
+
# for transactional runners such as `supabase db push`.
|
|
18
|
+
# - Database URLs resolve flag > named config.environments entry via --env >
|
|
19
|
+
# SUPASCHEMA_DATABASE_URL > the nearest supabase/config.toml; do not
|
|
20
|
+
# hard-code connection strings.
|
|
21
|
+
# - Decode any SUPA_* diagnostic with `supaschema explain <CODE>`; recovery
|
|
22
|
+
# steps live in docs/hints.md.
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# supaschema Agent Brief
|
|
2
|
+
|
|
3
|
+
## Contract
|
|
4
|
+
|
|
5
|
+
This file is the operator brief for AI agents working in this repository or in a repository that uses supaschema. Durable policy lives in `.claude/rules/supaschema.md`; the repeatable workflow lives in `.claude/skills/supaschema/SKILL.md`; write-time enforcement lives in `.claude/hooks/**` and `.codex/hooks/**`.
|
|
6
|
+
|
|
7
|
+
supaschema generates deterministic, replay-safe PostgreSQL/Supabase migrations from declarative SQL tree diffs. It is a generator and prover only: it never stages, commits, or applies to a tracked database.
|
|
8
|
+
|
|
9
|
+
## Invariants
|
|
10
|
+
|
|
11
|
+
- Migrations are generated, never hand-authored. Schema intent changes in the declarative tree (`supabase/schemas/**` or the project's tree); `supaschema diff` renders the migration.
|
|
12
|
+
- A `.sql` file containing the `-- supaschema: lineage` marker is a generated artifact. Never edit it by hand — change the source tree and regenerate. Hooks in both agent runtimes block such edits.
|
|
13
|
+
- Destructive intent is explicit. Drops, column type changes, and PostgreSQL-incompatible replacements stay blocked until the exact object key is added to `hints.destructive` after review. Never add `"*"` to committed config.
|
|
14
|
+
- Verification must match the runner: keep `transactionMode: "per-migration"` for `supabase db push`-style runners. Run `supaschema check` always and `supaschema verify` before merge when a database is reachable.
|
|
15
|
+
- Respect the lineage chain gate. When `diff` refuses with `SUPA_DIFF_LINEAGE_BROKEN` or `SUPA_DIFF_LINEAGE_DUPLICATE`, regenerate from the post-migration state (`--from database:<applied db>`); `--no-check-chain` is for explicit human-approved bypasses only.
|
|
16
|
+
- Database URLs are never hard-coded: flag (`$ENV` supported) > named `config.environments` entry via `--env` > `SUPASCHEMA_DATABASE_URL` > auto-discovery from the nearest `supabase/config.toml`.
|
|
17
|
+
- `supaschema explain <CODE>` decodes any `SUPA_*` diagnostic offline; `docs/hints.md` has recovery steps for blocked plans.
|
|
18
|
+
|
|
19
|
+
## Common Commands
|
|
20
|
+
|
|
21
|
+
Zero-flag defaults: `--from` resolves to the database (then `git:HEAD`), `--to` to the config schema tree, output to `config.migrationsDir` with a derived name, `check` to the whole migrations directory, `verify --migration` to the newest pending file. Applied defaults print to stderr; flags override.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
supaschema diff # render the migration from applied state -> schema tree (refreshes the types file when present)
|
|
25
|
+
supaschema check # replay-safety gate for the migrations directory
|
|
26
|
+
supaschema verify # apply-twice proof for the newest pending migration
|
|
27
|
+
supaschema types # Supabase-compatible TypeScript types + Zod validators from the tree; no database or introspection
|
|
28
|
+
supaschema diff --fail-on-diff --quiet # CI drift gate (exit 3 on drift)
|
|
29
|
+
supaschema diff --summary # blocked-plan triage: operation/diagnostic counts by kind and schema
|
|
30
|
+
supaschema diff --write-hints <file> # reviewable hints.destructive skeleton for gated keys
|
|
31
|
+
supaschema audit --from <source> # support-matrix coverage + out-of-contract statements by code
|
|
32
|
+
supaschema selfcheck # cross-lane identity parity proof against a live catalog (SUPA_SELFCHECK_*)
|
|
33
|
+
supaschema migrations # applied/pending/ghost/out-of-order vs history table
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Repository Development (supaschema itself)
|
|
37
|
+
|
|
38
|
+
- `npm run check` — lint + typecheck + tests + build; database-gated suites auto-resolve a URL or skip.
|
|
39
|
+
- `npm run fixture:verify` / `npm run benchmark` — execution proof and threshold-enforced performance lanes.
|
|
40
|
+
- Source is AST-only: statement classification and SQL mutation come from `libpg-query` parse trees, never regex over SQL.
|