From c1927882995b1000733a9b4a56aad0e416171082 Mon Sep 17 00:00:00 2001 From: Alan Roe Date: Fri, 14 Aug 2026 13:48:58 +0100 Subject: [PATCH 1/2] fix(cli): accept v0.0.13/v0.0.14 schema fingerprints as legacy v0 Stores created by v0.0.13 or v0.0.14 carry schema fingerprints (ea2d7ee4f385544e / 06d3f9912027129b) that were never registered in the local-schema history, so 'maple schema plan' fails with 'the store fingerprint ... is not registered' and those stores are locked out of the v0->v5 migration chain entirely. The v0.0.12->v0.0.14 DDL delta touches only derived tables/MVs (raw tables are byte-identical), and the legacy->v1 migration replays only the six raw tables, so the existing v0 edge is correct for all three schemas. - register the three fingerprint/projectRevision variants on the v0 history entry - classify any registered variant as legacy v0 in identityFromMarker and resolveMigrationChain (unknown fingerprints still fail closed) - accept a registered variant as the journal's source identity when validating the chain's first step (the frozen legacy edge records the canonical v0 identity; the journal records the marker as found) Verified end-to-end on a production v0.0.14 store: plan resolves v0->v5 and migrate promotes cleanly. --- apps/cli/src/server/local-schema-history.ts | 18 +++++++++ apps/cli/src/server/local-store-migrations.ts | 36 ++++++++++++----- apps/cli/src/server/schema-identity.ts | 14 +++++++ apps/cli/test/local-store-migrations.test.ts | 40 +++++++++++++++++++ 4 files changed, 99 insertions(+), 9 deletions(-) diff --git a/apps/cli/src/server/local-schema-history.ts b/apps/cli/src/server/local-schema-history.ts index 775481ca8..8825d4810 100644 --- a/apps/cli/src/server/local-schema-history.ts +++ b/apps/cli/src/server/local-schema-history.ts @@ -13,6 +13,10 @@ export interface LocalSchemaHistoryEntry { readonly digest: string readonly manifestDigest: string readonly projectRevision: string + readonly variants?: ReadonlyArray<{ + readonly fingerprint: string + readonly projectRevision: string + }> } export const LOCAL_SCHEMA_HISTORY: ReadonlyArray = Object.freeze([ @@ -22,6 +26,20 @@ export const LOCAL_SCHEMA_HISTORY: ReadonlyArray = Obje digest: "", manifestDigest: "", projectRevision: "d58ce4a83d3ad3f3a29b9bb972272b757547ae793c050194354454634f3abccd", + variants: Object.freeze([ + Object.freeze({ + fingerprint: "428701854f9fd30e", + projectRevision: "d58ce4a83d3ad3f3a29b9bb972272b757547ae793c050194354454634f3abccd", + }), + Object.freeze({ + fingerprint: "ea2d7ee4f385544e", + projectRevision: "ffade4bccc59af00fd33a561c4c919fd0229e0505f659d3242081f670f034a41", + }), + Object.freeze({ + fingerprint: "06d3f9912027129b", + projectRevision: "c3f2be342187b6f6cf09010043e775b68f0901081a9b861803e55f5fe299e4c6", + }), + ]), }), Object.freeze({ version: 1, diff --git a/apps/cli/src/server/local-store-migrations.ts b/apps/cli/src/server/local-store-migrations.ts index 0e9e6fbe8..a92f7edb6 100644 --- a/apps/cli/src/server/local-store-migrations.ts +++ b/apps/cli/src/server/local-store-migrations.ts @@ -15,6 +15,7 @@ import { LOCAL_SCHEMA_V1, LOCAL_SCHEMA_SQL, identityLabel, + legacyLocalSchemaIdentity, type LocalSchemaIdentity, } from "./schema-identity" import { @@ -190,8 +191,8 @@ export const identityFromMarker = (marker: StoreMarker): LocalSchemaIdentity | n chdb: marker.chdb, } } - if (marker.schema === LEGACY_LOCAL_SCHEMA.fingerprint) - return { ...LEGACY_LOCAL_SCHEMA, chdb: marker.chdb } + const legacyIdentity = legacyLocalSchemaIdentity(marker.schema) + if (legacyIdentity) return { ...legacyIdentity, chdb: marker.chdb } if (marker.schema === LOCAL_SCHEMA_V1.fingerprint) return { ...LOCAL_SCHEMA_V1, chdb: marker.chdb } return null } @@ -240,7 +241,9 @@ export const resolveMigrationChain = ( const migration = byFrom.get(current.version) if ( !migration || - migration.from.fingerprint !== current.fingerprint || + (migration.from.version === LEGACY_LOCAL_SCHEMA.version + ? legacyLocalSchemaIdentity(current.fingerprint) === null + : migration.from.fingerprint !== current.fingerprint) || (migration.from.digest !== "" && current.digest !== "" && migration.from.digest !== current.digest) @@ -423,17 +426,32 @@ const parseStep = (value: unknown, index: number): MigrationStepJournal => { const sameJournalIdentity = (a: LocalSchemaIdentity, b: LocalSchemaIdentity): boolean => a.version === b.version && a.fingerprint === b.fingerprint && a.digest === b.digest && a.chdb === b.chdb +/** The journal's top-level source identity records the store marker as found + * (any registered legacy variant), while chain steps carry the frozen module + * identities — for the legacy edge, the canonical v0 identity. Both are + * correct, so the first-step invariant accepts any registered v0 variant. */ +const sourceMatchesFirstStep = (source: LocalSchemaIdentity, firstFrom: LocalSchemaIdentity): boolean => + sameJournalIdentity(source, firstFrom) || + (source.version === LEGACY_LOCAL_SCHEMA.version && + firstFrom.version === LEGACY_LOCAL_SCHEMA.version && + source.chdb === firstFrom.chdb && + legacyLocalSchemaIdentity(source.fingerprint) !== null && + legacyLocalSchemaIdentity(firstFrom.fingerprint) !== null) + const assertJournalChainInvariants = (journal: MigrationJournal): void => { const { chain, currentStepIndex: current } = journal if (current < 0 || current > chain.length) throw new Error("migration journal currentStepIndex is invalid") if ( - !sameJournalIdentity(chain[0]!.from, { - version: journal.sourceVersion, - fingerprint: journal.sourceFingerprint, - digest: journal.sourceDigest, - chdb: journal.sourceChdb, - }) + !sourceMatchesFirstStep( + { + version: journal.sourceVersion, + fingerprint: journal.sourceFingerprint, + digest: journal.sourceDigest, + chdb: journal.sourceChdb, + }, + chain[0]!.from, + ) ) throw new Error("migration journal source identity does not match its first step") if ( diff --git a/apps/cli/src/server/schema-identity.ts b/apps/cli/src/server/schema-identity.ts index aedf4d40a..42c224f78 100644 --- a/apps/cli/src/server/schema-identity.ts +++ b/apps/cli/src/server/schema-identity.ts @@ -25,6 +25,20 @@ export const LEGACY_LOCAL_SCHEMA_VERSION = 0 as const export const LEGACY_SCHEMA_PROJECT_REVISION = "d58ce4a83d3ad3f3a29b9bb972272b757547ae793c050194354454634f3abccd" export const LEGACY_SCHEMA_FINGERPRINT = "428701854f9fd30e" +export const LEGACY_SCHEMA_VARIANTS = LOCAL_SCHEMA_HISTORY[0]!.variants! +export const LEGACY_SCHEMA_FINGERPRINTS: ReadonlyArray = Object.freeze( + LEGACY_SCHEMA_VARIANTS.map((variant) => variant.fingerprint), +) + +export const legacyLocalSchemaIdentity = (fingerprint: string): LocalSchemaIdentity | null => { + const variant = LEGACY_SCHEMA_VARIANTS.find((candidate) => candidate.fingerprint === fingerprint) + if (!variant) return null + return { + ...LEGACY_LOCAL_SCHEMA, + fingerprint: variant.fingerprint, + projectRevision: variant.projectRevision, + } +} export const CURRENT_SCHEMA_PROJECT_REVISION = "3bf63ab19fcba1a20ebaf6a97b49acfc2ecf00f1589c11438349bb87d21f77b7" diff --git a/apps/cli/test/local-store-migrations.test.ts b/apps/cli/test/local-store-migrations.test.ts index a66ab3630..454cf547a 100644 --- a/apps/cli/test/local-store-migrations.test.ts +++ b/apps/cli/test/local-store-migrations.test.ts @@ -5,6 +5,8 @@ import { ISSUE_297_TARGET_SCHEMA_PROJECT_REVISION, LEGACY_LOCAL_SCHEMA, LEGACY_SCHEMA_FINGERPRINT, + LEGACY_SCHEMA_VARIANTS, + legacyLocalSchemaIdentity, LOCAL_SCHEMA_MANIFEST, LOCAL_SCHEMA_V1, LOCAL_SCHEMA_V2, @@ -143,6 +145,44 @@ describe("local migration registry", () => { expect(typeof chain[0]?.apply).toBe("function") }) + it("admits every released legacy v0 fingerprint to the frozen v0-to-v5 chain", () => { + expect(LEGACY_SCHEMA_VARIANTS).toEqual([ + { + fingerprint: "428701854f9fd30e", + projectRevision: "d58ce4a83d3ad3f3a29b9bb972272b757547ae793c050194354454634f3abccd", + }, + { + fingerprint: "ea2d7ee4f385544e", + projectRevision: "ffade4bccc59af00fd33a561c4c919fd0229e0505f659d3242081f670f034a41", + }, + { + fingerprint: "06d3f9912027129b", + projectRevision: "c3f2be342187b6f6cf09010043e775b68f0901081a9b861803e55f5fe299e4c6", + }, + ]) + for (const variant of LEGACY_SCHEMA_VARIANTS) { + const identity = legacyLocalSchemaIdentity(variant.fingerprint) + expect(identity).toMatchObject({ version: 0, ...variant }) + expect(planMigration(identity!).chain.map((migration) => migration.id)).toEqual([ + "local-0000-to-0001-raw-replay", + "local-0001-to-0002-error-rollup", + "local-0002-to-0003-service-map-ingest-bridge", + "local-0003-to-0004-web-events", + "local-0004-to-0005-service-overview-minutely", + ]) + expect( + identityFromMarker({ + formatVersion: 1, + chdb: "dev", + maple: "dev", + createdAt: "unknown", + schema: variant.fingerprint, + }), + ).toMatchObject({ version: 0, ...variant }) + } + expect(legacyLocalSchemaIdentity("not-known")).toBeNull() + }) + it("recognizes legacy and current markers without treating the fingerprint as physical proof", () => { expect( identityFromMarker({ From 827691aeb9369b8366a92b10ee16aac3d9028ab0 Mon Sep 17 00:00:00 2001 From: Alan Roe Date: Sat, 15 Aug 2026 12:22:06 +0100 Subject: [PATCH 2/2] fix(cli): register legacy fingerprint variants outside the frozen identity history The append-only gate (scripts/check-local-schema-manifest.ts) compares every existing LOCAL_SCHEMA_HISTORY entry against the base branch byte-for-byte, so adding a variants field inside the v0 entry tripped it. The variant list now lives in schema-identity.ts as its own constant; the history array is byte-identical to main. --- apps/cli/src/server/local-schema-history.ts | 18 --------------- apps/cli/src/server/schema-identity.ts | 25 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/apps/cli/src/server/local-schema-history.ts b/apps/cli/src/server/local-schema-history.ts index 8825d4810..775481ca8 100644 --- a/apps/cli/src/server/local-schema-history.ts +++ b/apps/cli/src/server/local-schema-history.ts @@ -13,10 +13,6 @@ export interface LocalSchemaHistoryEntry { readonly digest: string readonly manifestDigest: string readonly projectRevision: string - readonly variants?: ReadonlyArray<{ - readonly fingerprint: string - readonly projectRevision: string - }> } export const LOCAL_SCHEMA_HISTORY: ReadonlyArray = Object.freeze([ @@ -26,20 +22,6 @@ export const LOCAL_SCHEMA_HISTORY: ReadonlyArray = Obje digest: "", manifestDigest: "", projectRevision: "d58ce4a83d3ad3f3a29b9bb972272b757547ae793c050194354454634f3abccd", - variants: Object.freeze([ - Object.freeze({ - fingerprint: "428701854f9fd30e", - projectRevision: "d58ce4a83d3ad3f3a29b9bb972272b757547ae793c050194354454634f3abccd", - }), - Object.freeze({ - fingerprint: "ea2d7ee4f385544e", - projectRevision: "ffade4bccc59af00fd33a561c4c919fd0229e0505f659d3242081f670f034a41", - }), - Object.freeze({ - fingerprint: "06d3f9912027129b", - projectRevision: "c3f2be342187b6f6cf09010043e775b68f0901081a9b861803e55f5fe299e4c6", - }), - ]), }), Object.freeze({ version: 1, diff --git a/apps/cli/src/server/schema-identity.ts b/apps/cli/src/server/schema-identity.ts index 42c224f78..9dd9ed459 100644 --- a/apps/cli/src/server/schema-identity.ts +++ b/apps/cli/src/server/schema-identity.ts @@ -25,7 +25,30 @@ export const LEGACY_LOCAL_SCHEMA_VERSION = 0 as const export const LEGACY_SCHEMA_PROJECT_REVISION = "d58ce4a83d3ad3f3a29b9bb972272b757547ae793c050194354454634f3abccd" export const LEGACY_SCHEMA_FINGERPRINT = "428701854f9fd30e" -export const LEGACY_SCHEMA_VARIANTS = LOCAL_SCHEMA_HISTORY[0]!.variants! +/** + * Every schema fingerprint that identifies a pre-versioning (legacy v0) local + * store. v0.0.12 shipped the original; v0.0.13/v0.0.14 shipped structurally + * identical raw tables under refreshed fingerprints that predate + * LOCAL_SCHEMA_HISTORY, so they are registered here rather than as history + * entries (the identity history is append-only and frozen). + */ +export const LEGACY_SCHEMA_VARIANTS: ReadonlyArray<{ + readonly fingerprint: string + readonly projectRevision: string +}> = Object.freeze([ + Object.freeze({ + fingerprint: "428701854f9fd30e", + projectRevision: "d58ce4a83d3ad3f3a29b9bb972272b757547ae793c050194354454634f3abccd", + }), + Object.freeze({ + fingerprint: "ea2d7ee4f385544e", + projectRevision: "ffade4bccc59af00fd33a561c4c919fd0229e0505f659d3242081f670f034a41", + }), + Object.freeze({ + fingerprint: "06d3f9912027129b", + projectRevision: "c3f2be342187b6f6cf09010043e775b68f0901081a9b861803e55f5fe299e4c6", + }), +]) export const LEGACY_SCHEMA_FINGERPRINTS: ReadonlyArray = Object.freeze( LEGACY_SCHEMA_VARIANTS.map((variant) => variant.fingerprint), )