Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions apps/cli/src/server/local-store-migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
LOCAL_SCHEMA_V1,
LOCAL_SCHEMA_SQL,
identityLabel,
legacyLocalSchemaIdentity,
type LocalSchemaIdentity,
} from "./schema-identity"
import {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 (
Expand Down
37 changes: 37 additions & 0 deletions apps/cli/src/server/schema-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = 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"
Expand Down
40 changes: 40 additions & 0 deletions apps/cli/test/local-store-migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand Down