create-ailk 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -8
- package/component-catalog.json +336 -27
- package/component-catalog.md +25 -12
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- package/dist/surfaces.js +105 -7
- package/dist/template-manifest.d.ts +33 -0
- package/dist/template-manifest.js +99 -16
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +3 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
- package/site-templates/marketing-site/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
- package/site-templates/minimalism-site/ailk-template.json +2 -1
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.env.example +72 -7
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +26 -2
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +439 -49
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +70 -31
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +13 -14
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +23 -2
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +57 -24
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +2 -0
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +59 -35
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +6 -4
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +71 -5
- package/templates/apps/web/CLAUDE.md +44 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/layout.tsx +133 -87
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/jest.config.cjs +8 -4
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- package/templates/apps/web/package.json +2 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +142 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/versions.json +34 -34
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
-- #6254 — the tenancy fence keystone. THE ONE schema migration M32 emits
|
|
2
|
+
-- (AC-1): #6256 #6257 #6258 #6259 #6260 #6171 #6314 land their model
|
|
3
|
+
-- changes INTO this file rather than each adding their own (issue #6254
|
|
4
|
+
-- comment, Sam, 2026-09-20; spec docs/specs/6254-the-composite-fk-pattern-
|
|
5
|
+
-- workspacesiteidentity-spec.md D4).
|
|
6
|
+
--
|
|
7
|
+
-- ── What this file does ─────────────────────────────────────────────────────
|
|
8
|
+
-- 1. `WorkspaceSiteIdentity` gains `@@unique([id, workspaceId])` (AC-2) — a
|
|
9
|
+
-- named Postgres unique constraint, redundant today (`id` is already the
|
|
10
|
+
-- PK) but the target a future composite FK can bind to (spec D3).
|
|
11
|
+
-- 2. Four ALREADY-KEYED-but-nullable models — `leads`, `project_listings`,
|
|
12
|
+
-- `flow_definitions`, `flow_sessions` — are retrofitted to NOT NULL
|
|
13
|
+
-- `workspace_id` plus a `CHECK (workspace_id = site_id)` (spec D1b).
|
|
14
|
+
-- 3. #6374 — the fence fan-out. ELEVEN `site_id` tables gain `workspace_id`
|
|
15
|
+
-- and take the same five steps in the same order: `subscribers`,
|
|
16
|
+
-- `callbacks`, `consent_grants`, `appointments`, `deliverable_assets`,
|
|
17
|
+
-- `flow_checkouts`, `waitlist_experiments`, `waitlist_scores`,
|
|
18
|
+
-- `waitlist_followup_sends`, `playbook_instances`, `site_listing_configs`.
|
|
19
|
+
-- 4. #6374 — `listing_csv_sends` has NO `site_id`, so it gains `workspace_id`
|
|
20
|
+
-- backfilled from its parent `project_listings` row, NOT NULL and the
|
|
21
|
+
-- format CHECK, and NO equality CHECK (spec D2, operator 2026-09-20).
|
|
22
|
+
-- 5. #6374 — `site_settings` has NO `site_id` either. It gains `workspace_id`
|
|
23
|
+
-- backfilled to the sentinel workspace — but ONLY after an abort-not-guess
|
|
24
|
+
-- guard (AC-14) refuses any unattributed row whose `key` already embeds a
|
|
25
|
+
-- NON-sentinel workspace id, which the sentinel assignment would misfile —
|
|
26
|
+
-- NOT NULL and the format CHECK, and
|
|
27
|
+
-- its UNIQUE INDEX moves from `(key)` to `(workspace_id, key)` so two
|
|
28
|
+
-- sellers may each hold a `feature_flags` row (spec D3, operator
|
|
29
|
+
-- 2026-09-20). The `key` TEXT is left verbatim.
|
|
30
|
+
-- 6. #6374 — `deliverable_grants` gains NO column and NO row is touched: its
|
|
31
|
+
-- `workspace_id` stays nullable (NULL is the serve path's fail-closed
|
|
32
|
+
-- sentinel). It gains only the NULL-TOLERANT
|
|
33
|
+
-- `CHECK (workspace_id IS NULL OR workspace_id = site_id)`, so a STAMPED
|
|
34
|
+
-- grant can never disagree with its site while a legacy one stays
|
|
35
|
+
-- storable (spec D4).
|
|
36
|
+
--
|
|
37
|
+
-- ── Why a CHECK and not a foreign key (spec §0, §1.3, §1.4) ────────────────
|
|
38
|
+
-- The issue's literal ask — a composite FK from these four models to
|
|
39
|
+
-- `WorkspaceSiteIdentity` — is not buildable against this tree: (F1) the
|
|
40
|
+
-- relation crosses the inbox -> config namespace boundary ADR-0009
|
|
41
|
+
-- invariant #1 forbids, and `tools/lint-architecture.ts`'s
|
|
42
|
+
-- cross-namespace-prisma-relation rule lints it; (F2) `site_id` never holds
|
|
43
|
+
-- a `WorkspaceSiteIdentity.id` (`siteid_<uuidv7>`) — it holds the resolved
|
|
44
|
+
-- WORKSPACE id (`wksp_<uuidv7>`) or the legacy alias `"default"`, so
|
|
45
|
+
-- `references: [id, workspaceId]` would reject 100% of existing rows and
|
|
46
|
+
-- 100% of writes from every current path; (F3) `WorkspaceSiteIdentity` is a
|
|
47
|
+
-- per-workspace config blob written lazily by an upsert, not a site
|
|
48
|
+
-- registry — a required FK parent would make anonymous lead capture (a
|
|
49
|
+
-- never-gate surface) depend on a config row that need not exist.
|
|
50
|
+
--
|
|
51
|
+
-- AC-4's stated GOAL — "Postgres rejects a row whose workspaceId disagrees
|
|
52
|
+
-- with its site's workspace" — is delivered without a FK: since `site_id`
|
|
53
|
+
-- IS the workspace id (F2), the disagreement is exactly
|
|
54
|
+
-- `workspace_id <> site_id`, and an equality CHECK makes it unstorable. This
|
|
55
|
+
-- is strictly TIGHTER than the FK the issue asked for (the FK would admit
|
|
56
|
+
-- any pair present in `workspace_site_identities`; the CHECK admits exactly
|
|
57
|
+
-- one value per row). Disposed by the operator 2026-09-20, spec §10.1 Q1 ->
|
|
58
|
+
-- Option A' (docs/internal/decisions/tenancy-fence-model-classification.md
|
|
59
|
+
-- D1 amendment, same commit).
|
|
60
|
+
--
|
|
61
|
+
-- `DeliverableGrant` (the fifth already-keyed model) is DELIBERATELY NOT
|
|
62
|
+
-- touched here. Its `workspace_id` has no source column to derive from (its
|
|
63
|
+
-- own migration, 20260921120000_deliverable_grant_workspace_key, says so
|
|
64
|
+
-- explicitly) and NULL is the serve path's fail-closed sentinel
|
|
65
|
+
-- (`deliverable-asset-store.ts` `readDeliverableAsset`) — retrofitting it
|
|
66
|
+
-- here would hand a legacy grant an authorization scope no mint granted, on
|
|
67
|
+
-- a bearer-token download path. Carved out of this unit (spec §10.1 Q2 ->
|
|
68
|
+
-- carve out); #6257 fences it at mint grain, where the workspace is known.
|
|
69
|
+
--
|
|
70
|
+
-- ── The CHECK requires `site_id` normalized FIRST (spec D1a, SEC-01) ───────
|
|
71
|
+
-- A bare `CHECK (workspace_id = site_id)` is unsatisfiable for the shipped
|
|
72
|
+
-- default: `site_id = 'default'` is the canonical seeded row, and both
|
|
73
|
+
-- precedent backfills (20260919140000, 20260919120000) already resolve
|
|
74
|
+
-- `'default'` to the sentinel workspace id in `workspace_id` while leaving
|
|
75
|
+
-- `site_id` untouched. So this migration rewrites `site_id = 'default'` to
|
|
76
|
+
-- the same sentinel on every fenced table BEFORE the CHECK lands — the
|
|
77
|
+
-- application-code half of D1a (the write paths that still stamp the raw
|
|
78
|
+
-- alias) is fixed in the same PR (apps/api/src/lib/site.ts,
|
|
79
|
+
-- routes/flows/index.ts, routes/flows/recommender.ts, bin/seed-presets.ts,
|
|
80
|
+
-- services/listing-config.ts, services/flow-engine.ts,
|
|
81
|
+
-- services/project-listings.ts, services/recommender-capture.ts).
|
|
82
|
+
--
|
|
83
|
+
-- ── Ordering (spec D2) — never guess, never delete, abort if unresolvable ──
|
|
84
|
+
-- 1. Normalize site_id ('default' -> sentinel).
|
|
85
|
+
-- 2. Resolve any remaining NULL workspace_id via an INNER JOIN to
|
|
86
|
+
-- "workspaces" only (never an assignment) — a row whose site_id resolves
|
|
87
|
+
-- to nothing is left NULL, never guessed. This is mostly a no-op today:
|
|
88
|
+
-- 20260919140000/20260919120000 already backfilled these four tables:
|
|
89
|
+
-- kept here so this migration proves its OWN correctness re-run against
|
|
90
|
+
-- fixture rows (NFR3, § 8.3) rather than depending on migration order.
|
|
91
|
+
-- 3. Post-assert: nothing lost, nothing guessed, nothing missed — carried
|
|
92
|
+
-- forward from the precedent — PLUS a NEW assertion this migration adds
|
|
93
|
+
-- and the precedent could not: zero rows remain NULL. Unlike the
|
|
94
|
+
-- precedent (which only RAISE NOTICEs on a remaining NULL, because it
|
|
95
|
+
-- never enforces NOT NULL), THIS migration is about to enforce NOT NULL,
|
|
96
|
+
-- so an unresolvable row must abort the transaction rather than pass
|
|
97
|
+
-- through silently — "the same silent data loss AC-7 exists to close"
|
|
98
|
+
-- (spec §0), never a delete, never a guess.
|
|
99
|
+
-- 4. THEN `SET NOT NULL`, then the CHECKs.
|
|
100
|
+
--
|
|
101
|
+
-- ── Re-runnability (NFR3) ───────────────────────────────────────────────────
|
|
102
|
+
-- `ADD COLUMN`/`CREATE INDEX` are not needed here (the columns/indexes
|
|
103
|
+
-- already exist from the precedent migrations). The equality CHECKs use
|
|
104
|
+
-- `DROP CONSTRAINT IF EXISTS` then `ADD CONSTRAINT` — NOT the precedent's
|
|
105
|
+
-- existence-guarded `DO $$` block, which would silently no-op the
|
|
106
|
+
-- TIGHTENING this migration performs on any database that already carries a
|
|
107
|
+
-- same-named constraint (spec D5, SEC-06: a false-green control). The
|
|
108
|
+
-- `WorkspaceSiteIdentity` composite unique is a pure ADD (nothing to
|
|
109
|
+
-- tighten), so it keeps the existence-guarded idiom.
|
|
110
|
+
--
|
|
111
|
+
-- ── Rollback (NFR5) ─────────────────────────────────────────────────────────
|
|
112
|
+
-- Reverses the CONSTRAINTS only, not the tenant attribution:
|
|
113
|
+
-- ALTER TABLE "leads" DROP CONSTRAINT "leads_workspace_site_agree", ALTER COLUMN "workspace_id" DROP NOT NULL;
|
|
114
|
+
-- ALTER TABLE "project_listings" DROP CONSTRAINT "project_listings_workspace_site_agree", ALTER COLUMN "workspace_id" DROP NOT NULL;
|
|
115
|
+
-- ALTER TABLE "flow_definitions" DROP CONSTRAINT "flow_definitions_workspace_site_agree", ALTER COLUMN "workspace_id" DROP NOT NULL;
|
|
116
|
+
-- ALTER TABLE "flow_sessions" DROP CONSTRAINT "flow_sessions_workspace_site_agree", ALTER COLUMN "workspace_id" DROP NOT NULL;
|
|
117
|
+
-- ALTER TABLE "workspace_site_identities" DROP CONSTRAINT "workspace_site_identities_id_workspace_id_key";
|
|
118
|
+
-- #6374's own reversal, same posture — CONSTRAINTS only, never the tenant
|
|
119
|
+
-- attribution: for each of the twelve tables it fences
|
|
120
|
+
-- ALTER TABLE "<table>" DROP CONSTRAINT "<table>_workspace_site_agree",
|
|
121
|
+
-- DROP CONSTRAINT "<table>_workspace_id_format",
|
|
122
|
+
-- ALTER COLUMN "workspace_id" DROP NOT NULL;
|
|
123
|
+
-- (`listing_csv_sends` and `site_settings` have no `_workspace_site_agree`;
|
|
124
|
+
-- `site_settings` additionally needs its old `site_settings_key_key` unique
|
|
125
|
+
-- index recreated, which is only possible while no two workspaces hold the
|
|
126
|
+
-- same key.) `deliverable_grants` reverses by dropping its one new CHECK.
|
|
127
|
+
-- Unlike the four keystone tables, `workspace_id` does NOT predate this
|
|
128
|
+
-- migration on the twelve, so a full DOWN would also DROP COLUMN it — which
|
|
129
|
+
-- DOES discard tenant attribution. Stated so it is not read as a safe revert.
|
|
130
|
+
-- `workspace_id` predates this migration on all four tables (§1.6), and the
|
|
131
|
+
-- `site_id` normalization (D1a) is one-way — a DOWN does not un-normalize
|
|
132
|
+
-- `site_id`, because the pre-normalization alias value is not recoverable
|
|
133
|
+
-- from the row. NOT a full revert; stated so it is not read as one.
|
|
134
|
+
|
|
135
|
+
-- The census the post-assertions compare against. ON COMMIT DROP: `prisma
|
|
136
|
+
-- migrate` runs each migration in one transaction, so this disappears with it.
|
|
137
|
+
CREATE TEMP TABLE "_6254_fence_census" ON COMMIT DROP AS
|
|
138
|
+
SELECT (SELECT count(*) FROM "leads") AS l,
|
|
139
|
+
(SELECT count(*) FROM "project_listings") AS p,
|
|
140
|
+
(SELECT count(*) FROM "flow_definitions") AS d,
|
|
141
|
+
(SELECT count(*) FROM "flow_sessions") AS s;
|
|
142
|
+
|
|
143
|
+
-- ── AC-2 — WorkspaceSiteIdentity composite unique ──────────────────────────
|
|
144
|
+
DO $$ BEGIN
|
|
145
|
+
IF NOT EXISTS (
|
|
146
|
+
SELECT 1 FROM pg_constraint WHERE conname = 'workspace_site_identities_id_workspace_id_key'
|
|
147
|
+
) THEN
|
|
148
|
+
ALTER TABLE "workspace_site_identities"
|
|
149
|
+
ADD CONSTRAINT "workspace_site_identities_id_workspace_id_key" UNIQUE ("id", "workspace_id");
|
|
150
|
+
END IF;
|
|
151
|
+
END $$;
|
|
152
|
+
|
|
153
|
+
-- ── Step 1 (D1a) — normalize site_id: 'default' -> the sentinel workspace id ─
|
|
154
|
+
-- The sentinel id is a literal because SQL cannot import database/tenancy.ts
|
|
155
|
+
-- (spec §5.3 step 5); database/__tests__/composite-fence.db.test.ts pins the
|
|
156
|
+
-- same literal against the exported DEFAULT_WORKSPACE_ID constant, mirroring
|
|
157
|
+
-- the #6155/#6203 precedents' own technique.
|
|
158
|
+
UPDATE "leads"
|
|
159
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
160
|
+
WHERE "site_id" = 'default';
|
|
161
|
+
|
|
162
|
+
UPDATE "project_listings"
|
|
163
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
164
|
+
WHERE "site_id" = 'default';
|
|
165
|
+
|
|
166
|
+
UPDATE "flow_definitions"
|
|
167
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
168
|
+
WHERE "site_id" = 'default';
|
|
169
|
+
|
|
170
|
+
UPDATE "flow_sessions"
|
|
171
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
172
|
+
WHERE "site_id" = 'default';
|
|
173
|
+
|
|
174
|
+
-- ── Step 2 (D2) — resolve any remaining NULL workspace_id via INNER JOIN ────
|
|
175
|
+
-- site_id is already normalized (step 1), so no CASE/alias branch is needed
|
|
176
|
+
-- here, unlike the precedent migrations this runs after. A row whose site_id
|
|
177
|
+
-- resolves to nothing is simply not touched and stays NULL — never guessed.
|
|
178
|
+
UPDATE "leads" l
|
|
179
|
+
SET "workspace_id" = w."id"
|
|
180
|
+
FROM "workspaces" w
|
|
181
|
+
WHERE l."workspace_id" IS NULL
|
|
182
|
+
AND w."id" = l."site_id";
|
|
183
|
+
|
|
184
|
+
UPDATE "project_listings" p
|
|
185
|
+
SET "workspace_id" = w."id"
|
|
186
|
+
FROM "workspaces" w
|
|
187
|
+
WHERE p."workspace_id" IS NULL
|
|
188
|
+
AND w."id" = p."site_id";
|
|
189
|
+
|
|
190
|
+
UPDATE "flow_definitions" d
|
|
191
|
+
SET "workspace_id" = w."id"
|
|
192
|
+
FROM "workspaces" w
|
|
193
|
+
WHERE d."workspace_id" IS NULL
|
|
194
|
+
AND w."id" = d."site_id";
|
|
195
|
+
|
|
196
|
+
UPDATE "flow_sessions" s
|
|
197
|
+
SET "workspace_id" = w."id"
|
|
198
|
+
FROM "workspaces" w
|
|
199
|
+
WHERE s."workspace_id" IS NULL
|
|
200
|
+
AND w."id" = s."site_id";
|
|
201
|
+
|
|
202
|
+
-- ── Step 3 — POST-ASSERTIONS ─────────────────────────────────────────────────
|
|
203
|
+
DO $$
|
|
204
|
+
DECLARE
|
|
205
|
+
before_l bigint; before_p bigint; before_d bigint; before_s bigint;
|
|
206
|
+
after_l bigint; after_p bigint; after_d bigint; after_s bigint;
|
|
207
|
+
bad bigint;
|
|
208
|
+
BEGIN
|
|
209
|
+
SELECT l, p, d, s INTO before_l, before_p, before_d, before_s FROM "_6254_fence_census";
|
|
210
|
+
SELECT count(*) INTO after_l FROM "leads";
|
|
211
|
+
SELECT count(*) INTO after_p FROM "project_listings";
|
|
212
|
+
SELECT count(*) INTO after_d FROM "flow_definitions";
|
|
213
|
+
SELECT count(*) INTO after_s FROM "flow_sessions";
|
|
214
|
+
|
|
215
|
+
-- 1. Nothing lost: row counts unchanged.
|
|
216
|
+
IF before_l <> after_l OR before_p <> after_p OR before_d <> after_d OR before_s <> after_s THEN
|
|
217
|
+
RAISE EXCEPTION
|
|
218
|
+
'#6254 post-assertion: row count changed across a purely additive migration (leads % -> %, project_listings % -> %, flow_definitions % -> %, flow_sessions % -> %)',
|
|
219
|
+
before_l, after_l, before_p, after_p, before_d, after_d, before_s, after_s;
|
|
220
|
+
END IF;
|
|
221
|
+
|
|
222
|
+
-- 2. Nothing guessed: no row's workspace_id names a workspace absent from "workspaces".
|
|
223
|
+
SELECT count(*) INTO bad FROM "leads" l
|
|
224
|
+
WHERE l."workspace_id" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = l."workspace_id");
|
|
225
|
+
IF bad <> 0 THEN
|
|
226
|
+
RAISE EXCEPTION '#6254 post-assertion: % leads row(s) carry a workspace_id absent from workspaces', bad;
|
|
227
|
+
END IF;
|
|
228
|
+
SELECT count(*) INTO bad FROM "project_listings" p
|
|
229
|
+
WHERE p."workspace_id" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = p."workspace_id");
|
|
230
|
+
IF bad <> 0 THEN
|
|
231
|
+
RAISE EXCEPTION '#6254 post-assertion: % project_listings row(s) carry a workspace_id absent from workspaces', bad;
|
|
232
|
+
END IF;
|
|
233
|
+
SELECT count(*) INTO bad FROM "flow_definitions" d
|
|
234
|
+
WHERE d."workspace_id" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = d."workspace_id");
|
|
235
|
+
IF bad <> 0 THEN
|
|
236
|
+
RAISE EXCEPTION '#6254 post-assertion: % flow_definitions row(s) carry a workspace_id absent from workspaces', bad;
|
|
237
|
+
END IF;
|
|
238
|
+
SELECT count(*) INTO bad FROM "flow_sessions" s
|
|
239
|
+
WHERE s."workspace_id" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = s."workspace_id");
|
|
240
|
+
IF bad <> 0 THEN
|
|
241
|
+
RAISE EXCEPTION '#6254 post-assertion: % flow_sessions row(s) carry a workspace_id absent from workspaces', bad;
|
|
242
|
+
END IF;
|
|
243
|
+
|
|
244
|
+
-- 3. Nothing missed: no row whose (normalized) site_id names a real workspace
|
|
245
|
+
-- came out of the backfill with workspace_id still NULL.
|
|
246
|
+
SELECT count(*) INTO bad FROM "leads" l
|
|
247
|
+
WHERE l."workspace_id" IS NULL AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = l."site_id");
|
|
248
|
+
IF bad <> 0 THEN
|
|
249
|
+
RAISE EXCEPTION '#6254 post-assertion: % leads row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
250
|
+
END IF;
|
|
251
|
+
SELECT count(*) INTO bad FROM "project_listings" p
|
|
252
|
+
WHERE p."workspace_id" IS NULL AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = p."site_id");
|
|
253
|
+
IF bad <> 0 THEN
|
|
254
|
+
RAISE EXCEPTION '#6254 post-assertion: % project_listings row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
255
|
+
END IF;
|
|
256
|
+
SELECT count(*) INTO bad FROM "flow_definitions" d
|
|
257
|
+
WHERE d."workspace_id" IS NULL AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = d."site_id");
|
|
258
|
+
IF bad <> 0 THEN
|
|
259
|
+
RAISE EXCEPTION '#6254 post-assertion: % flow_definitions row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
260
|
+
END IF;
|
|
261
|
+
SELECT count(*) INTO bad FROM "flow_sessions" s
|
|
262
|
+
WHERE s."workspace_id" IS NULL AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = s."site_id");
|
|
263
|
+
IF bad <> 0 THEN
|
|
264
|
+
RAISE EXCEPTION '#6254 post-assertion: % flow_sessions row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
265
|
+
END IF;
|
|
266
|
+
|
|
267
|
+
-- 4. AC-7 / spec D2 point 4 — an unresolvable row must ABORT, never pass
|
|
268
|
+
-- through as a silent NULL: this migration is about to SET NOT NULL, so
|
|
269
|
+
-- (unlike the precedent's NOTICE-only posture) a remaining NULL here
|
|
270
|
+
-- means a row's site_id names no workspace this database holds, and
|
|
271
|
+
-- there is no correct value to write. Never deleted, never guessed.
|
|
272
|
+
SELECT count(*) INTO bad FROM "leads" WHERE "workspace_id" IS NULL;
|
|
273
|
+
IF bad <> 0 THEN
|
|
274
|
+
RAISE EXCEPTION '#6254: % leads row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
275
|
+
END IF;
|
|
276
|
+
SELECT count(*) INTO bad FROM "project_listings" WHERE "workspace_id" IS NULL;
|
|
277
|
+
IF bad <> 0 THEN
|
|
278
|
+
RAISE EXCEPTION '#6254: % project_listings row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
279
|
+
END IF;
|
|
280
|
+
SELECT count(*) INTO bad FROM "flow_definitions" WHERE "workspace_id" IS NULL;
|
|
281
|
+
IF bad <> 0 THEN
|
|
282
|
+
RAISE EXCEPTION '#6254: % flow_definitions row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
283
|
+
END IF;
|
|
284
|
+
SELECT count(*) INTO bad FROM "flow_sessions" WHERE "workspace_id" IS NULL;
|
|
285
|
+
IF bad <> 0 THEN
|
|
286
|
+
RAISE EXCEPTION '#6254: % flow_sessions row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
287
|
+
END IF;
|
|
288
|
+
END $$;
|
|
289
|
+
|
|
290
|
+
-- ── Step 4 — SET NOT NULL (AC-3, AC-4, AC-7) ────────────────────────────────
|
|
291
|
+
ALTER TABLE "leads" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
292
|
+
ALTER TABLE "project_listings" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
293
|
+
ALTER TABLE "flow_definitions" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
294
|
+
ALTER TABLE "flow_sessions" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
295
|
+
|
|
296
|
+
-- ── Step 5 — equality CHECKs (AC-3, AC-4, AC-5) — DROP then ADD, not guarded ─
|
|
297
|
+
ALTER TABLE "leads" DROP CONSTRAINT IF EXISTS "leads_workspace_site_agree";
|
|
298
|
+
ALTER TABLE "leads" ADD CONSTRAINT "leads_workspace_site_agree" CHECK ("workspace_id" = "site_id");
|
|
299
|
+
|
|
300
|
+
ALTER TABLE "project_listings" DROP CONSTRAINT IF EXISTS "project_listings_workspace_site_agree";
|
|
301
|
+
ALTER TABLE "project_listings" ADD CONSTRAINT "project_listings_workspace_site_agree" CHECK ("workspace_id" = "site_id");
|
|
302
|
+
|
|
303
|
+
ALTER TABLE "flow_definitions" DROP CONSTRAINT IF EXISTS "flow_definitions_workspace_site_agree";
|
|
304
|
+
ALTER TABLE "flow_definitions" ADD CONSTRAINT "flow_definitions_workspace_site_agree" CHECK ("workspace_id" = "site_id");
|
|
305
|
+
|
|
306
|
+
ALTER TABLE "flow_sessions" DROP CONSTRAINT IF EXISTS "flow_sessions_workspace_site_agree";
|
|
307
|
+
ALTER TABLE "flow_sessions" ADD CONSTRAINT "flow_sessions_workspace_site_agree" CHECK ("workspace_id" = "site_id");
|
|
308
|
+
|
|
309
|
+
-- ════════════════════════════════════════════════════════════════════════════
|
|
310
|
+
-- #6374 — the fence fan-out. THIRTEEN more models join the fence, amended INTO
|
|
311
|
+
-- this file rather than added as a new migration, because #6254 AC-1 makes
|
|
312
|
+
-- this THE ONE schema migration M32 emits. Amending a merged migration is a
|
|
313
|
+
-- silent no-op on a database that already applied it; the operator disposed
|
|
314
|
+
-- the assumption that makes that safe on 2026-09-20 (spec § 1.5 — no
|
|
315
|
+
-- persistent database carries state that must survive; every site is
|
|
316
|
+
-- rebuildable). If that assumption ever stops holding, everything below moves
|
|
317
|
+
-- verbatim into a new migration directory and nothing else changes.
|
|
318
|
+
--
|
|
319
|
+
-- Eleven tables carry `site_id` and take the keystone shape above unchanged,
|
|
320
|
+
-- in the keystone's own five-step order: normalize -> backfill by INNER JOIN
|
|
321
|
+
-- -> four post-assertions -> SET NOT NULL -> the two CHECKs. Two do not carry
|
|
322
|
+
-- `site_id` and take the operator-disposed shapes (spec § 5 D2/D3), and
|
|
323
|
+
-- `deliverable_grants` gains a NULL-TOLERANT equality CHECK only (D4).
|
|
324
|
+
-- ════════════════════════════════════════════════════════════════════════════
|
|
325
|
+
|
|
326
|
+
-- The #6374 census, the twin of the #6254 one above. ON COMMIT DROP for the
|
|
327
|
+
-- same reason: `prisma migrate` runs each migration in one transaction.
|
|
328
|
+
CREATE TEMP TABLE "_6374_fence_census" ON COMMIT DROP AS
|
|
329
|
+
SELECT (SELECT count(*) FROM "subscribers") AS "subscribers"
|
|
330
|
+
, (SELECT count(*) FROM "callbacks") AS "callbacks"
|
|
331
|
+
, (SELECT count(*) FROM "consent_grants") AS "consent_grants"
|
|
332
|
+
, (SELECT count(*) FROM "appointments") AS "appointments"
|
|
333
|
+
, (SELECT count(*) FROM "deliverable_assets") AS "deliverable_assets"
|
|
334
|
+
, (SELECT count(*) FROM "flow_checkouts") AS "flow_checkouts"
|
|
335
|
+
, (SELECT count(*) FROM "waitlist_experiments") AS "waitlist_experiments"
|
|
336
|
+
, (SELECT count(*) FROM "waitlist_scores") AS "waitlist_scores"
|
|
337
|
+
, (SELECT count(*) FROM "waitlist_followup_sends") AS "waitlist_followup_sends"
|
|
338
|
+
, (SELECT count(*) FROM "playbook_instances") AS "playbook_instances"
|
|
339
|
+
, (SELECT count(*) FROM "site_listing_configs") AS "site_listing_configs"
|
|
340
|
+
;
|
|
341
|
+
|
|
342
|
+
-- ── Step 1 (D1 step 1) — the column and its index ─────────────────────────
|
|
343
|
+
-- Unlike the four keystone tables, these eleven have no `workspace_id`
|
|
344
|
+
-- column yet, so it is ADDED nullable here and tightened in step 4. ONE
|
|
345
|
+
-- index per table and nothing query-shaped (D6): a read-side unit that turns
|
|
346
|
+
-- out to need a composite gets a post-M32 migration, filed with the query
|
|
347
|
+
-- that justifies it.
|
|
348
|
+
ALTER TABLE "subscribers" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
349
|
+
CREATE INDEX IF NOT EXISTS "subscribers_workspace_id_idx" ON "subscribers" ("workspace_id");
|
|
350
|
+
ALTER TABLE "callbacks" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
351
|
+
CREATE INDEX IF NOT EXISTS "callbacks_workspace_id_idx" ON "callbacks" ("workspace_id");
|
|
352
|
+
ALTER TABLE "consent_grants" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
353
|
+
CREATE INDEX IF NOT EXISTS "consent_grants_workspace_id_idx" ON "consent_grants" ("workspace_id");
|
|
354
|
+
ALTER TABLE "appointments" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
355
|
+
CREATE INDEX IF NOT EXISTS "appointments_workspace_id_idx" ON "appointments" ("workspace_id");
|
|
356
|
+
ALTER TABLE "deliverable_assets" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
357
|
+
CREATE INDEX IF NOT EXISTS "deliverable_assets_workspace_id_idx" ON "deliverable_assets" ("workspace_id");
|
|
358
|
+
ALTER TABLE "flow_checkouts" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
359
|
+
CREATE INDEX IF NOT EXISTS "flow_checkouts_workspace_id_idx" ON "flow_checkouts" ("workspace_id");
|
|
360
|
+
ALTER TABLE "waitlist_experiments" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
361
|
+
CREATE INDEX IF NOT EXISTS "waitlist_experiments_workspace_id_idx" ON "waitlist_experiments" ("workspace_id");
|
|
362
|
+
ALTER TABLE "waitlist_scores" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
363
|
+
CREATE INDEX IF NOT EXISTS "waitlist_scores_workspace_id_idx" ON "waitlist_scores" ("workspace_id");
|
|
364
|
+
ALTER TABLE "waitlist_followup_sends" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
365
|
+
CREATE INDEX IF NOT EXISTS "waitlist_followup_sends_workspace_id_idx" ON "waitlist_followup_sends" ("workspace_id");
|
|
366
|
+
ALTER TABLE "playbook_instances" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
367
|
+
CREATE INDEX IF NOT EXISTS "playbook_instances_workspace_id_idx" ON "playbook_instances" ("workspace_id");
|
|
368
|
+
ALTER TABLE "site_listing_configs" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
369
|
+
CREATE INDEX IF NOT EXISTS "site_listing_configs_workspace_id_idx" ON "site_listing_configs" ("workspace_id");
|
|
370
|
+
|
|
371
|
+
-- ── Step 2 (D1 step 2 / keystone D1a) — normalize site_id -> the sentinel ──
|
|
372
|
+
-- The same literal, and the same one-way rewrite, the four keystone tables
|
|
373
|
+
-- above already took. database/__tests__/composite-fence.db.test.ts pins
|
|
374
|
+
-- EVERY `wksp_` literal in this file against the exported
|
|
375
|
+
-- DEFAULT_WORKSPACE_ID, so these eleven cannot drift from it either.
|
|
376
|
+
UPDATE "subscribers"
|
|
377
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
378
|
+
WHERE "site_id" = 'default';
|
|
379
|
+
|
|
380
|
+
UPDATE "callbacks"
|
|
381
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
382
|
+
WHERE "site_id" = 'default';
|
|
383
|
+
|
|
384
|
+
UPDATE "consent_grants"
|
|
385
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
386
|
+
WHERE "site_id" = 'default';
|
|
387
|
+
|
|
388
|
+
UPDATE "appointments"
|
|
389
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
390
|
+
WHERE "site_id" = 'default';
|
|
391
|
+
|
|
392
|
+
UPDATE "deliverable_assets"
|
|
393
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
394
|
+
WHERE "site_id" = 'default';
|
|
395
|
+
|
|
396
|
+
UPDATE "flow_checkouts"
|
|
397
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
398
|
+
WHERE "site_id" = 'default';
|
|
399
|
+
|
|
400
|
+
UPDATE "waitlist_experiments"
|
|
401
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
402
|
+
WHERE "site_id" = 'default';
|
|
403
|
+
|
|
404
|
+
UPDATE "waitlist_scores"
|
|
405
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
406
|
+
WHERE "site_id" = 'default';
|
|
407
|
+
|
|
408
|
+
UPDATE "waitlist_followup_sends"
|
|
409
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
410
|
+
WHERE "site_id" = 'default';
|
|
411
|
+
|
|
412
|
+
UPDATE "playbook_instances"
|
|
413
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
414
|
+
WHERE "site_id" = 'default';
|
|
415
|
+
|
|
416
|
+
UPDATE "site_listing_configs"
|
|
417
|
+
SET "site_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
418
|
+
WHERE "site_id" = 'default';
|
|
419
|
+
|
|
420
|
+
-- ── Step 3 (D1 step 3 / keystone D2) — backfill by INNER JOIN, never by assignment ─
|
|
421
|
+
-- site_id is already normalized (step 1), so no CASE/alias branch is needed.
|
|
422
|
+
-- A row whose site_id resolves to nothing is simply not touched and stays
|
|
423
|
+
-- NULL — never guessed. Step 3's assertion 4 then aborts on it.
|
|
424
|
+
UPDATE "subscribers" x
|
|
425
|
+
SET "workspace_id" = w."id"
|
|
426
|
+
FROM "workspaces" w
|
|
427
|
+
WHERE x."workspace_id" IS NULL
|
|
428
|
+
AND w."id" = x."site_id";
|
|
429
|
+
|
|
430
|
+
UPDATE "callbacks" x
|
|
431
|
+
SET "workspace_id" = w."id"
|
|
432
|
+
FROM "workspaces" w
|
|
433
|
+
WHERE x."workspace_id" IS NULL
|
|
434
|
+
AND w."id" = x."site_id";
|
|
435
|
+
|
|
436
|
+
UPDATE "consent_grants" x
|
|
437
|
+
SET "workspace_id" = w."id"
|
|
438
|
+
FROM "workspaces" w
|
|
439
|
+
WHERE x."workspace_id" IS NULL
|
|
440
|
+
AND w."id" = x."site_id";
|
|
441
|
+
|
|
442
|
+
UPDATE "appointments" x
|
|
443
|
+
SET "workspace_id" = w."id"
|
|
444
|
+
FROM "workspaces" w
|
|
445
|
+
WHERE x."workspace_id" IS NULL
|
|
446
|
+
AND w."id" = x."site_id";
|
|
447
|
+
|
|
448
|
+
UPDATE "deliverable_assets" x
|
|
449
|
+
SET "workspace_id" = w."id"
|
|
450
|
+
FROM "workspaces" w
|
|
451
|
+
WHERE x."workspace_id" IS NULL
|
|
452
|
+
AND w."id" = x."site_id";
|
|
453
|
+
|
|
454
|
+
UPDATE "flow_checkouts" x
|
|
455
|
+
SET "workspace_id" = w."id"
|
|
456
|
+
FROM "workspaces" w
|
|
457
|
+
WHERE x."workspace_id" IS NULL
|
|
458
|
+
AND w."id" = x."site_id";
|
|
459
|
+
|
|
460
|
+
UPDATE "waitlist_experiments" x
|
|
461
|
+
SET "workspace_id" = w."id"
|
|
462
|
+
FROM "workspaces" w
|
|
463
|
+
WHERE x."workspace_id" IS NULL
|
|
464
|
+
AND w."id" = x."site_id";
|
|
465
|
+
|
|
466
|
+
UPDATE "waitlist_scores" x
|
|
467
|
+
SET "workspace_id" = w."id"
|
|
468
|
+
FROM "workspaces" w
|
|
469
|
+
WHERE x."workspace_id" IS NULL
|
|
470
|
+
AND w."id" = x."site_id";
|
|
471
|
+
|
|
472
|
+
UPDATE "waitlist_followup_sends" x
|
|
473
|
+
SET "workspace_id" = w."id"
|
|
474
|
+
FROM "workspaces" w
|
|
475
|
+
WHERE x."workspace_id" IS NULL
|
|
476
|
+
AND w."id" = x."site_id";
|
|
477
|
+
|
|
478
|
+
UPDATE "playbook_instances" x
|
|
479
|
+
SET "workspace_id" = w."id"
|
|
480
|
+
FROM "workspaces" w
|
|
481
|
+
WHERE x."workspace_id" IS NULL
|
|
482
|
+
AND w."id" = x."site_id";
|
|
483
|
+
|
|
484
|
+
UPDATE "site_listing_configs" x
|
|
485
|
+
SET "workspace_id" = w."id"
|
|
486
|
+
FROM "workspaces" w
|
|
487
|
+
WHERE x."workspace_id" IS NULL
|
|
488
|
+
AND w."id" = x."site_id";
|
|
489
|
+
|
|
490
|
+
-- ── Step 4 (D1 step 4) — POST-ASSERTIONS, four per table ──────────────────
|
|
491
|
+
-- Nothing lost (row census unchanged), nothing guessed (no workspace_id
|
|
492
|
+
-- absent from "workspaces"), nothing missed (no resolvable site_id left
|
|
493
|
+
-- NULL), and ABORT on any remaining NULL — this migration is about to
|
|
494
|
+
-- SET NOT NULL, so an unresolvable row has no correct value to write. Never
|
|
495
|
+
-- deleted, never guessed (NFR3).
|
|
496
|
+
DO $$
|
|
497
|
+
DECLARE
|
|
498
|
+
before_n bigint; after_n bigint; bad bigint;
|
|
499
|
+
BEGIN
|
|
500
|
+
-- 1. Nothing lost — "subscribers"
|
|
501
|
+
SELECT "subscribers" INTO before_n FROM "_6374_fence_census";
|
|
502
|
+
SELECT count(*) INTO after_n FROM "subscribers";
|
|
503
|
+
IF before_n <> after_n THEN
|
|
504
|
+
RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (subscribers % -> %)', before_n, after_n;
|
|
505
|
+
END IF;
|
|
506
|
+
|
|
507
|
+
-- 2. Nothing guessed — "subscribers"
|
|
508
|
+
SELECT count(*) INTO bad FROM "subscribers" x
|
|
509
|
+
WHERE x."workspace_id" IS NOT NULL
|
|
510
|
+
AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
|
|
511
|
+
IF bad <> 0 THEN
|
|
512
|
+
RAISE EXCEPTION '#6374 post-assertion: % subscribers row(s) carry a workspace_id absent from workspaces', bad;
|
|
513
|
+
END IF;
|
|
514
|
+
|
|
515
|
+
-- 3. Nothing missed — "subscribers"
|
|
516
|
+
SELECT count(*) INTO bad FROM "subscribers" x
|
|
517
|
+
WHERE x."workspace_id" IS NULL
|
|
518
|
+
AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
|
|
519
|
+
IF bad <> 0 THEN
|
|
520
|
+
RAISE EXCEPTION '#6374 post-assertion: % subscribers row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
521
|
+
END IF;
|
|
522
|
+
|
|
523
|
+
-- 4. Abort on any remaining NULL — "subscribers"
|
|
524
|
+
SELECT count(*) INTO bad FROM "subscribers" WHERE "workspace_id" IS NULL;
|
|
525
|
+
IF bad <> 0 THEN
|
|
526
|
+
RAISE EXCEPTION '#6374: % subscribers row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
527
|
+
END IF;
|
|
528
|
+
|
|
529
|
+
-- 1. Nothing lost — "callbacks"
|
|
530
|
+
SELECT "callbacks" INTO before_n FROM "_6374_fence_census";
|
|
531
|
+
SELECT count(*) INTO after_n FROM "callbacks";
|
|
532
|
+
IF before_n <> after_n THEN
|
|
533
|
+
RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (callbacks % -> %)', before_n, after_n;
|
|
534
|
+
END IF;
|
|
535
|
+
|
|
536
|
+
-- 2. Nothing guessed — "callbacks"
|
|
537
|
+
SELECT count(*) INTO bad FROM "callbacks" x
|
|
538
|
+
WHERE x."workspace_id" IS NOT NULL
|
|
539
|
+
AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
|
|
540
|
+
IF bad <> 0 THEN
|
|
541
|
+
RAISE EXCEPTION '#6374 post-assertion: % callbacks row(s) carry a workspace_id absent from workspaces', bad;
|
|
542
|
+
END IF;
|
|
543
|
+
|
|
544
|
+
-- 3. Nothing missed — "callbacks"
|
|
545
|
+
SELECT count(*) INTO bad FROM "callbacks" x
|
|
546
|
+
WHERE x."workspace_id" IS NULL
|
|
547
|
+
AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
|
|
548
|
+
IF bad <> 0 THEN
|
|
549
|
+
RAISE EXCEPTION '#6374 post-assertion: % callbacks row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
550
|
+
END IF;
|
|
551
|
+
|
|
552
|
+
-- 4. Abort on any remaining NULL — "callbacks"
|
|
553
|
+
SELECT count(*) INTO bad FROM "callbacks" WHERE "workspace_id" IS NULL;
|
|
554
|
+
IF bad <> 0 THEN
|
|
555
|
+
RAISE EXCEPTION '#6374: % callbacks row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
556
|
+
END IF;
|
|
557
|
+
|
|
558
|
+
-- 1. Nothing lost — "consent_grants"
|
|
559
|
+
SELECT "consent_grants" INTO before_n FROM "_6374_fence_census";
|
|
560
|
+
SELECT count(*) INTO after_n FROM "consent_grants";
|
|
561
|
+
IF before_n <> after_n THEN
|
|
562
|
+
RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (consent_grants % -> %)', before_n, after_n;
|
|
563
|
+
END IF;
|
|
564
|
+
|
|
565
|
+
-- 2. Nothing guessed — "consent_grants"
|
|
566
|
+
SELECT count(*) INTO bad FROM "consent_grants" x
|
|
567
|
+
WHERE x."workspace_id" IS NOT NULL
|
|
568
|
+
AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
|
|
569
|
+
IF bad <> 0 THEN
|
|
570
|
+
RAISE EXCEPTION '#6374 post-assertion: % consent_grants row(s) carry a workspace_id absent from workspaces', bad;
|
|
571
|
+
END IF;
|
|
572
|
+
|
|
573
|
+
-- 3. Nothing missed — "consent_grants"
|
|
574
|
+
SELECT count(*) INTO bad FROM "consent_grants" x
|
|
575
|
+
WHERE x."workspace_id" IS NULL
|
|
576
|
+
AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
|
|
577
|
+
IF bad <> 0 THEN
|
|
578
|
+
RAISE EXCEPTION '#6374 post-assertion: % consent_grants row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
579
|
+
END IF;
|
|
580
|
+
|
|
581
|
+
-- 4. Abort on any remaining NULL — "consent_grants"
|
|
582
|
+
SELECT count(*) INTO bad FROM "consent_grants" WHERE "workspace_id" IS NULL;
|
|
583
|
+
IF bad <> 0 THEN
|
|
584
|
+
RAISE EXCEPTION '#6374: % consent_grants row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
585
|
+
END IF;
|
|
586
|
+
|
|
587
|
+
-- 1. Nothing lost — "appointments"
|
|
588
|
+
SELECT "appointments" INTO before_n FROM "_6374_fence_census";
|
|
589
|
+
SELECT count(*) INTO after_n FROM "appointments";
|
|
590
|
+
IF before_n <> after_n THEN
|
|
591
|
+
RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (appointments % -> %)', before_n, after_n;
|
|
592
|
+
END IF;
|
|
593
|
+
|
|
594
|
+
-- 2. Nothing guessed — "appointments"
|
|
595
|
+
SELECT count(*) INTO bad FROM "appointments" x
|
|
596
|
+
WHERE x."workspace_id" IS NOT NULL
|
|
597
|
+
AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
|
|
598
|
+
IF bad <> 0 THEN
|
|
599
|
+
RAISE EXCEPTION '#6374 post-assertion: % appointments row(s) carry a workspace_id absent from workspaces', bad;
|
|
600
|
+
END IF;
|
|
601
|
+
|
|
602
|
+
-- 3. Nothing missed — "appointments"
|
|
603
|
+
SELECT count(*) INTO bad FROM "appointments" x
|
|
604
|
+
WHERE x."workspace_id" IS NULL
|
|
605
|
+
AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
|
|
606
|
+
IF bad <> 0 THEN
|
|
607
|
+
RAISE EXCEPTION '#6374 post-assertion: % appointments row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
608
|
+
END IF;
|
|
609
|
+
|
|
610
|
+
-- 4. Abort on any remaining NULL — "appointments"
|
|
611
|
+
SELECT count(*) INTO bad FROM "appointments" WHERE "workspace_id" IS NULL;
|
|
612
|
+
IF bad <> 0 THEN
|
|
613
|
+
RAISE EXCEPTION '#6374: % appointments row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
614
|
+
END IF;
|
|
615
|
+
|
|
616
|
+
-- 1. Nothing lost — "deliverable_assets"
|
|
617
|
+
SELECT "deliverable_assets" INTO before_n FROM "_6374_fence_census";
|
|
618
|
+
SELECT count(*) INTO after_n FROM "deliverable_assets";
|
|
619
|
+
IF before_n <> after_n THEN
|
|
620
|
+
RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (deliverable_assets % -> %)', before_n, after_n;
|
|
621
|
+
END IF;
|
|
622
|
+
|
|
623
|
+
-- 2. Nothing guessed — "deliverable_assets"
|
|
624
|
+
SELECT count(*) INTO bad FROM "deliverable_assets" x
|
|
625
|
+
WHERE x."workspace_id" IS NOT NULL
|
|
626
|
+
AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
|
|
627
|
+
IF bad <> 0 THEN
|
|
628
|
+
RAISE EXCEPTION '#6374 post-assertion: % deliverable_assets row(s) carry a workspace_id absent from workspaces', bad;
|
|
629
|
+
END IF;
|
|
630
|
+
|
|
631
|
+
-- 3. Nothing missed — "deliverable_assets"
|
|
632
|
+
SELECT count(*) INTO bad FROM "deliverable_assets" x
|
|
633
|
+
WHERE x."workspace_id" IS NULL
|
|
634
|
+
AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
|
|
635
|
+
IF bad <> 0 THEN
|
|
636
|
+
RAISE EXCEPTION '#6374 post-assertion: % deliverable_assets row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
637
|
+
END IF;
|
|
638
|
+
|
|
639
|
+
-- 4. Abort on any remaining NULL — "deliverable_assets"
|
|
640
|
+
SELECT count(*) INTO bad FROM "deliverable_assets" WHERE "workspace_id" IS NULL;
|
|
641
|
+
IF bad <> 0 THEN
|
|
642
|
+
RAISE EXCEPTION '#6374: % deliverable_assets row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
643
|
+
END IF;
|
|
644
|
+
|
|
645
|
+
-- 1. Nothing lost — "flow_checkouts"
|
|
646
|
+
SELECT "flow_checkouts" INTO before_n FROM "_6374_fence_census";
|
|
647
|
+
SELECT count(*) INTO after_n FROM "flow_checkouts";
|
|
648
|
+
IF before_n <> after_n THEN
|
|
649
|
+
RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (flow_checkouts % -> %)', before_n, after_n;
|
|
650
|
+
END IF;
|
|
651
|
+
|
|
652
|
+
-- 2. Nothing guessed — "flow_checkouts"
|
|
653
|
+
SELECT count(*) INTO bad FROM "flow_checkouts" x
|
|
654
|
+
WHERE x."workspace_id" IS NOT NULL
|
|
655
|
+
AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
|
|
656
|
+
IF bad <> 0 THEN
|
|
657
|
+
RAISE EXCEPTION '#6374 post-assertion: % flow_checkouts row(s) carry a workspace_id absent from workspaces', bad;
|
|
658
|
+
END IF;
|
|
659
|
+
|
|
660
|
+
-- 3. Nothing missed — "flow_checkouts"
|
|
661
|
+
SELECT count(*) INTO bad FROM "flow_checkouts" x
|
|
662
|
+
WHERE x."workspace_id" IS NULL
|
|
663
|
+
AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
|
|
664
|
+
IF bad <> 0 THEN
|
|
665
|
+
RAISE EXCEPTION '#6374 post-assertion: % flow_checkouts row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
666
|
+
END IF;
|
|
667
|
+
|
|
668
|
+
-- 4. Abort on any remaining NULL — "flow_checkouts"
|
|
669
|
+
SELECT count(*) INTO bad FROM "flow_checkouts" WHERE "workspace_id" IS NULL;
|
|
670
|
+
IF bad <> 0 THEN
|
|
671
|
+
RAISE EXCEPTION '#6374: % flow_checkouts row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
672
|
+
END IF;
|
|
673
|
+
|
|
674
|
+
-- 1. Nothing lost — "waitlist_experiments"
|
|
675
|
+
SELECT "waitlist_experiments" INTO before_n FROM "_6374_fence_census";
|
|
676
|
+
SELECT count(*) INTO after_n FROM "waitlist_experiments";
|
|
677
|
+
IF before_n <> after_n THEN
|
|
678
|
+
RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (waitlist_experiments % -> %)', before_n, after_n;
|
|
679
|
+
END IF;
|
|
680
|
+
|
|
681
|
+
-- 2. Nothing guessed — "waitlist_experiments"
|
|
682
|
+
SELECT count(*) INTO bad FROM "waitlist_experiments" x
|
|
683
|
+
WHERE x."workspace_id" IS NOT NULL
|
|
684
|
+
AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
|
|
685
|
+
IF bad <> 0 THEN
|
|
686
|
+
RAISE EXCEPTION '#6374 post-assertion: % waitlist_experiments row(s) carry a workspace_id absent from workspaces', bad;
|
|
687
|
+
END IF;
|
|
688
|
+
|
|
689
|
+
-- 3. Nothing missed — "waitlist_experiments"
|
|
690
|
+
SELECT count(*) INTO bad FROM "waitlist_experiments" x
|
|
691
|
+
WHERE x."workspace_id" IS NULL
|
|
692
|
+
AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
|
|
693
|
+
IF bad <> 0 THEN
|
|
694
|
+
RAISE EXCEPTION '#6374 post-assertion: % waitlist_experiments row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
695
|
+
END IF;
|
|
696
|
+
|
|
697
|
+
-- 4. Abort on any remaining NULL — "waitlist_experiments"
|
|
698
|
+
SELECT count(*) INTO bad FROM "waitlist_experiments" WHERE "workspace_id" IS NULL;
|
|
699
|
+
IF bad <> 0 THEN
|
|
700
|
+
RAISE EXCEPTION '#6374: % waitlist_experiments row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
701
|
+
END IF;
|
|
702
|
+
|
|
703
|
+
-- 1. Nothing lost — "waitlist_scores"
|
|
704
|
+
SELECT "waitlist_scores" INTO before_n FROM "_6374_fence_census";
|
|
705
|
+
SELECT count(*) INTO after_n FROM "waitlist_scores";
|
|
706
|
+
IF before_n <> after_n THEN
|
|
707
|
+
RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (waitlist_scores % -> %)', before_n, after_n;
|
|
708
|
+
END IF;
|
|
709
|
+
|
|
710
|
+
-- 2. Nothing guessed — "waitlist_scores"
|
|
711
|
+
SELECT count(*) INTO bad FROM "waitlist_scores" x
|
|
712
|
+
WHERE x."workspace_id" IS NOT NULL
|
|
713
|
+
AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
|
|
714
|
+
IF bad <> 0 THEN
|
|
715
|
+
RAISE EXCEPTION '#6374 post-assertion: % waitlist_scores row(s) carry a workspace_id absent from workspaces', bad;
|
|
716
|
+
END IF;
|
|
717
|
+
|
|
718
|
+
-- 3. Nothing missed — "waitlist_scores"
|
|
719
|
+
SELECT count(*) INTO bad FROM "waitlist_scores" x
|
|
720
|
+
WHERE x."workspace_id" IS NULL
|
|
721
|
+
AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
|
|
722
|
+
IF bad <> 0 THEN
|
|
723
|
+
RAISE EXCEPTION '#6374 post-assertion: % waitlist_scores row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
724
|
+
END IF;
|
|
725
|
+
|
|
726
|
+
-- 4. Abort on any remaining NULL — "waitlist_scores"
|
|
727
|
+
SELECT count(*) INTO bad FROM "waitlist_scores" WHERE "workspace_id" IS NULL;
|
|
728
|
+
IF bad <> 0 THEN
|
|
729
|
+
RAISE EXCEPTION '#6374: % waitlist_scores row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
730
|
+
END IF;
|
|
731
|
+
|
|
732
|
+
-- 1. Nothing lost — "waitlist_followup_sends"
|
|
733
|
+
SELECT "waitlist_followup_sends" INTO before_n FROM "_6374_fence_census";
|
|
734
|
+
SELECT count(*) INTO after_n FROM "waitlist_followup_sends";
|
|
735
|
+
IF before_n <> after_n THEN
|
|
736
|
+
RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (waitlist_followup_sends % -> %)', before_n, after_n;
|
|
737
|
+
END IF;
|
|
738
|
+
|
|
739
|
+
-- 2. Nothing guessed — "waitlist_followup_sends"
|
|
740
|
+
SELECT count(*) INTO bad FROM "waitlist_followup_sends" x
|
|
741
|
+
WHERE x."workspace_id" IS NOT NULL
|
|
742
|
+
AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
|
|
743
|
+
IF bad <> 0 THEN
|
|
744
|
+
RAISE EXCEPTION '#6374 post-assertion: % waitlist_followup_sends row(s) carry a workspace_id absent from workspaces', bad;
|
|
745
|
+
END IF;
|
|
746
|
+
|
|
747
|
+
-- 3. Nothing missed — "waitlist_followup_sends"
|
|
748
|
+
SELECT count(*) INTO bad FROM "waitlist_followup_sends" x
|
|
749
|
+
WHERE x."workspace_id" IS NULL
|
|
750
|
+
AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
|
|
751
|
+
IF bad <> 0 THEN
|
|
752
|
+
RAISE EXCEPTION '#6374 post-assertion: % waitlist_followup_sends row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
753
|
+
END IF;
|
|
754
|
+
|
|
755
|
+
-- 4. Abort on any remaining NULL — "waitlist_followup_sends"
|
|
756
|
+
SELECT count(*) INTO bad FROM "waitlist_followup_sends" WHERE "workspace_id" IS NULL;
|
|
757
|
+
IF bad <> 0 THEN
|
|
758
|
+
RAISE EXCEPTION '#6374: % waitlist_followup_sends row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
759
|
+
END IF;
|
|
760
|
+
|
|
761
|
+
-- 1. Nothing lost — "playbook_instances"
|
|
762
|
+
SELECT "playbook_instances" INTO before_n FROM "_6374_fence_census";
|
|
763
|
+
SELECT count(*) INTO after_n FROM "playbook_instances";
|
|
764
|
+
IF before_n <> after_n THEN
|
|
765
|
+
RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (playbook_instances % -> %)', before_n, after_n;
|
|
766
|
+
END IF;
|
|
767
|
+
|
|
768
|
+
-- 2. Nothing guessed — "playbook_instances"
|
|
769
|
+
SELECT count(*) INTO bad FROM "playbook_instances" x
|
|
770
|
+
WHERE x."workspace_id" IS NOT NULL
|
|
771
|
+
AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
|
|
772
|
+
IF bad <> 0 THEN
|
|
773
|
+
RAISE EXCEPTION '#6374 post-assertion: % playbook_instances row(s) carry a workspace_id absent from workspaces', bad;
|
|
774
|
+
END IF;
|
|
775
|
+
|
|
776
|
+
-- 3. Nothing missed — "playbook_instances"
|
|
777
|
+
SELECT count(*) INTO bad FROM "playbook_instances" x
|
|
778
|
+
WHERE x."workspace_id" IS NULL
|
|
779
|
+
AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
|
|
780
|
+
IF bad <> 0 THEN
|
|
781
|
+
RAISE EXCEPTION '#6374 post-assertion: % playbook_instances row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
782
|
+
END IF;
|
|
783
|
+
|
|
784
|
+
-- 4. Abort on any remaining NULL — "playbook_instances"
|
|
785
|
+
SELECT count(*) INTO bad FROM "playbook_instances" WHERE "workspace_id" IS NULL;
|
|
786
|
+
IF bad <> 0 THEN
|
|
787
|
+
RAISE EXCEPTION '#6374: % playbook_instances row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
788
|
+
END IF;
|
|
789
|
+
|
|
790
|
+
-- 1. Nothing lost — "site_listing_configs"
|
|
791
|
+
SELECT "site_listing_configs" INTO before_n FROM "_6374_fence_census";
|
|
792
|
+
SELECT count(*) INTO after_n FROM "site_listing_configs";
|
|
793
|
+
IF before_n <> after_n THEN
|
|
794
|
+
RAISE EXCEPTION '#6374 post-assertion: row count changed across a purely additive migration (site_listing_configs % -> %)', before_n, after_n;
|
|
795
|
+
END IF;
|
|
796
|
+
|
|
797
|
+
-- 2. Nothing guessed — "site_listing_configs"
|
|
798
|
+
SELECT count(*) INTO bad FROM "site_listing_configs" x
|
|
799
|
+
WHERE x."workspace_id" IS NOT NULL
|
|
800
|
+
AND NOT EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."workspace_id");
|
|
801
|
+
IF bad <> 0 THEN
|
|
802
|
+
RAISE EXCEPTION '#6374 post-assertion: % site_listing_configs row(s) carry a workspace_id absent from workspaces', bad;
|
|
803
|
+
END IF;
|
|
804
|
+
|
|
805
|
+
-- 3. Nothing missed — "site_listing_configs"
|
|
806
|
+
SELECT count(*) INTO bad FROM "site_listing_configs" x
|
|
807
|
+
WHERE x."workspace_id" IS NULL
|
|
808
|
+
AND EXISTS (SELECT 1 FROM "workspaces" w WHERE w."id" = x."site_id");
|
|
809
|
+
IF bad <> 0 THEN
|
|
810
|
+
RAISE EXCEPTION '#6374 post-assertion: % site_listing_configs row(s) had a resolvable site_id but came out of the backfill NULL', bad;
|
|
811
|
+
END IF;
|
|
812
|
+
|
|
813
|
+
-- 4. Abort on any remaining NULL — "site_listing_configs"
|
|
814
|
+
SELECT count(*) INTO bad FROM "site_listing_configs" WHERE "workspace_id" IS NULL;
|
|
815
|
+
IF bad <> 0 THEN
|
|
816
|
+
RAISE EXCEPTION '#6374: % site_listing_configs row(s) have a site_id that resolves to no workspace this database holds — resolve or remove them before re-running this migration', bad;
|
|
817
|
+
END IF;
|
|
818
|
+
|
|
819
|
+
END $$;
|
|
820
|
+
|
|
821
|
+
-- ── Step 5a (D1 step 5) — SET NOT NULL (spec FR1, AC-1) ───────────────────
|
|
822
|
+
ALTER TABLE "subscribers" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
823
|
+
ALTER TABLE "callbacks" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
824
|
+
ALTER TABLE "consent_grants" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
825
|
+
ALTER TABLE "appointments" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
826
|
+
ALTER TABLE "deliverable_assets" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
827
|
+
ALTER TABLE "flow_checkouts" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
828
|
+
ALTER TABLE "waitlist_experiments" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
829
|
+
ALTER TABLE "waitlist_scores" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
830
|
+
ALTER TABLE "waitlist_followup_sends" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
831
|
+
ALTER TABLE "playbook_instances" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
832
|
+
ALTER TABLE "site_listing_configs" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
833
|
+
|
|
834
|
+
-- ── Step 5b (D1 step 5) — the two CHECKs, DROP then ADD, never the guarded DO block ─
|
|
835
|
+
-- keystone D5 / SEC-06: an existence guard would silently no-op a TIGHTENING
|
|
836
|
+
-- on any database that already carries a same-named constraint — a
|
|
837
|
+
-- false-green control. The format CHECK carries NO `IS NULL OR` arm (the
|
|
838
|
+
-- precedent's did, because the precedent never enforced NOT NULL; step 4
|
|
839
|
+
-- just did).
|
|
840
|
+
ALTER TABLE "subscribers" DROP CONSTRAINT IF EXISTS "subscribers_workspace_id_format";
|
|
841
|
+
ALTER TABLE "subscribers" ADD CONSTRAINT "subscribers_workspace_id_format"
|
|
842
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
843
|
+
ALTER TABLE "subscribers" DROP CONSTRAINT IF EXISTS "subscribers_workspace_site_agree";
|
|
844
|
+
ALTER TABLE "subscribers" ADD CONSTRAINT "subscribers_workspace_site_agree"
|
|
845
|
+
CHECK ("workspace_id" = "site_id");
|
|
846
|
+
|
|
847
|
+
ALTER TABLE "callbacks" DROP CONSTRAINT IF EXISTS "callbacks_workspace_id_format";
|
|
848
|
+
ALTER TABLE "callbacks" ADD CONSTRAINT "callbacks_workspace_id_format"
|
|
849
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
850
|
+
ALTER TABLE "callbacks" DROP CONSTRAINT IF EXISTS "callbacks_workspace_site_agree";
|
|
851
|
+
ALTER TABLE "callbacks" ADD CONSTRAINT "callbacks_workspace_site_agree"
|
|
852
|
+
CHECK ("workspace_id" = "site_id");
|
|
853
|
+
|
|
854
|
+
ALTER TABLE "consent_grants" DROP CONSTRAINT IF EXISTS "consent_grants_workspace_id_format";
|
|
855
|
+
ALTER TABLE "consent_grants" ADD CONSTRAINT "consent_grants_workspace_id_format"
|
|
856
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
857
|
+
ALTER TABLE "consent_grants" DROP CONSTRAINT IF EXISTS "consent_grants_workspace_site_agree";
|
|
858
|
+
ALTER TABLE "consent_grants" ADD CONSTRAINT "consent_grants_workspace_site_agree"
|
|
859
|
+
CHECK ("workspace_id" = "site_id");
|
|
860
|
+
|
|
861
|
+
ALTER TABLE "appointments" DROP CONSTRAINT IF EXISTS "appointments_workspace_id_format";
|
|
862
|
+
ALTER TABLE "appointments" ADD CONSTRAINT "appointments_workspace_id_format"
|
|
863
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
864
|
+
ALTER TABLE "appointments" DROP CONSTRAINT IF EXISTS "appointments_workspace_site_agree";
|
|
865
|
+
ALTER TABLE "appointments" ADD CONSTRAINT "appointments_workspace_site_agree"
|
|
866
|
+
CHECK ("workspace_id" = "site_id");
|
|
867
|
+
|
|
868
|
+
ALTER TABLE "deliverable_assets" DROP CONSTRAINT IF EXISTS "deliverable_assets_workspace_id_format";
|
|
869
|
+
ALTER TABLE "deliverable_assets" ADD CONSTRAINT "deliverable_assets_workspace_id_format"
|
|
870
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
871
|
+
ALTER TABLE "deliverable_assets" DROP CONSTRAINT IF EXISTS "deliverable_assets_workspace_site_agree";
|
|
872
|
+
ALTER TABLE "deliverable_assets" ADD CONSTRAINT "deliverable_assets_workspace_site_agree"
|
|
873
|
+
CHECK ("workspace_id" = "site_id");
|
|
874
|
+
|
|
875
|
+
ALTER TABLE "flow_checkouts" DROP CONSTRAINT IF EXISTS "flow_checkouts_workspace_id_format";
|
|
876
|
+
ALTER TABLE "flow_checkouts" ADD CONSTRAINT "flow_checkouts_workspace_id_format"
|
|
877
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
878
|
+
ALTER TABLE "flow_checkouts" DROP CONSTRAINT IF EXISTS "flow_checkouts_workspace_site_agree";
|
|
879
|
+
ALTER TABLE "flow_checkouts" ADD CONSTRAINT "flow_checkouts_workspace_site_agree"
|
|
880
|
+
CHECK ("workspace_id" = "site_id");
|
|
881
|
+
|
|
882
|
+
ALTER TABLE "waitlist_experiments" DROP CONSTRAINT IF EXISTS "waitlist_experiments_workspace_id_format";
|
|
883
|
+
ALTER TABLE "waitlist_experiments" ADD CONSTRAINT "waitlist_experiments_workspace_id_format"
|
|
884
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
885
|
+
ALTER TABLE "waitlist_experiments" DROP CONSTRAINT IF EXISTS "waitlist_experiments_workspace_site_agree";
|
|
886
|
+
ALTER TABLE "waitlist_experiments" ADD CONSTRAINT "waitlist_experiments_workspace_site_agree"
|
|
887
|
+
CHECK ("workspace_id" = "site_id");
|
|
888
|
+
|
|
889
|
+
ALTER TABLE "waitlist_scores" DROP CONSTRAINT IF EXISTS "waitlist_scores_workspace_id_format";
|
|
890
|
+
ALTER TABLE "waitlist_scores" ADD CONSTRAINT "waitlist_scores_workspace_id_format"
|
|
891
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
892
|
+
ALTER TABLE "waitlist_scores" DROP CONSTRAINT IF EXISTS "waitlist_scores_workspace_site_agree";
|
|
893
|
+
ALTER TABLE "waitlist_scores" ADD CONSTRAINT "waitlist_scores_workspace_site_agree"
|
|
894
|
+
CHECK ("workspace_id" = "site_id");
|
|
895
|
+
|
|
896
|
+
ALTER TABLE "waitlist_followup_sends" DROP CONSTRAINT IF EXISTS "waitlist_followup_sends_workspace_id_format";
|
|
897
|
+
ALTER TABLE "waitlist_followup_sends" ADD CONSTRAINT "waitlist_followup_sends_workspace_id_format"
|
|
898
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
899
|
+
ALTER TABLE "waitlist_followup_sends" DROP CONSTRAINT IF EXISTS "waitlist_followup_sends_workspace_site_agree";
|
|
900
|
+
ALTER TABLE "waitlist_followup_sends" ADD CONSTRAINT "waitlist_followup_sends_workspace_site_agree"
|
|
901
|
+
CHECK ("workspace_id" = "site_id");
|
|
902
|
+
|
|
903
|
+
ALTER TABLE "playbook_instances" DROP CONSTRAINT IF EXISTS "playbook_instances_workspace_id_format";
|
|
904
|
+
ALTER TABLE "playbook_instances" ADD CONSTRAINT "playbook_instances_workspace_id_format"
|
|
905
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
906
|
+
ALTER TABLE "playbook_instances" DROP CONSTRAINT IF EXISTS "playbook_instances_workspace_site_agree";
|
|
907
|
+
ALTER TABLE "playbook_instances" ADD CONSTRAINT "playbook_instances_workspace_site_agree"
|
|
908
|
+
CHECK ("workspace_id" = "site_id");
|
|
909
|
+
|
|
910
|
+
ALTER TABLE "site_listing_configs" DROP CONSTRAINT IF EXISTS "site_listing_configs_workspace_id_format";
|
|
911
|
+
ALTER TABLE "site_listing_configs" ADD CONSTRAINT "site_listing_configs_workspace_id_format"
|
|
912
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
913
|
+
ALTER TABLE "site_listing_configs" DROP CONSTRAINT IF EXISTS "site_listing_configs_workspace_site_agree";
|
|
914
|
+
ALTER TABLE "site_listing_configs" ADD CONSTRAINT "site_listing_configs_workspace_site_agree"
|
|
915
|
+
CHECK ("workspace_id" = "site_id");
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
-- ── D2 — `listing_csv_sends`: NOT NULL + format CHECK, no equality CHECK ────
|
|
919
|
+
-- It has no `site_id`, so an equality CHECK has nothing to compare. The
|
|
920
|
+
-- backfill takes the workspace from the PARENT listing row, which the
|
|
921
|
+
-- keystone above already fenced. A composite FK to `project_listings` is
|
|
922
|
+
-- legal here (both models are `inbox`) and was offered; the operator chose
|
|
923
|
+
-- ONE fence shape over a second, stricter one for an audit-log table
|
|
924
|
+
-- (2026-09-20, spec § 8 Q1 -> option (a)).
|
|
925
|
+
ALTER TABLE "listing_csv_sends" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
926
|
+
CREATE INDEX IF NOT EXISTS "listing_csv_sends_workspace_id_idx"
|
|
927
|
+
ON "listing_csv_sends" ("workspace_id");
|
|
928
|
+
|
|
929
|
+
UPDATE "listing_csv_sends" s
|
|
930
|
+
SET "workspace_id" = p."workspace_id"
|
|
931
|
+
FROM "project_listings" p
|
|
932
|
+
WHERE s."workspace_id" IS NULL
|
|
933
|
+
AND p."id" = s."listing_id";
|
|
934
|
+
|
|
935
|
+
DO $$
|
|
936
|
+
DECLARE
|
|
937
|
+
bad bigint;
|
|
938
|
+
BEGIN
|
|
939
|
+
-- The same abort-on-NULL rule the eleven take: a send row whose listing is
|
|
940
|
+
-- gone (or was never fenced) has no correct workspace to write, and this
|
|
941
|
+
-- is about to SET NOT NULL. Never deleted, never guessed (NFR3).
|
|
942
|
+
SELECT count(*) INTO bad FROM "listing_csv_sends" WHERE "workspace_id" IS NULL;
|
|
943
|
+
IF bad <> 0 THEN
|
|
944
|
+
RAISE EXCEPTION '#6374: % listing_csv_sends row(s) have a listing_id that resolves to no project_listings row — resolve or remove them before re-running this migration', bad;
|
|
945
|
+
END IF;
|
|
946
|
+
END $$;
|
|
947
|
+
|
|
948
|
+
ALTER TABLE "listing_csv_sends" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
949
|
+
|
|
950
|
+
ALTER TABLE "listing_csv_sends" DROP CONSTRAINT IF EXISTS "listing_csv_sends_workspace_id_format";
|
|
951
|
+
ALTER TABLE "listing_csv_sends" ADD CONSTRAINT "listing_csv_sends_workspace_id_format"
|
|
952
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
953
|
+
|
|
954
|
+
-- ── D3 — `site_settings`: keyed BY the workspace, read through the alias ────
|
|
955
|
+
-- It has no `site_id` either, and its one read
|
|
956
|
+
-- (apps/api/src/services/dunning-policy.ts) arrives with a site ALIAS. Every
|
|
957
|
+
-- existing row backfills to the SENTINEL workspace: a pre-fence row belonged
|
|
958
|
+
-- to "the site", which is exactly what the alias `default` resolves to — the
|
|
959
|
+
-- same rule step 1 applies to `site_id` on the eleven.
|
|
960
|
+
--
|
|
961
|
+
-- This is the one block that moves a UNIQUE INDEX, which is why index DDL
|
|
962
|
+
-- appears here and in none of the five steps above: `key` was globally
|
|
963
|
+
-- unique, so two sellers could not both hold `feature_flags`. The `key` TEXT
|
|
964
|
+
-- itself is left VERBATIM — `dunning-policy.ts` embeds the site alias in the
|
|
965
|
+
-- key string, and rebuilding it from the resolved id would match no row and
|
|
966
|
+
-- silently hand every seller the built-in default policy (spec D3).
|
|
967
|
+
ALTER TABLE "site_settings" ADD COLUMN IF NOT EXISTS "workspace_id" TEXT;
|
|
968
|
+
CREATE INDEX IF NOT EXISTS "site_settings_workspace_id_idx"
|
|
969
|
+
ON "site_settings" ("workspace_id");
|
|
970
|
+
|
|
971
|
+
-- ── ABORT, NEVER GUESS — before the sentinel assignment below (AC-14) ──────
|
|
972
|
+
-- NFR3 ("an unresolvable row aborts the migration; it is never deleted and
|
|
973
|
+
-- never guessed") applied to the ONE table whose tenant lives in its key TEXT
|
|
974
|
+
-- rather than in a column. The assignment below attributes every unattributed
|
|
975
|
+
-- row to the sentinel, which is right for a row that belonged to "the site" —
|
|
976
|
+
-- and WRONG for a row whose key already names a DIFFERENT workspace. Such a
|
|
977
|
+
-- row would be silently misfiled under the sentinel, and
|
|
978
|
+
-- `dunning-policy.ts`'s read (keyed on the resolved workspace AND the verbatim
|
|
979
|
+
-- key) would then miss it forever, falling back to the built-in default
|
|
980
|
+
-- policy — MORE permissive than a configured one, on a delinquent-
|
|
981
|
+
-- subscription path. Found by this unit's own security review; disposed by
|
|
982
|
+
-- the operator 2026-09-20 as abort-not-derive.
|
|
983
|
+
--
|
|
984
|
+
-- This DETECTS; it never DERIVES. The embedded id is matched and counted, and
|
|
985
|
+
-- is never read back out and used as the backfill value — key parsing is
|
|
986
|
+
-- exactly what spec D3 forbids, because the alias in the key is load-bearing
|
|
987
|
+
-- for the read. An operator whose database trips this resolves or removes the
|
|
988
|
+
-- rows by hand and re-runs.
|
|
989
|
+
--
|
|
990
|
+
-- The negative lookahead is what makes this precise: it matches a well-formed
|
|
991
|
+
-- `wksp_`+UUIDv7 that is NOT the sentinel literal. A key carrying the sentinel
|
|
992
|
+
-- id, a bare alias (`…:default`), or no id at all is left for the sentinel
|
|
993
|
+
-- assignment below, exactly as before.
|
|
994
|
+
--
|
|
995
|
+
-- Guarded on `workspace_id IS NULL`, so on an already-migrated database every
|
|
996
|
+
-- row is attributed, the count is zero, and a verbatim re-run stays the no-op
|
|
997
|
+
-- NFR2 requires.
|
|
998
|
+
DO $$
|
|
999
|
+
DECLARE
|
|
1000
|
+
bad bigint;
|
|
1001
|
+
BEGIN
|
|
1002
|
+
SELECT count(*) INTO bad
|
|
1003
|
+
FROM "site_settings"
|
|
1004
|
+
WHERE "workspace_id" IS NULL
|
|
1005
|
+
AND "key" ~ 'wksp_(?!00000000-0000-7000-8000-000000000001)[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
|
|
1006
|
+
IF bad <> 0 THEN
|
|
1007
|
+
RAISE EXCEPTION '#6374: % site_settings row(s) carry a NON-SENTINEL workspace id embedded in their key and no workspace_id of their own — assigning them to the sentinel would misfile them and make their read miss forever; resolve or remove them before re-running this migration', bad;
|
|
1008
|
+
END IF;
|
|
1009
|
+
END $$;
|
|
1010
|
+
|
|
1011
|
+
UPDATE "site_settings"
|
|
1012
|
+
SET "workspace_id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
1013
|
+
WHERE "workspace_id" IS NULL
|
|
1014
|
+
AND EXISTS (
|
|
1015
|
+
SELECT 1 FROM "workspaces" w
|
|
1016
|
+
WHERE w."id" = 'wksp_00000000-0000-7000-8000-000000000001'
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
DO $$
|
|
1020
|
+
DECLARE
|
|
1021
|
+
bad bigint;
|
|
1022
|
+
BEGIN
|
|
1023
|
+
-- Abort rather than guess: a settings row on a database with no sentinel
|
|
1024
|
+
-- workspace has no workspace to belong to, and this is about to
|
|
1025
|
+
-- SET NOT NULL.
|
|
1026
|
+
SELECT count(*) INTO bad FROM "site_settings" WHERE "workspace_id" IS NULL;
|
|
1027
|
+
IF bad <> 0 THEN
|
|
1028
|
+
RAISE EXCEPTION '#6374: % site_settings row(s) remain unattributed — this database holds no sentinel workspace to backfill them to; resolve or remove them before re-running this migration', bad;
|
|
1029
|
+
END IF;
|
|
1030
|
+
END $$;
|
|
1031
|
+
|
|
1032
|
+
ALTER TABLE "site_settings" ALTER COLUMN "workspace_id" SET NOT NULL;
|
|
1033
|
+
|
|
1034
|
+
ALTER TABLE "site_settings" DROP CONSTRAINT IF EXISTS "site_settings_workspace_id_format";
|
|
1035
|
+
ALTER TABLE "site_settings" ADD CONSTRAINT "site_settings_workspace_id_format"
|
|
1036
|
+
CHECK ("workspace_id" ~ '^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
|
|
1037
|
+
|
|
1038
|
+
-- The unique key moves from (key) to (workspace_id, key).
|
|
1039
|
+
--
|
|
1040
|
+
-- DROP-then-CREATE for the REPLACEMENT too, never `CREATE UNIQUE INDEX IF NOT
|
|
1041
|
+
-- EXISTS` — the same keystone D5 / SEC-06 rule the CHECKs above follow, and
|
|
1042
|
+
-- this is the one place in this file where a constraint REPLACES a dropped
|
|
1043
|
+
-- one, so it is the one place the false-green would actually cost something.
|
|
1044
|
+
-- `IF NOT EXISTS` matches on NAME ALONE: an index of this name that was
|
|
1045
|
+
-- non-unique, or over different columns, would silently satisfy the guard and
|
|
1046
|
+
-- leave `site_settings` with NO uniqueness at all — having already dropped the
|
|
1047
|
+
-- old one unconditionally on the line above. The old index is a plain index,
|
|
1048
|
+
-- not a table constraint (20260508000000_add_inbox_namespace created it with
|
|
1049
|
+
-- `CREATE UNIQUE INDEX`), so `DROP INDEX` is the correct verb for both.
|
|
1050
|
+
DROP INDEX IF EXISTS "site_settings_key_key";
|
|
1051
|
+
DROP INDEX IF EXISTS "site_settings_workspace_id_key_key";
|
|
1052
|
+
CREATE UNIQUE INDEX "site_settings_workspace_id_key_key"
|
|
1053
|
+
ON "site_settings" ("workspace_id", "key");
|
|
1054
|
+
|
|
1055
|
+
-- ── D4 — `deliverable_grants`: stamped at mint, and STILL nullable ──────────
|
|
1056
|
+
-- No column is added and no existing row is touched. NULL remains the serve
|
|
1057
|
+
-- path's fail-closed sentinel (#6254 § 10.1 Q2): a legacy `dasset_` grant and
|
|
1058
|
+
-- a `playbook:` virtual one keep their NULL and keep meaning "serve nothing".
|
|
1059
|
+
-- `deliverable_grants.site_id` is deliberately NOT normalized — a legacy row
|
|
1060
|
+
-- keeps its alias, which with a NULL workspace still means serve nothing.
|
|
1061
|
+
--
|
|
1062
|
+
-- What IS added is the NULL-TOLERANT equality CHECK, so a STAMPED grant can
|
|
1063
|
+
-- never disagree with its site while a legacy grant stays storable. Both mint
|
|
1064
|
+
-- sites stamp the workspace they already hold (AC-8).
|
|
1065
|
+
ALTER TABLE "deliverable_grants" DROP CONSTRAINT IF EXISTS "deliverable_grants_workspace_site_agree";
|
|
1066
|
+
ALTER TABLE "deliverable_grants" ADD CONSTRAINT "deliverable_grants_workspace_site_agree"
|
|
1067
|
+
CHECK ("workspace_id" IS NULL OR "workspace_id" = "site_id");
|