Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
49e0bd7
Add M4 execution plan for online CLI commands and live introspection
wmadden Apr 13, 2026
95c3321
feat(mongo-adapter): implement live schema introspection
wmadden Apr 13, 2026
49feb2a
feat(family-mongo): wire introspect() on MongoControlFamilyInstance
wmadden Apr 13, 2026
e8fb4bb
feat(family-mongo): implement toSchemaView for MongoSchemaIR
wmadden Apr 13, 2026
db910d6
refactor(cli): generalize inspect-live-schema for non-SQL families
wmadden Apr 13, 2026
1a7a070
test(integration): add db schema end-to-end test for Mongo
wmadden Apr 13, 2026
4b69861
feat(family-mongo): implement verify() for marker-only verification
wmadden Apr 13, 2026
b7ba16c
feat(family-mongo): implement schemaVerify() for live schema diffing
wmadden Apr 13, 2026
640fa06
feat(family-mongo): implement sign() with CAS marker write
wmadden Apr 13, 2026
432dc09
test(integration): add db verify + db sign end-to-end tests for Mongo
wmadden Apr 13, 2026
6c59b8d
extract schema diffing logic from control-instance.ts into schema-dif…
wmadden Apr 13, 2026
b40ceb1
clean up control-instance.ts and introspect-schema.ts
wmadden Apr 13, 2026
9e60d01
add unit tests for diffMongoSchemas covering strict mode and all elem…
wmadden Apr 13, 2026
a9e39af
add unit test for inspect-live-schema non-SQL path
wmadden Apr 13, 2026
df73184
add Mongo CLI e2e tests for db schema, db verify, and db sign
wmadden Apr 13, 2026
33322e1
fix lint and typecheck errors in schema-diff.ts
wmadden Apr 13, 2026
8cc5d13
fix toSchemaView root label from "contract" to "database"
wmadden Apr 13, 2026
f7fa8ea
improve db schema tree display for Mongo
wmadden Apr 13, 2026
a7988dc
remove meaningless default type param from SchemaViewCapable
wmadden Apr 13, 2026
5b7762c
fix profileHash omitted from migration plan destination and verify er…
wmadden Apr 13, 2026
6f93fe2
fix runner reads profileHash from contract, not migration plan
wmadden Apr 13, 2026
8ee4e1f
promote MongoSchemaIR from plain interface to proper AST node
wmadden Apr 13, 2026
71ddfdf
extract toSchemaView conversion logic to schema-to-view.ts
wmadden Apr 13, 2026
05c6dc2
promote SchemaTreeNode from interface to frozen class
wmadden Apr 13, 2026
0dffb27
make SchemaViewCapable generic and declare implements on families
wmadden Apr 13, 2026
92b3c2c
replace conditional spread patterns with ifDefined utility
wmadden Apr 13, 2026
966e2e1
extract verify error codes to shared framework constants
wmadden Apr 13, 2026
275edc0
add TODO(TML-2253) for marker-ledger query AST migration
wmadden Apr 13, 2026
6120512
fix typecheck errors in introspect-schema
wmadden Apr 13, 2026
fc97bb0
move marker-ledger from adapter to driver layer, replace findOne with…
wmadden Apr 13, 2026
3d334c0
add agent-agnostic rule: prefer aggregate over obsolete find/findOne
wmadden Apr 13, 2026
0e76e45
rewrite marker-ledger to use query AST commands instead of raw driver…
wmadden Apr 13, 2026
484657e
move marker-ledger to target package where it belongs
wmadden Apr 13, 2026
62d0ccb
fix(F11): remove = unknown default from ControlFamilyInstance, requir…
wmadden Apr 13, 2026
fb2404d
fix: address CodeRabbit review findings
wmadden Apr 13, 2026
df67496
fix: resolve CI failures — missing type args, lint, and e2e assertion
wmadden Apr 13, 2026
7a48bfa
fix: remaining CI failures — unused imports, snapshot label, TS2379 e…
wmadden Apr 13, 2026
30a5699
harden mongodb-memory-server setup across the repo
wmadden Apr 14, 2026
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
23 changes: 23 additions & 0 deletions .agents/rules/mongo-no-obsolete-commands.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# MongoDB: no obsolete commands

