Skip to content

Commit f90ea26

Browse files
committed
fix(metadata-core, metadata-protocol): two refusals say what 18.0 supports, not the ownership command 18.0 refuses
LEGACY_ORGANIZATION_ROWS_UNREADABLE and the seed refusal for a natural key held by a row with no organization no longer prescribe `os migrate organization-ownership`. Each closes on the release sentence the CLI, the boot refusal and the upgrade entries carry. The legacy read's text also says that a fresh 18.0 install meets it only through the cause named in the detail. Codes and outcomes are unchanged. Claude-Session: https://claude.ai/code/session_01KQiKEtTxcLqE3TeLATbWFb Co-authored-by: Claude <noreply@anthropic.com>
1 parent 73d4cc7 commit f90ea26

6 files changed

Lines changed: 57 additions & 6 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/metadata-core': patch
3+
'@objectstack/metadata-protocol': patch
4+
---
5+
6+
fix(metadata-core, metadata-protocol): two refusals say what 18.0 supports for a v17 database, not `os migrate organization-ownership`, which 18.0 refuses on every database
7+
8+
Clause-②: no (text only: no refusal code, outcome, accept set or served shape moves)
9+
10+
In 18.0, `os migrate organization-ownership` refuses plan and apply on every database (`NOT_IMPLEMENTED`): 18.0 supports fresh installs only. Two refusals still told the operator to run it, so following them led to a second refusal.
11+
12+
- **`LEGACY_ORGANIZATION_ROWS_UNREADABLE`** (`LegacyOrganizationRowsUnreadable`, `@objectstack/metadata-core`). It used to close with "Run `os migrate organization-ownership` on this database first." This read runs on every SQL database, a fresh 18.0 one included, and several of its refusals are the driver's (a dialect it cannot name, no raw-SQL seam, a column list that is not a list or comes back empty). For those, the command was never the remedy. The message now closes: "A fresh 18.0 install holds no legacy organization rows: there, the cause is the one named above, and the fix is in the SQL driver or the caller that met it. A database that does hold them is a v17 one. 18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path."
13+
- **The seed refusal for a natural key held by a row with no organization** (`SeedLoaderService`, `@objectstack/metadata-protocol`). It used to close with "Attribute the unowned rows first (`os migrate organization-ownership`, then the same command with --apply), then re-run this seed". Even before 18.0, that bare `--apply` was refused, since the command requires `--plan-file`. The message now closes: "A row with no organization predates organization stamping, so it is a v17 database's. 18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path."
14+
- **Unchanged:** both refusal codes, "Nothing was listed or written.", the seed refusal's opening sentences (what was refused and why), its `errored` count, and the dropped record.

‎packages/metadata-core/src/legacy-organization-row.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,28 @@ describe('readLegacyOrganizationRows — fails closed', () => {
115115
await expect(readLegacyOrganizationRows({ dialectName: 'sqlite' })).rejects.toThrow(/no raw-SQL seam/);
116116
});
117117

