55 lines
5.1 KiB
Markdown
55 lines
5.1 KiB
Markdown
<!-- markdownlint-disable MD024 -->
|
|
|
|
# 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>:\`<escaped-id>\`` where `<escaped-id>` is `record.id` with every backtick doubled. The adapter SHALL NOT use the `${table}:$name` form — SurrealDB v3 parses `$param` in 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 successful `insertApplied` invocation, a subsequent `selectApplied(table)` on the same connection SHALL return a row whose `name`, `checksum`, and `duration` match the inserted record.
|
|
|
|
`createSurrealqlAdapter.deleteApplied(table, id)` SHALL produce the matching `<table>:\`<escaped-id>\`` reference so the round-trip from `selectApplied` → `deleteApplied` correctly targets the row that `insertApplied` wrote, regardless of whether `id` 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` (NOT `CREATE _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)"`
|
|
|
|
<!-- markdownlint-enable MD024 -->
|