Files
2026-05-27 23:29:53 +01:00

5.1 KiB

Capability Spec Delta: api-migrations (fix-api-migrations-surrealql-ledger)

Package: @suaveplan/api-migrations Status: PROPOSED

The existing Requirement: MigrationAdapter is the per-dialect surface covers the full adapter contract, including insertApplied/deleteApplied/selectApplied. The body of that requirement is unchanged. This change adds a new requirement that pins the v3-safe record-id production for the SurrealQL adapter specifically — it is additive to the existing per-dialect surface, not a replacement.

ADDED Requirements

Requirement: SurrealQL adapter records the ledger row under SurrealDB v3

createSurrealqlAdapter.insertApplied(table, record) SHALL produce a record-id reference in the form <table>:\`whereisrecord.idwith every backtick doubled. The adapter SHALL NOT use the${table}:$nameform — SurrealDB v3 parses$paramin the record-id position as a literal ident (not a parameter substitution), so the CREATE is acknowledged but silently records no row, leaving the ledger empty and causing the runner to re-execute completed migrations. After a successfulinsertAppliedinvocation, a subsequentselectApplied(table)on the same connection SHALL return a row whosename, checksum, and duration` match the inserted record.

createSurrealqlAdapter.deleteApplied(table, id) SHALL produce the matching <table>:\`reference so the round-trip fromselectApplied→deleteAppliedcorrectly targets the row thatinsertAppliedwrote, regardless of whetherid` is a bare SurrealQL ident.

createSurrealqlAdapter.selectApplied(table) SHALL normalise the SurrealDB-returned id field to the bare migration name — strip the ${table}: prefix and any wrapping backticks, and for object-form RecordId { tb, id } return the inner id string when present. This guarantees rollback()'s call to deleteApplied(table, last.id) produces the same record-ref shape as the original insertApplied.

createSurrealqlAdapter.insertApplied SHALL bind record.appliedAt as the raw Date value, NOT as an ISO 8601 string. SurrealDB v3's TYPE datetime column refuses to coerce a string into datetime and rejects the CREATE with InternalError: Couldn't coerce value for field 'appliedAt'. The SDK CBOR-encodes the Date binding as a SurrealDB datetime value.

Scenario: insertApplied uses backticked record-id literal (v3-safe)

  • WHEN createSurrealqlAdapter(client).insertApplied("_migrations", { id: "001", name: "001", appliedAt, checksum: "h", duration: 5 }) is called
  • THEN the issued SurrealQL is CREATE _migrations:\001` SET name = $name, appliedAt = $appliedAt, checksum = $checksum, duration = $duration(NOTCREATE _migrations:$name SET ...`)

Scenario: backticks in id are escaped

  • WHEN record.id is weird\id`
  • THEN the issued SurrealQL contains CREATE _migrations:\weird``id` SET ...`

Scenario: ledger row is queryable after the migration body succeeds (live v3)

  • WHEN runFromDirectory(dir) applies a single migration against a real SurrealDB v3.x testcontainer
  • THEN SELECT name, checksum, duration FROM _migrations on the same connection returns exactly one row with the migration name and a 64-char-hex checksum

Scenario: second run sees the ledger and skips already-applied migrations (live v3)

  • WHEN runFromDirectory(dir) runs a second time after the first has applied one migration
  • THEN the result array is empty AND the ledger still contains exactly one row

Scenario: rollback removes the ledger row written by insertApplied (live v3)

  • WHEN runner.rollback() is awaited after one successful apply
  • THEN the rollback result is { name, success: true } AND the ledger table is empty

Scenario: selectApplied normalises returned ids to the bare migration name

  • WHEN SurrealDB returns id: "_migrations:\001_initial`"` for a row
  • THEN selectApplied("_migrations") returns { id: "001_initial", ... }

Verified by: src/adapters/surrealql/surrealql.test.ts::"insertApplied uses backticked record-id literal (v3-safe)" AND src/adapters/surrealql/surrealql.test.ts::"insertApplied escapes backticks inside the migration id" AND src/adapters/surrealql/surrealql.test.ts::"insertApplied binds appliedAt as a Date (not a string)" AND src/adapters/surrealql/surrealql.test.ts::"deleteApplied uses backticked record-id literal (v3-safe)" AND src/adapters/surrealql/surrealql.test.ts::"selectApplied strips backtick-quoted id form" AND src/adapters/surrealql/surrealql.test.ts::"selectApplied strips table prefix from string ids" AND tests/integration/surrealql-ledger.integration.test.ts::"ledger row is queryable after a migration body succeeds (regression: silent CREATE drop)" AND tests/integration/surrealql-ledger.integration.test.ts::"second run sees the ledger and skips already-applied migrations" AND tests/integration/surrealql-ledger.integration.test.ts::"rollback() removes the ledger row written by insertApplied (round-trip)"