@voltro/cli 0.1.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 +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +20236 -0
- package/bin/voltro.mjs +43 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +9 -0
- package/dist/commands-CfPH2Wf4.js +18061 -0
- package/dist/frameworkInspectState-CX2250XB.js +86 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +5 -0
- package/dist/inspectState.d.ts +116 -0
- package/dist/inspectState.js +2 -0
- package/dist/startup.d.ts +26 -0
- package/dist/startup.js +2 -0
- package/dist/startupRunner-CRhuUl91.js +71 -0
- package/package.json +88 -0
- package/templates/AGENTS.core.md +258 -0
- package/templates/AGENTS.md +351 -0
- package/templates/agent-docs/_index.md +93 -0
- package/templates/agent-docs/_manifest.json +655 -0
- package/templates/agent-docs/ai.md +1845 -0
- package/templates/agent-docs/authentication.md +1788 -0
- package/templates/agent-docs/caching.md +624 -0
- package/templates/agent-docs/cli.md +1650 -0
- package/templates/agent-docs/configuration.md +295 -0
- package/templates/agent-docs/data.md +2172 -0
- package/templates/agent-docs/database/advancedqueries.md +1583 -0
- package/templates/agent-docs/database/columntypes.md +1200 -0
- package/templates/agent-docs/database/hosting.md +881 -0
- package/templates/agent-docs/database/migrations.md +2938 -0
- package/templates/agent-docs/database/misc.md +270 -0
- package/templates/agent-docs/database/overview.md +108 -0
- package/templates/agent-docs/database/querying.md +1622 -0
- package/templates/agent-docs/database/scaling.md +331 -0
- package/templates/agent-docs/database/schema.md +1458 -0
- package/templates/agent-docs/database/seedsdialects.md +1235 -0
- package/templates/agent-docs/database/transactions.md +285 -0
- package/templates/agent-docs/deployment.md +999 -0
- package/templates/agent-docs/internationalization.md +359 -0
- package/templates/agent-docs/introduction.md +438 -0
- package/templates/agent-docs/multi-tenancy.md +610 -0
- package/templates/agent-docs/observability.md +350 -0
- package/templates/agent-docs/plugins.md +1273 -0
- package/templates/agent-docs/reference.md +978 -0
- package/templates/agent-docs/routing.md +1553 -0
- package/templates/agent-docs/scheduling.md +666 -0
- package/templates/agent-docs/schema-driven-ui.md +607 -0
- package/templates/agent-docs/security.md +42 -0
- package/templates/agent-docs/templates/apibackends.md +3214 -0
- package/templates/agent-docs/templates/appshells.md +2062 -0
- package/templates/agent-docs/templates/custom.md +128 -0
- package/templates/agent-docs/templates/overview.md +122 -0
- package/templates/agent-docs/templates/serverless.md +315 -0
- package/templates/agent-docs/testing.md +376 -0
- package/templates/agent-docs/workflows.md +1351 -0
- package/templates/apps/api-ai/README.md +105 -0
- package/templates/apps/api-ai/actions/summarize.action.server.tsx +32 -0
- package/templates/apps/api-ai/actions/summarize.action.ts +33 -0
- package/templates/apps/api-ai/agents/support.agent.server.tsx +31 -0
- package/templates/apps/api-ai/agents/support.agent.tsx +27 -0
- package/templates/apps/api-ai/app.config.ts +57 -0
- package/templates/apps/api-ai/database/schema.ts +67 -0
- package/templates/apps/api-ai/package.json +27 -0
- package/templates/apps/api-ai/seeds/docs.seed.ts +67 -0
- package/templates/apps/api-ai/template.json +6 -0
- package/templates/apps/api-ai/tests/summarize.test.ts +50 -0
- package/templates/apps/api-ai/tools/searchDocs.tool.tsx +60 -0
- package/templates/apps/api-ai/tsconfig.json +5 -0
- package/templates/apps/api-auth/.env +17 -0
- package/templates/apps/api-auth/README.md +109 -0
- package/templates/apps/api-auth/actions/me.action.server.ts +20 -0
- package/templates/apps/api-auth/actions/me.action.ts +19 -0
- package/templates/apps/api-auth/app.config.ts +59 -0
- package/templates/apps/api-auth/database/schema.ts +26 -0
- package/templates/apps/api-auth/package.json +28 -0
- package/templates/apps/api-auth/template.json +6 -0
- package/templates/apps/api-auth/tests/me.test.ts +37 -0
- package/templates/apps/api-auth/tsconfig.json +5 -0
- package/templates/apps/api-backend/README.md +25 -0
- package/templates/apps/api-backend/app.config.ts +39 -0
- package/templates/apps/api-backend/database/schema.ts +57 -0
- package/templates/apps/api-backend/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend/package.json +28 -0
- package/templates/apps/api-backend/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend/template.json +6 -0
- package/templates/apps/api-backend/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend/tsconfig.json +5 -0
- package/templates/apps/api-backend-deactivation/README.md +49 -0
- package/templates/apps/api-backend-deactivation/actions/users.get.action.server.ts +17 -0
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +18 -0
- package/templates/apps/api-backend-deactivation/app.config.ts +20 -0
- package/templates/apps/api-backend-deactivation/database/schema.ts +41 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.server.ts +6 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +16 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.server.ts +10 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +19 -0
- package/templates/apps/api-backend-deactivation/package.json +28 -0
- package/templates/apps/api-backend-deactivation/template.json +6 -0
- package/templates/apps/api-backend-deactivation/tests/users.deactivate.test.ts +47 -0
- package/templates/apps/api-backend-deactivation/tsconfig.json +5 -0
- package/templates/apps/api-backend-mail/README.md +39 -0
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.server.ts +15 -0
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +21 -0
- package/templates/apps/api-backend-mail/app.config.ts +34 -0
- package/templates/apps/api-backend-mail/database/schema.ts +57 -0
- package/templates/apps/api-backend-mail/emails/welcome.email.tsx +106 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-mail/package.json +30 -0
- package/templates/apps/api-backend-mail/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-mail/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-mail/template.json +6 -0
- package/templates/apps/api-backend-mail/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-mail/tsconfig.json +5 -0
- package/templates/apps/api-backend-mariadb/.env.example +35 -0
- package/templates/apps/api-backend-mariadb/README.md +31 -0
- package/templates/apps/api-backend-mariadb/app.config.ts +68 -0
- package/templates/apps/api-backend-mariadb/database/schema.ts +57 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-mariadb/package.json +30 -0
- package/templates/apps/api-backend-mariadb/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-mariadb/template.json +6 -0
- package/templates/apps/api-backend-mariadb/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-mariadb/tsconfig.json +5 -0
- package/templates/apps/api-backend-storage/README.md +86 -0
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.server.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.server.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +20 -0
- package/templates/apps/api-backend-storage/app.config.ts +38 -0
- package/templates/apps/api-backend-storage/database/schema.ts +57 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-storage/package.json +27 -0
- package/templates/apps/api-backend-storage/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-storage/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-storage/template.json +6 -0
- package/templates/apps/api-backend-storage/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-storage/tsconfig.json +5 -0
- package/templates/apps/api-data-advanced/.env +17 -0
- package/templates/apps/api-data-advanced/README.md +88 -0
- package/templates/apps/api-data-advanced/app.config.ts +56 -0
- package/templates/apps/api-data-advanced/database/actors.entity.ts +14 -0
- package/templates/apps/api-data-advanced/database/authors.entity.ts +30 -0
- package/templates/apps/api-data-advanced/database/authors.relations.ts +15 -0
- package/templates/apps/api-data-advanced/database/books.entity.ts +59 -0
- package/templates/apps/api-data-advanced/database/books.relations.ts +10 -0
- package/templates/apps/api-data-advanced/database/index.ts +23 -0
- package/templates/apps/api-data-advanced/database/tenants.entity.ts +11 -0
- package/templates/apps/api-data-advanced/package.json +28 -0
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.server.ts +18 -0
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +40 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.server.ts +17 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +39 -0
- package/templates/apps/api-data-advanced/seeds/catalog.seed.ts +101 -0
- package/templates/apps/api-data-advanced/template.json +6 -0
- package/templates/apps/api-data-advanced/tests/queries.test.ts +124 -0
- package/templates/apps/api-data-advanced/tsconfig.json +5 -0
- package/templates/apps/api-durable/README.md +81 -0
- package/templates/apps/api-durable/aggregates/orderStats.aggregate.ts +49 -0
- package/templates/apps/api-durable/app.config.ts +41 -0
- package/templates/apps/api-durable/database/schema.ts +65 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.server.ts +43 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +23 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +45 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +40 -0
- package/templates/apps/api-durable/package.json +28 -0
- package/templates/apps/api-durable/schedules/nightlyReport.cron.tsx +37 -0
- package/templates/apps/api-durable/startup/warm.startup.tsx +33 -0
- package/templates/apps/api-durable/subscribers/orderChanges.subscribe.ts +27 -0
- package/templates/apps/api-durable/template.json +6 -0
- package/templates/apps/api-durable/tests/orders.place.test.ts +91 -0
- package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +21 -0
- package/templates/apps/api-durable/tsconfig.json +5 -0
- package/templates/apps/api-durable/workflows/order.fulfill.workflow.server.tsx +103 -0
- package/templates/apps/api-durable/workflows/order.fulfill.workflow.tsx +25 -0
- package/templates/apps/api-feature-flags/README.md +63 -0
- package/templates/apps/api-feature-flags/actions/notes.export.action.server.ts +11 -0
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +18 -0
- package/templates/apps/api-feature-flags/app.config.ts +41 -0
- package/templates/apps/api-feature-flags/database/schema.ts +37 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.server.ts +29 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +28 -0
- package/templates/apps/api-feature-flags/package.json +29 -0
- package/templates/apps/api-feature-flags/template.json +6 -0
- package/templates/apps/api-feature-flags/tests/notes.create.test.ts +67 -0
- package/templates/apps/api-feature-flags/tsconfig.json +5 -0
- package/templates/apps/api-governance/.env +4 -0
- package/templates/apps/api-governance/README.md +61 -0
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -0
- package/templates/apps/api-governance/actions/profiles.get.action.ts +17 -0
- package/templates/apps/api-governance/app.config.ts +35 -0
- package/templates/apps/api-governance/database/schema.ts +41 -0
- package/templates/apps/api-governance/mutations/profiles.create.mutation.server.ts +17 -0
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +21 -0
- package/templates/apps/api-governance/package.json +29 -0
- package/templates/apps/api-governance/template.json +6 -0
- package/templates/apps/api-governance/tests/profiles.create.test.ts +59 -0
- package/templates/apps/api-governance/tsconfig.json +5 -0
- package/templates/apps/api-kv/README.md +100 -0
- package/templates/apps/api-kv/actions/sync.pull.action.server.ts +75 -0
- package/templates/apps/api-kv/actions/sync.pull.action.ts +25 -0
- package/templates/apps/api-kv/actions/sync.reset.action.server.ts +25 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +22 -0
- package/templates/apps/api-kv/actions/sync.status.action.server.ts +28 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +17 -0
- package/templates/apps/api-kv/app.config.ts +48 -0
- package/templates/apps/api-kv/database/schema.ts +62 -0
- package/templates/apps/api-kv/package.json +28 -0
- package/templates/apps/api-kv/queries/events.list.query.server.ts +14 -0
- package/templates/apps/api-kv/queries/events.list.query.ts +23 -0
- package/templates/apps/api-kv/template.json +6 -0
- package/templates/apps/api-kv/tests/sync.test.ts +103 -0
- package/templates/apps/api-kv/tsconfig.json +5 -0
- package/templates/apps/api-moderation/README.md +46 -0
- package/templates/apps/api-moderation/app.config.ts +33 -0
- package/templates/apps/api-moderation/database/schema.ts +45 -0
- package/templates/apps/api-moderation/mutations/comments.create.mutation.server.ts +8 -0
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +20 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.server.ts +9 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +25 -0
- package/templates/apps/api-moderation/package.json +29 -0
- package/templates/apps/api-moderation/template.json +6 -0
- package/templates/apps/api-moderation/tests/posts.create.test.ts +79 -0
- package/templates/apps/api-moderation/tsconfig.json +5 -0
- package/templates/apps/api-observability/README.md +106 -0
- package/templates/apps/api-observability/app.config.ts +32 -0
- package/templates/apps/api-observability/database/schema.ts +38 -0
- package/templates/apps/api-observability/mutations/notes.create.mutation.server.ts +33 -0
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +15 -0
- package/templates/apps/api-observability/package.json +29 -0
- package/templates/apps/api-observability/queries/notes.list.query.server.ts +7 -0
- package/templates/apps/api-observability/queries/notes.list.query.ts +14 -0
- package/templates/apps/api-observability/template.json +6 -0
- package/templates/apps/api-observability/tests/notes.create.test.ts +36 -0
- package/templates/apps/api-observability/tsconfig.json +5 -0
- package/templates/apps/api-ratelimit/README.md +49 -0
- package/templates/apps/api-ratelimit/app.config.ts +42 -0
- package/templates/apps/api-ratelimit/database/schema.ts +37 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.server.ts +16 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +34 -0
- package/templates/apps/api-ratelimit/package.json +29 -0
- package/templates/apps/api-ratelimit/template.json +6 -0
- package/templates/apps/api-ratelimit/tests/notes.create.test.ts +69 -0
- package/templates/apps/api-ratelimit/tsconfig.json +5 -0
- package/templates/apps/api-rbac/README.md +59 -0
- package/templates/apps/api-rbac/app.config.ts +48 -0
- package/templates/apps/api-rbac/database/schema.ts +37 -0
- package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +22 -0
- package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +26 -0
- package/templates/apps/api-rbac/package.json +29 -0
- package/templates/apps/api-rbac/template.json +6 -0
- package/templates/apps/api-rbac/tests/notes.create.test.ts +80 -0
- package/templates/apps/api-rbac/tsconfig.json +5 -0
- package/templates/apps/api-rest/README.md +85 -0
- package/templates/apps/api-rest/app.config.ts +70 -0
- package/templates/apps/api-rest/database/schema.ts +52 -0
- package/templates/apps/api-rest/lib/product.ts +30 -0
- package/templates/apps/api-rest/package.json +27 -0
- package/templates/apps/api-rest/routes/v1/products.create.route.tsx +45 -0
- package/templates/apps/api-rest/routes/v1/products.delete.route.tsx +25 -0
- package/templates/apps/api-rest/routes/v1/products.get.route.tsx +29 -0
- package/templates/apps/api-rest/routes/v1/products.list.route.tsx +43 -0
- package/templates/apps/api-rest/template.json +6 -0
- package/templates/apps/api-rest/tests/products.create.test.ts +53 -0
- package/templates/apps/api-rest/tsconfig.json +5 -0
- package/templates/apps/api-saas/README.md +106 -0
- package/templates/apps/api-saas/app.config.ts +51 -0
- package/templates/apps/api-saas/database/schema.ts +50 -0
- package/templates/apps/api-saas/mutations/projects.create.mutation.server.ts +52 -0
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +18 -0
- package/templates/apps/api-saas/package.json +31 -0
- package/templates/apps/api-saas/queries/projects.list.query.server.ts +7 -0
- package/templates/apps/api-saas/queries/projects.list.query.ts +18 -0
- package/templates/apps/api-saas/template.json +6 -0
- package/templates/apps/api-saas/tests/projects.create.test.ts +47 -0
- package/templates/apps/api-saas/tsconfig.json +5 -0
- package/templates/apps/api-search/README.md +68 -0
- package/templates/apps/api-search/app.config.ts +31 -0
- package/templates/apps/api-search/database/schema.ts +47 -0
- package/templates/apps/api-search/lib/search.ts +27 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.server.ts +19 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +35 -0
- package/templates/apps/api-search/package.json +29 -0
- package/templates/apps/api-search/queries/articles.list.query.server.ts +14 -0
- package/templates/apps/api-search/queries/articles.list.query.ts +19 -0
- package/templates/apps/api-search/seeds/articles.seed.ts +41 -0
- package/templates/apps/api-search/startup/searchBackfill.startup.tsx +24 -0
- package/templates/apps/api-search/template.json +6 -0
- package/templates/apps/api-search/tests/articles.create.test.ts +69 -0
- package/templates/apps/api-search/tsconfig.json +5 -0
- package/templates/apps/api-versioning/README.md +51 -0
- package/templates/apps/api-versioning/actions/documents.asOf.action.server.ts +13 -0
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +14 -0
- package/templates/apps/api-versioning/actions/documents.history.action.server.ts +15 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +17 -0
- package/templates/apps/api-versioning/app.config.ts +24 -0
- package/templates/apps/api-versioning/database/schema.ts +38 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.server.ts +8 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +21 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.server.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +21 -0
- package/templates/apps/api-versioning/package.json +29 -0
- package/templates/apps/api-versioning/template.json +6 -0
- package/templates/apps/api-versioning/tests/documents.create.test.ts +37 -0
- package/templates/apps/api-versioning/tsconfig.json +5 -0
- package/templates/apps/api-webhooks/.env +6 -0
- package/templates/apps/api-webhooks/README.md +106 -0
- package/templates/apps/api-webhooks/app.config.ts +16 -0
- package/templates/apps/api-webhooks/database/schema.ts +49 -0
- package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +22 -0
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.server.ts +37 -0
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +15 -0
- package/templates/apps/api-webhooks/package.json +28 -0
- package/templates/apps/api-webhooks/queries/orders.list.query.server.ts +7 -0
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +15 -0
- package/templates/apps/api-webhooks/template.json +6 -0
- package/templates/apps/api-webhooks/tests/orders.fulfill.test.ts +51 -0
- package/templates/apps/api-webhooks/tsconfig.json +5 -0
- package/templates/apps/api-webhooks/webhooks/orders.webhook.tsx +35 -0
- package/templates/apps/changelog/README.md +77 -0
- package/templates/apps/changelog/app.config.ts +27 -0
- package/templates/apps/changelog/content/releases/0.1.0.mdx +19 -0
- package/templates/apps/changelog/content/releases/0.2.0.mdx +28 -0
- package/templates/apps/changelog/package.json +30 -0
- package/templates/apps/changelog/scripts/generate-rss.mjs +38 -0
- package/templates/apps/changelog/src/globals.css +66 -0
- package/templates/apps/changelog/src/globals.d.ts +16 -0
- package/templates/apps/changelog/src/lib/locale.test.ts +72 -0
- package/templates/apps/changelog/src/lib/locale.ts +55 -0
- package/templates/apps/changelog/src/lib/releases.ts +21 -0
- package/templates/apps/changelog/src/locales/de.ts +22 -0
- package/templates/apps/changelog/src/locales/en.ts +30 -0
- package/templates/apps/changelog/src/pages/[locale]/[slug].tsx +24 -0
- package/templates/apps/changelog/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +95 -0
- package/templates/apps/changelog/src/pages/[slug].test.tsx +127 -0
- package/templates/apps/changelog/src/pages/[slug].tsx +52 -0
- package/templates/apps/changelog/src/pages/index.test.tsx +112 -0
- package/templates/apps/changelog/src/pages/index.tsx +55 -0
- package/templates/apps/changelog/src/pages/layout.tsx +84 -0
- package/templates/apps/changelog/template.json +6 -0
- package/templates/apps/changelog/tsconfig.json +11 -0
- package/templates/apps/edge-functions/README.md +55 -0
- package/templates/apps/edge-functions/functions/aiComplete.serverless.ts +58 -0
- package/templates/apps/edge-functions/functions/currencyConvert.serverless.ts +47 -0
- package/templates/apps/edge-functions/functions/geoGreeting.serverless.ts +44 -0
- package/templates/apps/edge-functions/functions/health.serverless.ts +29 -0
- package/templates/apps/edge-functions/functions/resolveLink.serverless.ts +32 -0
- package/templates/apps/edge-functions/functions/shareLink.serverless.ts +30 -0
- package/templates/apps/edge-functions/functions/slackNotify.serverless.ts +40 -0
- package/templates/apps/edge-functions/functions/verifySignature.serverless.ts +52 -0
- package/templates/apps/edge-functions/package.json +21 -0
- package/templates/apps/edge-functions/template.json +6 -0
- package/templates/apps/edge-functions/tsconfig.json +10 -0
- package/templates/apps/frontend-admin/README.md +40 -0
- package/templates/apps/frontend-admin/app.config.ts +37 -0
- package/templates/apps/frontend-admin/package.json +30 -0
- package/templates/apps/frontend-admin/src/config.ts +8 -0
- package/templates/apps/frontend-admin/src/globals.css +76 -0
- package/templates/apps/frontend-admin/src/globals.d.ts +6 -0
- package/templates/apps/frontend-admin/src/lib/admin.ts +16 -0
- package/templates/apps/frontend-admin/src/lib/auth.ts +24 -0
- package/templates/apps/frontend-admin/src/locales/de.ts +67 -0
- package/templates/apps/frontend-admin/src/locales/en.ts +79 -0
- package/templates/apps/frontend-admin/src/locales/index.ts +15 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/index.test.tsx +55 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/index.tsx +32 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/layout.tsx +29 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/login.test.tsx +73 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/login.tsx +35 -0
- package/templates/apps/frontend-admin/src/pages/admin/[entity].tsx +121 -0
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +119 -0
- package/templates/apps/frontend-admin/src/pages/admin/error.tsx +20 -0
- package/templates/apps/frontend-admin/src/pages/admin/fallbacks.test.tsx +68 -0
- package/templates/apps/frontend-admin/src/pages/admin/index.test.tsx +88 -0
- package/templates/apps/frontend-admin/src/pages/admin/index.tsx +65 -0
- package/templates/apps/frontend-admin/src/pages/admin/layout.test.tsx +114 -0
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +97 -0
- package/templates/apps/frontend-admin/src/pages/admin/loading.tsx +15 -0
- package/templates/apps/frontend-admin/src/pages/admin/not-found.tsx +15 -0
- package/templates/apps/frontend-admin/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-admin/src/pages/layouts.test.tsx +54 -0
- package/templates/apps/frontend-admin/template.json +6 -0
- package/templates/apps/frontend-admin/tsconfig.json +11 -0
- package/templates/apps/frontend-app/README.md +98 -0
- package/templates/apps/frontend-app/app.config.ts +43 -0
- package/templates/apps/frontend-app/package.json +30 -0
- package/templates/apps/frontend-app/src/locales/de.ts +36 -0
- package/templates/apps/frontend-app/src/locales/en.ts +43 -0
- package/templates/apps/frontend-app/src/locales/index.ts +15 -0
- package/templates/apps/frontend-app/src/pages/index.test.tsx +167 -0
- package/templates/apps/frontend-app/src/pages/index.tsx +136 -0
- package/templates/apps/frontend-app/src/pages/layout.tsx +41 -0
- package/templates/apps/frontend-app/src/pages/schema-ui.test.tsx +99 -0
- package/templates/apps/frontend-app/src/pages/schema-ui.tsx +74 -0
- package/templates/apps/frontend-app/template.json +6 -0
- package/templates/apps/frontend-app/tsconfig.json +11 -0
- package/templates/apps/frontend-blank/README.md +18 -0
- package/templates/apps/frontend-blank/app.config.ts +29 -0
- package/templates/apps/frontend-blank/package.json +29 -0
- package/templates/apps/frontend-blank/src/locales/de.ts +15 -0
- package/templates/apps/frontend-blank/src/locales/en.ts +22 -0
- package/templates/apps/frontend-blank/src/locales/index.ts +15 -0
- package/templates/apps/frontend-blank/src/pages/index.test.tsx +55 -0
- package/templates/apps/frontend-blank/src/pages/index.tsx +27 -0
- package/templates/apps/frontend-blank/src/pages/layout.test.tsx +54 -0
- package/templates/apps/frontend-blank/src/pages/layout.tsx +35 -0
- package/templates/apps/frontend-blank/template.json +6 -0
- package/templates/apps/frontend-blank/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/README.md +65 -0
- package/templates/apps/frontend-contact/app.config.ts +25 -0
- package/templates/apps/frontend-contact/functions/sendMessage.serverless.ts +69 -0
- package/templates/apps/frontend-contact/package.json +33 -0
- package/templates/apps/frontend-contact/src/components/ContactForm.island.test.tsx +142 -0
- package/templates/apps/frontend-contact/src/components/ContactForm.island.tsx +104 -0
- package/templates/apps/frontend-contact/src/config.ts +12 -0
- package/templates/apps/frontend-contact/src/globals.css +84 -0
- package/templates/apps/frontend-contact/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-contact/src/locales/de.ts +26 -0
- package/templates/apps/frontend-contact/src/locales/en.ts +29 -0
- package/templates/apps/frontend-contact/src/pages/[locale]/index.tsx +14 -0
- package/templates/apps/frontend-contact/src/pages/index.test.tsx +70 -0
- package/templates/apps/frontend-contact/src/pages/index.tsx +63 -0
- package/templates/apps/frontend-contact/src/pages/layout.tsx +65 -0
- package/templates/apps/frontend-contact/template.json +6 -0
- package/templates/apps/frontend-contact/tsconfig.json +11 -0
- package/templates/apps/frontend-dashboard/README.md +54 -0
- package/templates/apps/frontend-dashboard/app.config.ts +39 -0
- package/templates/apps/frontend-dashboard/package.json +29 -0
- package/templates/apps/frontend-dashboard/src/config.ts +8 -0
- package/templates/apps/frontend-dashboard/src/globals.css +72 -0
- package/templates/apps/frontend-dashboard/src/globals.d.ts +6 -0
- package/templates/apps/frontend-dashboard/src/lib/auth.ts +27 -0
- package/templates/apps/frontend-dashboard/src/locales/de.ts +49 -0
- package/templates/apps/frontend-dashboard/src/locales/en.ts +60 -0
- package/templates/apps/frontend-dashboard/src/locales/index.ts +15 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.test.tsx +55 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.tsx +34 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/layout.tsx +32 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.test.tsx +74 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.tsx +40 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/error.tsx +21 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/fallbacks.test.tsx +68 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/index.test.tsx +55 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/index.tsx +38 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.test.tsx +88 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.tsx +69 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/loading.tsx +21 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/not-found.tsx +19 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.test.tsx +69 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.tsx +35 -0
- package/templates/apps/frontend-dashboard/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-dashboard/src/pages/layouts.test.tsx +54 -0
- package/templates/apps/frontend-dashboard/template.json +6 -0
- package/templates/apps/frontend-dashboard/tsconfig.json +11 -0
- package/templates/apps/frontend-docs/README.md +19 -0
- package/templates/apps/frontend-docs/app.config.ts +25 -0
- package/templates/apps/frontend-docs/package.json +29 -0
- package/templates/apps/frontend-docs/src/globals.css +32 -0
- package/templates/apps/frontend-docs/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-docs/src/lib/locale.ts +56 -0
- package/templates/apps/frontend-docs/src/locales/de.ts +24 -0
- package/templates/apps/frontend-docs/src/locales/en.ts +28 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug].tsx +18 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +67 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug].test.tsx +115 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug].tsx +82 -0
- package/templates/apps/frontend-docs/src/pages/index.test.tsx +90 -0
- package/templates/apps/frontend-docs/src/pages/index.tsx +45 -0
- package/templates/apps/frontend-docs/src/pages/layout.test.tsx +84 -0
- package/templates/apps/frontend-docs/src/pages/layout.tsx +66 -0
- package/templates/apps/frontend-docs/template.json +6 -0
- package/templates/apps/frontend-docs/tsconfig.json +11 -0
- package/templates/apps/frontend-i18n/README.md +61 -0
- package/templates/apps/frontend-i18n/app.config.ts +33 -0
- package/templates/apps/frontend-i18n/package.json +28 -0
- package/templates/apps/frontend-i18n/src/globals.css +46 -0
- package/templates/apps/frontend-i18n/src/globals.d.ts +6 -0
- package/templates/apps/frontend-i18n/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-i18n/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-i18n/src/locales/de.ts +24 -0
- package/templates/apps/frontend-i18n/src/locales/en.ts +25 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/about.tsx +11 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +50 -0
- package/templates/apps/frontend-i18n/src/pages/about.test.tsx +75 -0
- package/templates/apps/frontend-i18n/src/pages/about.tsx +31 -0
- package/templates/apps/frontend-i18n/src/pages/index.test.tsx +102 -0
- package/templates/apps/frontend-i18n/src/pages/index.tsx +43 -0
- package/templates/apps/frontend-i18n/src/pages/layout.test.tsx +86 -0
- package/templates/apps/frontend-i18n/src/pages/layout.tsx +70 -0
- package/templates/apps/frontend-i18n/template.json +6 -0
- package/templates/apps/frontend-i18n/tsconfig.json +11 -0
- package/templates/apps/frontend-landing/README.md +17 -0
- package/templates/apps/frontend-landing/app.config.ts +25 -0
- package/templates/apps/frontend-landing/package.json +29 -0
- package/templates/apps/frontend-landing/src/globals.css +23 -0
- package/templates/apps/frontend-landing/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-landing/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-landing/src/locales/de.ts +24 -0
- package/templates/apps/frontend-landing/src/locales/en.ts +26 -0
- package/templates/apps/frontend-landing/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +37 -0
- package/templates/apps/frontend-landing/src/pages/index.test.tsx +110 -0
- package/templates/apps/frontend-landing/src/pages/index.tsx +75 -0
- package/templates/apps/frontend-landing/src/pages/layout.test.tsx +84 -0
- package/templates/apps/frontend-landing/src/pages/layout.tsx +66 -0
- package/templates/apps/frontend-landing/template.json +6 -0
- package/templates/apps/frontend-landing/tsconfig.json +11 -0
- package/templates/apps/frontend-spa/README.md +45 -0
- package/templates/apps/frontend-spa/app.config.ts +27 -0
- package/templates/apps/frontend-spa/package.json +29 -0
- package/templates/apps/frontend-spa/src/globals.css +84 -0
- package/templates/apps/frontend-spa/src/locales/de.ts +22 -0
- package/templates/apps/frontend-spa/src/locales/en.ts +29 -0
- package/templates/apps/frontend-spa/src/locales/index.ts +15 -0
- package/templates/apps/frontend-spa/src/pages/index.test.tsx +137 -0
- package/templates/apps/frontend-spa/src/pages/index.tsx +123 -0
- package/templates/apps/frontend-spa/src/pages/layout.tsx +27 -0
- package/templates/apps/frontend-spa/template.json +6 -0
- package/templates/apps/frontend-spa/tsconfig.json +11 -0
- package/templates/apps/frontend-ssr/README.md +68 -0
- package/templates/apps/frontend-ssr/app.config.ts +32 -0
- package/templates/apps/frontend-ssr/package.json +29 -0
- package/templates/apps/frontend-ssr/src/globals.css +67 -0
- package/templates/apps/frontend-ssr/src/locales/de.ts +41 -0
- package/templates/apps/frontend-ssr/src/locales/en.ts +54 -0
- package/templates/apps/frontend-ssr/src/locales/index.ts +16 -0
- package/templates/apps/frontend-ssr/src/pages/feed-swr.test.tsx +69 -0
- package/templates/apps/frontend-ssr/src/pages/feed-swr.tsx +54 -0
- package/templates/apps/frontend-ssr/src/pages/feed.test.tsx +73 -0
- package/templates/apps/frontend-ssr/src/pages/feed.tsx +64 -0
- package/templates/apps/frontend-ssr/src/pages/index.test.tsx +89 -0
- package/templates/apps/frontend-ssr/src/pages/index.tsx +72 -0
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +37 -0
- package/templates/apps/frontend-ssr/template.json +6 -0
- package/templates/apps/frontend-ssr/tsconfig.json +11 -0
- package/templates/apps/frontend-ssr-api/README.md +50 -0
- package/templates/apps/frontend-ssr-api/app.config.ts +43 -0
- package/templates/apps/frontend-ssr-api/package.json +30 -0
- package/templates/apps/frontend-ssr-api/src/globals.css +38 -0
- package/templates/apps/frontend-ssr-api/src/globals.d.ts +6 -0
- package/templates/apps/frontend-ssr-api/src/locales/de.ts +20 -0
- package/templates/apps/frontend-ssr-api/src/locales/en.ts +31 -0
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +16 -0
- package/templates/apps/frontend-ssr-api/src/pages/index.test.tsx +105 -0
- package/templates/apps/frontend-ssr-api/src/pages/index.tsx +83 -0
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +28 -0
- package/templates/apps/frontend-ssr-api/template.json +6 -0
- package/templates/apps/frontend-ssr-api/tsconfig.json +11 -0
- package/templates/apps/frontend-static-blog/README.md +49 -0
- package/templates/apps/frontend-static-blog/app.config.ts +34 -0
- package/templates/apps/frontend-static-blog/package.json +28 -0
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.test.tsx +65 -0
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.tsx +35 -0
- package/templates/apps/frontend-static-blog/src/content/posts.ts +64 -0
- package/templates/apps/frontend-static-blog/src/globals.css +75 -0
- package/templates/apps/frontend-static-blog/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-static-blog/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-static-blog/src/locales/de.ts +19 -0
- package/templates/apps/frontend-static-blog/src/locales/en.ts +26 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug].tsx +20 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +62 -0
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug].test.tsx +116 -0
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug].tsx +69 -0
- package/templates/apps/frontend-static-blog/src/pages/index.test.tsx +100 -0
- package/templates/apps/frontend-static-blog/src/pages/index.tsx +58 -0
- package/templates/apps/frontend-static-blog/src/pages/layout.tsx +63 -0
- package/templates/apps/frontend-static-blog/template.json +6 -0
- package/templates/apps/frontend-static-blog/tsconfig.json +11 -0
- package/templates/baselines/bare/.env.example +37 -0
- package/templates/baselines/bare/README.md +47 -0
- package/templates/baselines/bare/baseline.json +31 -0
- package/templates/baselines/bare/deploy/README.md +43 -0
- package/templates/baselines/bare/deploy/voltro.service.example +36 -0
- package/templates/baselines/compose/.env.example +46 -0
- package/templates/baselines/compose/README.md +69 -0
- package/templates/baselines/compose/baseline.json +51 -0
- package/templates/baselines/compose/docker/.dockerignore +38 -0
- package/templates/baselines/compose/docker/api.Dockerfile +57 -0
- package/templates/baselines/compose/docker/dev.Dockerfile +35 -0
- package/templates/baselines/compose/docker/web.Dockerfile +59 -0
- package/templates/baselines/compose/docker-compose.dev.yml +89 -0
- package/templates/baselines/compose/docker-compose.prod.yml +87 -0
- package/templates/baselines/compose/docker-compose.yml +41 -0
- package/templates/baselines/compose-mariadb/.env.example +57 -0
- package/templates/baselines/compose-mariadb/README.md +78 -0
- package/templates/baselines/compose-mariadb/baseline.json +51 -0
- package/templates/baselines/compose-mariadb/docker/.dockerignore +38 -0
- package/templates/baselines/compose-mariadb/docker/api.Dockerfile +57 -0
- package/templates/baselines/compose-mariadb/docker/dev.Dockerfile +35 -0
- package/templates/baselines/compose-mariadb/docker/mariadb-init.sql +6 -0
- package/templates/baselines/compose-mariadb/docker/web.Dockerfile +59 -0
- package/templates/baselines/compose-mariadb/docker-compose.dev.yml +117 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +114 -0
- package/templates/baselines/compose-mariadb/docker-compose.yml +79 -0
- package/templates/baselines/helm/.env.example +39 -0
- package/templates/baselines/helm/README.md +98 -0
- package/templates/baselines/helm/baseline.json +53 -0
- package/templates/baselines/helm/charts/voltro-app/.helmignore +10 -0
- package/templates/baselines/helm/charts/voltro-app/Chart.yaml +10 -0
- package/templates/baselines/helm/charts/voltro-app/templates/_helpers.tpl +36 -0
- package/templates/baselines/helm/charts/voltro-app/templates/configmap.yaml +13 -0
- package/templates/baselines/helm/charts/voltro-app/templates/deployment-api.yaml +120 -0
- package/templates/baselines/helm/charts/voltro-app/templates/deployment-web.yaml +45 -0
- package/templates/baselines/helm/charts/voltro-app/templates/ingress.yaml +37 -0
- package/templates/baselines/helm/charts/voltro-app/templates/postgres-service.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/templates/postgres-statefulset.yaml +73 -0
- package/templates/baselines/helm/charts/voltro-app/templates/secret.yaml +33 -0
- package/templates/baselines/helm/charts/voltro-app/templates/service-api.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/templates/service-web.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/values-dev.yaml +21 -0
- package/templates/baselines/helm/charts/voltro-app/values-prod.yaml +58 -0
- package/templates/baselines/helm/charts/voltro-app/values-staging.yaml +25 -0
- package/templates/baselines/helm/charts/voltro-app/values.yaml +109 -0
- package/templates/baselines/helm/deploy/README.md +94 -0
- package/templates/patches/@effect__cluster@0.59.0.patch +262 -0
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
# database.columnTypes
|
|
2
|
+
|
|
3
|
+
> Postgres-native ENUM types via dbEnum() — cheap ADD VALUE migrations, full type-narrowing. Falls back to CHECK constraints on other dialects.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<!-- source: en/database/enums.md -->
|
|
10
|
+
## Enums (dbEnum)
|
|
11
|
+
|
|
12
|
+
_Postgres-native ENUM types via dbEnum() — cheap ADD VALUE migrations, full type-narrowing. Falls back to CHECK constraints on other dialects._
|
|
13
|
+
|
|
14
|
+
`dbEnum('name', [values])` declares a postgres-native ENUM type
|
|
15
|
+
that's reusable across columns and tables. The literal value union
|
|
16
|
+
narrows the row type at compile time; cheap `ALTER TYPE ... ADD
|
|
17
|
+
VALUE` migrations replace CHECK-rewrite pain.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { dbEnum, id, table } from '@voltro/database'
|
|
23
|
+
|
|
24
|
+
export const orderStatus = dbEnum('order_status', ['pending', 'paid', 'shipped'] as const)
|
|
25
|
+
|
|
26
|
+
export const orders = table('orders', {
|
|
27
|
+
id: id(),
|
|
28
|
+
status: orderStatus.column().default('pending'),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// row.status is typed `'pending' | 'paid' | 'shipped'`, not plain string
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Why this exists
|
|
35
|
+
|
|
36
|
+
Voltro has two ways to enforce a closed value set on a text column:
|
|
37
|
+
|
|
38
|
+
1. **`text().oneOf(['a', 'b'])`** — emits a CHECK constraint.
|
|
39
|
+
Cross-dialect, works on every backend, but ADD VALUE is a CHECK
|
|
40
|
+
rewrite (expensive on a 10M-row table).
|
|
41
|
+
2. **`dbEnum('name', [...])`** — postgres-native ENUM type.
|
|
42
|
+
`ALTER TYPE ... ADD VALUE 'new_value'` is O(1).
|
|
43
|
+
|
|
44
|
+
Use `dbEnum` when:
|
|
45
|
+
- You're on postgres AND
|
|
46
|
+
- The value set is reasonably stable but might grow occasionally AND
|
|
47
|
+
- You want the value set self-documenting in the schema
|
|
48
|
+
|
|
49
|
+
Use `oneOf` when:
|
|
50
|
+
- You're on multiple dialects (the framework's cross-dialect fallback
|
|
51
|
+
for `dbEnum` is the same CHECK constraint, but `oneOf` keeps the
|
|
52
|
+
source code aligned with reality).
|
|
53
|
+
- The value set is highly stable (the cheap ADD VALUE doesn't help).
|
|
54
|
+
|
|
55
|
+
## Reuse across tables
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
export const status = dbEnum('item_status', ['draft', 'published', 'archived'] as const)
|
|
59
|
+
|
|
60
|
+
export const articles = table('articles', { id: id(), status: status.column() })
|
|
61
|
+
export const videos = table('videos', { id: id(), state: status.column() })
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The `CREATE TYPE` DDL emits exactly once — the framework dedups by
|
|
65
|
+
enum name across all schema tables. Defining the same name with
|
|
66
|
+
different value sets throws at DDL time.
|
|
67
|
+
|
|
68
|
+
## Defaults
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
status: orderStatus.column().default('pending')
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Default must be one of the declared values — TypeScript narrows the
|
|
75
|
+
`.default(...)` parameter to the literal union.
|
|
76
|
+
|
|
77
|
+
## ADD VALUE migrations
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// Day 1
|
|
81
|
+
export const orderStatus = dbEnum('order_status', ['pending', 'paid', 'shipped'] as const)
|
|
82
|
+
|
|
83
|
+
// Day 30 — add a value
|
|
84
|
+
export const orderStatus = dbEnum('order_status', ['pending', 'paid', 'shipped', 'returned'] as const)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`voltro db plan` detects the new value and emits an `add-enum-value`
|
|
88
|
+
op:
|
|
89
|
+
|
|
90
|
+
```sql
|
|
91
|
+
ALTER TYPE "order_status" ADD VALUE 'returned';
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
On postgres this is O(1) — no table rewrite. On
|
|
95
|
+
mysql/mariadb/mssql/sqlite/turso (CHECK fallback) the planner emits a
|
|
96
|
+
CHECK-recreate, which is a brief lock but doesn't rewrite the table.
|
|
97
|
+
|
|
98
|
+
## Renaming values
|
|
99
|
+
|
|
100
|
+
A value rename looks identical to a drop+add in a naive value-set
|
|
101
|
+
diff — and a drop+add would orphan every row already storing the old
|
|
102
|
+
label. So a rename needs explicit INTENT, exactly like a column rename
|
|
103
|
+
needs `.renamedFrom('oldName')`. Declare it with `declareEnumRename`,
|
|
104
|
+
placed next to the `dbEnum(...)` whose value array now carries the new
|
|
105
|
+
label:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { dbEnum, declareEnumRename } from '@voltro/database'
|
|
109
|
+
|
|
110
|
+
// The enum now spells the NEW value.
|
|
111
|
+
export const orderStatus = dbEnum('order_status', ['awaiting', 'paid', 'shipped'] as const)
|
|
112
|
+
|
|
113
|
+
// Record that 'pending' was renamed to 'awaiting'.
|
|
114
|
+
declareEnumRename('order_status', { from: 'pending', to: 'awaiting' })
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The migrator then emits a guarded, data-preserving rename — it fires
|
|
118
|
+
only when the OLD label still exists and the NEW one doesn't, so
|
|
119
|
+
re-runs are no-ops:
|
|
120
|
+
|
|
121
|
+
```sql
|
|
122
|
+
DO $$
|
|
123
|
+
DECLARE type_oid oid;
|
|
124
|
+
BEGIN
|
|
125
|
+
SELECT oid INTO type_oid FROM pg_type WHERE typname = 'order_status';
|
|
126
|
+
IF type_oid IS NOT NULL
|
|
127
|
+
AND EXISTS (SELECT 1 FROM pg_enum WHERE enumtypid = type_oid AND enumlabel = 'pending')
|
|
128
|
+
AND NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumtypid = type_oid AND enumlabel = 'awaiting') THEN
|
|
129
|
+
IF current_setting('server_version_num')::int >= 140000 THEN
|
|
130
|
+
EXECUTE format('ALTER TYPE %I RENAME VALUE %L TO %L', 'order_status', 'pending', 'awaiting');
|
|
131
|
+
ELSE
|
|
132
|
+
UPDATE pg_enum SET enumlabel = 'awaiting' WHERE enumtypid = type_oid AND enumlabel = 'pending';
|
|
133
|
+
END IF;
|
|
134
|
+
END IF;
|
|
135
|
+
END $$;
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- **Postgres 14+** uses the native `ALTER TYPE … RENAME VALUE 'old' TO
|
|
139
|
+
'new'` — cheap, in-place, no table rewrite.
|
|
140
|
+
- **Postgres 13 and below** falls back to a data-preserving
|
|
141
|
+
`pg_enum.enumlabel` catalog update — every row keeps its value, the
|
|
142
|
+
label is simply re-spelled.
|
|
143
|
+
- **Non-postgres dialects** store enums as text + a CHECK constraint,
|
|
144
|
+
so the new value list lands in the recreated CHECK on its own — no
|
|
145
|
+
dedicated rename DDL is needed.
|
|
146
|
+
|
|
147
|
+
Keep the `declareEnumRename(...)` declaration until the rename has
|
|
148
|
+
applied in every environment you care about, then remove it (the new
|
|
149
|
+
value is by then the only label the enum knows).
|
|
150
|
+
|
|
151
|
+
## Cross-dialect
|
|
152
|
+
|
|
153
|
+
| Dialect | Emits |
|
|
154
|
+
|------------------|------------------------------------------------------------------|
|
|
155
|
+
| postgres | `CREATE TYPE "name" AS ENUM (...)` + `<col> name` (native) |
|
|
156
|
+
| mysql / mariadb | `<col> ENUM('a','b','c')` (native enum column type) |
|
|
157
|
+
| mssql / sqlite | `<col> NVARCHAR(255) / TEXT` + `CHECK (col IN ('a','b','c'))` (fallback) |
|
|
158
|
+
|
|
159
|
+
The postgres path is the cheap one; the rest treat enums as
|
|
160
|
+
text + CHECK behind the scenes.
|
|
161
|
+
|
|
162
|
+
## When NOT to use
|
|
163
|
+
|
|
164
|
+
- **You're on mysql or sqlite primarily** — the postgres-only benefit
|
|
165
|
+
doesn't apply. Use `text().oneOf([...])` for consistency.
|
|
166
|
+
- **You need value-level metadata** (descriptions, sort orders) —
|
|
167
|
+
enums are just strings. Build a lookup table.
|
|
168
|
+
- **The value set changes weekly** — every change is still a schema
|
|
169
|
+
migration. For high-churn lists, store the values in a table.
|
|
170
|
+
|
|
171
|
+
## See also
|
|
172
|
+
|
|
173
|
+
- [Columns](/docs/database/columns) — `text().oneOf([...])` for the
|
|
174
|
+
cross-dialect CHECK form
|
|
175
|
+
- [Migrations](/docs/database/migrations/operation-classes) — the
|
|
176
|
+
planner's classification for enum changes
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
<!-- source: en/database/generated-columns.md -->
|
|
183
|
+
## Generated columns (DB-level computed)
|
|
184
|
+
|
|
185
|
+
_Columns the DB engine computes from other columns. Distinct from `.computed()` which runs at INSERT in the app._
|
|
186
|
+
|
|
187
|
+
`.generatedAs(expression, { stored? })` declares a column whose
|
|
188
|
+
value the DB engine computes from other columns on the same row.
|
|
189
|
+
The framework keeps it consistent on INSERT + UPDATE automatically
|
|
190
|
+
— no app-side stamping needed.
|
|
191
|
+
|
|
192
|
+
## Quick start
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { text, id, table } from '@voltro/database'
|
|
196
|
+
|
|
197
|
+
export const people = table('people', {
|
|
198
|
+
id: id(),
|
|
199
|
+
firstName: text(),
|
|
200
|
+
lastName: text(),
|
|
201
|
+
fullName: text().generatedAs(`"firstName" || ' ' || "lastName"`, { stored: true }),
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Insert without setting fullName — the DB fills it
|
|
205
|
+
await ctx.store.insert('people', { firstName: 'Mario', lastName: 'Lima' })
|
|
206
|
+
// Read it back
|
|
207
|
+
const rows = await ctx.store.query(database.people.descriptor)
|
|
208
|
+
rows[0]?.fullName // → 'Mario Lima'
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## STORED vs VIRTUAL
|
|
212
|
+
|
|
213
|
+
`{ stored: true }` (the rare-but-useful case):
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
fullName: text().generatedAs(`"firstName" || ' ' || "lastName"`, { stored: true })
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- Value is **persisted on disk** — same storage cost as a regular column.
|
|
220
|
+
- **Indexable** — combine with `.expressionIndex('byFullName', ['fullName'])`
|
|
221
|
+
for cheap lookups.
|
|
222
|
+
- Required when the column drives full-text search (see [Full-text search](/docs/database/full-text-search))
|
|
223
|
+
or any other indexed access pattern.
|
|
224
|
+
|
|
225
|
+
`{ stored: false }` (the default):
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
fullName: text().generatedAs(`"firstName" || ' ' || "lastName"`)
|
|
229
|
+
// stored: false
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- Value is **recomputed on every read** — no disk cost.
|
|
233
|
+
- **NOT indexable** — the DB has nothing physical to index.
|
|
234
|
+
- Postgres has no VIRTUAL form; the framework auto-promotes to
|
|
235
|
+
STORED and logs a console.warn at boot so you know.
|
|
236
|
+
|
|
237
|
+
| Dialect | STORED | VIRTUAL |
|
|
238
|
+
|------------------|-------------------------------------------------|--------------------------|
|
|
239
|
+
| postgres 12+ | `GENERATED ALWAYS AS (expr) STORED` | not supported → STORED (warns) |
|
|
240
|
+
| mysql 5.7+ | `GENERATED ALWAYS AS (expr) STORED` | `GENERATED ALWAYS AS (expr) VIRTUAL` |
|
|
241
|
+
| mariadb 10.2+ | same as mysql | same as mysql |
|
|
242
|
+
| mssql | `AS (expr) PERSISTED` | `AS (expr)` (computed col, not persisted) |
|
|
243
|
+
| sqlite 3.31+ | `GENERATED ALWAYS AS (expr) STORED` | `GENERATED ALWAYS AS (expr) VIRTUAL` (default) |
|
|
244
|
+
|
|
245
|
+
## Common patterns
|
|
246
|
+
|
|
247
|
+
### Search-friendly text column
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
posts: table('posts', {
|
|
251
|
+
id: id(),
|
|
252
|
+
title: text(),
|
|
253
|
+
body: text(),
|
|
254
|
+
searchVec: text().generatedAs(
|
|
255
|
+
`to_tsvector('english', "title" || ' ' || "body")`,
|
|
256
|
+
{ stored: true },
|
|
257
|
+
),
|
|
258
|
+
}).expressionIndex('posts_search_gin', [{ expr: '"searchVec"' }], { kind: 'gin' })
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
See [Full-text search](/docs/database/full-text-search) — the
|
|
262
|
+
framework's `.fullTextIndex()` helper builds this exact pattern
|
|
263
|
+
for you in one chain.
|
|
264
|
+
|
|
265
|
+
### Numeric derived column
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
orders: table('orders', {
|
|
269
|
+
id: id(),
|
|
270
|
+
qty: integer(),
|
|
271
|
+
price: integer(),
|
|
272
|
+
total: integer().generatedAs(`"qty" * "price"`, { stored: true }),
|
|
273
|
+
})
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Cheap to read (no per-query arithmetic), atomic on update — change
|
|
277
|
+
`qty` or `price` and `total` updates in the same statement.
|
|
278
|
+
|
|
279
|
+
### Slug from a title
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
posts: table('posts', {
|
|
283
|
+
id: id(),
|
|
284
|
+
title: text(),
|
|
285
|
+
// Postgres: LOWER + replace spaces — pure expression, no functions
|
|
286
|
+
slug: text().generatedAs(`LOWER(REPLACE("title", ' ', '-'))`, { stored: true }),
|
|
287
|
+
})
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
For complex slug logic (transliteration, deduplication), use
|
|
291
|
+
`.computed(row => ...)` instead — it runs in the MutationStore
|
|
292
|
+
middleware where you have the full JS stdlib.
|
|
293
|
+
|
|
294
|
+
## Distinct from `.computed(row => ...)`
|
|
295
|
+
|
|
296
|
+
The framework has two "derive a column from other columns" hooks.
|
|
297
|
+
They look similar but solve different problems:
|
|
298
|
+
|
|
299
|
+
| Property | `.computed(row => ...)` | `.generatedAs(expr, ...)` |
|
|
300
|
+
|----------------------------------|---------------------------------------------|-------------------------------------------|
|
|
301
|
+
| Runs in | App (MutationStore middleware) | DB engine |
|
|
302
|
+
| Can reference subject/tenant context | Yes — the row carries them at stamp time | NO — only same-row columns |
|
|
303
|
+
| Fires on | INSERT only (v1) | INSERT + UPDATE |
|
|
304
|
+
| Indexable | Yes — the column is a regular `text()` | Only when `stored: true` |
|
|
305
|
+
| Cross-dialect | Identical on every backend | Per-dialect syntax (framework dispatches) |
|
|
306
|
+
| Value visible to raw SQL? | Yes (after insert) | Yes always |
|
|
307
|
+
|
|
308
|
+
**Reach for `.computed()` when** you need subject context, complex
|
|
309
|
+
JS logic, or the value comes from outside the row.
|
|
310
|
+
|
|
311
|
+
**Reach for `.generatedAs()` when** the value is a pure expression
|
|
312
|
+
over other columns + you want UPDATE to keep it in sync.
|
|
313
|
+
|
|
314
|
+
## Suppressed modifiers
|
|
315
|
+
|
|
316
|
+
The framework suppresses `NOT NULL` / `UNIQUE` / `DEFAULT` on
|
|
317
|
+
generated columns automatically. The DDL would conflict in some
|
|
318
|
+
drivers:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
// These modifiers are SUPPRESSED on a generated column:
|
|
322
|
+
title: text().unique().default('untitled')
|
|
323
|
+
.generatedAs(`LOWER("rawTitle")`, { stored: true })
|
|
324
|
+
// Emitted: ... text GENERATED ALWAYS AS (LOWER("rawTitle")) STORED
|
|
325
|
+
// ^^ no NOT NULL, no UNIQUE, no DEFAULT
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
To enforce uniqueness on a generated column, declare a UNIQUE INDEX
|
|
329
|
+
on it explicitly via `.expressionIndex(name, [...], { ... })`.
|
|
330
|
+
|
|
331
|
+
## When NOT to use
|
|
332
|
+
|
|
333
|
+
- **You need conditional logic** (`if A then B else C`) — most
|
|
334
|
+
DB-level expressions support this via `CASE WHEN`, but complex
|
|
335
|
+
cases land in `.computed()` more naturally.
|
|
336
|
+
- **You need context** (subject id, tenant id, request metadata) —
|
|
337
|
+
not visible to a DB-level expression. Use `.computed()` or
|
|
338
|
+
`.default(() => ...)`.
|
|
339
|
+
- **You need it on a different table** — generated columns can only
|
|
340
|
+
reference columns on the SAME row.
|
|
341
|
+
|
|
342
|
+
## See also
|
|
343
|
+
|
|
344
|
+
- [Columns](/docs/database/columns) — `.computed(row => ...)` and
|
|
345
|
+
`.default(() => ...)` for the app-side variants
|
|
346
|
+
- [Indexes](/docs/database/indexes#expression) — `.expressionIndex()`
|
|
347
|
+
for indexing a generated column
|
|
348
|
+
- [Full-text search](/docs/database/full-text-search) — the FTS
|
|
349
|
+
pattern uses STORED tsvector generated columns
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
<!-- source: en/database/arrays-intervals.md -->
|
|
356
|
+
## Arrays + intervals
|
|
357
|
+
|
|
358
|
+
_Postgres-native array() and interval() columns. App-side codec keeps the API portable across mysql/mariadb/mssql/sqlite/turso._
|
|
359
|
+
|
|
360
|
+
`array(elementType)` and `interval()` ship native Postgres types
|
|
361
|
+
with transparent fallbacks for the other dialects. Your code reads
|
|
362
|
+
and writes JS arrays / strings; the framework handles the per-
|
|
363
|
+
dialect serialization.
|
|
364
|
+
|
|
365
|
+
## Arrays
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
import { array, text, integer, id, table } from '@voltro/database'
|
|
369
|
+
|
|
370
|
+
export const posts = table('posts', {
|
|
371
|
+
id: id(),
|
|
372
|
+
title: text(),
|
|
373
|
+
tags: array(text()), // ReadonlyArray<string>
|
|
374
|
+
scores: array(integer()), // ReadonlyArray<number>
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
// Insert — pass a regular JS array
|
|
378
|
+
await ctx.store.insert('posts', {
|
|
379
|
+
title: 'voltro is great',
|
|
380
|
+
tags: ['voltro', 'effect', 'pg'],
|
|
381
|
+
scores: [10, 20, 30],
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
// Read — get a regular JS array back
|
|
385
|
+
const rows = await ctx.store.query(database.posts.descriptor)
|
|
386
|
+
rows[0]?.tags // → ['voltro', 'effect', 'pg']
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The element type drives both the row type AND the per-dialect
|
|
390
|
+
storage:
|
|
391
|
+
|
|
392
|
+
| Element | Postgres column | Fallback dialects |
|
|
393
|
+
|----------------|-----------------|---------------------|
|
|
394
|
+
| `array(text())` | `TEXT[]` | JSON / NVARCHAR(MAX) / TEXT |
|
|
395
|
+
| `array(integer())` | `INTEGER[]` | JSON / NVARCHAR(MAX) / TEXT |
|
|
396
|
+
| `array(boolean())` | `BOOLEAN[]` | JSON / NVARCHAR(MAX) / TEXT |
|
|
397
|
+
| `array(timestamp())` | `TIMESTAMPTZ[]` | JSON / NVARCHAR(MAX) / TEXT |
|
|
398
|
+
|
|
399
|
+
### App-side codec
|
|
400
|
+
|
|
401
|
+
On non-postgres dialects, the framework's storeMiddleware
|
|
402
|
+
transparently:
|
|
403
|
+
|
|
404
|
+
- **Write**: `JSON.stringify(array)` before binding to the SQL
|
|
405
|
+
statement. Native JSON serialisation, preserves types correctly.
|
|
406
|
+
- **Read**: `JSON.parse(string)` on the way out. Defensive — pre-
|
|
407
|
+
array values (some drivers do this themselves) pass through;
|
|
408
|
+
null / undefined pass through; malformed JSON leaves the raw
|
|
409
|
+
string (logs no error so caller can debug).
|
|
410
|
+
|
|
411
|
+
Postgres is a no-op — the driver binds arrays natively, no codec
|
|
412
|
+
needed.
|
|
413
|
+
|
|
414
|
+
### Defaults
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
tags: array(text()).default([])
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
The literal `[]` becomes `'{}'::text[]` on postgres + `'[]'` (JSON
|
|
421
|
+
empty array) on the fallbacks. Use `[]` rather than `'{}'` even
|
|
422
|
+
though postgres accepts the latter — the JS-array form keeps the
|
|
423
|
+
schema portable.
|
|
424
|
+
|
|
425
|
+
### Querying
|
|
426
|
+
|
|
427
|
+
Array containment operators are in the query builder:
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
import { arrayHas, arrayContains, arrayOverlaps } from '@voltro/database'
|
|
431
|
+
|
|
432
|
+
queryFor(database.posts).where(arrayHas('tags', 'effect')) // 'effect' ∈ tags
|
|
433
|
+
queryFor(database.posts).where(arrayContains('tags', ['effect', 'ts'])) // tags ⊇ both
|
|
434
|
+
queryFor(database.posts).where(arrayOverlaps('tags', ['effect', 'go'])) // shares ≥1
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
On postgres these compile to the native `= ANY(col)` / `@>` / `&&`
|
|
438
|
+
operators. On the other dialects, where arrays are stored as JSON,
|
|
439
|
+
they lower to a JSON-containment check (`JSON_CONTAINS` on mysql/mariadb,
|
|
440
|
+
`json_each` on sqlite, `OPENJSON` on mssql) — so the same query is
|
|
441
|
+
portable. Postgres is fastest here (a GIN index on the array column
|
|
442
|
+
accelerates `@>`/`&&`); the JSON paths scan.
|
|
443
|
+
|
|
444
|
+
For fallback dialects (JSON-shaped storage), index-friendly
|
|
445
|
+
querying is even harder — the framework recommends extracting the
|
|
446
|
+
array contents to a separate join table if you need filtering by
|
|
447
|
+
contents.
|
|
448
|
+
|
|
449
|
+
## Intervals
|
|
450
|
+
|
|
451
|
+
Time durations — SLA deadlines, rate-limit windows, "expires-in":
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
import { interval, id, text, table } from '@voltro/database'
|
|
455
|
+
|
|
456
|
+
export const tickets = table('tickets', {
|
|
457
|
+
id: id(),
|
|
458
|
+
title: text(),
|
|
459
|
+
slaDeadline: interval(), // postgres native, others fall back
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
// Insert — string form on postgres, ms-number on others
|
|
463
|
+
await ctx.store.insert('tickets', {
|
|
464
|
+
title: 'investigate logs',
|
|
465
|
+
slaDeadline: '4 hours', // postgres-native literal
|
|
466
|
+
})
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
| Dialect | Storage | Input form |
|
|
470
|
+
|------------------|-----------|-------------------------------------------|
|
|
471
|
+
| postgres | `INTERVAL` | string (any pg interval literal: '1 day', '45 minutes', etc.) |
|
|
472
|
+
| mysql / mariadb | `BIGINT` | number of milliseconds |
|
|
473
|
+
| mssql | `BIGINT` | number of milliseconds |
|
|
474
|
+
| sqlite | `INTEGER` | number of milliseconds |
|
|
475
|
+
|
|
476
|
+
For portability across dialects you'd need a per-dialect codec; the
|
|
477
|
+
framework doesn't ship one in v1. Apps that target multiple
|
|
478
|
+
dialects with intervals should store milliseconds-as-BIGINT
|
|
479
|
+
explicitly via `integer()` and convert at read time.
|
|
480
|
+
|
|
481
|
+
### Querying
|
|
482
|
+
|
|
483
|
+
Postgres lets you compute against intervals natively:
|
|
484
|
+
|
|
485
|
+
```sql
|
|
486
|
+
WHERE "createdAt" + "slaDeadline" < NOW()
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
The query builder doesn't have a typed wrapper for this — use
|
|
490
|
+
sub-query helpers or drop to raw SQL.
|
|
491
|
+
|
|
492
|
+
## When NOT to use
|
|
493
|
+
|
|
494
|
+
- **You're not on postgres** — the array fallback (JSON-shaped
|
|
495
|
+
storage) is correct but slow for any non-trivial query. If you
|
|
496
|
+
need cross-dialect array support with index-friendly access,
|
|
497
|
+
use a join table (the `manyToMany` mixin pattern).
|
|
498
|
+
- **Intervals on mysql/mssql/sqlite** — the BIGINT-ms fallback
|
|
499
|
+
works but loses the postgres-native arithmetic. For cross-
|
|
500
|
+
dialect interval semantics, store milliseconds in `integer()`
|
|
501
|
+
and do the math in JS.
|
|
502
|
+
- **Heavy spatial work** — see [PostGIS](/docs/database/postgis)
|
|
503
|
+
instead of trying to roll your own geometry-as-array column.
|
|
504
|
+
|
|
505
|
+
## See also
|
|
506
|
+
|
|
507
|
+
- [Columns](/docs/database/columns) — the regular column types you
|
|
508
|
+
pass to `array(...)`
|
|
509
|
+
- [PostGIS](/docs/database/postgis) — for location-aware apps,
|
|
510
|
+
the spatial types are better than arrays of coordinates
|
|
511
|
+
- [JSON](/docs/database/json) — `json<T>()` for arbitrary nested
|
|
512
|
+
structures (arrays are special-cased; JSON is the general form)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
<!-- source: en/database/full-text-search.md -->
|
|
519
|
+
## Full-text search
|
|
520
|
+
|
|
521
|
+
_.fullTextIndex() + .matching() for tsvector / FULLTEXT INDEX search across columns. Cross-dialect per-engine where available._
|
|
522
|
+
|
|
523
|
+
`.fullTextIndex(name, columns, options?)` declares an FTS index on
|
|
524
|
+
one or more text columns. `.matching('indexName', 'query')` on the
|
|
525
|
+
query builder runs the search.
|
|
526
|
+
|
|
527
|
+
## Quick start
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
import { id, text, table } from '@voltro/database'
|
|
531
|
+
|
|
532
|
+
export const posts = table('posts', {
|
|
533
|
+
id: id(),
|
|
534
|
+
title: text(),
|
|
535
|
+
body: text(),
|
|
536
|
+
}).fullTextIndex('postSearch', ['title', 'body'], {
|
|
537
|
+
config: 'english',
|
|
538
|
+
weights: { title: 'A', body: 'B' },
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
// Query
|
|
542
|
+
const hits = await ctx.store.query(
|
|
543
|
+
queryFor(database.posts).matching('postSearch', 'voltro effect').descriptor,
|
|
544
|
+
)
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
## What the framework emits
|
|
548
|
+
|
|
549
|
+
### Postgres — tsvector + GIN
|
|
550
|
+
|
|
551
|
+
Postgres' canonical pattern:
|
|
552
|
+
|
|
553
|
+
```sql
|
|
554
|
+
ALTER TABLE "posts"
|
|
555
|
+
ADD COLUMN IF NOT EXISTS "postSearch_tsv" tsvector
|
|
556
|
+
GENERATED ALWAYS AS (
|
|
557
|
+
setweight(to_tsvector('english', coalesce("title", '')), 'A') ||
|
|
558
|
+
setweight(to_tsvector('english', coalesce("body", '')), 'B')
|
|
559
|
+
) STORED;
|
|
560
|
+
|
|
561
|
+
CREATE INDEX IF NOT EXISTS "postSearch" ON "posts" USING GIN ("postSearch_tsv");
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
The framework adds the synthetic `<index>_tsv` STORED tsvector
|
|
565
|
+
column + a GIN index. `.matching(...)` queries compile to:
|
|
566
|
+
|
|
567
|
+
```sql
|
|
568
|
+
"postSearch_tsv" @@ plainto_tsquery('voltro effect')
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### MySQL / MariaDB — FULLTEXT INDEX
|
|
572
|
+
|
|
573
|
+
```sql
|
|
574
|
+
CREATE FULLTEXT INDEX `postSearch` ON `posts` (`title`, `body`);
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
`.matching(...)` queries compile to:
|
|
578
|
+
|
|
579
|
+
```sql
|
|
580
|
+
MATCH("title", "body") AGAINST('voltro effect' IN NATURAL LANGUAGE MODE)
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
The `IN NATURAL LANGUAGE MODE` is mysql's default-tier ranking —
|
|
584
|
+
relevance-weighted, no Boolean operators. For Boolean mode
|
|
585
|
+
(`+voltro +effect`), use the raw SQL escape hatch.
|
|
586
|
+
|
|
587
|
+
### SQLite — native FTS5
|
|
588
|
+
|
|
589
|
+
The migrator creates a native FTS5 virtual table named
|
|
590
|
+
`<table>_<index>_fts` plus three triggers that keep it in lockstep
|
|
591
|
+
with the base table on every INSERT / UPDATE / DELETE:
|
|
592
|
+
|
|
593
|
+
```sql
|
|
594
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS "posts_postSearch_fts"
|
|
595
|
+
USING fts5("title", "body", row_id UNINDEXED, tokenize='porter');
|
|
596
|
+
-- + AFTER INSERT / UPDATE / DELETE triggers syncing row_id = posts.id
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
`row_id` carries the base row's string `id` (FTS5's integer rowid
|
|
600
|
+
can't hold a TypeID). `.matching(...)` queries compile to:
|
|
601
|
+
|
|
602
|
+
```sql
|
|
603
|
+
"id" IN (SELECT "row_id" FROM "posts_postSearch_fts" WHERE "posts_postSearch_fts" MATCH 'voltro effect')
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
`tokenize='porter'` gives stemming comparable to postgres' `'english'`
|
|
607
|
+
config + mysql's natural-language mode.
|
|
608
|
+
|
|
609
|
+
### MSSQL — ranked LIKE fallback
|
|
610
|
+
|
|
611
|
+
MSSQL native full-text needs a server-managed `CREATE FULLTEXT
|
|
612
|
+
CATALOG` (operations work, not always available). Rather than require
|
|
613
|
+
that, the framework emits a **portable ranked-LIKE search** at query
|
|
614
|
+
time — no catalog, no `CONTAINS`, works on every MSSQL install. The
|
|
615
|
+
migrator emits no FTS DDL (every base column is already present) and
|
|
616
|
+
notes the fallback at boot:
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
[voltro:migrate] full-text index 'postSearch' on 'posts':
|
|
620
|
+
mssql uses a ranked LIKE search at query time (no native FTS catalog required).
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
`.matching(...)` compiles to a per-column case-insensitive LIKE
|
|
624
|
+
disjunction:
|
|
625
|
+
|
|
626
|
+
```sql
|
|
627
|
+
(LOWER("title") LIKE LOWER('%voltro effect%') ESCAPE '\'
|
|
628
|
+
OR LOWER("body") LIKE LOWER('%voltro effect%') ESCAPE '\')
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
This is substring matching, not tokenised FTS — it won't stem or rank
|
|
632
|
+
on term frequency the way postgres/mysql/sqlite do. It's the honest
|
|
633
|
+
portable path; for true server FTS on MSSQL set up a catalog + use raw
|
|
634
|
+
SQL with `CONTAINS`.
|
|
635
|
+
|
|
636
|
+
## Configuring per-language
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
.fullTextIndex('postSearch', ['title', 'body'], {
|
|
640
|
+
config: 'german', // postgres tsvector config
|
|
641
|
+
weights: { title: 'A', body: 'B', tags: 'C' },
|
|
642
|
+
})
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
`config` defaults to `'english'`. Postgres ships configs for
|
|
646
|
+
~20 languages; install additional ones via `CREATE TEXT SEARCH
|
|
647
|
+
CONFIGURATION` if needed. The framework emits the config name
|
|
648
|
+
verbatim into the `to_tsvector(...)` call.
|
|
649
|
+
|
|
650
|
+
`weights` is ignored on non-postgres dialects (no per-column
|
|
651
|
+
weight concept in FULLTEXT INDEX).
|
|
652
|
+
|
|
653
|
+
## Multi-column with per-column weights
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
.fullTextIndex('postSearch', ['title', 'body', 'tags'], {
|
|
657
|
+
weights: { title: 'A', body: 'B', tags: 'C' },
|
|
658
|
+
})
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
Postgres combines them in the generated tsvector — a query that
|
|
662
|
+
matches `title` ranks higher than one that only matches `tags`.
|
|
663
|
+
|
|
664
|
+
## Relevance ranking — `.rankBy()`
|
|
665
|
+
|
|
666
|
+
Chain `.rankBy()` after `.matching(...)` to surface a relevance score
|
|
667
|
+
column AND order results best-match-first — on every dialect:
|
|
668
|
+
|
|
669
|
+
```ts
|
|
670
|
+
queryFor(database.posts)
|
|
671
|
+
.matching('postSearch', input.query)
|
|
672
|
+
.rankBy() // adds a `rank` column + ORDER BY rank DESC
|
|
673
|
+
.limit(20)
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
Each returned row carries a numeric `rank` (higher = better). The
|
|
677
|
+
score expression is per-dialect, but the API is identical:
|
|
678
|
+
|
|
679
|
+
| Dialect | Score expression |
|
|
680
|
+
|------------------|-------------------------------------------|
|
|
681
|
+
| postgres | `ts_rank(<tsvCol>, plainto_tsquery(...))` |
|
|
682
|
+
| mysql / mariadb | `MATCH(cols) AGAINST(...)` score |
|
|
683
|
+
| sqlite | `-bm25(<fts>)` (negated so higher = better) |
|
|
684
|
+
| mssql | count of columns whose `LOWER LIKE` the query |
|
|
685
|
+
|
|
686
|
+
Options:
|
|
687
|
+
|
|
688
|
+
- `.rankBy({ alias: 'score' })` — name the column `score` instead of `rank`.
|
|
689
|
+
- `.rankBy({ order: false })` — surface the score column WITHOUT forcing
|
|
690
|
+
`ORDER BY` (combine with your own `.orderBy(...)`).
|
|
691
|
+
|
|
692
|
+
```ts
|
|
693
|
+
// Score column, but sort by recency with relevance as a secondary key:
|
|
694
|
+
queryFor(database.posts)
|
|
695
|
+
.matching('postSearch', input.query)
|
|
696
|
+
.rankBy({ order: false })
|
|
697
|
+
.orderBy('createdAt', 'desc')
|
|
698
|
+
.limit(20)
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
On postgres the `ts_rank` reads the same stored tsvector column the
|
|
702
|
+
GIN index covers, so ranking stays index-accelerated.
|
|
703
|
+
|
|
704
|
+
## Querying
|
|
705
|
+
|
|
706
|
+
`.matching('indexName', 'query')` is composable with other chain
|
|
707
|
+
methods:
|
|
708
|
+
|
|
709
|
+
```ts
|
|
710
|
+
queryFor(database.posts)
|
|
711
|
+
.where(eq('orgId', oid)) // pre-filter
|
|
712
|
+
.matching('postSearch', input.query) // FTS
|
|
713
|
+
.orderBy('createdAt', 'desc') // tie-breaker
|
|
714
|
+
.limit(20)
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
Multiple `.matching()` calls override — FTS is a single-string
|
|
718
|
+
concept, no AND-merge across queries.
|
|
719
|
+
|
|
720
|
+
## Limitations
|
|
721
|
+
|
|
722
|
+
- **MSSQL is a ranked LIKE fallback, not tokenised FTS.** It matches
|
|
723
|
+
substrings (`LOWER(col) LIKE '%query%'`) and ranks by per-column
|
|
724
|
+
hit count — no stemming, no term-frequency weighting. For true
|
|
725
|
+
server FTS on MSSQL, set up a `CREATE FULLTEXT CATALOG` and use raw
|
|
726
|
+
SQL with `CONTAINS`.
|
|
727
|
+
- **Boolean / phrase queries on postgres** — `.matching(...)` uses
|
|
728
|
+
`plainto_tsquery` which strips operators. For Boolean
|
|
729
|
+
(`+voltro -effect`) drop to `store.raw` with `to_tsquery(...)`:
|
|
730
|
+
```ts
|
|
731
|
+
import { sql } from '@voltro/database/sql'
|
|
732
|
+
await ctx.store.raw!<{ id: string }>(sql`
|
|
733
|
+
SELECT id FROM posts
|
|
734
|
+
WHERE "postSearch_tsv" @@ to_tsquery(${input.query})
|
|
735
|
+
`, { dependsOn: ['posts'] })
|
|
736
|
+
```
|
|
737
|
+
The query string binds as a parameter; `to_tsquery` parses its
|
|
738
|
+
operators (`+`, `-`, `&`, `|`, `<->`).
|
|
739
|
+
- **Reactivity** — FTS subscriptions ARE reactive on postgres (the
|
|
740
|
+
tsvector column is a regular column the dispatcher tracks). On
|
|
741
|
+
mysql/mariadb the MATCH AGAINST predicate isn't bucketed into
|
|
742
|
+
the matcher's index — subscriptions re-evaluate on every write
|
|
743
|
+
to the source columns.
|
|
744
|
+
|
|
745
|
+
## When NOT to use
|
|
746
|
+
|
|
747
|
+
- **Trigram / fuzzy match** — postgres' `pg_trgm` extension is a
|
|
748
|
+
different access pattern. Install + use raw SQL.
|
|
749
|
+
- **Vector search** — see [Vector columns](/docs/database/vectors)
|
|
750
|
+
for embeddings-based semantic search.
|
|
751
|
+
- **Substring search on a small table** — `LIKE '%term%'` is fine
|
|
752
|
+
up to ~100k rows. Don't carry the FTS overhead for tiny tables.
|
|
753
|
+
|
|
754
|
+
## See also
|
|
755
|
+
|
|
756
|
+
- [Generated columns](/docs/database/generated-columns) — the FTS
|
|
757
|
+
pattern is "generated tsvector + GIN expression index"
|
|
758
|
+
- [Indexes](/docs/database/indexes) — `.expressionIndex({ kind: 'gist' })`
|
|
759
|
+
for the GIN/GiST plumbing under FTS
|
|
760
|
+
- [Vector columns](/docs/database/vectors) — for embedding-based
|
|
761
|
+
semantic search (different problem from keyword FTS)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
---
|
|
766
|
+
|
|
767
|
+
<!-- source: en/database/postgis.md -->
|
|
768
|
+
## PostGIS (geography / geometry)
|
|
769
|
+
|
|
770
|
+
_Postgres-native geography/geometry columns, geometry constructors, spatial predicates (ST_DWithin, ST_Within, ST_Contains, ST_Intersects, ST_Buffer, bbox), ST_Distance projection + <-> KNN ordering for location-aware apps. Postgres only._
|
|
771
|
+
|
|
772
|
+
The `@voltro/plugin-postgis` package adds first-class PostGIS columns
|
|
773
|
+
and spatial operators to the Voltro schema DSL. **Postgres only.**
|
|
774
|
+
PostGIS is a postgres extension; other dialects have no portable
|
|
775
|
+
equivalent, so schema emission (`voltro migrate` / auto-migrate)
|
|
776
|
+
FAILS LOUD with a `postgres-only` error on non-postgres backends,
|
|
777
|
+
and spatial predicates throw in the SQL compiler.
|
|
778
|
+
|
|
779
|
+
## Install + enable
|
|
780
|
+
|
|
781
|
+
```bash
|
|
782
|
+
pnpm add @voltro/plugin-postgis
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
On postgres, the migrator emits a one-time
|
|
786
|
+
`CREATE EXTENSION IF NOT EXISTS postgis;` alongside the first spatial
|
|
787
|
+
column's DDL. If your database user lacks the privilege to create
|
|
788
|
+
extensions, run that statement once per database as a superuser
|
|
789
|
+
before migrating.
|
|
790
|
+
|
|
791
|
+
## Geography vs geometry
|
|
792
|
+
|
|
793
|
+
| Type | Coordinate system | Distance calculation | When to use |
|
|
794
|
+
|---|---|---|---|
|
|
795
|
+
| `geography(...)` | Spheroidal (curves with the earth) | Great-circle | Global apps, WGS-84 lat/lon (SRID 4326). Easy mental model. |
|
|
796
|
+
| `geometry(...)` | Planar / projected | Straight-line in the SRID's frame | Local apps with projected coords (UTM zone, state plane). Faster + works with non-spherical analysis. |
|
|
797
|
+
|
|
798
|
+
If you're unsure: pick `geography` with SRID 4326. It's the
|
|
799
|
+
default mental model for "GPS coordinates" and the distance
|
|
800
|
+
calculations are correct for any global scale.
|
|
801
|
+
|
|
802
|
+
## Declare a location column
|
|
803
|
+
|
|
804
|
+
```ts
|
|
805
|
+
import { geography, point } from '@voltro/plugin-postgis'
|
|
806
|
+
import { id, text, table, queryFor } from '@voltro/database'
|
|
807
|
+
|
|
808
|
+
export const venues = table('venues', {
|
|
809
|
+
id: id(),
|
|
810
|
+
name: text(),
|
|
811
|
+
location: geography('Point', 4326), // lon/lat
|
|
812
|
+
})
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
Insert a row using the `point()` helper to build the EWKT literal:
|
|
816
|
+
|
|
817
|
+
```ts
|
|
818
|
+
await ctx.store.insert('venues', {
|
|
819
|
+
name: 'Café Mockup',
|
|
820
|
+
location: point(8.682, 50.110, 4326), // Frankfurt
|
|
821
|
+
})
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
The literal serialises as `SRID=4326;POINT(8.682 50.11)`, which
|
|
825
|
+
postgres parses into the geography column via implicit cast.
|
|
826
|
+
|
|
827
|
+
## Query within a radius
|
|
828
|
+
|
|
829
|
+
```ts
|
|
830
|
+
import { ST_DWithin, point } from '@voltro/plugin-postgis'
|
|
831
|
+
import { queryFor } from '@voltro/database'
|
|
832
|
+
|
|
833
|
+
const nearMe = await ctx.store.query(
|
|
834
|
+
queryFor(venues)
|
|
835
|
+
.where(ST_DWithin('location', point(8.68, 50.11), 1000)) // 1 km
|
|
836
|
+
.descriptor,
|
|
837
|
+
)
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
`ST_DWithin(column, point, meters)` is the canonical "find rows
|
|
841
|
+
within X distance" predicate. PostGIS handles the great-circle math.
|
|
842
|
+
|
|
843
|
+
## Query inside a polygon
|
|
844
|
+
|
|
845
|
+
```ts
|
|
846
|
+
import { ST_Intersects, polygon } from '@voltro/plugin-postgis'
|
|
847
|
+
import { queryFor } from '@voltro/database'
|
|
848
|
+
|
|
849
|
+
const cityCenter = polygon([
|
|
850
|
+
[[8.65, 50.10], [8.72, 50.10], [8.72, 50.13], [8.65, 50.13], [8.65, 50.10]],
|
|
851
|
+
])
|
|
852
|
+
|
|
853
|
+
const inside = await ctx.store.query(
|
|
854
|
+
queryFor(venues)
|
|
855
|
+
.where(ST_Intersects('location', cityCenter)) // venue location ∈ polygon
|
|
856
|
+
.descriptor,
|
|
857
|
+
)
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
Every predicate takes the **column first**, then the WKT geometry:
|
|
861
|
+
|
|
862
|
+
- `ST_DWithin(column, geom, meters)` — within a metric radius.
|
|
863
|
+
- `ST_Within(column, geom)` — the column's geometry is fully inside
|
|
864
|
+
`geom` (the right shape for "point column inside a polygon").
|
|
865
|
+
- `ST_Contains(column, geom)` — the column's geometry fully contains
|
|
866
|
+
`geom` (e.g. a region `boundary` column containing a point).
|
|
867
|
+
- `ST_Intersects(column, geom)` — any shared point.
|
|
868
|
+
- `ST_Buffer(column, geom, meters)` — the column intersects `geom`
|
|
869
|
+
grown by `meters` (a metric buffer; shape-aware "within N metres").
|
|
870
|
+
- `bboxOverlaps(column, geom)` — the `&&` bounding-box overlap
|
|
871
|
+
operator: cheap, GiST-index-only, approximate. Build the box with
|
|
872
|
+
`envelope(minLon, minLat, maxLon, maxLat, srid)`.
|
|
873
|
+
|
|
874
|
+
Spatial predicates work in one-shot `ctx.store.query` reads only;
|
|
875
|
+
they throw in the in-memory reactive matcher, so keep them out of
|
|
876
|
+
subscriptions.
|
|
877
|
+
|
|
878
|
+
## Geometry constructors
|
|
879
|
+
|
|
880
|
+
`point` / `lineString` / `polygon` / `multiPoint` /
|
|
881
|
+
`multiLineString` / `multiPolygon` build EWKT literals; `geoJson`
|
|
882
|
+
lowers a GeoJSON geometry to the same EWKT (SRID 4326 by the GeoJSON
|
|
883
|
+
spec). Coordinates are `[lon, lat]` (x before y) throughout.
|
|
884
|
+
|
|
885
|
+
```ts
|
|
886
|
+
import { point, lineString, polygon, multiPolygon, geoJson } from '@voltro/plugin-postgis'
|
|
887
|
+
|
|
888
|
+
const route = lineString([[8.68, 50.11], [8.69, 50.12]], 4326)
|
|
889
|
+
const zones = multiPolygon([[[[8.6, 50.1], [8.7, 50.1], [8.7, 50.2], [8.6, 50.1]]]], 4326)
|
|
890
|
+
const fromGeoJson = geoJson({ type: 'Point', coordinates: [8.682, 50.110] })
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
## Surfacing distance + nearest-neighbour ordering
|
|
894
|
+
|
|
895
|
+
`withDistance(...)` projects `ST_Distance(column, geom)` as a
|
|
896
|
+
selectable column (metric metres by default), and `nearestBy(...)`
|
|
897
|
+
adds a `<->` KNN `ORDER BY` for indexed nearest-neighbour search.
|
|
898
|
+
Both are `QueryTransform`s applied via the builder's `.use(...)` seam
|
|
899
|
+
(the same mechanism as `hybridSearch`), so they compose with
|
|
900
|
+
`.where(...)` and `.limit(k)`. Postgres-only.
|
|
901
|
+
|
|
902
|
+
```ts
|
|
903
|
+
import { withDistance, nearestBy, ST_DWithin, point } from '@voltro/plugin-postgis'
|
|
904
|
+
import { queryFor } from '@voltro/database'
|
|
905
|
+
|
|
906
|
+
// Distance-as-a-value in metres.
|
|
907
|
+
const withMeters = await ctx.store.query(
|
|
908
|
+
queryFor(venues)
|
|
909
|
+
.use(withDistance('location', point(8.68, 50.11), { as: 'meters' }))
|
|
910
|
+
.where(ST_DWithin('location', point(8.68, 50.11), 5000))
|
|
911
|
+
.descriptor,
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
// The 5 nearest venues via the indexed `<->` operator.
|
|
915
|
+
const nearest5 = await ctx.store.query(
|
|
916
|
+
queryFor(venues).use(nearestBy('location', point(8.68, 50.11))).limit(5).descriptor,
|
|
917
|
+
)
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
`withDistance(column, geom, { as?, useGeography? })` aliases the
|
|
921
|
+
distance column (default `distance`); `nearestBy(column, geom,
|
|
922
|
+
{ direction? })` leads any explicit `.orderBy(...)`. `<->` is planar
|
|
923
|
+
(postgres has no geography `<->`), so KNN ranks by planar distance —
|
|
924
|
+
add `withDistance` when you need a metric value.
|
|
925
|
+
|
|
926
|
+
## Indexes
|
|
927
|
+
|
|
928
|
+
GiST is the PostGIS-recommended index access method for
|
|
929
|
+
geometry/geography columns. The framework's regular `.index([...])`
|
|
930
|
+
uses B-tree, which is useless for spatial queries — declare the
|
|
931
|
+
index with `kind: 'gist'` on `.expressionIndex()`:
|
|
932
|
+
|
|
933
|
+
```ts
|
|
934
|
+
export const venues = table('venues', {
|
|
935
|
+
id: id(),
|
|
936
|
+
name: text(),
|
|
937
|
+
location: geography('Point', 4326),
|
|
938
|
+
}).expressionIndex(
|
|
939
|
+
'venues_loc_gist',
|
|
940
|
+
[{ expr: '"location"' }],
|
|
941
|
+
{ kind: 'gist' },
|
|
942
|
+
)
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
The framework emits:
|
|
946
|
+
|
|
947
|
+
```sql
|
|
948
|
+
CREATE INDEX IF NOT EXISTS "venues_loc_gist"
|
|
949
|
+
ON "venues" USING GIST (("location"));
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
on postgres. On every other dialect the migrator warns + falls back
|
|
953
|
+
to a plain B-tree index (spatial queries will then fall back to
|
|
954
|
+
sequential scan — there's no portable equivalent of GiST).
|
|
955
|
+
|
|
956
|
+
Combine `kind: 'gist'` with `where: …` for a partial GiST index:
|
|
957
|
+
|
|
958
|
+
```ts
|
|
959
|
+
.expressionIndex(
|
|
960
|
+
'active_venues_loc_gist',
|
|
961
|
+
[{ expr: '"location"' }],
|
|
962
|
+
{ kind: 'gist', where: `"active" = true` },
|
|
963
|
+
)
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
## Cross-dialect
|
|
967
|
+
|
|
968
|
+
PostGIS is a postgres extension. A schema using `geography()` /
|
|
969
|
+
`geometry()` fails loud at schema emission when the active dialect
|
|
970
|
+
isn't postgres — no silent TEXT fallback, no graceful degradation.
|
|
971
|
+
Apps that need spatial queries on
|
|
972
|
+
MySQL/MariaDB/MSSQL/SQLite should either:
|
|
973
|
+
|
|
974
|
+
- Use the dialect's native spatial type (mysql's `POINT`/`GEOMETRY`,
|
|
975
|
+
mssql's `geography`/`geometry`) via a file-based migration + raw SQL
|
|
976
|
+
- Store coords as numeric columns + filter app-side (acceptable up to
|
|
977
|
+
~100k rows)
|
|
978
|
+
|
|
979
|
+
Voltro doesn't ship a cross-dialect spatial abstraction — the
|
|
980
|
+
behavioural gaps are too large to paper over.
|
|
981
|
+
|
|
982
|
+
## See also
|
|
983
|
+
|
|
984
|
+
- [Columns](/docs/database/columns) — the regular schema-DSL types
|
|
985
|
+
- [Expression indexes](/docs/database/indexes#expression) — the
|
|
986
|
+
framework's index API (`kind: 'gist' | 'gin'` on postgres)
|
|
987
|
+
- [PostGIS docs](https://postgis.net/docs/) — the official manual,
|
|
988
|
+
authoritative for every spatial function the framework re-exports
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
<!-- source: en/database/vectors.md -->
|
|
995
|
+
## Vector columns
|
|
996
|
+
|
|
997
|
+
_Vectors and RAG — embedding columns, HNSW indexing, nearestNeighbours queries, the vectorEmbedding() mixin, across all five dialects._
|
|
998
|
+
|
|
999
|
+
For RAG and semantic search, vectors live in the same table as the rest of your data, in the same transactions, with the same tenant scoping. No separate vector DB. No two-store consistency model.
|
|
1000
|
+
|
|
1001
|
+
Vector storage works on **all five dialects** — the float array is always persisted. The HNSW *index* is emitted on **postgres** (via pgvector); MariaDB exposes native `VEC_DISTANCE_*` operators so its nearest-neighbour queries are correct, and the other dialects compute distance by sequential scan. The rule across the board: **portable result everywhere, index-accelerated on postgres**.
|
|
1002
|
+
|
|
1003
|
+
## Prerequisites
|
|
1004
|
+
|
|
1005
|
+
On postgres, the `vector` extension provides the `VECTOR(n)` type, the distance operators, and the HNSW access method. The framework emits it automatically — `voltro migrate` prepends `CREATE EXTENSION IF NOT EXISTS vector;` to the migration whenever any table declares a `vector(...)` column. MariaDB / MySQL ship vectors natively (no extension); MSSQL / SQLite store the bytes with no extra step.
|
|
1006
|
+
|
|
1007
|
+
## Declaring a vector column
|
|
1008
|
+
|
|
1009
|
+
```ts
|
|
1010
|
+
import { table, id, text, vector } from '@voltro/database'
|
|
1011
|
+
|
|
1012
|
+
const docs = table('docs', {
|
|
1013
|
+
id: id(),
|
|
1014
|
+
body: text(),
|
|
1015
|
+
embedding: vector(1536),
|
|
1016
|
+
})
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
The number is the **dimensionality**, persisted on the column so the migration emitter can size the DDL (`VECTOR(1536)` on postgres / MariaDB / MySQL; `VARBINARY(MAX)` on MSSQL; `BLOB` on SQLite):
|
|
1020
|
+
|
|
1021
|
+
| Provider | Model | Dimensions |
|
|
1022
|
+
|---|---|---|
|
|
1023
|
+
| OpenAI | `text-embedding-3-small` | 1536 |
|
|
1024
|
+
| OpenAI | `text-embedding-3-large` | 3072 |
|
|
1025
|
+
| Anthropic | (via Voyage) `voyage-3` | 1024 |
|
|
1026
|
+
| Cohere | `embed-english-v3.0` | 1024 |
|
|
1027
|
+
|
|
1028
|
+
Pick once. Changing dimensionality means rebuilding the entire column. `vector(0)` and non-integer dimensions throw at declaration time.
|
|
1029
|
+
|
|
1030
|
+
## Generating embeddings
|
|
1031
|
+
|
|
1032
|
+
```ts
|
|
1033
|
+
import { embed } from '@voltro/ai'
|
|
1034
|
+
|
|
1035
|
+
const queryVec = yield* embed('how do mutations work?')
|
|
1036
|
+
// ReadonlyArray<number> of length 1536 (or whatever your provider returns)
|
|
1037
|
+
|
|
1038
|
+
await ctx.store.update('docs', id, { embedding: queryVec })
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
`embed` is an Effect — it runs on the server only (the provider call lives in `@voltro/ai`, never a browser bundle). For most apps you don't write this code at all — use the [`vectorEmbedding()` mixin](#auto-embedding-via-vectorembedding) instead and the runtime embeds on write for you.
|
|
1042
|
+
|
|
1043
|
+
## ANN search
|
|
1044
|
+
|
|
1045
|
+
```ts
|
|
1046
|
+
ctx.store.select('docs')
|
|
1047
|
+
.nearestNeighbours('embedding', queryVec, { distance: 'cosine' })
|
|
1048
|
+
.limit(5)
|
|
1049
|
+
.all()
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
Each result row carries a synthetic `distance` field. Available distance metrics:
|
|
1053
|
+
|
|
1054
|
+
| Metric | Operator (pgvector) | When |
|
|
1055
|
+
|---|---|---|
|
|
1056
|
+
| `cosine` *(default)* | `<=>` | Normalised text embeddings — most common. |
|
|
1057
|
+
| `l2` | `<->` | Euclidean. Image embeddings often need this. |
|
|
1058
|
+
| `inner` | `<#>` | Inner product. Useful for some recommendation models. |
|
|
1059
|
+
|
|
1060
|
+
The choice **must match** the index's opclass (see HNSW below), or the index can't accelerate the search.
|
|
1061
|
+
|
|
1062
|
+
On MariaDB / MySQL the same query compiles to `VEC_DISTANCE_COSINE(...)` / `VEC_DISTANCE_EUCLIDEAN(...)`. On MSSQL / SQLite there's no portable distance operator — the query still returns rows (sequential scan) but unranked; the migrate-time warning flags that the dialect has no ANN acceleration.
|
|
1063
|
+
|
|
1064
|
+
## HNSW index
|
|
1065
|
+
|
|
1066
|
+
Without an index, `nearestNeighbours` is a sequential scan — fine for ≤10k rows, painful beyond. Indexes are declared **at the table level** (there is no column-level `.index()` modifier). Add an HNSW index over the vector column:
|
|
1067
|
+
|
|
1068
|
+
```ts
|
|
1069
|
+
const docs = table('docs', {
|
|
1070
|
+
id: id(),
|
|
1071
|
+
body: text(),
|
|
1072
|
+
embedding: vector(1536),
|
|
1073
|
+
}).index('docsEmbeddingHnsw', ['embedding'], { kind: 'hnsw' })
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
With explicit tuning:
|
|
1077
|
+
|
|
1078
|
+
```ts
|
|
1079
|
+
const docs = table('docs', {
|
|
1080
|
+
id: id(),
|
|
1081
|
+
body: text(),
|
|
1082
|
+
embedding: vector(1536),
|
|
1083
|
+
}).index('docsEmbeddingHnsw', ['embedding'], {
|
|
1084
|
+
kind: 'hnsw',
|
|
1085
|
+
kindOptions: {
|
|
1086
|
+
hnsw: {
|
|
1087
|
+
m: 24, // links per node — higher = better recall + more memory (default 16)
|
|
1088
|
+
efConstruction: 128, // index build effort — higher = better quality + slower build (default 64)
|
|
1089
|
+
distance: 'cosine', // selects the opclass; MUST match query distance (default 'cosine')
|
|
1090
|
+
},
|
|
1091
|
+
},
|
|
1092
|
+
})
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
The `distance` metric selects the pgvector opclass (`cosine` → `vector_cosine_ops`, `l2` → `vector_l2_ops`, `inner` → `vector_ip_ops`). Set `opclass` directly to override (e.g. `halfvec_cosine_ops` for a half-precision column). Declaring `kind: 'hnsw'` on a non-vector column, on multiple fields, or on an expression throws at declaration time.
|
|
1096
|
+
|
|
1097
|
+
The HNSW DDL is postgres-only; on MariaDB / MySQL / MSSQL / SQLite the index is skipped with a migrate-time warning (a btree on a vector is meaningless). MariaDB queries still run correctly via its native `VEC_DISTANCE_*` operators — just without index acceleration.
|
|
1098
|
+
|
|
1099
|
+
## Per-query effort
|
|
1100
|
+
|
|
1101
|
+
```ts
|
|
1102
|
+
ctx.store.select('docs')
|
|
1103
|
+
.nearestNeighbours('embedding', queryVec)
|
|
1104
|
+
.efSearch(80) // search effort (default: 40)
|
|
1105
|
+
.limit(10)
|
|
1106
|
+
.all()
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
Higher `efSearch` = better recall, slower query. Sweet spot is usually 40-100; benchmark on your data. (pgvector `hnsw.ef_search`.)
|
|
1110
|
+
|
|
1111
|
+
## Tenant scoping + vectors
|
|
1112
|
+
|
|
1113
|
+
If the table has the `tenant()` mixin, vector searches stay scoped — they only consider rows in the caller's tenant. The runtime AND-merges the tenant filter before the ANN order/limit, so cross-tenant rows never enter the candidate set. No cross-tenant leakage via similarity search.
|
|
1114
|
+
|
|
1115
|
+
```ts
|
|
1116
|
+
const docs = table('docs', {
|
|
1117
|
+
id: id(),
|
|
1118
|
+
body: text(),
|
|
1119
|
+
embedding: vector(1536),
|
|
1120
|
+
})
|
|
1121
|
+
.with(tenant())
|
|
1122
|
+
.index('docsEmbeddingHnsw', ['embedding'], { kind: 'hnsw' })
|
|
1123
|
+
|
|
1124
|
+
// Cross-tenant searches IMPOSSIBLE — the runtime injects the tenant filter.
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
## Auto-embedding via `vectorEmbedding()`
|
|
1128
|
+
|
|
1129
|
+
Most RAG apps want: write text → embedding gets generated automatically. The mixin adds the vector column, its HNSW index, and the re-embed behaviors in one line:
|
|
1130
|
+
|
|
1131
|
+
```ts
|
|
1132
|
+
import { vectorEmbedding } from '@voltro/database'
|
|
1133
|
+
|
|
1134
|
+
const docs = table('docs', {
|
|
1135
|
+
id: id(),
|
|
1136
|
+
body: text(),
|
|
1137
|
+
}).with(vectorEmbedding({
|
|
1138
|
+
from: 'body',
|
|
1139
|
+
model: 'text-embedding-3-small',
|
|
1140
|
+
dimensions: 1536,
|
|
1141
|
+
}))
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
Behaviour:
|
|
1145
|
+
|
|
1146
|
+
- On INSERT, the runtime computes the embedding from `body` + writes it into the `embedding` column (override the column name with `as`).
|
|
1147
|
+
- On UPDATE of `body`, the embedding is recomputed.
|
|
1148
|
+
- The mixin also contributes the HNSW index (pass `index: false` to skip it, `distance` to pick the metric).
|
|
1149
|
+
- A query helper `nearestNeighbours(queryString, k)` embeds the string for you:
|
|
1150
|
+
|
|
1151
|
+
```ts
|
|
1152
|
+
ctx.store.select('docs')
|
|
1153
|
+
.nearestNeighbours('how do mutations work?', 5)
|
|
1154
|
+
.all()
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
The runtime injects `@voltro/ai`'s `embed` into the mixin's write hook — the embedding API calls are billed to your provider account and traced by `@voltro/plugin-audit`.
|
|
1158
|
+
|
|
1159
|
+
### Backfilling pre-existing rows
|
|
1160
|
+
|
|
1161
|
+
The mixin only embeds rows written *after* it's in place. For rows that already existed (or after switching embedding model), seed them with the CLI:
|
|
1162
|
+
|
|
1163
|
+
```bash
|
|
1164
|
+
voltro embeddings backfill docs --text body --vector embedding [--model text-embedding-3-small] [--batch 50] [--dry-run]
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
It selects rows whose vector column is empty (or, with `--model-field`/`--model`, embedded under a different model), embeds them in batches via `@voltro/ai`, and writes the vectors back. `--dry-run` reports what *would* be embedded without writing.
|
|
1168
|
+
|
|
1169
|
+
## Hybrid search
|
|
1170
|
+
|
|
1171
|
+
For best results, combine vector similarity with full-text search (FTS):
|
|
1172
|
+
|
|
1173
|
+
```ts
|
|
1174
|
+
import { hybridSearch } from '@voltro/database'
|
|
1175
|
+
|
|
1176
|
+
ctx.store.select('docs').use(hybridSearch({
|
|
1177
|
+
vector: { col: 'embedding', query: 'how to deploy' },
|
|
1178
|
+
fts: { indexName: 'docsBody', query: 'how to deploy' },
|
|
1179
|
+
alpha: 0.7, // 0=pure FTS, 1=pure vector
|
|
1180
|
+
})).limit(10).all()
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
The FTS clause narrows the candidate set (lexical recall); the vector clause ranks those candidates by embedding distance (semantic ordering). `alpha` weights how much the vector ranking dominates when the two rank signals are fused (Reciprocal Rank Fusion). Beats pure vector on most knowledge-base style retrievals.
|
|
1184
|
+
|
|
1185
|
+
## Storage cost
|
|
1186
|
+
|
|
1187
|
+
A 1536-dim float32 vector is ~6 KB per row. With HNSW overhead it's roughly 9-10 KB.
|
|
1188
|
+
|
|
1189
|
+
- 10k docs → ~100 MB
|
|
1190
|
+
- 1M docs → ~10 GB
|
|
1191
|
+
|
|
1192
|
+
For very large corpora, downcast to half-precision — `vector(1536, { precision: 'half' })` emits pgvector's `HALFVEC(n)`, half the bytes, marginally lower recall. (MariaDB / MySQL have no half type — the emitter upcasts to float32 there.)
|
|
1193
|
+
|
|
1194
|
+
## When NOT to use postgres / pgvector
|
|
1195
|
+
|
|
1196
|
+
- **Billions of vectors.** Postgres + pgvector tops out around 10-50M vectors with reasonable latency. Beyond that, Qdrant / Weaviate / Pinecone.
|
|
1197
|
+
- **Frequent re-embedding of entire corpus.** A separate vector DB is easier to wipe + rebuild than a column.
|
|
1198
|
+
- **Multi-tenant where each tenant has their own embedding model.** A vector column pins one dimensionality.
|
|
1199
|
+
|
|
1200
|
+
For most B2B SaaS, postgres + pgvector is more than enough + you get transactional + tenant-scoped + same-backup-line semantics for free.
|