diff --git a/.gitignore b/.gitignore index 4c09d6be..ebc47753 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ __pycache__/ *.pyc .DS_Store target/ +node_modules/ +baseline-catalog.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54ae50fc..82e925f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,9 @@ service_role key: KEY The *API URL* and *anon key* values will be used in the next section to setup environment variables. +### Schema management (declarative) + +The repo supports a **declarative schema** workflow using [pg-delta](https://github.com/supabase/pg-toolbelt/tree/main/packages/pg-delta) to export the database shape into version-controlled `.sql` files and generate migrations by diffing desired state against the running Supabase DB. For full details, prerequisites, and commands, see [docs/declarative-schema.md](docs/declarative-schema.md). ### Website (database.dev) diff --git a/docs/declarative-schema.md b/docs/declarative-schema.md new file mode 100644 index 00000000..ba3d33a1 --- /dev/null +++ b/docs/declarative-schema.md @@ -0,0 +1,164 @@ +# Declarative schema workflow + +This document describes how the dbdev repo uses [pg-delta](https://github.com/supabase/pg-toolbelt/tree/main/packages/pg-delta) for a **declarative schema** workflow: exporting the database shape as version-controlled `.sql` files and generating migrations by diffing desired state against the running database. + +## What is declarative schema? + +**Declarative schema** means the desired database shape is described in files (under `declarative-schemas/`) rather than only in a linear migration history. pg-delta can: + +- **Export** the current database (minus a baseline) into those files. +- **Apply** those files to a temporary “shadow” database. +- **Plan** the diff between the running Supabase DB and the shadow DB to produce a migration script. + +So you can edit the declarative files (or re-export after manual DB changes), then run a script to generate a single migration that brings the real database in line with the desired state. + +This complements the normal **migration-based** workflow: `supabase/migrations/` remains how changes are applied; the declarative flow is a way to derive a migration from a desired-state snapshot. + +## How it fits this repo + +dbdev is a Supabase project with many migrations. The declarative flow: + +1. Uses a **shadow DB** (same Docker image as Supabase Postgres) so the real DB is not modified until you apply a migration. +2. Keeps desired state in `declarative-schemas/` (exported from the live DB, then editable). +3. Diffs that state against the running Supabase DB and writes a migration under `supabase/migrations/`. +4. You apply the migration with `psql` (or the script can do it), then use Supabase as usual. + +```mermaid +flowchart LR + subgraph init [Init or refresh] + A1[Shadow DB] --> A2[Baseline catalog] + A2 --> A3[supabase start] + A3 --> A4[Declarative export] + A4 --> A5[declarative-schemas] + A5 --> A6[Roundtrip verify] + end + subgraph update [After editing schema] + B1[Shadow DB] --> B2[Declarative apply] + B2 --> B3[plan: DB vs shadow] + B3 --> B4[Migration file] + B4 --> B5[Apply via psql] + B5 --> B6[Roundtrip verify] + end +``` + +## Prerequisites + +- **Node.js and npm** – for `npx pgdelta` (run `npm install` in the repo root). +- **Docker** – for the shadow DB and Supabase local stack. +- **Supabase CLI** – for `supabase start` / `supabase stop`. +- **psql** – for applying the generated migration (or use the script’s default apply step). + +From the repo root: + +```bash +npm install +npx pgdelta --help # sanity check +``` + +## Scripts and usage + +Both scripts must be run **from the dbdev repo root** so `npx pgdelta` resolves from `node_modules/`. + +### 1. Init (one-time or refresh): export declarative schema + +Initializes or refreshes `declarative-schemas/` from the running Supabase DB: + +1. Starts a shadow DB (Supabase Postgres image on port 6544). +2. Snapshots the clean shadow DB as a baseline catalog. +3. Starts the dbdev Supabase project (if not already running) on port 54322. +4. Exports the declarative schema (diff: baseline → Supabase DB) into `declarative-schemas/`. +5. Verifies roundtrip: applies the export to a fresh shadow DB and diffs against Supabase (expect 0 changes, or minor GRANT ordering differences). + +```bash +bash scripts/declarative-dbdev-init.sh +``` + +Optional env vars: + +| Variable | Default | Description | +|----------|---------|-------------| +| `SKIP_SUPABASE_START` | (unset) | Set to skip `supabase start` (e.g. DB already running). | +| `SKIP_VERIFY` | (unset) | Set to skip the roundtrip verification step. | +| `PGDELTA` | `npx pgdelta` | Override the pg-delta CLI command. | +| `SHADOW_IMAGE` | `supabase/postgres:15.6.1.143` | Docker image for the shadow DB. | +| `OUTPUT_DIR` | `./declarative-schemas` | Where to write the exported schema. | +| `BASELINE_SNAPSHOT` | `./baseline-catalog.json` | Path for the baseline catalog (not committed; in `.gitignore`). | + +### Changes made in Studio: re-export with init + +If you change the database through **Supabase Studio** (or any direct SQL), the declarative schema files will no longer match the running database. To pull those changes back into the repo: + +1. Leave the Supabase project running (or start it with the init script). +2. Re-run the init script from the repo root: + + ```bash + bash scripts/declarative-dbdev-init.sh + ``` + + The export step uses the **`--force`** flag, so existing files under `declarative-schemas/` are overwritten and the directory is updated to match the current database. You get a clean declarative snapshot of whatever is in the DB, including your Studio changes. + +3. Commit the updated `declarative-schemas/` so the desired state stays in version control. To also add a migration file that records the Studio changes in `supabase/migrations/`, use the Supabase CLI (e.g. `supabase db diff`) to generate a migration from the current DB state, or run the update script after the re-export if you have further edits to the declarative schema. + +You can skip the roundtrip verification when re-exporting after Studio changes by setting `SKIP_VERIFY=1` if the verification step is noisy or fails due to known export limitations. + +### 2. Update (after editing schema): generate and apply migration + +After you edit files under `declarative-schemas/`: + +1. Starts a shadow DB. +2. Applies the declarative schema to the shadow DB (desired state). +3. Runs `pgdelta plan` (source = Supabase DB, target = shadow) to generate a migration. +4. Saves the migration to `supabase/migrations/_.sql`. +5. Applies the migration to the Supabase DB with `psql`. +6. Verifies roundtrip (diff Supabase vs shadow; expect 0 changes). + +```bash +MIGRATION_NAME=my_change bash scripts/declarative-dbdev-update.sh +``` + +Optional env vars: + +| Variable | Default | Description | +|----------|---------|-------------| +| `MIGRATION_NAME` | `declarative_update` | Suffix for the migration filename. | +| `SKIP_APPLY` | (unset) | Set to only write the migration file; do not apply it. | +| `SKIP_VERIFY` | (unset) | Set to skip the roundtrip verification step. | +| `PGDELTA` | `npx pgdelta` | Override the pg-delta CLI command. | +| `SHADOW_IMAGE` | `supabase/postgres:15.6.1.143` | Docker image for the shadow DB. | +| `OUTPUT_DIR` | `./declarative-schemas` | Declarative schema directory. | + +### 3. Squash: one migration for full schema + +Generates a **single migration** that brings an empty DB (clean shadow) to the current state—equivalent to squashing all existing migrations into one file. Useful for greenfield deploys or new projects. + +1. Starts a shadow DB (clean baseline, no migrations applied). +2. Runs `pgdelta plan` (source = shadow, target = Supabase DB). +3. Writes the SQL to `supabase/migrations/_.sql`. + +```bash +bash scripts/declarative-dbdev-squash.sh +MIGRATION_NAME=initial_schema bash scripts/declarative-dbdev-squash.sh +``` + +The script does **not** apply the migration (your running DB is already current). Use the generated file for new environments or to replace a long migration history with one file. + +| Variable | Default | Description | +|----------|---------|-------------| +| `MIGRATION_NAME` | `squashed` | Suffix for the migration filename. | +| `PGDELTA` | `npx pgdelta` | Override the pg-delta CLI command. | +| `SHADOW_IMAGE` | `supabase/postgres:15.6.1.143` | Docker image for the shadow DB. | + +## File layout + +- **`declarative-schemas/`** – Version-controlled desired schema (exported by init, edited by you, applied to shadow in update). + - **`cluster/`** – Cluster-level objects: extensions (`extensions/`), event triggers (`event_triggers.sql`). + - **`schemas/`** – Per-schema objects: `app/` (tables, functions, domains, types), `public/` (views, functions). +- **`supabase/migrations/`** – Supabase migration history; generated migrations are written here and applied via `psql` (or the script). +- **`baseline-catalog.json`** – Generated by the init script, used as the “clean” baseline for export; not committed (in `.gitignore`). + +## Troubleshooting + +- **Shadow image** – The shadow DB must use a `supabase/postgres` image (e.g. `15.6.1.143`) so system schemas (`auth`, `storage`, `extensions`) match. If you use a different Postgres major version, set `SHADOW_IMAGE` to a matching Supabase image tag. +- **Roundtrip fails** – If verification reports changes after a roundtrip, check that extension and domain ordering in the export is correct. Re-running the init script with the current pg-delta can refresh the export; if the tool was recently upgraded, a fresh export may fix serialization issues (e.g. domain base types). The roundtrip may also report **missing RLS policies** (policies that exist in the DB but were not captured in the export) or **GRANT differences** (due to `ALTER DEFAULT PRIVILEGES` ordering). You can skip verification for init with `SKIP_VERIFY=1` and still use the declarative schema for edit-and-migrate workflows. +- **Supabase not running** – The update script requires the Supabase DB on port 54322. Run `supabase start` from the repo root first, or use the init script once to start it. +- **`npx pgdelta` not found** – Run `npm install` from the repo root; the scripts expect to be run from the root so `npx` resolves the local `@supabase/pg-delta` dependency. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..bcd0434f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,972 @@ +{ + "name": "dbdev", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@supabase/pg-delta": "https://pkg.pr.new/supabase/pg-toolbelt/@supabase/pg-delta@7cb3a19" + }, + "devDependencies": { + "supabase": "^2.76.16" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/parser/node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@launchql/protobufjs": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/@launchql/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-vwi1nG2/heVFsIMHQU1KxTjUp5c757CTtRAZn/jutApCkFlle1iv8tzM/DHlSZJKDldxaYqnNYTg0pTyp8Bbtg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@libpg-query/parser": { + "version": "17.6.3", + "resolved": "https://registry.npmjs.org/@libpg-query/parser/-/parser-17.6.3.tgz", + "integrity": "sha512-AvbS7b9GJZfCzqt4tLMqTaYK7Css9pJRTA2dKWLoTlob/XO2VNc30Q3g9DmxNBmokVTrmibkn/dy9bw8hfosQQ==", + "license": "LICENSE IN LICENSE", + "dependencies": { + "@launchql/protobufjs": "7.2.6", + "@pgsql/types": "^17.6.0" + } + }, + "node_modules/@pgsql/quotes": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@pgsql/quotes/-/quotes-17.1.0.tgz", + "integrity": "sha512-J/H+LcrENBpYgL45WW6aTjb5Yk4tX4+AmB2/k8KZa+Zh3wiCtqmNIag+HZz5HmWaF6EZK9ZGC95NBD1fs+rUvg==", + "license": "MIT" + }, + "node_modules/@pgsql/traverse": { + "version": "17.2.4", + "resolved": "https://registry.npmjs.org/@pgsql/traverse/-/traverse-17.2.4.tgz", + "integrity": "sha512-7dnst2U3qXXSFbS+HvVktW/C4aVKe/niaRxZ1cfhmdy9E7SMYM0OukFO7gvoZb6I3dnKm/eTfpv23JPKq5z6DA==", + "license": "MIT", + "dependencies": { + "@pgsql/types": "^17.6.2", + "pg-proto-parser": "1.30.4" + } + }, + "node_modules/@pgsql/types": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/@pgsql/types/-/types-17.6.2.tgz", + "integrity": "sha512-1UtbELdbqNdyOShhrVfSz3a1gDi0s9XXiQemx+6QqtsrXe62a6zOGU+vjb2GRfG5jeEokI1zBBcfD42enRv0Rw==", + "license": "MIT" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@stricli/core": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@stricli/core/-/core-1.2.6.tgz", + "integrity": "sha512-j5fa1wyOLrP9WJqqLFEJeQviqb3cK46K+FXTuISEkG/H5C820YfKDoVQ3CDVdM5WLhEx1ARdpiW6+hU4tYHjCQ==", + "license": "Apache-2.0" + }, + "node_modules/@supabase/pg-delta": { + "version": "1.0.0-alpha.4", + "resolved": "https://pkg.pr.new/supabase/pg-toolbelt/@supabase/pg-delta@7cb3a19", + "integrity": "sha512-emPkV31AzayC2AxvBzNKMMQPb8VaJSyKGKKP/r6DmpKMtMbNku0MnEMSYTWYUixzniF/f3l0nL9Z4KMbpk/JHw==", + "license": "MIT", + "dependencies": { + "@stricli/core": "^1.2.4", + "@supabase/pg-topo": "https://pkg.pr.new/supabase/pg-toolbelt/@supabase/pg-topo@7cb3a19bb27e78709cbc6a6d7e299a47e4d96371", + "@ts-safeql/sql-tag": "^0.2.0", + "chalk": "^5.6.2", + "debug": "^4.3.7", + "pg": "^8.17.2", + "zod": "^4.2.1" + }, + "bin": { + "pgdelta": "dist/cli/bin/cli.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/pg-topo": { + "version": "1.0.0-alpha.0", + "resolved": "https://pkg.pr.new/supabase/pg-toolbelt/@supabase/pg-topo@7cb3a19bb27e78709cbc6a6d7e299a47e4d96371", + "integrity": "sha512-ku7OOhiIpS6DRRGTZwaWKfJ5MrTljCLZxuXVSkEWsJaZdnNaz9p0nbOmyGRppOOtSDuXM+S9aQTQFJBu/sG3aQ==", + "license": "MIT", + "dependencies": { + "@pgsql/traverse": "^17.2.4", + "plpgsql-parser": "^0.5.4" + } + }, + "node_modules/@ts-safeql/sql-tag": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@ts-safeql/sql-tag/-/sql-tag-0.2.1.tgz", + "integrity": "sha512-TaDtQjobByWIn95dM2H2ucltJyL8w2B9mZdr6/1o1ZdbLRLS16XqzY6ssWkviHomQwIxB+CfK3Qz5a6AHDswsA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bin-links": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "proc-log": "^6.0.0", + "read-cmd-shim": "^6.0.0", + "write-file-atomic": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/case": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.3.tgz", + "integrity": "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==", + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cmd-shim": { + "version": "8.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/nested-obj": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/nested-obj/-/nested-obj-0.1.10.tgz", + "integrity": "sha512-5V2kUPrBee/tmoS2p0IJ35BcaJuW1p1yXF5GP8JpXIkDoPbaYeYypAHizUeZkAUxcC7Rago7izWmEq7qa8+Mhw==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/pg": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.19.0.tgz", + "integrity": "sha512-QIcLGi508BAHkQ3pJNptsFz5WQMlpGbuBGBaIaXsWK8mel2kQ/rThYI+DbgjUvZrIr7MiuEuc9LcChJoEZK1xQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.12.0", + "pg-protocol": "^1.12.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.12.0.tgz", + "integrity": "sha512-eIJ0DES8BLaziFHW7VgJEBPi5hg3Nyng5iKpYtj3wbcAUV9A1wLgWiY7ajf/f/oO1wfxt83phXPY8Emztg7ITg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-proto-parser": { + "version": "1.30.4", + "resolved": "https://registry.npmjs.org/pg-proto-parser/-/pg-proto-parser-1.30.4.tgz", + "integrity": "sha512-+9/n8zfYQVNRc8KGhxxNXO8NA5OKni01IPtit6+C3sLMtcRVVFCj4W0XtrEGFivNjz2qwUtFmRhG8OGMTxs6hg==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.23.6", + "@babel/parser": "^7.23.6", + "@babel/traverse": "7.28.5", + "@babel/types": "7.28.5", + "@launchql/protobufjs": "7.2.6", + "case": "1.6.3", + "deepmerge": "4.3.1", + "nested-obj": "^0.1.5", + "strfy-js": "^3.1.5" + } + }, + "node_modules/pg-protocol": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.12.0.tgz", + "integrity": "sha512-uOANXNRACNdElMXJ0tPz6RBM0XQ61nONGAwlt8da5zs/iUOOCLBQOHSXnrC6fMsvtjxbOJrZZl5IScGv+7mpbg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pgsql-deparser": { + "version": "17.18.0", + "resolved": "https://registry.npmjs.org/pgsql-deparser/-/pgsql-deparser-17.18.0.tgz", + "integrity": "sha512-LFjdKKYHVo8lUPbOVfO6dRm5L28hBrfqnBrX3DhiC97k6Hsbi+7LXzvL30gahLUbN5dLai9MvsJ8Gbg/7joD1Q==", + "license": "MIT", + "dependencies": { + "@pgsql/quotes": "17.1.0", + "@pgsql/types": "^17.6.2" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/plpgsql-deparser": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/plpgsql-deparser/-/plpgsql-deparser-0.7.4.tgz", + "integrity": "sha512-XXCOVEUI1G+Teg1oZE6ZfSJqNSRKbei/7bVcc1uwQEWc2LxOk1Y6NN+UQQulnwLz0nvKueHrMAyL8/aam2fMWg==", + "license": "MIT", + "dependencies": { + "@pgsql/types": "^17.6.2", + "pgsql-deparser": "17.18.0" + } + }, + "node_modules/plpgsql-parser": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/plpgsql-parser/-/plpgsql-parser-0.5.5.tgz", + "integrity": "sha512-HgWqHTaOKiyNkO5EhpO7kugWYNmpgcCB6EJKlYm+wPiWpHTwpG52/3gGqm9NyAJ3kH0p1M4cBecNA7dDepmVgg==", + "license": "MIT", + "dependencies": { + "@libpg-query/parser": "^17.6.3", + "@pgsql/traverse": "17.2.4", + "@pgsql/types": "^17.6.2", + "pgsql-deparser": "17.18.0", + "plpgsql-deparser": "0.7.4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/read-cmd-shim": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/strfy-js": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/strfy-js/-/strfy-js-3.2.1.tgz", + "integrity": "sha512-HSw2lkUJVPZ75I+E3HM7UqHMKvBCwjRt1MIAxPPNtLFjuqCrnDVKQQGfotdj/3qHxuhB6NDQ1rYmNjVpPBiNEA==", + "license": "MIT", + "dependencies": { + "minimatch": "^10.1.1" + } + }, + "node_modules/supabase": { + "version": "2.76.16", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bin-links": "^6.0.0", + "https-proxy-agent": "^7.0.2", + "node-fetch": "^3.3.2", + "tar": "7.5.9" + }, + "bin": { + "supabase": "bin/supabase" + }, + "engines": { + "npm": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.9", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/write-file-atomic": { + "version": "7.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..9dd2e8ca --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "private": true, + "dependencies": { + "@supabase/pg-delta": "https://pkg.pr.new/supabase/pg-toolbelt/@supabase/pg-delta@7cb3a19" + }, + "devDependencies": { + "supabase": "^2.76.16" + } +} diff --git a/scripts/declarative-dbdev-init.sh b/scripts/declarative-dbdev-init.sh new file mode 100755 index 00000000..3ac31149 --- /dev/null +++ b/scripts/declarative-dbdev-init.sh @@ -0,0 +1,142 @@ +#!/bin/bash +set -e + +# ────────────────────────────────────────────────────────────── +# declarative-dbdev-init.sh +# +# Initializes a declarative schema from the dbdev supabase project. +# 1. Starts a shadow DB (supabase/postgres image) for the baseline +# 2. Snapshots the clean baseline catalog +# 3. Starts the dbdev supabase project (applies all migrations) +# 4. Exports the declarative schema (diff: baseline → supabase DB) +# 5. Verifies roundtrip: apply to fresh shadow DB, then diff +# +# Run from the dbdev repo root so npx pgdelta resolves from node_modules: +# bash scripts/declarative-dbdev-init.sh +# ────────────────────────────────────────────────────────────── + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DBDEV_DIR="${DBDEV_DIR:-${SCRIPT_DIR}/..}" + +SHADOW_IMAGE="${SHADOW_IMAGE:-supabase/postgres:15.8.1.085}" +SHADOW_CONTAINER="pgdelta-dbdev-shadow" +SHADOW_PORT="${SHADOW_PORT:-6544}" +SHADOW_URL="postgres://supabase_admin:postgres@localhost:${SHADOW_PORT}/postgres" + +SUPABASE_DB_PORT=54322 +SUPABASE_DB_URL="postgres://supabase_admin:postgres@localhost:${SUPABASE_DB_PORT}/postgres" + +PGDELTA="${PGDELTA:-npx pgdelta}" +OUTPUT_DIR="${OUTPUT_DIR:-${DBDEV_DIR}/declarative-schemas}" +BASELINE_SNAPSHOT="${BASELINE_SNAPSHOT:-${DBDEV_DIR}/baseline-catalog.json}" +SKIP_SUPABASE_START="${SKIP_SUPABASE_START:-}" +SKIP_VERIFY="${SKIP_VERIFY:-}" + +FORMAT_OPTIONS='{"keywordCase":"lower","maxWidth":180,"indent":4}' + +cleanup_shadow() { + echo "Cleaning up shadow container..." + docker rm -f "$SHADOW_CONTAINER" >/dev/null 2>&1 || true + rm -f "$BASELINE_SNAPSHOT" +} + +start_shadow() { + echo "Starting shadow DB (${SHADOW_IMAGE}) on port ${SHADOW_PORT}..." + docker rm -f "$SHADOW_CONTAINER" 2>/dev/null || true + docker run -d --name "$SHADOW_CONTAINER" \ + -e POSTGRES_PASSWORD=postgres \ + -p "${SHADOW_PORT}:5432" \ + "$SHADOW_IMAGE" + + echo "Waiting for shadow DB to be ready..." + until docker exec "$SHADOW_CONTAINER" pg_isready -U postgres 2>/dev/null; do + sleep 1 + done + sleep 3 +} + +# ────────────────────────────────────────────────────────────── +# 1. Start shadow DB and snapshot baseline +# ────────────────────────────────────────────────────────────── +start_shadow + +echo "Snapshotting clean shadow DB as catalog baseline..." +$PGDELTA catalog-export --target "$SHADOW_URL" --output "$BASELINE_SNAPSHOT" + +# ────────────────────────────────────────────────────────────── +# 2. Start the dbdev supabase project +# ────────────────────────────────────────────────────────────── +if [ -z "$SKIP_SUPABASE_START" ]; then + echo "Starting dbdev supabase project..." + (cd "$DBDEV_DIR" && supabase start) +else + echo "Skipping supabase start (SKIP_SUPABASE_START is set)." + echo "Checking supabase DB is reachable..." + until pg_isready -h localhost -p "$SUPABASE_DB_PORT" -U postgres 2>/dev/null; do + echo " Waiting for supabase DB on port ${SUPABASE_DB_PORT}..." + sleep 2 + done +fi + +# ────────────────────────────────────────────────────────────── +# 3. Export declarative schema (baseline → supabase DB) +# ────────────────────────────────────────────────────────────── +EXPORT_OPTS=( + --source "$BASELINE_SNAPSHOT" + --target "$SUPABASE_DB_URL" + --output "$OUTPUT_DIR" + --integration supabase + --force + --format-options "$FORMAT_OPTIONS" +) + +echo "Exporting declarative schema..." +$PGDELTA declarative export "${EXPORT_OPTS[@]}" + +# ────────────────────────────────────────────────────────────── +# 4. Verify roundtrip: apply to fresh shadow DB, then diff +# ────────────────────────────────────────────────────────────── +if [ -z "$SKIP_VERIFY" ]; then + echo "" + echo "=== Roundtrip verification ===" + + echo "Resetting shadow DB for verification..." + docker rm -f "$SHADOW_CONTAINER" >/dev/null 2>&1 + start_shadow + + echo "Applying declarative schema to shadow DB..." + $PGDELTA declarative apply \ + --path "$OUTPUT_DIR" \ + --target "$SHADOW_URL" + + echo "Verifying roundtrip: diff shadow DB vs supabase DB (expect 0 changes)..." + VERIFY_OPTS=( + --source "$SHADOW_URL" + --target "$SUPABASE_DB_URL" + --integration supabase + ) + VERIFY_OUTPUT=$($PGDELTA plan "${VERIFY_OPTS[@]}" 2>&1) || true + + if echo "$VERIFY_OUTPUT" | grep -q "No changes detected."; then + echo "Verification PASSED: 0 changes (declarative schema roundtrip OK)." + else + echo "$VERIFY_OUTPUT" + echo "" + echo "Writing full diff for debugging..." + $PGDELTA plan "${VERIFY_OPTS[@]}" --format sql || true + echo "" + echo "Verification FAILED: diff reported changes after roundtrip." + cleanup_shadow + exit 1 + fi +else + echo "Skipping verification (SKIP_VERIFY is set)." +fi + +# ────────────────────────────────────────────────────────────── +# 5. Cleanup +# ────────────────────────────────────────────────────────────── +cleanup_shadow +echo "" +echo "Done. Declarative schema written to: ${OUTPUT_DIR}" +echo "Supabase project is still running (use 'cd ${DBDEV_DIR} && supabase stop' to stop)." diff --git a/scripts/declarative-dbdev-update.sh b/scripts/declarative-dbdev-update.sh new file mode 100755 index 00000000..9a9edce2 --- /dev/null +++ b/scripts/declarative-dbdev-update.sh @@ -0,0 +1,177 @@ +#!/bin/bash +set -e + +# ────────────────────────────────────────────────────────────── +# declarative-dbdev-update.sh +# +# After editing declarative schema files, this script: +# 1. Starts a shadow DB (supabase/postgres image) +# 2. Applies the declarative schema to the shadow DB (desired state) +# 3. Diffs the running supabase DB against the shadow DB +# 4. Generates a migration file from the diff +# 5. Applies the migration to the supabase project DB +# 6. Verifies the roundtrip (expect 0 remaining changes) +# +# Run from the dbdev repo root so npx pgdelta resolves from node_modules: +# MIGRATION_NAME=my_change bash scripts/declarative-dbdev-update.sh +# ────────────────────────────────────────────────────────────── + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DBDEV_DIR="${DBDEV_DIR:-${SCRIPT_DIR}/..}" + +SHADOW_IMAGE="${SHADOW_IMAGE:-supabase/postgres:15.8.1.085}" +SHADOW_CONTAINER="pgdelta-dbdev-shadow" +SHADOW_PORT="${SHADOW_PORT:-6544}" +SHADOW_URL="postgres://postgres:postgres@localhost:${SHADOW_PORT}/postgres" + +SUPABASE_DB_PORT=54322 +SUPABASE_DB_URL="postgres://postgres:postgres@localhost:${SUPABASE_DB_PORT}/postgres" + +PGDELTA="${PGDELTA:-npx pgdelta}" +OUTPUT_DIR="${OUTPUT_DIR:-${DBDEV_DIR}/declarative-schemas}" +MIGRATION_NAME="${MIGRATION_NAME:-declarative_update}" +SKIP_APPLY="${SKIP_APPLY:-}" +SKIP_VERIFY="${SKIP_VERIFY:-}" + +MIGRATIONS_DIR="${DBDEV_DIR}/supabase/migrations" + +cleanup_shadow() { + echo "Cleaning up shadow container..." + docker rm -f "$SHADOW_CONTAINER" >/dev/null 2>&1 || true +} + +# ────────────────────────────────────────────────────────────── +# 0. Pre-flight checks +# ────────────────────────────────────────────────────────────── +if [ ! -d "$OUTPUT_DIR" ]; then + echo "Error: Declarative schema directory not found at ${OUTPUT_DIR}" + echo "Run declarative-dbdev-init.sh first." + exit 1 +fi + +echo "Checking supabase DB is reachable on port ${SUPABASE_DB_PORT}..." +if ! pg_isready -h localhost -p "$SUPABASE_DB_PORT" -U postgres 2>/dev/null; then + echo "Error: Supabase DB is not running on port ${SUPABASE_DB_PORT}." + echo "Start the dbdev project first: cd ${DBDEV_DIR} && supabase start" + exit 1 +fi + +# ────────────────────────────────────────────────────────────── +# 1. Start shadow DB +# ────────────────────────────────────────────────────────────── +echo "Starting shadow DB (${SHADOW_IMAGE}) on port ${SHADOW_PORT}..." +docker rm -f "$SHADOW_CONTAINER" 2>/dev/null || true +docker run -d --name "$SHADOW_CONTAINER" \ + -e POSTGRES_PASSWORD=postgres \ + -p "${SHADOW_PORT}:5432" \ + "$SHADOW_IMAGE" + +echo "Waiting for shadow DB to be ready..." +until docker exec "$SHADOW_CONTAINER" pg_isready -U postgres 2>/dev/null; do + sleep 1 +done +sleep 3 + +# ────────────────────────────────────────────────────────────── +# 2. Apply declarative schema to shadow DB (desired state) +# ────────────────────────────────────────────────────────────── +echo "Applying declarative schema to shadow DB..." +$PGDELTA declarative apply \ + --path "$OUTPUT_DIR" \ + --target "$SHADOW_URL" \ + --verbose + +# ────────────────────────────────────────────────────────────── +# 3. Generate migration: diff supabase DB → shadow DB +# ────────────────────────────────────────────────────────────── +TIMESTAMP=$(date +%Y%m%d%H%M%S) +MIGRATION_FILE="${MIGRATIONS_DIR}/${TIMESTAMP}_${MIGRATION_NAME}.sql" +TMP_MIGRATION="/tmp/pgdelta-dbdev-migration-${TIMESTAMP}.sql" + +echo "Generating migration (supabase DB → shadow DB)..." +rm -f "$TMP_MIGRATION" +PLAN_EXIT=0 +$PGDELTA plan \ + --source "$SUPABASE_DB_URL" \ + --target "$SHADOW_URL" \ + --integration supabase \ + --format sql \ + --output "$TMP_MIGRATION" || PLAN_EXIT=$? + +if [ "$PLAN_EXIT" -ne 0 ] && [ "$PLAN_EXIT" -ne 2 ]; then + echo "Error: pgdelta plan failed with exit code ${PLAN_EXIT}" + cleanup_shadow + exit 1 +fi + +if [ "$PLAN_EXIT" -eq 0 ]; then + echo "" + echo "No changes detected. Declarative schema matches the supabase DB." + cleanup_shadow + exit 0 +fi + +echo "" +echo "Changes detected. Migration preview:" +echo "──────────────────────────────────────" +cat "$TMP_MIGRATION" +echo "──────────────────────────────────────" +echo "" + +if [ -n "$SKIP_APPLY" ]; then + echo "Saving migration to: ${MIGRATION_FILE}" + mkdir -p "$MIGRATIONS_DIR" + mv "$TMP_MIGRATION" "$MIGRATION_FILE" + echo "Skipping apply (SKIP_APPLY is set). Apply manually with:" + echo " psql '${SUPABASE_DB_URL}' -f '${MIGRATION_FILE}'" + cleanup_shadow + exit 0 +fi + +# ────────────────────────────────────────────────────────────── +# 4. Save and apply migration to supabase DB +# ────────────────────────────────────────────────────────────── +echo "Saving migration to: ${MIGRATION_FILE}" +mkdir -p "$MIGRATIONS_DIR" +mv "$TMP_MIGRATION" "$MIGRATION_FILE" + +echo "Applying migration to supabase DB..." +psql "$SUPABASE_DB_URL" -f "$MIGRATION_FILE" + +# ────────────────────────────────────────────────────────────── +# 5. Verify roundtrip +# ────────────────────────────────────────────────────────────── +if [ -z "$SKIP_VERIFY" ]; then + echo "" + echo "=== Roundtrip verification ===" + echo "Verifying: diff supabase DB vs shadow DB (expect 0 changes)..." + + VERIFY_OPTS=( + --source "$SUPABASE_DB_URL" + --target "$SHADOW_URL" + --integration supabase + ) + VERIFY_OUTPUT=$($PGDELTA plan "${VERIFY_OPTS[@]}" 2>&1) || true + + if echo "$VERIFY_OUTPUT" | grep -q "No changes detected."; then + echo "Verification PASSED: 0 changes after migration." + else + echo "$VERIFY_OUTPUT" + echo "" + echo "Writing full diff for debugging..." + $PGDELTA plan "${VERIFY_OPTS[@]}" --format sql || true + echo "" + echo "Verification FAILED: diff still reports changes after applying migration." + cleanup_shadow + exit 1 + fi +else + echo "Skipping verification (SKIP_VERIFY is set)." +fi + +# ────────────────────────────────────────────────────────────── +# 6. Cleanup +# ────────────────────────────────────────────────────────────── +cleanup_shadow +echo "" +echo "Done. Migration applied: ${MIGRATION_FILE}"