MongoDB's `find` and `findOne` are legacy commands from older MongoDB versions. The MongoDB product team considers them obsolete.

## Use `aggregate` instead of `find`/`findOne`

```typescript
// BAD — obsolete command
const doc = await db.collection('foo').findOne({ _id: id });

// GOOD — modern aggregate pipeline
const docs = await db
.collection('foo')
.aggregate([{ $match: { _id: id } }, { $limit: 1 }])
.toArray();
const doc = docs[0] ?? null;
```

For multiple documents, use `aggregate` with `$match` (and `$sort`, `$skip`, `$limit` as needed) instead of `find().sort().skip().limit()`.

## `insertOne`, `findOneAndUpdate`, `findOneAndDelete` are fine

These are standard CRUD operations and are **not** obsolete. Continue using them directly.
3 changes: 3 additions & 0 deletions .cursor/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ Thresholds are defined in `.cursor/rules-footprint.config.json`.
- `.cursor/rules/declarative-config.mdc` — Prefer declarative configuration over hardcoded logic
- `architecture.config.json` — Domain/Layer/Plane map

## MongoDB
- `.cursor/rules/mongo-no-obsolete-commands.mdc` — Use `aggregate` instead of obsolete `find`/`findOne`

## SQL & Query Patterns
- `.cursor/rules/query-patterns.mdc` — Query DSL patterns
- `.cursor/rules/kysely-lane-boundary.mdc` — Kysely lane ownership and build-only interop boundaries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const sqlFamilyDescriptor: ControlFamilyDescriptor<'sql'> = {
create: (_stack) =>
({
familyId: 'sql',
}) as unknown as ControlFamilyInstance<'sql'>,
}) as unknown as ControlFamilyInstance<'sql', unknown>,
};

