Files
skills/docs/api-stack/api/api-migrations/src/adapters/surrealql/surrealql.md
2026-05-29 08:20:56 +01:00

4.6 KiB

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

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

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<T>(sql, vars?): Promise<T[]> } — 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.