Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export type {
GeneratedValueSpec,
JsonPrimitive,
JsonValue,
LedgerEntryRecord,
PlanMeta,
ProfileHashBase,
Source,
Expand Down
14 changes: 14 additions & 0 deletions packages/1-framework/0-foundation/contract/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,17 @@ export interface ContractMarkerRecord {
readonly meta: Record<string, unknown>;
readonly invariants: readonly string[];
}

/**
* One applied migration edge from the per-space ledger journal.
* Returned by `readLedger` in append (apply) order.
*/
export interface LedgerEntryRecord {
readonly space: string;
readonly migrationName: string;
readonly migrationHash: string;
readonly from: string | null;
readonly to: string;
readonly appliedAt: Date;
readonly operationCount: number;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
import type {
Contract,
ContractMarkerRecord,
LedgerEntryRecord,
} from '@prisma-next/contract/types';
import type {
AdapterInstance,
DriverInstance,
Expand Down Expand Up @@ -91,6 +95,16 @@ export interface ControlFamilyInstance<TFamilyId extends string, TSchemaIR>
readonly driver: ControlDriverInstance<TFamilyId, string>;
}): Promise<ReadonlyMap<string, ContractMarkerRecord>>;

/**
* Reads the per-migration ledger journal for `space` in apply order.
* Returns an empty array when the ledger table/collection has no rows
* for that space (or when the ledger store does not yet exist).
*/
readLedger(options: {
readonly driver: ControlDriverInstance<TFamilyId, string>;
readonly space: string;
}): Promise<readonly LedgerEntryRecord[]>;

introspect(options: {
readonly driver: ControlDriverInstance<TFamilyId, string>;
readonly contract?: unknown;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,20 @@ export interface MigrationRunnerPerSpaceOptions<
* Paths and metadata forwarded to schema verification diagnostics.
*/
readonly context?: OperationContext;
/**
* Per-edge breakdown from graph-walk planning. When present, runners that
* support a per-migration ledger write one row per edge instead of one
* collapsed row per apply.
*/
readonly migrationEdges?:
| ReadonlyArray<{
readonly migrationHash: string;
readonly dirName: string;
readonly from: string;
readonly to: string;
readonly operationCount: number;
}>
| undefined;
}

export interface MigrationRunner<
Expand Down
11 changes: 10 additions & 1 deletion packages/1-framework/3-tooling/cli/src/control-api/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
import type {
Contract,
ContractMarkerRecord,
LedgerEntryRecord,
} from '@prisma-next/contract/types';
import { emit as emitContractArtifacts } from '@prisma-next/emitter';
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
import type {
Expand Down Expand Up @@ -447,6 +451,11 @@ class ControlClientImpl implements ControlClient {
return familyInstance.readAllMarkers({ driver });
}

async readLedger(space = APP_SPACE_ID): Promise<readonly LedgerEntryRecord[]> {
const { driver, familyInstance } = await this.ensureConnected();
return familyInstance.readLedger({ driver, space });
}

async migrationApply(options: MigrationApplyOptions): Promise<MigrationApplyResult> {
const { onProgress } = options;
await this.connectWithProgress(options.connection, 'migrationApply', onProgress);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export async function applyMigration<TFamilyId extends string, TTargetId extends
destinationContract: r.entry.destinationContract,
policy,
frameworkComponents,
migrationEdges: r.entry.migrationEdges,
// Per-space post-apply schema verification is non-strict: each
// space's `destinationContract` describes only its own slice; a
// strict verifier would treat every other space's tables as
Expand Down
13 changes: 12 additions & 1 deletion packages/1-framework/3-tooling/cli/src/control-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import type {
ContractSourceDiagnostics,
ContractSourceProvider,
} from '@prisma-next/config/config-types';
import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
import type {
Contract,
ContractMarkerRecord,
LedgerEntryRecord,
} from '@prisma-next/contract/types';
import type {
ControlAdapterDescriptor,
ControlDriverDescriptor,
Expand Down Expand Up @@ -876,6 +880,13 @@ export interface ControlClient {
*/
readAllMarkers(): Promise<ReadonlyMap<string, ContractMarkerRecord>>;

/**
* Reads the per-migration ledger journal for `space` in apply order.
* Returns an empty array when the ledger store does not yet exist or
* has no rows for that space.
*/
readLedger(space?: string): Promise<readonly LedgerEntryRecord[]>;

/**
* Applies pre-planned on-disk migrations to the database.
* Each migration runs in its own transaction with full execution checks.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ describe('defineConfig', () => {
}),
readMarker: async () => null,
readAllMarkers: async () => new Map(),
readLedger: async () => [],
introspect: async () => ({ tables: {}, extensionPacks: [] }),
}),
},
Expand Down
20 changes: 18 additions & 2 deletions packages/2-mongo-family/9-family/src/core/control-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ContractMarkerRecord } from '@prisma-next/contract/types';
import type { ContractMarkerRecord, LedgerEntryRecord } from '@prisma-next/contract/types';
import type {
ControlAdapterDescriptor,
ControlAdapterInstance,
Expand Down Expand Up @@ -79,9 +79,25 @@ export interface MongoControlAdapter<TTarget extends string = string>
writeLedgerEntry(
driver: ControlDriverInstance<'mongo', TTarget>,
space: string,
entry: { readonly edgeId: string; readonly from: string; readonly to: string },
entry: {
readonly edgeId: string;
readonly from: string;
readonly to: string;
readonly migrationName: string;
readonly migrationHash: string;
readonly operations: readonly unknown[];
},
): Promise<void>;

/**
* Reads the per-migration ledger journal for `space` in apply order.
* Returns an empty array when no ledger entries exist for that space.
*/
readLedger(
driver: ControlDriverInstance<'mongo', TTarget>,
space: string,
): Promise<readonly LedgerEntryRecord[]>;

/**
* Introspects the live database and returns a `MongoSchemaIR`.
*/
Expand Down
16 changes: 14 additions & 2 deletions packages/2-mongo-family/9-family/src/core/control-instance.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
import type {
Contract,
ContractMarkerRecord,
LedgerEntryRecord,
} from '@prisma-next/contract/types';
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
import type {
ControlDriverInstance,
Expand All @@ -23,6 +27,7 @@ import { assertDescriptorSelfConsistency } from '@prisma-next/migration-tools/sp
import type { MongoContract } from '@prisma-next/mongo-contract';
import { mongoContractCanonicalizationHooks } from '@prisma-next/mongo-contract/canonicalization-hooks';
import type { MongoSchemaIR } from '@prisma-next/mongo-schema-ir';
import { blindCast } from '@prisma-next/utils/casts';
import { ifDefined } from '@prisma-next/utils/defined';
import type { MongoControlAdapter, MongoControlAdapterDescriptor } from './control-adapter';
import type { MongoControlExtensionDescriptor } from './control-types';
Expand Down Expand Up @@ -84,6 +89,9 @@ function isMongoControlAdapter(value: unknown): value is MongoControlAdapter<'mo
typeof (value as { readMarker: unknown }).readMarker === 'function' &&
'readAllMarkers' in value &&
typeof (value as { readAllMarkers: unknown }).readAllMarkers === 'function' &&
'readLedger' in value &&
typeof blindCast<{ readLedger: unknown }, 'MongoControlAdapter duck-type probe'>(value)
.readLedger === 'function' &&
'introspectSchema' in value &&
typeof (value as { introspectSchema: unknown }).introspectSchema === 'function'
);
Expand Down Expand Up @@ -167,7 +175,7 @@ export function createMongoFamilyInstance(controlStack: ControlStack): MongoCont
const controlAdapter = adapter.create(controlStack as ControlStack<'mongo', 'mongo'>);
if (!isMongoControlAdapter(controlAdapter)) {
throw new Error(
'Adapter does not implement MongoControlAdapter (missing readMarker, readAllMarkers, or introspectSchema)',
'Adapter does not implement MongoControlAdapter (missing readMarker, readAllMarkers, readLedger, or introspectSchema)',
);
}
return controlAdapter;
Expand Down Expand Up @@ -371,6 +379,10 @@ export function createMongoFamilyInstance(controlStack: ControlStack): MongoCont
return getControlAdapter().readAllMarkers(asMongoDriver(options.driver));
},

async readLedger(options): Promise<readonly LedgerEntryRecord[]> {
return getControlAdapter().readLedger(asMongoDriver(options.driver), options.space);
},

async introspect(options): Promise<MongoSchemaIR> {
return getControlAdapter().introspectSchema(asMongoDriver(options.driver));
},
Expand Down
1 change: 1 addition & 0 deletions packages/2-sql/9-family/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"./control": "./dist/control.mjs",
"./control-adapter": "./dist/control-adapter.mjs",
"./ir": "./dist/ir.mjs",
"./ledger-read": "./dist/ledger-read.mjs",
"./migration": "./dist/migration.mjs",
"./pack": "./dist/pack.mjs",
"./runtime": "./dist/runtime.mjs",
Expand Down
16 changes: 15 additions & 1 deletion packages/2-sql/9-family/src/core/control-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
import type {
Contract,
ContractMarkerRecord,
LedgerEntryRecord,
} from '@prisma-next/contract/types';
import type {
ControlAdapterInstance,
ControlDriverInstance,
Expand Down Expand Up @@ -52,6 +56,16 @@ export interface SqlControlAdapter<TTarget extends string = string>
driver: ControlDriverInstance<'sql', TTarget>,
): Promise<ReadonlyMap<string, ContractMarkerRecord>>;

/**
* Reads the per-migration ledger journal for `space` in apply order.
* Returns an empty array when the ledger store does not yet exist or
* has no rows for that space.
*/
readLedger(
driver: ControlDriverInstance<'sql', TTarget>,
space: string,
): Promise<readonly LedgerEntryRecord[]>;

/**
* Introspects a database schema and returns a raw SqlSchemaIR.
*
Expand Down
18 changes: 16 additions & 2 deletions packages/2-sql/9-family/src/core/control-instance.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
import type {
Contract,
ContractMarkerRecord,
LedgerEntryRecord,
} from '@prisma-next/contract/types';
import type {
TargetBoundComponentDescriptor,
TargetDescriptor,
Expand Down Expand Up @@ -41,6 +45,7 @@ import {
} from '@prisma-next/sql-runtime';
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
import type { SqlSchemaIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
import { blindCast } from '@prisma-next/utils/casts';
import { ifDefined } from '@prisma-next/utils/defined';
import type { SqlControlAdapter } from './control-adapter';
import { SqlContractSerializer } from './ir/sql-contract-serializer';
Expand Down Expand Up @@ -260,6 +265,9 @@ function isSqlControlAdapter<TTargetId extends string>(
typeof (value as { readMarker: unknown }).readMarker === 'function' &&
'readAllMarkers' in value &&
typeof (value as { readAllMarkers: unknown }).readAllMarkers === 'function' &&
'readLedger' in value &&
typeof blindCast<{ readLedger: unknown }, 'SqlControlAdapter duck-type probe'>(value)
.readLedger === 'function' &&
'lower' in value &&
typeof (value as { lower: unknown }).lower === 'function'
);
Expand Down Expand Up @@ -369,7 +377,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
const controlAdapter = adapter.create(stack);
if (!isSqlControlAdapter(controlAdapter)) {
throw new Error(
'Adapter does not implement SqlControlAdapter (missing introspect, readMarker, or readAllMarkers)',
'Adapter does not implement SqlControlAdapter (missing introspect, readMarker, readAllMarkers, readLedger, or lower)',
);
}
return controlAdapter;
Expand Down Expand Up @@ -651,6 +659,12 @@ export function createSqlFamilyInstance<TTargetId extends string>(
}): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
return getControlAdapter().readAllMarkers(options.driver);
},
async readLedger(options: {
readonly driver: ControlDriverInstance<'sql', string>;
readonly space: string;
}): Promise<readonly LedgerEntryRecord[]> {
return getControlAdapter().readLedger(options.driver, options.space);
},
async introspect(options: {
readonly driver: ControlDriverInstance<'sql', string>;
readonly contract?: unknown;
Expand Down
35 changes: 35 additions & 0 deletions packages/2-sql/9-family/src/core/ledger-read.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';

const DESIGNATOR_LESS_UTC_DATETIME = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/;

export function ledgerOriginFromStored(originCoreHash: string | null): string | null {
if (originCoreHash === null || originCoreHash === '' || originCoreHash === EMPTY_CONTRACT_HASH) {
return null;
}
return originCoreHash;
}

export function coerceLedgerAppliedAt(value: Date | string): Date {
if (value instanceof Date) {
return value;
}
if (DESIGNATOR_LESS_UTC_DATETIME.test(value)) {
return new Date(`${value.replace(' ', 'T')}Z`);
}
return new Date(value);
}

export function operationCountFromStored(operations: unknown): number {
if (Array.isArray(operations)) {
return operations.length;
}
if (typeof operations === 'string') {
try {
const parsed: unknown = JSON.parse(operations);
return Array.isArray(parsed) ? parsed.length : 0;
} catch {
return 0;
}
}
return 0;
}
6 changes: 6 additions & 0 deletions packages/2-sql/9-family/src/core/migrations/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
SchemaIssue,
SchemaVerifier,
} from '@prisma-next/framework-components/control';
import type { AggregateMigrationEdgeRef } from '@prisma-next/migration-tools/aggregate';
import type {
SqlStorage,
StorageColumn,
Expand Down Expand Up @@ -384,6 +385,11 @@ export interface SqlMigrationRunnerExecuteOptions<TTargetDetails> {
* All components must have matching familyId ('sql') and targetId.
*/
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
/**
* Per-edge breakdown from graph-walk planning. When present, the runner
* writes one ledger row per edge instead of one collapsed row per apply.
*/
readonly migrationEdges?: readonly AggregateMigrationEdgeRef[] | undefined;
}

export type SqlMigrationRunnerErrorCode =
Expand Down
5 changes: 5 additions & 0 deletions packages/2-sql/9-family/src/exports/ledger-read.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {
coerceLedgerAppliedAt,
ledgerOriginFromStored,
operationCountFromStored,
} from '../core/ledger-read';
Loading
Loading