118+
// What 18.0 supports, said to both readers: a driver-shaped refusal, which a
119+
// fresh install meets too, and one only a database still carrying the
120+
// column reaches. The ceremony 18.0 refuses is never the remedy.
121+
it.each([
122+
['driver-shaped: no raw seam (any database, a fresh one included)', { dialectName: 'sqlite' }],
123+
['database-shaped: the column is on disk, the legacy select answered nothing', sqlDriver({ dialectName: 'sqlite', columns: V17_COLUMNS, rows: null })],
124+
])('the refusal says what 18.0 supports, never the ceremony (%s)', async (_label, driver) => {
125+
const error = await readLegacyOrganizationRows(driver).then(() => null, (e: unknown) => e);
126+
expect(error).toBeInstanceOf(LegacyOrganizationRowsUnreadable);
127+
const refusal = error as LegacyOrganizationRowsUnreadable;
128+
expect(refusal.code).toBe('LEGACY_ORGANIZATION_ROWS_UNREADABLE');
129+
expect(refusal.message).toContain('Nothing was listed or written.');
130+
expect(refusal.message).toContain(
131+
'A fresh 18.0 install holds no legacy organization rows: there, the cause is the one named above, ' +
132+
'and the fix is in the SQL driver or the caller that met it.',
133+
);
134+
expect(refusal.message).toMatch(
135+
/A database that does hold them is a v17 one\. 18\.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17\.x release until the 18\.x release that carries the upgrade path\.$/,
136+
);
137+
expect(refusal.message).not.toContain('os migrate organization-ownership');
138+
});
139+
118140
it('a table outside the family is refused before anything runs', async () => {
119141
const driver = sqlDriver({ dialectName: 'sqlite', columns: V17_COLUMNS, rows: [] });
120142
await expect(readLegacyOrganizationRows(driver, 'sys_user')).rejects.toThrow(/not a sys_metadata-family table/);

‎packages/metadata-core/src/legacy-organization-row.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ export class LegacyOrganizationRowsUnreadable extends Error {
7575
constructor(readonly table: string, detail: string) {
7676
super(
7777
`Could not tell ${table}'s legacy organization rows from the environment's: ${detail}. ` +
78-
'Nothing was listed or written. Run `os migrate organization-ownership` on this database first.',
78+
'Nothing was listed or written. A fresh 18.0 install holds no legacy organization rows: there, the ' +
79+
'cause is the one named above, and the fix is in the SQL driver or the caller that met it. A database ' +
80+
'that does hold them is a v17 one. 18.0 supports fresh installs only and does not upgrade a v17 ' +
81+
'database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path.',
7982
);
8083
this.name = 'LegacyOrganizationRowsUnreadable';
8184
}

‎packages/metadata-protocol/src/seed-loader-null-organization-holder.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,15 @@ describe('[#24301] a scoped load over a NULL-organization holder of a natural ke
135135
const [refusal] = refusalsOf(result);
136136
expect(refusal).toMatchObject({ sourceObject: 'tw_widget', targetField: 'sku', attemptedValue: 'W-HELD', recordIndex: 0 });
137137
expect(refusal.message).toContain("'w_unowned'");
138+
// What 18.0 supports, never the ceremony it refuses; the first sentences
139+
// (what was refused, and why) stay.
140+
expect(refusal.message).toContain('which has no organization (organization_id is NULL)');
141+
expect(refusal.message).toContain('decided by the organization-ownership ceremony, not by a seed load.');
142+
expect(refusal.message).toMatch(
143+
/A row with no organization predates organization stamping, so it is a v17 database's\. 18\.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17\.x release until the 18\.x release that carries the upgrade path\.$/,
144+
);
145+
expect(refusal.message).not.toContain('os migrate organization-ownership');
146+
expect(refusal.message).not.toContain('--apply');
138147
});
139148

140149
it("the organization's own row wins: a key held both by it and by an unowned row is matched, not refused", async () => {

‎packages/metadata-protocol/src/seed-loader.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,9 +1735,10 @@ export class SeedLoaderService implements ISeedLoaderService {
17351735
`which has no organization (organization_id is NULL), and this seed load writes for organization ` +
17361736
`${config.organizationId}. Inserting the record would put a twin beside that row, and updating the ` +
17371737
`row would hand it to this organization: which organization an unowned row belongs to is decided by ` +
1738-
`the organization-ownership ceremony, not by a seed load. Attribute the unowned rows first ` +
1739-
`(\`os migrate organization-ownership\`, then the same command with --apply), then re-run this seed: ` +
1740-
`the row is this organization's own by then, and the upsert matches it.`,
1738+
`the organization-ownership ceremony, not by a seed load. A row with no organization predates ` +
1739+
`organization stamping, so it is a v17 database's. 18.0 supports fresh installs only and does not ` +
1740+
`upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries ` +
1741+
`the upgrade path.`,
17411742
};
17421743
errors.push(error);
17431744
allErrors.push(error);

‎packages/runtime/src/seed-loader-null-organization-holder-real-driver.integration.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,12 @@ describe('scoped seed load over NULL-organization holders (sqlite-file)', () =>
209209
expect(kickoff.targetObject).toBe('tw_event');
210210
expect(kickoff.targetField).toBe('subject');
211211
expect(kickoff.attemptedValue).toBe('Kickoff');
212-
// The message names the key, the unowned holder and the remedy's command.
212+
// The message names the key, the unowned holder and what 18.0 supports:
213+
// never the ceremony 18.0 refuses.
213214
expect(kickoff.message).toContain('subject=Kickoff');
214215
expect(kickoff.message).toContain("'ev_kickoff'");
215-
expect(kickoff.message).toContain('os migrate organization-ownership');
216+
expect(kickoff.message).toContain('18.0 supports fresh installs only and does not upgrade a v17 database');
217+
expect(kickoff.message).not.toContain('os migrate organization-ownership');
216218
expect(result.success).toBe(false);
217219
// Loud: said at `error`, once per dataset.
218220
const lines = logger.error.mock.calls.map((c) => String(c[0])).filter((m) => refusals.some((e) => m.includes(e.message)));

0 commit comments

Comments
 (0)