const postgresTargetDescriptor: ControlTargetDescriptor<'sql', 'postgres'> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function createValidConfig(overrides: Record<string, unknown> = {}): PrismaNextC
version: '0.0.1',
manifest: {},
emission: mockHook,
create: () => ({ familyId: 'sql' }) as unknown as ControlFamilyInstance<'sql'>,
create: () => ({ familyId: 'sql' }) as unknown as ControlFamilyInstance<'sql', unknown>,
},
target: {
kind: 'target',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export interface SchemaViewCapable<TSchemaIR = unknown> {
toSchemaView(schema: TSchemaIR): CoreSchemaView;
}

export function hasSchemaView<TFamilyId extends string>(
instance: ControlFamilyInstance<TFamilyId>,
): instance is ControlFamilyInstance<TFamilyId> & SchemaViewCapable {
export function hasSchemaView<TFamilyId extends string, TSchemaIR>(
instance: ControlFamilyInstance<TFamilyId, TSchemaIR>,
): instance is ControlFamilyInstance<TFamilyId, TSchemaIR> & SchemaViewCapable<TSchemaIR> {
return (
'toSchemaView' in instance &&
typeof (instance as Record<string, unknown>)['toSchemaView'] === 'function'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
TargetInstance,
} from './framework-components';

export interface ControlFamilyInstance<TFamilyId extends string, TSchemaIR = unknown>
export interface ControlFamilyInstance<TFamilyId extends string, TSchemaIR>
extends FamilyInstance<TFamilyId> {
validateContract(contractJson: unknown): Contract;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,10 @@ export interface MigrationRunner<
export interface TargetMigrationsCapability<
TFamilyId extends string = string,
TTargetId extends string = string,
TFamilyInstance extends ControlFamilyInstance<TFamilyId> = ControlFamilyInstance<TFamilyId>,
TFamilyInstance extends ControlFamilyInstance<TFamilyId, unknown> = ControlFamilyInstance<
TFamilyId,
unknown
>,
> {
createPlanner(family: TFamilyInstance): MigrationPlanner<TFamilyId, TTargetId>;
createRunner(family: TFamilyInstance): MigrationRunner<TFamilyId, TTargetId>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export const VERIFY_CODE_MARKER_MISSING = 'PN-RUN-3001';
export const VERIFY_CODE_HASH_MISMATCH = 'PN-RUN-3002';
export const VERIFY_CODE_TARGET_MISMATCH = 'PN-RUN-3003';
export const VERIFY_CODE_SCHEMA_FAILURE = 'PN-RUN-3010';

export interface OperationContext {
readonly contractPath?: string;
readonly configPath?: string;
Expand Down Expand Up @@ -41,6 +46,7 @@ export interface SchemaIssue {
| 'extra_foreign_key'
| 'extra_unique_constraint'
| 'extra_index'
| 'extra_validator'
| 'type_mismatch'
| 'type_missing'
| 'type_values_mismatch'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,65 +6,8 @@
*
* Families can optionally project their family-specific Schema IR into this
* core view via the `toSchemaView` method on `FamilyInstance`.
*
* ## Example: SQL Family Mapping
*
* For the SQL family, `SqlSchemaIR` can be mapped to `CoreSchemaView` as follows:
*
* ```ts
* // SqlSchemaIR structure:
* // {
* // tables: { user: { columns: {...}, primaryKey: {...}, ... }, ... },
* // dependencies: [{ id: 'postgres.extension.vector' }],
* // annotations: {...}
* // }
*
* // CoreSchemaView mapping:
* // {
* // root: {
* // kind: 'root',
* // id: 'sql-schema',
* // label: 'sql schema (tables: 2)',
* // children: [
* // {
* // kind: 'entity',
* // id: 'table-user',
* // label: 'table user',
* // meta: { primaryKey: ['id'], ... },
* // children: [
* // {
* // kind: 'field',
* // id: 'column-id',
* // label: 'id: int4 (pg/int4@1, not null)',
* // meta: { nativeType: 'int4', codecId: 'pg/int4@1', nullable: false, ... }
* // },
* // {
* // kind: 'index',
* // id: 'index-user-email',
* // label: 'index user_email_unique',
* // meta: { columns: ['email'], unique: true, ... }
* // }
* // ]
* // },
* // {
* // kind: 'dependency',
* // id: 'dependency-postgres.extension.pgvector',
* // label: 'pgvector extension is enabled',
* // meta: { ... }
* // }
* // ]
* // }
* // }
* ```
*
* This mapping demonstrates that the core view types are expressive enough
* to represent SQL schemas without being SQL-specific.
*/

/**
* Node kinds for schema tree nodes.
* Designed to be generic enough for SQL, document, KV, and future families.
*/
export type SchemaNodeKind =
| 'root'
| 'namespace'
Expand All @@ -74,16 +17,37 @@ export type SchemaNodeKind =
| 'index'
| 'dependency';

/**
* A node in the schema tree.
* Tree-shaped structure good for Command Tree-style CLI output.
*/
export interface SchemaTreeNode {
export interface SchemaTreeVisitor<R> {
visit(node: SchemaTreeNode): R;
}

export interface SchemaTreeNodeOptions {
readonly kind: SchemaNodeKind;
readonly id: string;
readonly label: string;
readonly meta?: Record<string, unknown>;
readonly children?: readonly SchemaTreeNode[];
}

export class SchemaTreeNode {
readonly kind: SchemaNodeKind;
readonly id: string;
readonly label: string;
readonly meta?: Record<string, unknown>;
readonly children?: readonly SchemaTreeNode[];

constructor(options: SchemaTreeNodeOptions) {
this.kind = options.kind;
this.id = options.id;
this.label = options.label;
if (options.meta !== undefined) this.meta = options.meta;
if (options.children !== undefined) this.children = options.children;
Object.freeze(this);
}

accept<R>(visitor: SchemaTreeVisitor<R>): R {
return visitor.visit(this);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,19 @@ export type {
VerifyDatabaseResult,
VerifyDatabaseSchemaResult,
} from '../control-result-types';
export type { CoreSchemaView, SchemaNodeKind, SchemaTreeNode } from '../control-schema-view';
export {
VERIFY_CODE_HASH_MISMATCH,
VERIFY_CODE_MARKER_MISSING,
VERIFY_CODE_SCHEMA_FAILURE,
VERIFY_CODE_TARGET_MISMATCH,
} from '../control-result-types';
export type {
CoreSchemaView,
SchemaNodeKind,
SchemaTreeNodeOptions,
SchemaTreeVisitor,
} from '../control-schema-view';
export { SchemaTreeNode } from '../control-schema-view';
export type {
AssembledAuthoringContributions,
ControlStack,
Expand Down
18 changes: 15 additions & 3 deletions packages/1-framework/3-tooling/cli/src/commands/contract-infer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { printPsl } from '@prisma-next/psl-printer';
import { errorRuntime } from '@prisma-next/errors/execution';
import { printPsl, validatePrintableSqlSchemaIR } from '@prisma-next/psl-printer';
import {
createPostgresDefaultMapping,
createPostgresTypeMap,
extractEnumInfo,
parseRawDefault,
} from '@prisma-next/psl-printer/postgres';
import { ok, type Result } from '@prisma-next/utils/result';
import { notOk, ok, type Result } from '@prisma-next/utils/result';
import { Command } from 'commander';
import { dirname, relative } from 'pathe';
import type { CliStructuredError } from '../utils/cli-errors';
Expand Down Expand Up @@ -58,7 +59,18 @@ async function executeContractInferCommand(
return inspectResult;
}

const { config, schema, target, meta } = inspectResult.value;
const { config, target, meta } = inspectResult.value;

if (target.familyId !== 'sql') {
return notOk(
errorRuntime(`contract infer is not supported for family "${target.familyId}"`, {
why: 'contract infer currently supports SQL targets only',
fix: 'Use an SQL target (e.g. Postgres) with this command',
}),
);
}

const schema = validatePrintableSqlSchemaIR(inspectResult.value.schema);
const outputPath = resolveContractInferOutputPath(options, config.contract?.output);
const enumInfo = extractEnumInfo(schema.annotations);
const pslContent = printPsl(schema, {
Expand Down
31 changes: 26 additions & 5 deletions packages/1-framework/3-tooling/cli/src/commands/db-verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import type {
VerifyDatabaseResult,
VerifyDatabaseSchemaResult,
} from '@prisma-next/framework-components/control';
import {
VERIFY_CODE_HASH_MISMATCH,
VERIFY_CODE_MARKER_MISSING,
VERIFY_CODE_TARGET_MISMATCH,
} from '@prisma-next/framework-components/control';
import { ifDefined } from '@prisma-next/utils/defined';
import { notOk, ok, type Result } from '@prisma-next/utils/result';
import { Command } from 'commander';
Expand Down Expand Up @@ -58,16 +63,32 @@ type DbVerifyMode = 'full' | 'marker-only' | 'schema-only';
*/
function mapVerifyFailure(verifyResult: VerifyDatabaseResult): CliStructuredError {
if (!verifyResult.ok && verifyResult.code) {
if (verifyResult.code === 'PN-RUN-3001') {
if (verifyResult.code === VERIFY_CODE_MARKER_MISSING) {
return errorMarkerMissing();
}
if (verifyResult.code === 'PN-RUN-3002') {
if (verifyResult.code === VERIFY_CODE_HASH_MISMATCH) {
const storageMatch = verifyResult.marker?.storageHash === verifyResult.contract.storageHash;
const profileMatch =
!verifyResult.contract.profileHash ||
verifyResult.marker?.profileHash === verifyResult.contract.profileHash;

if (!storageMatch) {
return errorHashMismatch({
why: 'Contract storageHash does not match database marker',
expected: verifyResult.contract.storageHash,
...ifDefined('actual', verifyResult.marker?.storageHash),
});
}

return errorHashMismatch({
expected: verifyResult.contract.storageHash,
...ifDefined('actual', verifyResult.marker?.storageHash),
why: profileMatch
? 'Contract hash does not match database marker'
: 'Contract profileHash does not match database marker',
expected: verifyResult.contract.profileHash,
...ifDefined('actual', verifyResult.marker?.profileHash),
});
}
if (verifyResult.code === 'PN-RUN-3003') {
if (verifyResult.code === VERIFY_CODE_TARGET_MISMATCH) {
return errorTargetMismatch(
verifyResult.target.expected,
verifyResult.target.actual ?? 'unknown',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import type { CoreSchemaView } from '@prisma-next/framework-components/control';
import {
type PslPrintableSqlSchemaIR,
validatePrintableSqlSchemaIR,
} from '@prisma-next/psl-printer';
import { validatePrintableSqlSchemaIR } from '@prisma-next/psl-printer';
import { notOk, ok, type Result } from '@prisma-next/utils/result';
import { relative, resolve } from 'pathe';
import { loadConfig } from '../config-loader';
Expand Down Expand Up @@ -34,7 +31,7 @@ type LoadedCliConfig = Awaited<ReturnType<typeof loadConfig>>;

export interface InspectLiveSchemaResult {
readonly config: LoadedCliConfig;
readonly schema: PslPrintableSqlSchemaIR;
readonly schema: unknown;
readonly schemaView: CoreSchemaView | undefined;
readonly target: {
readonly familyId: string;
Expand Down Expand Up @@ -129,7 +126,8 @@ export async function inspectLiveSchema(
connection: dbConnection,
onProgress,
});
const schema = validatePrintableSqlSchemaIR(schemaIR);
const schema =
config.family.familyId === 'sql' ? validatePrintableSqlSchemaIR(schemaIR) : schemaIR;
const schemaView = client.toSchemaView(schema);

const dbUrl = typeof dbConnection === 'string' ? maskConnectionUrl(dbConnection) : undefined;
Expand Down
4 changes: 2 additions & 2 deletions packages/1-framework/3-tooling/cli/src/control-api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class ControlClientImpl implements ControlClient {
private readonly options: ControlClientOptions;
private stack: ControlStack | null = null;
private driver: ControlDriverInstance<string, string> | null = null;
private familyInstance: ControlFamilyInstance<string> | null = null;
private familyInstance: ControlFamilyInstance<string, unknown> | null = null;
private frameworkComponents: ReadonlyArray<
TargetBoundComponentDescriptor<string, string>
> | null = null;
Expand Down Expand Up @@ -142,7 +142,7 @@ class ControlClientImpl implements ControlClient {

private async ensureConnected(): Promise<{
driver: ControlDriverInstance<string, string>;
familyInstance: ControlFamilyInstance<string>;
familyInstance: ControlFamilyInstance<string, unknown>;
frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<string, string>>;
}> {
// Auto-init if needed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ import { createOperationCallbacks, stripOperations } from './migration-helpers';
*/
export interface ExecuteDbInitOptions<TFamilyId extends string, TTargetId extends string> {
readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
readonly familyInstance: ControlFamilyInstance<TFamilyId>;
readonly familyInstance: ControlFamilyInstance<TFamilyId, unknown>;
readonly contract: Contract;
readonly mode: 'plan' | 'apply';
readonly migrations: TargetMigrationsCapability<
TFamilyId,
TTargetId,
ControlFamilyInstance<TFamilyId>
ControlFamilyInstance<TFamilyId, unknown>
>;
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
/** Optional progress callback for observing operation progress */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ const DB_UPDATE_POLICY = {
*/
export interface ExecuteDbUpdateOptions<TFamilyId extends string, TTargetId extends string> {
readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
readonly familyInstance: ControlFamilyInstance<TFamilyId>;
readonly familyInstance: ControlFamilyInstance<TFamilyId, unknown>;
readonly contract: Contract;
readonly mode: 'plan' | 'apply';
readonly migrations: TargetMigrationsCapability<
TFamilyId,
TTargetId,
ControlFamilyInstance<TFamilyId>
ControlFamilyInstance<TFamilyId, unknown>
>;
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
readonly acceptDataLoss?: boolean;
Expand Down
Loading
Loading