diff --git a/.changeset/driver-sql-readme-shipped-surface.md b/.changeset/driver-sql-readme-shipped-surface.md new file mode 100644 index 0000000000..3ba4e41bd6 --- /dev/null +++ b/.changeset/driver-sql-readme-shipped-surface.md @@ -0,0 +1,82 @@ +--- +"@objectstack/driver-sql": patch +--- + +docs(driver-sql): rewrite the published README to the shipped driver surface (#9867) + +`packages/drivers/driver-sql/README.md` is in the package's `files` array with +`private` unset, so it is the page npm renders. It told the reader to build a +stack with a static factory on a class that is not exported, at three call sites: + +```ts +driver: DriverSQL.configure(getDatabaseConfig()) +``` + +Measured against the built `dist/index.d.ts`: `DriverSQL` occurs **zero** times, +and no `configure` static exists on `SqlDriver` or on anything else the package +exports. `DriverSQL.configure()` was never real — the commit that first repaired +these snippets elsewhere in the same file (2026-05-07) called it "the imaginary +`.configure(...)` static factory", and it fixed only the Basic Usage section, so +the page has contradicted itself since: correct `SqlDriver` import at line 43, +fabricated `DriverSQL` at 448/482/516. The receiver is a free identifier that +imports nothing, which is why `check:published-readme-exports` — both halves of +which key on an *imported* name — could not see it. + +Renaming would not have produced working code, and the sweep this card asked for +found the surrounding shape was fabricated too. Every claim on the page was +re-measured; the ones that were wrong: + +- **`defineStack({ driver: … })` does not exist** — the six `driver:` call sites + (three `new SqlDriver(...)`, three `DriverSQL.configure(...)`) all named a key + `ObjectStackDefinitionSchema` never declared. Since #8687 that schema is + `.strict()`, so it does not merely drop the key: `defineStack` **throws** + (`Unrecognized key(s) on this stack definition: 'driver'`), and `tsc` refuses + the literal with `TS2353`. A driver is a plugin — + `plugins: [new DriverPlugin(new SqlDriver({ … }))]`, `DriverPlugin` from + `@objectstack/runtime`. The env-var route (`OS_DATABASE_URL`) is documented + alongside it. +- **Four of the six documented driver methods do not exist.** `driver.raw()` (six + call sites) is `execute()`; `checkConnection()` (two) is `checkHealth()`, which + resolves `false` rather than throwing, so the try/catch example was wrong in + shape as well as in name; `destroy()` is `disconnect()`; `transaction(cb)` is + `beginTransaction()` + `options.transaction` + `commit()`/`rollback()`, and the + callback's `trx.insert({ object, data })` names nothing at all. `getKnex()` was + the only one that resolved. +- **`kernel.getDriver()`** — three call sites; `ObjectKernel` has no such member + (`getDriver` is *private* on the engine). +- **The query AST was wrong in three places.** `find` takes the object name as + its first argument, so `find({ object, … })` is an arity error; the filter key + is `where` with the ObjectQL dialect (`{ amount: { $gte: 10000 } }`), not + `filters: [{ field, operator, value }]`; and sorting is + `orderBy: [{ field, order }]` — `sort`/`direction` is the spelling + `SortNodeSchema` lists as a retired alias. +- **The config type name was wrong.** The page declared + `interface SQLDriverConfig`; the export is `SqlDriverConfig` + (`TS2724 … Did you mean 'SqlDriverConfig'?`), it is `Knex.Config` plus four + ObjectStack keys, and all four — `schemaMode`, `autoMigrate`, + `sqliteJournalMode`, `sqliteAbsentFile` — were undocumented. +- **A config block that could not load.** The tenant-field example wrote + `tenancy: { enabled: true, strategy: 'shared', … }`; `tenancy.strategy` was + removed after spec 15.0 (#2763) and is now a tombstone that rejects with a + prescription. +- **The environment-config example did not compile even setting the fabricated + factory aside** — `configs[env]` with `env: string` is `TS7053`, and `ssl` sat + at the top level of the config, where Knex does not read it (it belongs to + `connection`). +- **Every raw-SQL example queried tables that do not exist.** The physical table + name *is* the namespace-prefixed object name (`crm_account`, `sys_user`); + nothing is prefixed `objectstack_`. +- **The Migrations section documented an off-platform workflow** — a `knexfile.js` + plus `npx knex migrate:latest`. Schema is reconciled from object metadata + (`schemaMode: 'managed'`, `autoMigrate`), reviewed with `os migrate plan` and + applied with `os migrate apply`; indexes are declared on the object + (`indexes: [{ fields, unique }]`), not issued as DDL. The "always use + migrations, never raw DDL" best-practice line said the opposite of how the + platform works. +- **A dead import.** The Vercel example imported `createClient` from + `@vercel/postgres` and never used it. + +All 19 TypeScript fences on the rewritten page are extracted verbatim and +compiled against the built `.d.ts` files the `exports` maps resolve; the two +`defineStack` shapes are additionally executed. Docs only — no runtime code +changed and no API was added. diff --git a/packages/drivers/driver-sql/README.md b/packages/drivers/driver-sql/README.md index 4d41357300..90b3cc2086 100644 --- a/packages/drivers/driver-sql/README.md +++ b/packages/drivers/driver-sql/README.md @@ -5,8 +5,8 @@ SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex.js. ## Features - **Multi-Database Support**: PostgreSQL, MySQL, SQLite, and other Knex-supported databases -- **Query Builder**: Powerful Knex.js query builder integration -- **Migrations**: Database schema migrations with version control +- **Query Builder**: the underlying Knex.js instance is reachable via `getKnex()` +- **Managed Schema**: tables, columns and indexes are reconciled from your object metadata - **Connection Pooling**: Efficient connection management - **Transactions**: Full ACID transaction support - **Raw SQL**: Execute raw SQL when needed @@ -16,12 +16,13 @@ SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex.js. ## Installation ```bash -pnpm add @objectstack/driver-sql knex +pnpm add @objectstack/driver-sql ``` ### Database-Specific Drivers -Install the driver for your database: +`pg`, `mysql2` and `tedious` are **optional peer dependencies** — install the one +your database needs: ```bash # PostgreSQL @@ -30,103 +31,135 @@ pnpm add pg # MySQL pnpm add mysql2 -# SQLite -pnpm add better-sqlite3 +# SQL Server +pnpm add tedious ``` +SQLite needs nothing extra: `better-sqlite3` ships as an optional dependency of +this package. + ## Basic Usage +A driver is not a `defineStack()` key — it is a **plugin**. Wrap it in +`DriverPlugin` from `@objectstack/runtime` and list it under `plugins`. + ### PostgreSQL ```typescript import { defineStack } from '@objectstack/spec'; +import { DriverPlugin } from '@objectstack/runtime'; import { SqlDriver } from '@objectstack/driver-sql'; -const stack = defineStack({ - driver: new SqlDriver({ - client: 'pg', - connection: { - host: 'localhost', - port: 5432, - user: 'postgres', - password: process.env.DB_PASSWORD, - database: 'myapp', - }, - pool: { - min: 2, - max: 10, - }, - }), +export default defineStack({ + manifest: { + id: 'com.example.myapp', + version: '1.0.0', + type: 'app', + name: 'My App', + }, + plugins: [ + new DriverPlugin( + new SqlDriver({ + client: 'pg', + connection: { + host: 'localhost', + port: 5432, + user: 'postgres', + password: process.env.DB_PASSWORD, + database: 'myapp', + }, + pool: { + min: 2, + max: 10, + }, + }), + ), + ], }); ``` ### MySQL +Same `plugins` entry — only the driver config changes: + ```typescript -const stack = defineStack({ - driver: new SqlDriver({ - client: 'mysql2', - connection: { - host: 'localhost', - port: 3306, - user: 'root', - password: process.env.DB_PASSWORD, - database: 'myapp', - }, - }), +import { SqlDriver } from '@objectstack/driver-sql'; + +const driver = new SqlDriver({ + client: 'mysql2', + connection: { + host: 'localhost', + port: 3306, + user: 'root', + password: process.env.DB_PASSWORD, + database: 'myapp', + }, }); ``` ### SQLite ```typescript -const stack = defineStack({ - driver: new SqlDriver({ - client: 'better-sqlite3', - connection: { - filename: './data/app.db', - }, - useNullAsDefault: true, - }), +import { SqlDriver } from '@objectstack/driver-sql'; + +const driver = new SqlDriver({ + client: 'better-sqlite3', + connection: { + filename: './data/app.db', + }, + useNullAsDefault: true, }); ``` +### Without writing any code + +`os dev` / `os serve` build this driver for you from the database URL, so a +project that needs no custom driver options can set an environment variable +instead of registering a plugin: + +```bash +OS_DATABASE_URL=postgres://user:pass@localhost:5432/myapp +OS_DATABASE_URL=mysql://user:pass@localhost:3306/myapp +OS_DATABASE_URL=file:./data/app.db +``` + ## Configuration Options +The exported config type is **`SqlDriverConfig`**. It is Knex's own +`Knex.Config` (`client`, `connection`, `pool`, `useNullAsDefault`, `debug`, …) +plus the four ObjectStack-specific keys below, which are stripped before the +config reaches Knex: + ```typescript -interface SQLDriverConfig { - /** Knex client (pg, mysql2, better-sqlite3, etc.) */ - client: string; +import type { SqlDriverConfig } from '@objectstack/driver-sql'; - /** Database connection config */ +const config: SqlDriverConfig = { + // ── Knex.Config ────────────────────────────────────────────────────────── + client: 'pg', connection: { - host?: string; - port?: number; - user?: string; - password?: string; - database?: string; - filename?: string; // For SQLite - }; - - /** Connection pool settings */ - pool?: { - min?: number; - max?: number; - idleTimeoutMillis?: number; - }; - - /** Use NULL as default for unsupported features (SQLite) */ - useNullAsDefault?: boolean; - - /** Enable query debugging */ - debug?: boolean; - - /** Migrations configuration */ - migrations?: { - directory?: string; - tableName?: string; - }; -} + host: 'localhost', + port: 5432, + user: 'postgres', + password: process.env.DB_PASSWORD, + database: 'myapp', + }, + pool: { min: 2, max: 10 }, + debug: false, + + // ── ObjectStack-specific ───────────────────────────────────────────────── + + /** 'managed' (default) reconciles tables from metadata; 'external' is read-only. */ + schemaMode: 'managed', + + /** Dev-only non-destructive auto-reconcile. 'off' (default) only warns. */ + autoMigrate: 'off', + + /** File-backed SQLite journal mode. Defaults to 'wal'. */ + sqliteJournalMode: 'wal', + + /** What to do when a file-backed SQLite target does not exist. Default 'create'. */ + sqliteAbsentFile: 'create', +}; ``` ## Database Operations @@ -140,129 +173,144 @@ import type { IDataDriver } from '@objectstack/spec/contracts'; // find, findOne, create, update, delete, count ``` +Every driver method takes the **object name as its first argument**; the query +AST that follows carries no `object` key. + ### Advanced Queries ```typescript -// The SQL driver supports all ObjectQL query features: -const results = await kernel.getDriver().find({ - object: 'opportunity', - filters: [ - { field: 'amount', operator: 'gte', value: 10000 }, - { field: 'stage', operator: 'in', value: ['proposal', 'negotiation'] }, - ], - sort: [{ field: 'amount', direction: 'desc' }], +import type { SqlDriver } from '@objectstack/driver-sql'; + +declare const driver: SqlDriver; + +// Filters are the ObjectQL filter dialect: `{ field: { $op: value } }`. +const results = await driver.find('crm_opportunity', { + where: { + amount: { $gte: 10000 }, + stage: { $in: ['proposal', 'negotiation'] }, + }, + orderBy: [{ field: 'amount', order: 'desc' }], limit: 100, offset: 0, }); ``` -## Migrations +## Schema Management -### Creating Migrations - -```typescript -// migrations/001_create_users.ts -export async function up(knex) { - await knex.schema.createTable('objectstack_user', (table) => { - table.string('id').primary(); - table.string('name').notNullable(); - table.string('email').notNullable().unique(); - table.timestamps(true, true); - }); -} +ObjectStack manages the physical schema **from your object metadata** — there +are no hand-written migration files. In the default `schemaMode: 'managed'`, the +driver creates each object's table on first boot and reports drift afterwards. -export async function down(knex) { - await knex.schema.dropTable('objectstack_user'); -} -``` +The physical table name **is** the object's (namespace-prefixed) name: +`crm_account` is stored in a table called `crm_account`, `sys_user` in +`sys_user`. Nothing is prefixed with `objectstack_`. -### Running Migrations +### Reviewing and applying schema changes ```bash -# Run all pending migrations -npx knex migrate:latest +# Dry-run diff of metadata vs the physical database (never mutates) +os migrate plan -# Rollback last migration -npx knex migrate:rollback +# Apply the reconcile +os migrate apply -# Check migration status -npx knex migrate:status +# Resume an interrupted apply +os migrate resume ``` -### Migration Configuration +### Indexes + +Declare indexes on the object, not in DDL — the driver materializes them: -Create `knexfile.js` in your project root: +```typescript +import { defineStack } from '@objectstack/spec'; -```javascript -module.exports = { - development: { - client: 'pg', - connection: { - host: 'localhost', - user: 'postgres', - password: process.env.DB_PASSWORD, - database: 'myapp_dev', - }, - migrations: { - directory: './migrations', - tableName: 'objectstack_migrations', +export default defineStack({ + manifest: { id: 'com.example.crm', version: '1.0.0', type: 'app', name: 'CRM' }, + objects: [ + { + name: 'crm_opportunity', + fields: { + account_id: { type: 'lookup', reference: 'crm_account' }, + stage: { type: 'text' }, + amount: { type: 'number' }, + }, + indexes: [ + { fields: ['account_id'] }, + { fields: ['stage'] }, + { name: 'crm_opportunity_created_stage', fields: ['created_at', 'stage'] }, + { fields: ['external_ref'], unique: 'organization' }, + ], }, - }, - production: { - client: 'pg', - connection: process.env.DATABASE_URL, - pool: { - min: 2, - max: 10, - }, - migrations: { - directory: './migrations', - tableName: 'objectstack_migrations', - }, - }, -}; + ], +}); ``` -## Transactions +`unique: 'organization'` makes the constraint one-holder-per-organization; +`unique: 'global'` makes it installation-wide. + +### Dev-only auto-reconcile ```typescript -const driver = kernel.getDriver(); - -await driver.transaction(async (trx) => { - // All operations within this callback use the same transaction - const account = await trx.insert({ - object: 'account', - data: { name: 'Acme Corp' }, - }); - - await trx.insert({ - object: 'contact', - data: { - name: 'John Doe', - account_id: account.id, - }, - }); +import { SqlDriver } from '@objectstack/driver-sql'; - // If an error is thrown, all changes are rolled back - // If successful, changes are committed +const driver = new SqlDriver({ + client: 'pg', + connection: process.env.DATABASE_URL, + // Applies non-destructive alters (relax NOT NULL, widen varchar) at boot. + // Force-disabled when NODE_ENV === 'production'. + autoMigrate: 'safe', }); ``` +## Transactions + +`beginTransaction()` returns the handle; pass it to every write as +`options.transaction`, then `commit()` or `rollback()`: + +```typescript +import type { SqlDriver } from '@objectstack/driver-sql'; + +declare const driver: SqlDriver; + +const trx = await driver.beginTransaction(); +try { + const account = await driver.create( + 'crm_account', + { name: 'Acme Corp' }, + { transaction: trx }, + ); + + await driver.create( + 'crm_contact', + { name: 'John Doe', account_id: account.id }, + { transaction: trx }, + ); + + await driver.commit(trx); +} catch (error) { + await driver.rollback(trx); + throw error; +} +``` + ## Raw SQL Queries -When ObjectQL isn't sufficient, execute raw SQL: +When ObjectQL isn't sufficient, execute raw SQL with `execute()`: ```typescript -const driver = kernel.getDriver(); +import type { SqlDriver } from '@objectstack/driver-sql'; + +declare const driver: SqlDriver; // Raw query -const results = await driver.raw(` +const rollup = await driver.execute(` SELECT c.name, COUNT(o.id) as opportunity_count, SUM(o.amount) as total_revenue - FROM objectstack_account c - LEFT JOIN objectstack_opportunity o ON o.account_id = c.id + FROM crm_account c + LEFT JOIN crm_opportunity o ON o.account_id = c.id WHERE o.stage = 'closed_won' GROUP BY c.id, c.name ORDER BY total_revenue DESC @@ -270,15 +318,15 @@ const results = await driver.raw(` `); // Raw query with parameters (prevent SQL injection) -const results = await driver.raw( - 'SELECT * FROM objectstack_user WHERE email = ?', - ['user@example.com'] +const users = await driver.execute( + 'SELECT * FROM sys_user WHERE email = ?', + ['user@example.com'], ); ``` > ⚠️ **Raw SQL bypasses driver-level tenant isolation.** The `WHERE > organization_id = ?` predicate that `find` / `update` / `delete` -> auto-apply is **not** added to `driver.raw()` or `engine.execute()` +> auto-apply is **not** added to `driver.execute()` or `engine.execute()` > output. Always include the tenant predicate yourself when running raw > queries against tenant-scoped tables. @@ -302,17 +350,29 @@ into options for you; manual `driver.find(...)` calls can pass ### Declaring the tenant field ```ts -// Custom tenant column (default is 'organization_id') -{ - name: 'workspace_item', - tenancy: { enabled: true, strategy: 'shared', tenantField: 'workspace_id' }, - fields: { - workspace_id: { type: 'string' }, - /* ... */ - }, -} +import { defineStack } from '@objectstack/spec'; + +export default defineStack({ + manifest: { id: 'com.example.ws', version: '1.0.0', type: 'app', name: 'Workspace' }, + objects: [ + { + name: 'ws_item', + // Custom tenant column (default is 'organization_id') + tenancy: { enabled: true, tenantField: 'workspace_id' }, + fields: { + workspace_id: { type: 'text' }, + title: { type: 'text' }, + }, + }, + ], +}); ``` +`tenancy.strategy` and `tenancy.crossTenantAccess` were removed after spec +15.0 and are now rejected outright — the two supported modes are +database-per-tenant (a deployment choice, no object config) and row-level +isolation (`tenancy.enabled` + `tenancy.tenantField`). + ### Bypasses (intentional, documented) | Path | Tenant-scoped? | Why | @@ -320,7 +380,7 @@ into options for you; manual `driver.find(...)` calls can pass | Callers that omit `options.tenantId` | No | Seed scripts, boot-time installers, admin tooling | | `ExecutionContext.isSystem === true` | No (auto-`bypassTenantAudit`) | Kernel-internal mirrors, scheduled hooks | | Explicit `organization_id` on insert row | Wins | Admin tooling can target a specific tenant | -| `driver.raw()` / `engine.execute(sql)` | No | Raw SQL is on you | +| `driver.execute()` / `engine.execute(sql)` | No | Raw SQL is on you | | `driver.bulkUpdate` | Yes (it loops `update`) | Same scope as `update` | ### Audit warning @@ -338,15 +398,19 @@ Override globally: `OS_TENANT_AUDIT=0`. ### PostgreSQL Features ```typescript +import type { SqlDriver } from '@objectstack/driver-sql'; + +declare const driver: SqlDriver; + // Use PostgreSQL-specific features -const results = await driver.raw(` - SELECT * FROM objectstack_opportunity +const tech = await driver.execute(` + SELECT * FROM crm_opportunity WHERE data @> '{"industry": "Technology"}'::jsonb `); // Full-text search -const results = await driver.raw(` - SELECT * FROM objectstack_article +const articles = await driver.execute(` + SELECT * FROM blog_article WHERE to_tsvector('english', title || ' ' || body) @@ to_tsquery('objectstack') `); ``` @@ -354,9 +418,13 @@ const results = await driver.raw(` ### MySQL Features ```typescript +import type { SqlDriver } from '@objectstack/driver-sql'; + +declare const driver: SqlDriver; + // Use MySQL-specific features -const results = await driver.raw(` - SELECT * FROM objectstack_product +const products = await driver.execute(` + SELECT * FROM shop_product WHERE MATCH(name, description) AGAINST ('widget' IN NATURAL LANGUAGE MODE) `); ``` @@ -364,42 +432,39 @@ const results = await driver.raw(` ## Connection Management ```typescript +import type { SqlDriver } from '@objectstack/driver-sql'; + +declare const driver: SqlDriver; + // Get underlying Knex instance const knex = driver.getKnex(); -// Check connection -await driver.checkConnection(); +// Check connection — resolves to false rather than throwing +const healthy: boolean = await driver.checkHealth(); // Close all connections -await driver.destroy(); +await driver.disconnect(); ``` ## Performance Optimization -### Indexes +### Query Optimization ```typescript -// Create index migration -export async function up(knex) { - await knex.schema.table('objectstack_opportunity', (table) => { - table.index('account_id'); - table.index('stage'); - table.index(['created_at', 'stage']); // Composite index - }); -} -``` +import type { SqlDriver } from '@objectstack/driver-sql'; -### Query Optimization +declare const driver: SqlDriver; -```typescript -// Use explain to analyze queries -const plan = await driver.raw('EXPLAIN ANALYZE SELECT ...'); +// Ask the database for the plan behind a query +const plan = await driver.explain('crm_opportunity', { + where: { stage: 'proposal' }, +}); -// Create covering indexes for frequently accessed columns -// Use partial indexes for filtered queries (PostgreSQL) -await knex.raw(` +// Partial / covering indexes that the declaration surface does not express are +// issued at the database layer, through the Knex instance. +await driver.getKnex().raw(` CREATE INDEX idx_active_opportunities - ON objectstack_opportunity(account_id, amount) + ON crm_opportunity(account_id, amount) WHERE stage NOT IN ('closed_won', 'closed_lost') `); ``` @@ -407,45 +472,50 @@ await knex.raw(` ## Best Practices 1. **Connection Pooling**: Configure appropriate pool size based on load -2. **Migrations**: Always use migrations for schema changes, never raw DDL +2. **Schema**: Declare fields and indexes in metadata; review changes with `os migrate plan` 3. **Transactions**: Use transactions for multi-step operations 4. **Prepared Statements**: Use parameterized queries to prevent SQL injection -5. **Indexes**: Create indexes on frequently queried fields +5. **Indexes**: Declare indexes on frequently queried fields 6. **Monitoring**: Monitor slow query logs and connection pool metrics 7. **Backups**: Implement regular database backups ## Environment-Specific Configuration ```typescript +import { defineStack } from '@objectstack/spec'; +import { DriverPlugin } from '@objectstack/runtime'; +import { SqlDriver, type SqlDriverConfig } from '@objectstack/driver-sql'; + // config/database.ts -export const getDatabaseConfig = () => { - const env = process.env.NODE_ENV || 'development'; - - const configs = { - development: { - client: 'better-sqlite3', - connection: { filename: './data/dev.db' }, - useNullAsDefault: true, - debug: true, - }, - test: { - client: 'better-sqlite3', - connection: { filename: ':memory:' }, - useNullAsDefault: true, - }, - production: { - client: 'pg', - connection: process.env.DATABASE_URL, - pool: { min: 2, max: 10 }, +const configs: Record = { + development: { + client: 'better-sqlite3', + connection: { filename: './data/dev.db' }, + useNullAsDefault: true, + debug: true, + }, + test: { + client: 'better-sqlite3', + connection: { filename: ':memory:' }, + useNullAsDefault: true, + }, + production: { + client: 'pg', + // `ssl` belongs to the CONNECTION, not the top level of the config. + connection: { + connectionString: process.env.DATABASE_URL, ssl: { rejectUnauthorized: false }, }, - }; - - return configs[env] || configs.development; + pool: { min: 2, max: 10 }, + }, }; -const stack = defineStack({ - driver: DriverSQL.configure(getDatabaseConfig()), +export const getDatabaseConfig = (): SqlDriverConfig => + configs[process.env.NODE_ENV ?? 'development'] ?? configs.development; + +export default defineStack({ + manifest: { id: 'com.example.myapp', version: '1.0.0', type: 'app', name: 'My App' }, + plugins: [new DriverPlugin(new SqlDriver(getDatabaseConfig()))], }); ``` @@ -454,37 +524,42 @@ const stack = defineStack({ ### Connection Issues ```typescript +import type { SqlDriver } from '@objectstack/driver-sql'; + +declare const driver: SqlDriver; + // Test database connection -try { - await driver.checkConnection(); +if (await driver.checkHealth()) { console.log('Database connected successfully'); -} catch (error) { - console.error('Database connection failed:', error); +} else { + console.error('Database connection failed'); } ``` -### Migration Errors +### Schema Drift ```bash -# Check migration status -npx knex migrate:status +# Review what metadata wants versus what the database has +os migrate plan -# Rollback and re-run -npx knex migrate:rollback -npx knex migrate:latest +# Apply it +os migrate apply ``` ### Query Debugging ```typescript +import { DriverPlugin } from '@objectstack/runtime'; +import { SqlDriver } from '@objectstack/driver-sql'; + // Enable query logging -const stack = defineStack({ - driver: DriverSQL.configure({ +const plugin = new DriverPlugin( + new SqlDriver({ client: 'pg', - connection: { /* ... */ }, + connection: process.env.DATABASE_URL, debug: true, // Log all queries }), -}); +); ``` ## Deployment @@ -495,8 +570,8 @@ const stack = defineStack({ # Heroku automatically provides DATABASE_URL heroku addons:create heroku-postgresql:hobby-dev -# Run migrations on deployment -echo "npx knex migrate:latest" > Procfile.release +# ObjectStack reads it directly +OS_DATABASE_URL="$DATABASE_URL" ``` ### Railway PostgreSQL @@ -509,14 +584,21 @@ railway up ### Vercel PostgreSQL ```typescript -// Vercel uses connection pooling -import { createClient } from '@vercel/postgres'; +import { defineStack } from '@objectstack/spec'; +import { DriverPlugin } from '@objectstack/runtime'; +import { SqlDriver } from '@objectstack/driver-sql'; -const stack = defineStack({ - driver: DriverSQL.configure({ - client: 'pg', - connection: process.env.POSTGRES_URL, - }), +export default defineStack({ + manifest: { id: 'com.example.myapp', version: '1.0.0', type: 'app', name: 'My App' }, + plugins: [ + new DriverPlugin( + new SqlDriver({ + client: 'pg', + // Vercel Postgres pools through this URL. + connection: process.env.POSTGRES_URL, + }), + ), + ], }); ``` @@ -530,4 +612,5 @@ Apache-2.0. See [LICENSING.md](../../../LICENSING.md). - [PostgreSQL Documentation](https://www.postgresql.org/docs/) - [MySQL Documentation](https://dev.mysql.com/doc/) - [@objectstack/driver-turso](../driver-turso/) - Edge-first SQLite alternative (extends this driver) +- [@objectstack/driver-sqlite-wasm](../driver-sqlite-wasm/) - In-process WASM SQLite (extends this driver) - [@objectstack/driver-memory](../driver-memory/) - In-memory driver for testing