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..9dd9ed459 100644 --- a/apps/cli/src/server/schema-identity.ts +++ b/apps/cli/src/server/schema-identity.ts @@ -25,6 +25,43 @@ export const LEGACY_LOCAL_SCHEMA_VERSION = 0 as const export const LEGACY_SCHEMA_PROJECT_REVISION = "d58ce4a83d3ad3f3a29b9bb972272b757547ae793c050194354454634f3abccd" export const LEGACY_SCHEMA_FINGERPRINT = "428701854f9fd30e" +/** + * 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), +) + +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({