# `adapters/surrealql` — SurrealDB migration adapter _(no submodules)_ ## Purpose Implements the `MigrationAdapter` contract for SurrealDB. The factory `createSurrealqlAdapter(client)` wraps any SurrealDB client that exposes a `query(sql, vars?)` method (the official `surrealdb` JavaScript driver and the in-process `SurrealAdapter` from `@suaveplan/surrealdb` both satisfy this structurally) and returns the six-method surface the runner consumes. The SurrealQL DDL for the tracking table lives in this adapter — not in the runner — so the runner stays dialect-agnostic and a sibling adapter package may ship a different DDL for the same logical schema. ## Features - `ensureTable(table)` issues idempotent `DEFINE TABLE / DEFINE FIELD / DEFINE INDEX` statements gated on `IF NOT EXISTS`, so concurrent boots converge rather than racing. - `selectApplied(table)` returns canonical `MigrationRecord` shapes — the adapter normalises the Surreal `id` (Record ID object or string) to a plain string and `appliedAt` (ISO string) to a `Date`. - `insertApplied(table, record)` writes the ledger row using a backtick-quoted record-id literal (`` CREATE table:`id` SET ... ``) so the CREATE is parsed as a record-id reference under SurrealDB v3. Field values use named bindings, never string interpolation. The pre-fix form `CREATE table:$name SET ...` was silently dropped by SurrealDB v3's parser (the `$param` was consumed as a literal ident in the record-id position, not substituted, so the statement returned no row). - `deleteApplied(table, id)` removes the tracking row via the matching `` table:`id` `` form so the `selectApplied` → `deleteApplied` round-trip targets the same row regardless of whether the migration name is a bare SurrealQL ident. Optionally locked behind `{ rollback: false }` to enforce forward-only deploys. - `execute(content)` runs the migration body as-is. - Bootstrap failures raise `MigrationError(MIGRATION.TABLE_BOOTSTRAP_FAILED)` with the underlying client error as `cause`. - Disabled rollback raises `MigrationError(MIGRATION.ROLLBACK_NOT_SUPPORTED)` from `deleteApplied`. ## Usage: Basic Example ```ts import { Surreal } from "surrealdb"; import { createSurrealqlAdapter } from "@suaveplan/api-migrations/adapters/surrealql/surrealql"; import { MigrationRunner } from "@suaveplan/api-migrations/runner/runner"; const client = new Surreal(); await client.connect("ws://localhost:8000/rpc"); await client.use({ namespace: "test", database: "test" }); await client.signin({ username: "root", password: "root" }); const runner = new MigrationRunner(createSurrealqlAdapter(client)); const results = await runner.runFromDirectory("./db/migrations"); ``` ## Usage: Advanced Example ```ts import { createSurrealqlAdapter } from "@suaveplan/api-migrations/adapters/surrealql/surrealql"; const forwardOnly = createSurrealqlAdapter(client, { rollback: false }); try { await forwardOnly.deleteApplied("_migrations", "001_initial"); } catch (err) { // err.code === "MIGRATION.ROLLBACK_NOT_SUPPORTED" } ``` ## API Reference - `interface SurrealqlClient { query(sql, vars?): Promise }` — structural client surface; satisfied by the official `surrealdb` driver. - `interface SurrealqlAdapterOptions { rollback?: boolean }` — defaults to `true`. When `false`, `deleteApplied` rejects with `MIGRATION.ROLLBACK_NOT_SUPPORTED`. - `createSurrealqlAdapter(client, options?): MigrationAdapter` — returns an adapter with `dialect: "surrealql"`. ## Implementation Notes - The DDL is emitted as a single multi-statement string so a single round trip bootstraps the tracking table on first run. - `appliedAt` is sent as an ISO string in `insertApplied`; SurrealQL accepts ISO 8601 for `datetime` fields and the round-trip preserves wall-clock ordering even when the client is in a different time zone. - `selectApplied` does not enforce ordering — the runner sorts by `name` before comparing against pending files, so order is a runner concern. - `selectApplied` normalises the returned `id` to the bare migration name: strips the `${table}:` prefix and any wrapping backticks, and for object- form `RecordId { tb, id }` returns the inner string id when present. This guarantees the value that round-trips into `deleteApplied(table, id)` produces the same `` table:`id` `` reference that `insertApplied` wrote. - The adapter never injects an `AbortSignal` into `client.query` because the upstream `surrealdb` driver does not yet honor one; if a future driver version gains signal support, the adapter will pick it up by forwarding the optional `signal` argument through.