From f2ff3b77d9081461bb7a042644433ea8e7a3fae5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 14:15:52 +0000 Subject: [PATCH] fix(metadata): state the raw-driver remedy once in migrateProjectIdToEnvironmentId (#13219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guard concatenated its instruction sentence twice, so an operator calling the migration with a driver that has no `raw()` read the same remedy twice in one message. A copy-paste artifact: the sibling `migrateEnvIdToProjectId` carries the correct single-sentence form of the identical guard. Cosmetic and operator-facing only — the guard fires on the same condition and names the same remedy. The surviving line keeps the trailing space inside its literal, which is what separates it from the sentence naming the conforming drivers; trimming it would run the two sentences together, this defect inverted. The package's refusal case now pins the properties of the assembled message (the instruction appears exactly once, no sentence runs into the next, and the supporting sentence is still present) instead of substring-matching it, which could not see a second copy and so passed either way. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry --- ...grate-project-id-guard-doubled-sentence.md | 24 +++++++++++++ ...grate-project-id-to-environment-id.test.ts | 34 +++++++++++++++++-- .../migrate-project-id-to-environment-id.ts | 1 - 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 .changeset/migrate-project-id-guard-doubled-sentence.md diff --git a/.changeset/migrate-project-id-guard-doubled-sentence.md b/.changeset/migrate-project-id-guard-doubled-sentence.md new file mode 100644 index 0000000000..8f7e45df18 --- /dev/null +++ b/.changeset/migrate-project-id-guard-doubled-sentence.md @@ -0,0 +1,24 @@ +--- +"@objectstack/metadata": patch +--- + +fix(metadata): `migrateProjectIdToEnvironmentId`'s raw-driver guard stated its instruction sentence twice (#13219) + +An operator who called `migrateProjectIdToEnvironmentId` with a driver that has +no `raw()` was refused correctly, but read the same remedy twice in one message: + +``` +migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms. +``` + +The sentence was concatenated twice, a copy-paste artifact — the sibling +`migrateEnvIdToProjectId` carries the correct single-sentence form of the +identical guard. Cosmetic and operator-facing only: the guard fires on exactly +the same condition, the remedy it names is unchanged, and nothing parses the +message. The duplicate line is deleted; the surviving sentence keeps the +trailing space that separates it from the one naming the conforming drivers. + +The refusal case in the package's tests now pins the properties — the +instruction appears exactly once, no sentence runs into the next, and the +supporting sentence is still present — rather than substring-matching the +message, which could not see a second copy and so passed either way. diff --git a/packages/metadata/src/migrations/migrate-project-id-to-environment-id.test.ts b/packages/metadata/src/migrations/migrate-project-id-to-environment-id.test.ts index 977cea41e6..69b51145fa 100644 --- a/packages/metadata/src/migrations/migrate-project-id-to-environment-id.test.ts +++ b/packages/metadata/src/migrations/migrate-project-id-to-environment-id.test.ts @@ -191,9 +191,37 @@ describe('migrateProjectIdToEnvironmentId — behaviour against a physically-sta expect(statements.filter((s) => s.startsWith('ALTER TABLE'))).toEqual([]); }); - it('still refuses a driver without .raw()', async () => { - await expect(migrateProjectIdToEnvironmentId({} as any)).rejects.toThrow( - /must expose a \.raw\(sql, bindings\?\) method/, + it('still refuses a driver without .raw(), stating the remedy exactly once', async () => { + // #13219 — the guard concatenated its instruction sentence TWICE, so an + // operator with a raw-less driver read the same remedy twice in one + // message. The assertion that used to stand here + // (`rejects.toThrow(/must expose a \.raw\(sql, bindings\?\) method/)`) + // passed either way: a substring match cannot see a second copy. So + // these pin the PROPERTIES of the assembled message, not a full-string + // copy of today's wording. + const outcome: unknown = await migrateProjectIdToEnvironmentId({} as any).then( + (value) => value, + (error: unknown) => error, ); + expect(outcome, 'a driver with no .raw() must be refused').toBeInstanceOf(Error); + const message = (outcome as Error).message; + + // 1. The remedy is stated exactly ONCE. Counted, not compared, so a + // later rewording of the sentence still leaves this asserting. + const instruction = /driver must expose a \.raw\(sql, bindings\?\) method\./g; + expect(message.match(instruction) ?? []).toHaveLength(1); + + // 2. ...and the sentences stay SEPARATED. Deleting the duplicate by + // trimming the surviving line's trailing space would satisfy (1) + // while gluing `method.SqlDriver` — this defect inverted, so it is + // pinned in the same case. A run-together sentence boundary is a + // lowercase letter, a period, then a capital; the `.raw(` in the + // text is lowercase-after-period and so is correctly not one. + expect(message).not.toMatch(/[a-z]\.[A-Z]/); + + // 3. Non-vacuity: deleting the SUPPORTING sentence instead would also + // satisfy (1) and (2). It names the drivers that do conform, which + // is the half of the message an operator acts on. + expect(message).toMatch(/SqlDriver/); }); }); diff --git a/packages/metadata/src/migrations/migrate-project-id-to-environment-id.ts b/packages/metadata/src/migrations/migrate-project-id-to-environment-id.ts index 21013e4410..0105db21f0 100644 --- a/packages/metadata/src/migrations/migrate-project-id-to-environment-id.ts +++ b/packages/metadata/src/migrations/migrate-project-id-to-environment-id.ts @@ -118,7 +118,6 @@ export async function migrateProjectIdToEnvironmentId( if (typeof driverAny.raw !== 'function') { throw new Error( - 'migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. ' + 'migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. ' + 'SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms.' );