Skip to content

Commit 0ae9e1e

Browse files
os-zhuangclaude
andauthored
fix(metadata): state the raw-driver remedy once in migrateProjectIdToEnvironmentId (#13219) (#13243)
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. Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry Co-authored-by: Claude <noreply@anthropic.com>
1 parent cc62146 commit 0ae9e1e

3 files changed

Lines changed: 55 additions & 4 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/metadata": patch
3+
---
4+
5+
fix(metadata): `migrateProjectIdToEnvironmentId`'s raw-driver guard stated its instruction sentence twice (#13219)
6+
7+
An operator who called `migrateProjectIdToEnvironmentId` with a driver that has
8+
no `raw()` was refused correctly, but read the same remedy twice in one message:
9+
10+
```
11+
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.
12+
```
13+
14+
The sentence was concatenated twice, a copy-paste artifact — the sibling
15+
`migrateEnvIdToProjectId` carries the correct single-sentence form of the
16+
identical guard. Cosmetic and operator-facing only: the guard fires on exactly
17+
the same condition, the remedy it names is unchanged, and nothing parses the
18+
message. The duplicate line is deleted; the surviving sentence keeps the
19+
trailing space that separates it from the one naming the conforming drivers.
20+
21+
The refusal case in the package's tests now pins the properties — the
22+
instruction appears exactly once, no sentence runs into the next, and the
23+
supporting sentence is still present — rather than substring-matching the
24+
message, which could not see a second copy and so passed either way.

packages/metadata/src/migrations/migrate-project-id-to-environment-id.test.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,37 @@ describe('migrateProjectIdToEnvironmentId — behaviour against a physically-sta
191191
expect(statements.filter((s) => s.startsWith('ALTER TABLE'))).toEqual([]);
192192
});
193193

194-
it('still refuses a driver without .raw()', async () => {
195-
await expect(migrateProjectIdToEnvironmentId({} as any)).rejects.toThrow(
196-
/must expose a \.raw\(sql, bindings\?\) method/,
194+
it('still refuses a driver without .raw(), stating the remedy exactly once', async () => {
195+
// #13219 — the guard concatenated its instruction sentence TWICE, so an
196+
// operator with a raw-less driver read the same remedy twice in one
197+
// message. The assertion that used to stand here
198+
// (`rejects.toThrow(/must expose a \.raw\(sql, bindings\?\) method/)`)
199+
// passed either way: a substring match cannot see a second copy. So
200+
// these pin the PROPERTIES of the assembled message, not a full-string
201+
// copy of today's wording.
202+
const outcome: unknown = await migrateProjectIdToEnvironmentId({} as any).then(
203+
(value) => value,
204+
(error: unknown) => error,
197205
);
206+
expect(outcome, 'a driver with no .raw() must be refused').toBeInstanceOf(Error);
207+
const message = (outcome as Error).message;
208+
209+
// 1. The remedy is stated exactly ONCE. Counted, not compared, so a
210+
// later rewording of the sentence still leaves this asserting.
211+
const instruction = /driver must expose a \.raw\(sql, bindings\?\) method\./g;
212+
expect(message.match(instruction) ?? []).toHaveLength(1);
213+
214+
// 2. ...and the sentences stay SEPARATED. Deleting the duplicate by
215+
// trimming the surviving line's trailing space would satisfy (1)
216+
// while gluing `method.SqlDriver` — this defect inverted, so it is
217+
// pinned in the same case. A run-together sentence boundary is a
218+
// lowercase letter, a period, then a capital; the `.raw(` in the
219+
// text is lowercase-after-period and so is correctly not one.
220+
expect(message).not.toMatch(/[a-z]\.[A-Z]/);
221+
222+
// 3. Non-vacuity: deleting the SUPPORTING sentence instead would also
223+
// satisfy (1) and (2). It names the drivers that do conform, which
224+
// is the half of the message an operator acts on.
225+
expect(message).toMatch(/SqlDriver/);
198226
});
199227
});

packages/metadata/src/migrations/migrate-project-id-to-environment-id.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ export async function migrateProjectIdToEnvironmentId(
118118

119119
if (typeof driverAny.raw !== 'function') {
120120
throw new Error(
121-
'migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. ' +
122121
'migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. ' +
123122
'SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms.'
124123
);

0 commit comments

Comments
 (0)