@voltro/cli 0.33.0 → 0.34.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/CHANGELOG.md +1801 -0
- package/bin/nodeEnvironment.d.mts +30 -0
- package/bin/nodeEnvironment.mjs +158 -0
- package/bin/voltro.mjs +69 -5
- package/dist/addCommand-BNeoeSxe.js +124 -0
- package/dist/addCommand-aXSQveak.js +2 -0
- package/dist/agentsMd-BTchIZku.js +2 -0
- package/dist/agentsMd-mhQMF1bx.js +254 -0
- package/dist/apiBuild-BICVGnEC.js +2 -0
- package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DZF_f0_I.js} +46 -46
- package/dist/appPort-B_HpJ_ck.js +48 -0
- package/dist/baselineCommand-C2ClWZN3.js +2 -0
- package/dist/baselineCommand-DIttzO8A.js +227 -0
- package/dist/bin.js +71 -28
- package/dist/build-CE7Ca9gm.js +711 -0
- package/dist/cacheCommand-DA4OH9xt.js +42 -0
- package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
- package/dist/checkCommand-Dv8r9tGO.js +231 -0
- package/dist/checkCommand-YZDmkAGd.js +2 -0
- package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
- package/dist/cliError-BmdYnghb.js +10 -0
- package/dist/cliOutput-D1tSBoRM.js +15 -0
- package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
- package/dist/cloudClient-DWL-Hw_T.js +67 -0
- package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
- package/dist/clusterCmd-CNXKlcvD.js +54 -0
- package/dist/codegen-CYM3Zqrf.js +605 -0
- package/dist/codegen-ChBi_hVa.js +2 -0
- package/dist/codegenCommand-DC6w2tNZ.js +30 -0
- package/dist/codemodRunner-DRRqXR74.js +5243 -0
- package/dist/commandRunner-BLAEFLjp.js +47 -0
- package/dist/commands-gutsz-Ac.js +808 -0
- package/dist/connectionConfig-UFlIEiys.js +66 -0
- package/dist/dashboardCommand-3YG8p-UA.js +25 -0
- package/dist/dataCommand-qL0r7fPO.js +535 -0
- package/dist/dataProfile-dW-PsfLB.js +15 -0
- package/dist/{dbCommand-DTLKAfbA.js → dbCommand-B6X0FZbq.js} +551 -401
- package/dist/dbCommand-CpdKLeQq.js +2 -0
- package/dist/{dev-C_P8FLSx.js → dev-CerMd0mW.js} +3122 -3129
- package/dist/dev-CoG-ZPx8.js +3 -0
- package/dist/devActivity-Dx_3nnGv.js +100 -0
- package/dist/devActivity.js +1 -1
- package/dist/dialectDriver-CgXnDfec.js +39 -0
- package/dist/discover-C9XKJDco.js +25 -0
- package/dist/doctorCommand-BvqGBwNG.js +2 -0
- package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-CnDXQxfa.js} +520 -1278
- package/dist/dormancyCommand-Dszo57d6.js +69 -0
- package/dist/e2eCmd-BRabZww-.js +147 -0
- package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
- package/dist/envCommand-DPgdV1Bq.js +60 -0
- package/dist/evalCommand-6RUfPen4.js +118 -0
- package/dist/evolveCommand-DHpkgjgH.js +281 -0
- package/dist/fileTaxonomy-CJfgOllU.js +457 -0
- package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
- package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
- package/dist/generateCommand-oibemh97.js +147 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +4 -3
- package/dist/infoCommand-BJw9nLUR.js +60 -0
- package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
- package/dist/inspect-C_T_WGvl.js +2 -0
- package/dist/inspectCmd-Bppy-GGw.js +224 -0
- package/dist/inspectFetch-Cm8_wVvp.js +151 -0
- package/dist/inspectMetrics-CfdKLh6t.js +72 -0
- package/dist/loadEnv-D9nEOClM.js +44 -0
- package/dist/logFileSink-C_D2wRN1.js +105 -0
- package/dist/logsCmd-CCca3KRZ.js +260 -0
- package/dist/manifestBuild-ChsKAhmn.js +2 -0
- package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
- package/dist/metaCommands-7MJfZ5cf.js +196 -0
- package/dist/migrate-CBwOt_iV.js +83 -0
- package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
- package/dist/newCommand-COWOJ1_E.js +156 -0
- package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
- package/dist/packageCommand-Cug_3Ogl.js +271 -0
- package/dist/pageConvention-cEiRxdab.js +5 -0
- package/dist/privacyCommand-C-Df56U_.js +146 -0
- package/dist/projectScaffold-DmzEKHib.js +2 -0
- package/dist/projectScaffold-LMMtaavR.js +814 -0
- package/dist/renderModeScan-D7J1B7Kw.js +105 -0
- package/dist/renderProfile-1OWWAAtx.js +81 -0
- package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
- package/dist/runtimeTrace-CRxalXTs.js +91 -0
- package/dist/scheduleCmd--jksTrf6.js +69 -0
- package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
- package/dist/schemaIr-UJybUUZW.js +103 -0
- package/dist/{sdkgen-C81QIkiL.js → sdkgen-CYJscZC7.js} +111 -209
- package/dist/seedRunner-TFHHiToI.js +329 -0
- package/dist/serveCommand-B_isw7q4.js +1647 -0
- package/dist/serveCommand-DOvbgRnQ.js +2 -0
- package/dist/serveEntry.js +5 -5
- package/dist/serverlessCommand-CfJZy6dS.js +482 -0
- package/dist/start-9LiUOfES.js +1087 -0
- package/dist/start-B-9Nsp-S.js +3 -0
- package/dist/startEntry.js +2 -2
- package/dist/staticCommand-Dr2M6tpU.js +304 -0
- package/dist/storageCommand-Co6NfLqN.js +42 -0
- package/dist/templates-De8IR5-c.js +102 -0
- package/dist/test-CI6iDsYc.js +115 -0
- package/dist/tracesCmd-CkEZQrtt.js +232 -0
- package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
- package/dist/tsxLoader-EuXmSJ1K.js +51 -0
- package/dist/typecheckCommand-BlsWiCNq.js +61 -0
- package/dist/updateCommand-Bkptutss.js +585 -0
- package/dist/updateCommand-us1_hdIC.js +2 -0
- package/dist/{inspectMetrics-1xzTKAFx.js → webDev-CBYvPqQr.js} +975 -1559
- package/dist/webDev-Cg-fFiyd2.js +2 -0
- package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
- package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
- package/dist/webhooksCommand-CID96Rga.js +267 -0
- package/dist/workflowsCmd-D1VTmLMY.js +608 -0
- package/package.json +179 -17
- package/templates/AGENTS.core.md +58 -3
- package/templates/AGENTS.md +64 -7
- package/templates/agent-docs/_index.md +6 -4
- package/templates/agent-docs/_manifest.json +22 -5
- package/templates/agent-docs/ai.md +370 -0
- package/templates/agent-docs/authentication.md +265 -31
- package/templates/agent-docs/caching.md +6 -0
- package/templates/agent-docs/cli.md +794 -50
- package/templates/agent-docs/data.md +550 -11
- package/templates/agent-docs/database/migrations.md +174 -25
- package/templates/agent-docs/database/misc.md +156 -40
- package/templates/agent-docs/database/querying.md +19 -1
- package/templates/agent-docs/database/scaling.md +60 -0
- package/templates/agent-docs/database/schema.md +1 -0
- package/templates/agent-docs/database/seedsdialects.md +208 -19
- package/templates/agent-docs/database/transactions.md +68 -0
- package/templates/agent-docs/deployment.md +69 -3
- package/templates/agent-docs/introduction.md +87 -16
- package/templates/agent-docs/local-first-mobile.md +79 -4
- package/templates/agent-docs/multi-tenancy.md +95 -20
- package/templates/agent-docs/observability.md +58 -3
- package/templates/agent-docs/plugins/ai-flows.md +161 -2
- package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
- package/templates/agent-docs/plugins/audit.md +37 -1
- package/templates/agent-docs/plugins/auth-social.md +143 -0
- package/templates/agent-docs/plugins/auth-workos.md +4 -2
- package/templates/agent-docs/plugins/auth.md +131 -6
- package/templates/agent-docs/plugins/billing.md +132 -15
- package/templates/agent-docs/plugins/cdc-out.md +46 -7
- package/templates/agent-docs/plugins/clickhouse.md +1 -1
- package/templates/agent-docs/plugins/duckdb.md +1 -1
- package/templates/agent-docs/plugins/flags.md +132 -0
- package/templates/agent-docs/plugins/governance.md +105 -7
- package/templates/agent-docs/plugins/multitenancy.md +9 -4
- package/templates/agent-docs/plugins/presence.md +13 -2
- package/templates/agent-docs/plugins/ratelimit.md +9 -0
- package/templates/agent-docs/plugins/search.md +157 -6
- package/templates/agent-docs/plugins/sso-saml.md +47 -8
- package/templates/agent-docs/plugins/webhooks.md +105 -0
- package/templates/agent-docs/plugins.md +150 -18
- package/templates/agent-docs/reference.md +60 -3
- package/templates/agent-docs/releases.md +1117 -0
- package/templates/agent-docs/routing.md +43 -25
- package/templates/agent-docs/scheduling.md +10 -0
- package/templates/agent-docs/schema-driven-ui.md +92 -12
- package/templates/agent-docs/security.md +426 -0
- package/templates/agent-docs/templates/apibackends.md +87 -18
- package/templates/agent-docs/templates/appshells.md +32 -14
- package/templates/agent-docs/templates/overview.md +13 -8
- package/templates/agent-docs/testing.md +211 -14
- package/templates/agent-docs/whats-new.md +1700 -104
- package/templates/agent-docs/workflows.md +130 -14
- package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
- package/templates/apps/api-ai/package.json +8 -7
- package/templates/apps/api-auth/actions/me.action.ts +13 -0
- package/templates/apps/api-auth/package.json +9 -8
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend/package.json +12 -8
- package/templates/apps/api-backend/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
- package/templates/apps/api-backend-deactivation/package.json +8 -7
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mail/package.json +9 -8
- package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-mariadb/.env.example +14 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mariadb/package.json +10 -9
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-sqlite/.env.example +14 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-sqlite/package.json +9 -8
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-storage/package.json +9 -8
- package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
- package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
- package/templates/apps/api-cms/actions/me.action.ts +13 -0
- package/templates/apps/api-cms/app.config.ts +19 -0
- package/templates/apps/api-cms/authz.ts +63 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
- package/templates/apps/api-cms/package.json +11 -10
- package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
- package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
- package/templates/apps/api-collab/package.json +9 -8
- package/templates/apps/api-collab/queries/documents.query.ts +19 -7
- package/templates/apps/api-data-advanced/package.json +9 -8
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
- package/templates/apps/api-durable/package.json +9 -8
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
- package/templates/apps/api-feature-flags/package.json +10 -9
- package/templates/apps/api-governance/README.md +30 -8
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
- package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
- package/templates/apps/api-governance/database/schema.ts +16 -4
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
- package/templates/apps/api-governance/package.json +9 -8
- package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
- package/templates/apps/api-kv/package.json +9 -8
- package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
- package/templates/apps/api-moderation/package.json +9 -8
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
- package/templates/apps/api-observability/package.json +9 -8
- package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
- package/templates/apps/api-ratelimit/package.json +9 -8
- package/templates/apps/api-rbac/package.json +9 -8
- package/templates/apps/api-rest/package.json +8 -7
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
- package/templates/apps/api-saas/package.json +12 -11
- package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
- package/templates/apps/api-saas-starter/app.config.ts +19 -0
- package/templates/apps/api-saas-starter/authz.ts +75 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
- package/templates/apps/api-saas-starter/package.json +15 -11
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
- package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
- package/templates/apps/api-search/package.json +9 -8
- package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
- package/templates/apps/api-status/README.md +10 -3
- package/templates/apps/api-status/app.config.ts +8 -3
- package/templates/apps/api-status/authz.ts +5 -3
- package/templates/apps/api-status/package.json +9 -8
- package/templates/apps/api-status/queries/components.list.query.ts +14 -6
- package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
- package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
- package/templates/apps/api-status/tests/status.test.ts +9 -1
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
- package/templates/apps/api-versioning/package.json +9 -8
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
- package/templates/apps/api-webhooks/package.json +10 -9
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
- package/templates/apps/changelog/package.json +8 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +10 -8
- package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
- package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
- package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
- package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
- package/templates/apps/frontend-app/app.config.ts +4 -3
- package/templates/apps/frontend-app/package.json +11 -8
- package/templates/apps/frontend-app/src/lib/api.ts +25 -0
- package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
- package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
- package/templates/apps/frontend-auth/package.json +10 -8
- package/templates/apps/frontend-blank/package.json +9 -7
- package/templates/apps/frontend-cms/package.json +11 -9
- package/templates/apps/frontend-collab/package.json +12 -9
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
- package/templates/apps/frontend-contact/package.json +9 -7
- package/templates/apps/frontend-dashboard/package.json +9 -7
- package/templates/apps/frontend-docs/package.json +9 -7
- package/templates/apps/frontend-i18n/package.json +8 -6
- package/templates/apps/frontend-landing/package.json +9 -7
- package/templates/apps/frontend-portal/package.json +10 -8
- package/templates/apps/frontend-saas/app.config.ts +10 -6
- package/templates/apps/frontend-saas/package.json +10 -8
- package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
- package/templates/apps/frontend-spa/package.json +9 -7
- package/templates/apps/frontend-ssr/package.json +9 -7
- package/templates/apps/frontend-ssr-api/package.json +10 -8
- package/templates/apps/frontend-static-blog/package.json +8 -6
- package/templates/apps/frontend-status/package.json +10 -8
- package/templates/apps/mobile-app/README.md +1 -0
- package/templates/apps/mobile-app/package.json +4 -2
- package/templates/apps/mobile-app/src/app/index.tsx +22 -12
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
- package/templates/apps/mobile-app/src/lib/api.ts +34 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
- package/templates/baselines/bare/.env.example +14 -0
- package/templates/baselines/bare/baseline.json +4 -4
- package/templates/baselines/compose/.env.example +14 -0
- package/templates/baselines/compose/README.md +1 -1
- package/templates/baselines/compose/baseline.json +5 -5
- package/templates/baselines/compose-mariadb/.env.example +14 -0
- package/templates/baselines/compose-mariadb/README.md +1 -1
- package/templates/baselines/compose-mariadb/baseline.json +5 -5
- package/templates/baselines/helm/.env.example +14 -0
- package/templates/baselines/helm/baseline.json +4 -4
- package/dist/apiBuild-C-x9YacA.js +0 -2
- package/dist/checkCommand-DRovTKza.js +0 -2
- package/dist/commands-CJfepbm4.js +0 -11541
- package/dist/dbCommand-b1gum4td.js +0 -2
- package/dist/dev-iiMtlkfs.js +0 -3
- package/dist/devActivity-BhIu6ncs.js +0 -159
- package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
- package/dist/inspect-mmBuRXmy.js +0 -2
- package/dist/manifestBuild-Dj8Jjoto.js +0 -2
- package/dist/seedRunner-Bqxgp7HZ.js +0 -230
- package/dist/serveCommand-DdaM4Hup.js +0 -1608
- package/dist/start-C0koT0UO.js +0 -1084
- /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
|
@@ -17,7 +17,7 @@ Voltro's migrator does not generate or apply migration files. Instead, on every
|
|
|
17
17
|
2. **Diffs** that against your declared schema (every `*.entity.ts` / `*.schema.ts` file in the project plus the framework's bookkeeping tables).
|
|
18
18
|
3. **Classifies** each pending DDL op into one of seven `OperationClass`es — `safe`, `needs-default`, `needs-backfill`, `needs-rename-annotation`, `lossy`, `online-required`, `multi-step`.
|
|
19
19
|
4. **Refuses** to apply anything that can't be made safe automatically. The diff output tells you exactly which DSL annotation to add (`.backfill()`, `.renamedFrom()`, `dropped()`, …).
|
|
20
|
-
5. **Applies** the rest under an advisory lock + records the result in `_voltro_migration_plans` with a fingerprint of the post-apply schema.
|
|
20
|
+
5. **Applies** the rest under an advisory lock + records the result in `_voltro_migration_plans` with a fingerprint of the post-apply schema. On a dialect that cannot apply the whole plan in one transaction (MySQL / MariaDB / SQLite / Turso), each operation is also recorded in a **resume ledger** (`_voltro_migration_ops`) as it lands, so a crashed apply is continued rather than re-planned blind — see [multi-dialect](./multi-dialect.md).
|
|
21
21
|
|
|
22
22
|
There are no generated SQL files to commit, no `migrations/` directory to rebase, no checksum manifest to repair. The source of truth is your schema TypeScript; the DB is the slave.
|
|
23
23
|
|
|
@@ -862,28 +862,32 @@ completed rows.
|
|
|
862
862
|
<!-- source: en/database/migrations/multi-dialect.md -->
|
|
863
863
|
## Multi-dialect strategy
|
|
864
864
|
|
|
865
|
-
_How the planner + applier behave across Postgres, MySQL, MariaDB, MSSQL, SQLite — the atomicity matrix,
|
|
865
|
+
_How the planner + applier behave across Postgres, MySQL, MariaDB, MSSQL, SQLite, Turso — the atomicity matrix, the resume ledger that carries a crashed apply on the non-transactional dialects, SQLite's table-rewrite mechanic, plus the per-dialect DDL idioms the framework hides._
|
|
866
866
|
|
|
867
867
|
The planner produces ONE `MigrationPlan` regardless of dialect. The applier executes it per-dialect, dispatching through `sql.onDialectOrElse` for every emit + falling back to runtime probes when behaviour diverges. The same `voltro db apply` invocation against the same schema produces structurally identical results on every backend.
|
|
868
868
|
|
|
869
869
|
What ISN'T uniform: **transactional DDL semantics**.
|
|
870
870
|
|
|
871
|
-
| Dialect |
|
|
871
|
+
| Dialect | Plan applied atomically | Advisory-lock mechanism | Recovery after a mid-plan crash |
|
|
872
872
|
|---|---|---|---|
|
|
873
|
-
| Postgres | ✓
|
|
874
|
-
| MSSQL | ✓
|
|
875
|
-
|
|
|
876
|
-
| **
|
|
873
|
+
| Postgres | ✓ one transaction | `pg_advisory_lock(KEY)` | nothing to recover — rolled back |
|
|
874
|
+
| MSSQL | ✓ one transaction | `sp_getapplock` | nothing to recover — rolled back |
|
|
875
|
+
| **MySQL / MariaDB** | **✗ implicit commit per DDL** | `GET_LOCK('voltro_migration', N)` | resume ledger |
|
|
876
|
+
| **SQLite / Turso** | **✗ per statement** | process-local mutex | resume ledger |
|
|
877
877
|
|
|
878
878
|
This is the operationally heaviest cross-dialect difference. The rest of the page covers what changes.
|
|
879
879
|
|
|
880
|
-
|
|
880
|
+
**SQLite is on the non-atomic side, and the reason is Turso.** SQLite the engine *can* do transactional DDL. The applier does not use it, because SQLite and Turso share one dialect token and Turso rejects DDL inside its default transaction — the applier cannot wrap one without wrapping the other. Both therefore take the per-statement path and the resume ledger below.
|
|
881
|
+
|
|
882
|
+
**On Postgres one class of operation is still not covered by the transaction:** `online-required` ops (`CREATE INDEX CONCURRENTLY`, the shadow-column type swap) are *rejected* inside a transaction, so they run after the commit. They are ledgered like a MySQL plan.
|
|
883
|
+
|
|
884
|
+
## Postgres / MSSQL — transactional happy path
|
|
881
885
|
|
|
882
886
|
A multi-step plan runs inside one `BEGIN ... COMMIT`. Mid-flight failure rolls EVERYTHING back; the next plan diff is identical to the pre-apply one. There's nothing to resume — re-running the apply re-runs the plan from scratch.
|
|
883
887
|
|
|
884
888
|
The advisory-lock variants serialise concurrent applies — two operators running `voltro db apply` against the same DB at the same time go through serially.
|
|
885
889
|
|
|
886
|
-
## MySQL / MariaDB —
|
|
890
|
+
## MySQL / MariaDB / SQLite / Turso — the resume ledger
|
|
887
891
|
|
|
888
892
|
Every DDL statement implicitly commits. A 5-op plan on MySQL is effectively 5 separate "atomic statements" with the prior ones already committed when a later one fails. If op 5 of 5 fails, ops 1–4 stay applied:
|
|
889
893
|
|
|
@@ -896,13 +900,36 @@ plan applying (mysql, env=prod):
|
|
|
896
900
|
5. add-index audit_logs(actorId) ✗ ER_DUP_KEYNAME
|
|
897
901
|
```
|
|
898
902
|
|
|
899
|
-
|
|
903
|
+
The `_voltro_migration_plans` row is still written only on a fully successful apply — that row means "this schema is live", and a half-applied plan is not. What the applier DOES write as it goes is a per-operation **resume ledger**, `_voltro_migration_ops`:
|
|
904
|
+
|
|
905
|
+
- every operation that will run outside a transaction is inserted `pending` **before any DDL runs**, so a crash on operation 1 still leaves the whole intended sequence on disk;
|
|
906
|
+
- each row flips to `started` immediately before its statement and `applied` immediately after;
|
|
907
|
+
- the rows are **deleted** once the apply converges and the plan row lands. The ledger is a work queue, not a history — the history is `_voltro_migration_plans.operations`.
|
|
908
|
+
|
|
909
|
+
There is still no `--resume` / `--abort` flag, because there is nothing to choose. Recovery is to re-run the apply once the cause is fixed:
|
|
900
910
|
|
|
901
911
|
```sh
|
|
902
912
|
voltro db apply
|
|
903
913
|
```
|
|
904
914
|
|
|
905
|
-
|
|
915
|
+
The next apply finds the ledger, says so in the log, and continues the interrupted run:
|
|
916
|
+
|
|
917
|
+
```
|
|
918
|
+
[voltro:migrate] migration resume: found an interrupted run (plan_msocz71h_x0qq5d) — 4 of 5 operation(s) completed, in flight: add-index index:audit_logs.audit_logs_actorId_idx
|
|
919
|
+
[voltro:migrate] migration resume: continuing run plan_msocz71h_x0qq5d — replaying 1 operation(s), 4 already applied
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
Three things are worth knowing about how it decides.
|
|
923
|
+
|
|
924
|
+
**The ledger cannot be atomic with the DDL it records** — on MySQL the statement commits itself, so there is always a window where the DDL landed and the `applied` flip did not. That window is not eliminated, it is BOUNDED: the ledger is written strictly sequentially, so **at most one operation can be `started`**, and it is the only one whose outcome is unknown. That one is resolved by asking the planner — the fresh diff was computed against the live database moments ago, so an operation it no longer mentions has already taken effect. Completed operations are never re-attempted.
|
|
925
|
+
|
|
926
|
+
**If you edited the schema in response to the failure, the recorded plan is dropped** and the freshly-diffed one is applied instead — the old plan aims at a target nobody wants any more. The interruption is still logged, and the artefacts below are still reconciled first.
|
|
927
|
+
|
|
928
|
+
**Two operations are repaired rather than re-diffed.** The Postgres online type change (shadow-column swap) and the SQLite table rebuild both build a temporary object and swap it into place, and interrupted mid-swap they leave a live schema that means something ELSE to a differ — a half-finished shadow swap looks like a *missing column*, and the plan a blind re-diff produces for that is `add-column`, which succeeds and loses the data sitting in `<col>__old`. The applier reconciles `<col>__shadow` / `<col>__old` and `<table>__voltro_rebuild` from their observable state before anything else reads the schema. If a shadow-swap state cannot be classified, the apply **refuses**, changes nothing, and names the three columns to inspect.
|
|
929
|
+
|
|
930
|
+
**What has not changed: convergence still gates the fingerprint.** After the DDL — resumed or not — the applier re-plans against the live schema and refuses to record a fingerprint while anything remains. A resumed run is held to exactly the same standard as a fresh one, and an apply that does not converge KEEPS its ledger, because an unfinished run's record is the only thing that tells the next boot it is looking at a half-migrated schema.
|
|
931
|
+
|
|
932
|
+
If someone finished the failed op out of band (a `mysql` shell, a corrective hot-fix), the fresh diff sees it as present and it is skipped.
|
|
906
933
|
|
|
907
934
|
## SQLite — table rewrite mechanic
|
|
908
935
|
|
|
@@ -954,7 +981,7 @@ The DDL emitter under `@voltro/database/src/migrate.ts` is one of the densest cr
|
|
|
954
981
|
[voltro:dev] auto-migrate: applied 3 op(s) in 412ms [safe=3 needs-default=0 needs-backfill=0 rename=0 lossy=0] fingerprint=8f507ba1e1aadad5
|
|
955
982
|
```
|
|
956
983
|
|
|
957
|
-
For MySQL the line notes the non-atomic-DDL constraint (a failed op leaves earlier ops committed; re-run apply
|
|
984
|
+
For MySQL the line notes the non-atomic-DDL constraint (a failed op leaves earlier ops committed; re-run apply and the resume ledger continues from there):
|
|
958
985
|
|
|
959
986
|
```
|
|
960
987
|
[voltro:dev] auto-migrate: planning schema dialect=mysql env=dev tables=22 (implicit-commit DDL — re-run apply after a mid-plan failure)
|
|
@@ -1302,6 +1329,36 @@ If `down` throws, the rollback is treated as failed — the schema stays
|
|
|
1302
1329
|
in the half-rolled-back state + the operator handles it manually. The
|
|
1303
1330
|
framework can't auto-recover from a broken inverse.
|
|
1304
1331
|
|
|
1332
|
+
## `voltro serve` refuses to boot while any are pending
|
|
1333
|
+
|
|
1334
|
+
Serve's schema guard is a DECLARATIVE fingerprint diff — the declared schema
|
|
1335
|
+
against the last applied plan. A file-based migration exists for the changes a
|
|
1336
|
+
state diff cannot infer: a data move, a backfill, a cross-table rewrite. Those
|
|
1337
|
+
move **no fingerprint at all**, so the guard passed and production ran
|
|
1338
|
+
un-migrated with nothing said.
|
|
1339
|
+
|
|
1340
|
+
On a real deploy environment (`NODE_ENV=production` / `staging`), against a SQL
|
|
1341
|
+
store, `voltro serve` now refuses to boot while any migration file has never run
|
|
1342
|
+
against that database, and names the pending ids:
|
|
1343
|
+
|
|
1344
|
+
```text
|
|
1345
|
+
serve: refusing to boot — 2 pending file-based migration(s) have never run
|
|
1346
|
+
against this database. They perform the changes a schema diff cannot infer
|
|
1347
|
+
(data moves, backfills, table splits), so the declarative fingerprint check
|
|
1348
|
+
below cannot see them.
|
|
1349
|
+
|
|
1350
|
+
Run them from your pre-deploy job — `voltro db migrate .` (schema + files)
|
|
1351
|
+
or `voltro db files .` (files alone).
|
|
1352
|
+
```
|
|
1353
|
+
|
|
1354
|
+
It does **not** apply them, and that is deliberate: a rolling deploy starts N
|
|
1355
|
+
replicas, each would try, and the migration lock turns that into N-1 processes
|
|
1356
|
+
blocked on boot. `VOLTRO_AUTO_MIGRATE=0` bypasses this exactly as it already
|
|
1357
|
+
bypassed the fingerprint check — one switch for "no boot-time schema checks".
|
|
1358
|
+
|
|
1359
|
+
A local `voltro serve` is untouched: `voltro dev` applies migrations there, so a
|
|
1360
|
+
preview serve has nothing to report.
|
|
1361
|
+
|
|
1305
1362
|
## Remote databases: boot will not apply them unattended
|
|
1306
1363
|
|
|
1307
1364
|
`voltro dev` applies pending migration files at boot. Against a local
|
|
@@ -1521,9 +1578,25 @@ The applier:
|
|
|
1521
1578
|
1. Introspects the live DB and plans the diff fresh (it does NOT ingest
|
|
1522
1579
|
a plan file — the diff is computed against live at apply time)
|
|
1523
1580
|
2. Refuses (exit 2) if any op is blocked, or refuses (exit 3) if
|
|
1524
|
-
`NODE_ENV=production` — so
|
|
1525
|
-
`NODE_ENV
|
|
1526
|
-
|
|
1581
|
+
`NODE_ENV=production` — so a bare apply runs in a one-shot job with
|
|
1582
|
+
`NODE_ENV=staging`, holding migration credentials, NOT in the serving
|
|
1583
|
+
process
|
|
1584
|
+
|
|
1585
|
+
> **`NODE_ENV` unset is no longer "not production".** Every `voltro db …`
|
|
1586
|
+
> and `voltro migrate` invocation resolves an unset `NODE_ENV` to
|
|
1587
|
+
> `production`, exactly as `voltro serve` and `voltro start` do — so a bare
|
|
1588
|
+
> `voltro db apply` in a pipeline that forgot the variable now refuses (exit
|
|
1589
|
+
> 3) instead of silently applying an un-reviewed diff to production. Set
|
|
1590
|
+
> `NODE_ENV=development` for a local database; use the `--plan` path below
|
|
1591
|
+
> for a real one.
|
|
1592
|
+
>
|
|
1593
|
+
> This is not only about the refusal. `_voltro_traces` and `_voltro_undo_log`
|
|
1594
|
+
> are created only when tracing / undo capture are on, and both are *on
|
|
1595
|
+
> unless production* — so an apply with `NODE_ENV` unset used to DECLARE two
|
|
1596
|
+
> tables the serving container did not. The declared set is what the schema
|
|
1597
|
+
> fingerprint hashes, so the apply recorded a fingerprint the container could
|
|
1598
|
+
> not reproduce and `voltro serve` refused to boot with `prod-mismatch`,
|
|
1599
|
+
> telling you to run the apply you had just run.
|
|
1527
1600
|
3. Acquires the advisory lock + executes the plan
|
|
1528
1601
|
4. Records the result in `_voltro_migration_plans` with
|
|
1529
1602
|
`source: 'auto-diff'` + `notes: 'PR #1234 — add user emails'`
|
|
@@ -1554,6 +1627,31 @@ up-to-date DB is a clean no-op (`schema is up to date — nothing to
|
|
|
1554
1627
|
apply`). That's what makes the apply safe to run in every pod of a
|
|
1555
1628
|
stateless deploy.
|
|
1556
1629
|
|
|
1630
|
+
Both spellings of the flag work: `--plan plan.json` and `--plan=plan.json`.
|
|
1631
|
+
|
|
1632
|
+
### The FIRST deploy, against an empty database
|
|
1633
|
+
|
|
1634
|
+
Nothing special is required, and the plan you review is the whole story: on a
|
|
1635
|
+
database with no tables, `voltro db plan --json` includes the framework's own
|
|
1636
|
+
`_voltro_*` tables (the migration ledger, api keys, kv, outbox, traces …) plus
|
|
1637
|
+
`actors`, alongside your own. They are part of the declared schema, so they are
|
|
1638
|
+
planned, classified and applied by exactly the same code as your tables — expect
|
|
1639
|
+
a first-deploy plan to be ~20 operations larger than the diff you wrote.
|
|
1640
|
+
|
|
1641
|
+
Two consequences worth knowing:
|
|
1642
|
+
|
|
1643
|
+
- The reviewed plan is complete. `db apply --plan` creates nothing beside it, so
|
|
1644
|
+
the fingerprint the plan was generated against is still the live schema when
|
|
1645
|
+
the guard checks it. (It did not used to be: the ledger tables were created
|
|
1646
|
+
before the fingerprint was taken, so the first deploy of every new database
|
|
1647
|
+
refused with `the live schema has drifted` one second after the plan was
|
|
1648
|
+
generated. Fixed.)
|
|
1649
|
+
- One table is deliberately absent from the plan: `_voltro_migration_ops`, the
|
|
1650
|
+
crash-resume ledger. It has to exist before the very first plan runs — the plan
|
|
1651
|
+
that creates everything else — so `voltro db apply` creates it itself, under the
|
|
1652
|
+
migration lock. A live `_voltro_*` table your schema does not declare is never
|
|
1653
|
+
planned for a drop, so it does not show up in the next diff either.
|
|
1654
|
+
|
|
1557
1655
|
## Apply timing relative to deploy
|
|
1558
1656
|
|
|
1559
1657
|
Two orderings, both common:
|
|
@@ -3036,9 +3134,23 @@ voltro db apply --note 'PR #1234'
|
|
|
3036
3134
|
|
|
3037
3135
|
4. Re-deploy. The new boot's fingerprint check passes.
|
|
3038
3136
|
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
the
|
|
3137
|
+
`--plan` IS a real flag, and it is the better answer here. This note used to
|
|
3138
|
+
say it was not, and told you to run a plain `voltro db apply` instead — which
|
|
3139
|
+
recomputes the diff and therefore applies something nobody reviewed. The claim
|
|
3140
|
+
came from a defect, not from the design: the app root was resolved as "the
|
|
3141
|
+
first argument that does not start with `-`", so `--plan plan.json` handed the
|
|
3142
|
+
plan FILE to schema discovery (`no schema files found, root: …/plan.json`)
|
|
3143
|
+
while `--plan=plan.json` worked. Both spellings work now.
|
|
3144
|
+
|
|
3145
|
+
Prefer the reviewed form in a pipeline:
|
|
3146
|
+
|
|
3147
|
+
```sh
|
|
3148
|
+
voltro db plan --json > plan.json # review this in the PR
|
|
3149
|
+
voltro db apply --plan plan.json # apply exactly it, fingerprint-guarded
|
|
3150
|
+
```
|
|
3151
|
+
|
|
3152
|
+
A plain `voltro db apply` stays correct for a developer machine, where the diff
|
|
3153
|
+
you would review is the one you just wrote.
|
|
3042
3154
|
|
|
3043
3155
|
If the cause is drift (someone DDL'd prod manually), see [drift.md](./drift.md) for reconciliation paths.
|
|
3044
3156
|
|
|
@@ -3170,13 +3282,19 @@ so a failure THERE can leave the index half-built; re-apply finishes it.)
|
|
|
3170
3282
|
|
|
3171
3283
|
**MySQL and MariaDB** (and sqlite / turso) implicit-commit every DDL
|
|
3172
3284
|
statement, so THERE a plan that fails on op N leaves ops 1..N-1 committed.
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3285
|
+
The `_voltro_migration_plans` row is still written only on full success —
|
|
3286
|
+
that row means "this schema is live". What the apply DOES write as it goes
|
|
3287
|
+
is a per-operation **resume ledger** (`_voltro_migration_ops`): every op is
|
|
3288
|
+
recorded before any DDL runs, flipped to `started` before its statement and
|
|
3289
|
+
`applied` after, and the rows are deleted once the apply converges. There is
|
|
3290
|
+
still no `--resume` / `--abort` flag because there is nothing to choose.
|
|
3291
|
+
|
|
3292
|
+
Recovery is just to re-run the apply. It finds the ledger, logs
|
|
3293
|
+
`migration resume: found an interrupted run …`, reconciles any half-finished
|
|
3294
|
+
shadow-column swap or table rebuild, skips the ops that already took effect,
|
|
3295
|
+
and replays the rest — including a `.backfill()` that was only partly done,
|
|
3296
|
+
which a plain re-diff cannot express. Fix the cause of the failed op first
|
|
3297
|
+
(e.g. the `ER_DUP_KEYNAME` that stopped op N), then:
|
|
3180
3298
|
|
|
3181
3299
|
```sh
|
|
3182
3300
|
voltro db apply --note 'completing partial apply after fixing op N'
|
|
@@ -3361,6 +3479,37 @@ db apply: change triggers converged (1501 statement(s))
|
|
|
3361
3479
|
|
|
3362
3480
|
The mirror case is reported the same way: a `.nonReactive()` table that still carries a trigger keeps paying `REPLICA IDENTITY FULL` and a `NOTIFY` on every write for a subscription nobody receives. `db apply` removes both.
|
|
3363
3481
|
|
|
3482
|
+
**Boot converges them too, on both boot paths.** `voltro dev` and `voltro serve`
|
|
3483
|
+
run the same check-and-repair at startup, so a schema-only restore or a
|
|
3484
|
+
`CDC=0` → `CDC=1` flip no longer waits for someone to notice:
|
|
3485
|
+
|
|
3486
|
+
```text
|
|
3487
|
+
reactive triggers: converged at boot — installed 500, removed 0 (1501 statement(s))
|
|
3488
|
+
```
|
|
3489
|
+
|
|
3490
|
+
Three things about it are worth knowing before you deploy a fleet:
|
|
3491
|
+
|
|
3492
|
+
- **It does not queue.** The repair takes the migration advisory lock with
|
|
3493
|
+
`pg_try_advisory_lock` and SKIPS if anything holds it, so N replicas booting
|
|
3494
|
+
together produce one repairing and N-1 logging `another instance … is
|
|
3495
|
+
converging it`. A concurrent `voltro db apply` holds the same lock, so the two
|
|
3496
|
+
can never run each other's DDL.
|
|
3497
|
+
- **It never fails a boot.** A check that cannot run warns and the process
|
|
3498
|
+
continues; reactivity may be degraded, and that is still better than a
|
|
3499
|
+
diagnostic taking the app down.
|
|
3500
|
+
- **It is a tunable**, `reactiveTriggers` in `app.config.ts`, default `'repair'`:
|
|
3501
|
+
|
|
3502
|
+
```ts
|
|
3503
|
+
export default defineApiApp({
|
|
3504
|
+
store: 'postgres',
|
|
3505
|
+
reactiveTriggers: 'report', // 'repair' (default) · 'report' · 'off'
|
|
3506
|
+
})
|
|
3507
|
+
```
|
|
3508
|
+
|
|
3509
|
+
`VOLTRO_REACTIVE_TRIGGERS` overrides the field. `VOLTRO_AUTO_MIGRATE=0`
|
|
3510
|
+
downgrades `'repair'` to `'report'` — that variable means "this boot issues no
|
|
3511
|
+
DDL", and it is deliberately not read as "and say nothing".
|
|
3512
|
+
|
|
3364
3513
|
## When the fix hint doesn't match reality
|
|
3365
3514
|
|
|
3366
3515
|
The fix hints come from the planner's classification logic — they should always be actionable. If you see one that doesn't make sense given your code:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Database
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Branch the live schema and REHEARSE your migration on it — apply the plan to a throwaway copy, flag every lossy operation, prove it converges, drop the branch. Plus the branch primitive itself (namespace snapshot on Postgres, Neon copy-on-write fast-path).
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
@@ -9,73 +9,178 @@
|
|
|
9
9
|
<!-- source: en/database/branching.md -->
|
|
10
10
|
## Data branching
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
_Branch the live schema and REHEARSE your migration on it — apply the plan to a throwaway copy, flag every lossy operation, prove it converges, drop the branch. Plus the branch primitive itself (namespace snapshot on Postgres, Neon copy-on-write fast-path)._
|
|
13
13
|
|
|
14
|
-
Data branching creates an **isolated copy of a
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
Data branching creates an **isolated copy of a schema** you can read, write and
|
|
15
|
+
migrate without touching the source. The headline use is not the branch — every
|
|
16
|
+
serverless-Postgres vendor sells one of those — it is what Voltro can do WITH a
|
|
17
|
+
branch that a vendor cannot: **rehearse your pending migration on it and tell you
|
|
18
|
+
what it would do.**
|
|
19
|
+
|
|
20
|
+
## `voltro db branch` — the migration rehearsal
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
voltro db branch --pr 128 # branch the live schema, rehearse, report, drop it
|
|
24
|
+
voltro db branch --pr 128 --seed copy # …with the parent's rows copied in
|
|
25
|
+
voltro db branch --pr 128 --keep # leave the branch standing to poke at
|
|
26
|
+
voltro db branch --pr 128 --json # machine-readable, for a PR comment
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
What it does, in order:
|
|
30
|
+
|
|
31
|
+
1. **Branches the LIVE schema** into a throwaway namespace (`br_pr128_<app>`).
|
|
32
|
+
2. **Checks fidelity** — plans the declared schema against the branch AND against
|
|
33
|
+
the parent, and aborts if the two disagree. A branch that is not a faithful
|
|
34
|
+
copy rehearses a different migration from the one you are about to run, and
|
|
35
|
+
saying nothing about that would be worse than not rehearsing at all.
|
|
36
|
+
3. **Plans your migration** against the branch and classifies every operation.
|
|
37
|
+
4. **Executes it there**, including the destructive operations.
|
|
38
|
+
5. **Re-plans.** An empty re-plan is the verdict; a migration that applies and
|
|
39
|
+
then re-proposes itself forever is not a migration.
|
|
40
|
+
6. **Drops the branch** (unless `--keep`), even when the apply failed.
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
branch rehearsal · br_pr128_shop · mechanism namespace
|
|
44
|
+
branched 18 table(s), replayed 3 foreign key(s)
|
|
45
|
+
plan: 2 operation(s), 1 lossy, 0 refused
|
|
46
|
+
• add-column members [safe]
|
|
47
|
+
✗ drop-column members [lossy]
|
|
48
|
+
|
|
49
|
+
⚠ 1 operation(s) DESTROY DATA. They were executed on the branch (it is
|
|
50
|
+
disposable) so they are rehearsed, but production refuses them until you set
|
|
51
|
+
VOLTRO_DESTRUCTIVE_OK — naming the tables, not `1`.
|
|
52
|
+
|
|
53
|
+
✓ applied on the branch, and the re-plan is EMPTY (the migration converges).
|
|
54
|
+
branch br_pr128_shop torn down.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Lossy operations are executed, not skipped
|
|
58
|
+
|
|
59
|
+
Production refuses a `drop-column` until a human acknowledges it. A branch that
|
|
60
|
+
is about to be dropped has no such reason — and refusing there would mean the one
|
|
61
|
+
operation most likely to fail is the one operation never rehearsed. So the
|
|
62
|
+
rehearsal unblocks lossy operations on the branch, runs them, and leads the report
|
|
63
|
+
with every one of them. That report is the thing you paste into the PR.
|
|
64
|
+
|
|
65
|
+
Operations the planner refuses **anywhere** (`needs-rename-annotation`,
|
|
66
|
+
`multi-step`) are NOT executed — the plan is reported as `blocked` instead.
|
|
67
|
+
|
|
68
|
+
### Exit codes
|
|
69
|
+
|
|
70
|
+
| code | meaning |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `0` | applied on the branch and converged, nothing lossy |
|
|
73
|
+
| `2` | a REVIEW signal: the plan destroys data, or the planner refuses part of it |
|
|
74
|
+
| `1` | the rehearsal could not answer — it failed, the branch was not faithful, or the migration did not converge |
|
|
75
|
+
|
|
76
|
+
`lossy` is deliberately not an error. A `drop-column` in a PR is a normal,
|
|
77
|
+
intentional thing; making it exit `1` trains people to pass `--force`, and the
|
|
78
|
+
next real failure goes with it.
|
|
79
|
+
|
|
80
|
+
### What `--seed` does and does not rehearse
|
|
81
|
+
|
|
82
|
+
`--seed empty` (the default) branches the SCHEMA only. That is enough for every
|
|
83
|
+
structural question and costs nothing. It does **not** rehearse anything
|
|
84
|
+
data-dependent: a `NOT NULL` meeting existing NULLs, a backfill meeting real
|
|
85
|
+
values, a unique constraint meeting duplicates, or the row-count threshold that
|
|
86
|
+
promotes an operation to `online-required`. Use `--seed copy` for those — it
|
|
87
|
+
copies every row, which is fast on a small database and slow on a large one.
|
|
88
|
+
|
|
89
|
+
## Mechanisms — what actually works
|
|
90
|
+
|
|
91
|
+
Be precise here, because the vendor landscape invites over-claiming. The branch
|
|
92
|
+
**plan** is dialect-agnostic. The shipped **executor** is not.
|
|
93
|
+
|
|
94
|
+
| mechanism | when | status |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `namespace` | Postgres — a schema per branch | shipped, and what `voltro db branch` uses |
|
|
97
|
+
| `neon-cow` | Postgres on Neon, `seed: 'copy'` | plan shipped; **executed by the cloud control plane**, not the CLI |
|
|
98
|
+
|
|
99
|
+
- **MySQL, MariaDB, SQLite and SQL Server are not supported by
|
|
100
|
+
`voltro db branch`.** `makeNamespaceBranchExecutor` emits `CREATE SCHEMA`,
|
|
101
|
+
`CREATE TABLE … (LIKE … INCLUDING ALL)` and `"`-quoted identifiers, none of
|
|
102
|
+
which those engines accept. The command refuses with that reason rather than
|
|
103
|
+
sending Postgres syntax at them. Writing your own `BranchExecutor` for another
|
|
104
|
+
dialect is the supported path — the plan is already portable.
|
|
105
|
+
- **Neon copy-on-write is a call to Neon's branch API** and needs a Neon token,
|
|
106
|
+
which the CLI does not hold. `voltro db branch` names the mechanism and refuses,
|
|
107
|
+
pointing at `--prefer namespace`. The cloud control plane executes it.
|
|
108
|
+
- **There is no Supabase mechanism and no template-database mechanism.** Neither
|
|
109
|
+
exists in the codebase.
|
|
19
110
|
|
|
20
111
|
## The plan/execute split
|
|
21
112
|
|
|
22
113
|
Branching is a **plan** (what to do) + an **executor** (do it), mirroring the
|
|
23
|
-
migration engine. The plan is pure
|
|
114
|
+
migration engine. The plan is pure and inspectable; the executor performs the I/O.
|
|
24
115
|
|
|
25
116
|
```ts
|
|
26
117
|
import { planBranch, resolveBranchMechanism } from '@voltro/database'
|
|
27
118
|
|
|
28
|
-
// Pick the mechanism: 'neon-cow' when seeding a copy on a Neon URL,
|
|
29
|
-
// 'namespace' (schema-per-branch) otherwise.
|
|
30
119
|
const mechanism = resolveBranchMechanism({ seed: 'copy', dbUrl: connectionString })
|
|
31
120
|
const steps = planBranch({
|
|
32
121
|
mechanism,
|
|
33
|
-
branchId: '
|
|
122
|
+
branchId: 'br_pr128_shop',
|
|
34
123
|
tableNames,
|
|
35
124
|
seed: 'copy',
|
|
36
|
-
parentNamespace: 'public',
|
|
125
|
+
parentNamespace: 'public',
|
|
37
126
|
})
|
|
38
|
-
// steps: e.g. [{ kind: 'neon-branch-create', … }] or per-table namespace snapshot steps
|
|
39
127
|
```
|
|
40
128
|
|
|
41
129
|
## Provision + tear down
|
|
42
130
|
|
|
43
131
|
`provisionBranch` / `teardownBranch` run a plan through an injected
|
|
44
|
-
`BranchExecutor
|
|
45
|
-
|
|
132
|
+
`BranchExecutor`. Injected so the lifecycle is unit-testable with a recording
|
|
133
|
+
executor — no live database:
|
|
46
134
|
|
|
47
135
|
```ts
|
|
48
|
-
import { provisionBranch, teardownBranch } from '@voltro/database'
|
|
136
|
+
import { provisionBranch, teardownBranch, makeNamespaceBranchExecutor } from '@voltro/database'
|
|
49
137
|
|
|
50
138
|
const result = await provisionBranch(
|
|
51
|
-
{
|
|
52
|
-
|
|
139
|
+
{
|
|
140
|
+
appSlug: 'shop', prNumber: 128, seed: 'copy', tableNames,
|
|
141
|
+
parentNamespace: 'public',
|
|
142
|
+
foreignKeys, // see below — LIKE does not copy these
|
|
143
|
+
indexNames, // see below — LIKE renames these
|
|
144
|
+
dbUrl: connectionString,
|
|
145
|
+
},
|
|
146
|
+
makeNamespaceBranchExecutor({ run: (sql) => client.query(sql) }),
|
|
53
147
|
)
|
|
54
|
-
// result: { branchId, mechanism, steps, … } — for 'neon-cow' also the
|
|
55
|
-
// branch's own connection string to bind the preview store to.
|
|
56
148
|
|
|
57
|
-
// …run the preview against result, then:
|
|
58
149
|
await teardownBranch(result.branchId, result.mechanism, executor)
|
|
59
150
|
```
|
|
60
151
|
|
|
61
|
-
`admitBranch`
|
|
62
|
-
|
|
152
|
+
`admitBranch` enforces the storage-cost caps (a TTL and a maximum number of live
|
|
153
|
+
branches) before a new one is provisioned.
|
|
63
154
|
|
|
64
|
-
|
|
155
|
+
### Two things `LIKE … INCLUDING ALL` does not carry
|
|
65
156
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
157
|
+
Both were found by pointing the rehearsal at a real Postgres and watching a
|
|
158
|
+
converged schema propose work. They are properties of Postgres, not of Voltro's
|
|
159
|
+
emission, and a branch missing either is not a copy:
|
|
160
|
+
|
|
161
|
+
- **Foreign keys are not copied.** There is no `INCLUDING` clause that copies
|
|
162
|
+
them. A branch without them accepts writes production rejects.
|
|
163
|
+
- **Index names are re-derived** from the table and columns. Measured on pg 17:
|
|
164
|
+
`byApiKeyTenant` came back as `_voltro_api_keys_tenantId_idx`, and
|
|
165
|
+
`_voltro_idempotency_scope_key_uq` as `…_scope_key_idx`. The migration planner
|
|
166
|
+
compares indexes by name, so every custom-named index reads as a different
|
|
167
|
+
index.
|
|
70
168
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
169
|
+
`provisionBranch` replays both (`foreignKeys` + `indexNames`, sourced from the
|
|
170
|
+
parent's introspected snapshot). `voltro db branch` does it for you, and refuses
|
|
171
|
+
to report anything about your migration if the branch still differs from the
|
|
172
|
+
parent.
|
|
173
|
+
|
|
174
|
+
## Branch-per-PR in CI
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
- run: voltro db branch --pr ${{ github.event.number }} --json > rehearsal.json
|
|
178
|
+
continue-on-error: true # exit 2 is a review signal, not a build failure
|
|
179
|
+
- run: node scripts/comment-rehearsal.mjs rehearsal.json
|
|
180
|
+
```
|
|
75
181
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
> left bound holds its namespace/Neon branch until torn down.
|
|
182
|
+
The `--json` report carries `operations`, `lossy`, `blocked`, `residual`,
|
|
183
|
+
`converged` and `infidelity` — everything a PR comment needs, already classified.
|
|
79
184
|
|
|
80
185
|
|
|
81
186
|
|
|
@@ -281,11 +386,22 @@ recoveryNote: text().encrypted(), // encrypted at rest, but the o
|
|
|
281
386
|
apiToken: text().encrypted().serverOnly(), // secret at rest AND never to a client
|
|
282
387
|
```
|
|
283
388
|
|
|
284
|
-
Enforcement
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
389
|
+
Enforcement runs in **both directions**, because "never crosses the wire" is not
|
|
390
|
+
a one-way claim:
|
|
391
|
+
|
|
392
|
+
- **Outbound** — the [`crud.*` read helpers](/docs/data/crud) strip `.serverOnly()`
|
|
393
|
+
columns from every returned row **automatically**. You declare the exposure
|
|
394
|
+
policy once at the schema and can't forget it on a handler. For a hand-written
|
|
395
|
+
query, omit the column from the `output` schema (and don't put it in the
|
|
396
|
+
returned object).
|
|
397
|
+
- **Inbound** — `crud.create` / `crud.update` **refuse** an input that sets a
|
|
398
|
+
`.serverOnly()` column, with `ServerOnlyColumnWrite` naming it, and write
|
|
399
|
+
nothing. A column the client may not read must not be one the client can set:
|
|
400
|
+
accepting it is mass assignment. It is refused rather than silently stripped
|
|
401
|
+
because a stripped field makes an attack indistinguishable from a no-op. When
|
|
402
|
+
the *server* needs to write one, do it from the handler with
|
|
403
|
+
`ctx.store.insert` / `ctx.store.update` — the refusal is on the generated
|
|
404
|
+
path, which is the one fed straight from client input.
|
|
289
405
|
|
|
290
406
|
A hand-written output is the case `crud.*` cannot cover, so an audit checks it:
|
|
291
407
|
a wire-reachable query whose `source` table carries a `.serverOnly()` column that
|
|
@@ -260,9 +260,27 @@ The `sql` tag is imported from the server-only `@voltro/database/sql` subpath
|
|
|
260
260
|
**Raw queries aren't tracked by the reactive engine** — the planner can't infer which tables an arbitrary SQL string touches. If you want a subscription to invalidate on a raw read's tables, declare them explicitly:
|
|
261
261
|
|
|
262
262
|
```ts
|
|
263
|
-
ctx.store.raw!<{
|
|
263
|
+
ctx.store.raw!<{ n: number }>(sql`SELECT count(*) AS n FROM events`, { dependsOn: ['events'] })
|
|
264
264
|
```
|
|
265
265
|
|
|
266
|
+
The tables can also live on the fragment itself (`{ ...frag, dependsOn: ['events'] }`), which is handy when the fragment is built somewhere else. They are taken at face value — the framework records what you declare and never checks it against the SQL.
|
|
267
|
+
|
|
268
|
+
**If you forget, the framework tells you.** A raw read taken while a live query's handler runs, with nothing declared, warns once at subscribe time and names both the query and the SQL:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
[voltro] reports.summary: a raw SQL read in this live query declares no
|
|
272
|
+
dependsOn, so no write can invalidate it — every subscriber keeps its first
|
|
273
|
+
result until it reconnects. Declare the tables it reads:
|
|
274
|
+
ctx.store.raw(fragment, { dependsOn: ['orders'] }) — or on the fragment itself.
|
|
275
|
+
The read: SELECT sum(total) FROM orders WHERE tenant = ?
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Only the STATIC text is logged; the bound values never are.
|
|
279
|
+
|
|
280
|
+
The diagnostic keeps a bounded record: one request remembers up to 32 distinct raw reads (deduplicated by SQL text), tunable with `VOLTRO_RAW_READ_TRACKING_LIMIT`. Reads past the bound are dropped rather than remembered — a handler that raw-reads in a loop must not turn a warning into a memory leak. Raise it only if a handler issues many distinct raw reads and you want the warning to name a later one.
|
|
281
|
+
|
|
282
|
+
**Where `dependsOn` works, and where it can only warn.** It drives recomputation for a query whose handler returns a **computed value** — the shape whose handler is genuinely re-run on a change (see [Subscriptions](/docs/data/subscriptions)). The declared tables join the query's own `source:`; they never replace it. A handler that returns a query **descriptor** is different: a change re-runs the descriptor's query, not your handler, so the raw result is not refreshed. There the warning is the whole answer — return a computed value if the raw read has to stay live.
|
|
283
|
+
|
|
266
284
|
## Tenant scoping (implicit)
|
|
267
285
|
|
|
268
286
|
If the table has the `tenant()` mixin, every `select` auto-merges `WHERE tenantId = ctx.subject.tenantId`. You don't write it; the runtime injects it. To opt out (admin queries crossing tenants), use `.unscoped()`:
|