Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4310a6a
docs(created-updated-at-authoring): add timestamp authoring plan
jkomyno May 4, 2026
682714e
feat(sql): add updatedAt timestamp authoring
jkomyno May 4, 2026
ece4e18
fix(sql): address updatedAt review findings
jkomyno May 5, 2026
40462aa
refactor(sql): collapse executionDefault into executionDefaults
jkomyno May 5, 2026
0d45548
refactor(psl): consolidate @updatedAt validation paths and improve di…
jkomyno May 5, 2026
89e15f5
feat(runtime): validate mutation default generators at execution cont…
jkomyno May 5, 2026
40d67af
refactor(sql): centralize timestampNow descriptor and runtime wiring …
jkomyno May 5, 2026
146242c
fix(orm-client): apply @updatedAt to ORM updates and stabilize across…
jkomyno May 5, 2026
14ff454
docs(plan): record Milestone 4 — ORM update wiring and across-rows st…
jkomyno May 5, 2026
f25ab85
docs(plan): pivot timestamp authoring to PSL field-preset surface
jkomyno May 6, 2026
ac8b147
feat(psl): add field-preset dispatch path with temporal namespace
jkomyno May 6, 2026
2f5c88e
feat(psl,authoring): add preset misuse diagnostics and registry colli…
jkomyno May 6, 2026
91178d7
docs(plan): correct RD11 — namespaced-call arg-error rename deferred
jkomyno May 6, 2026
f3b748d
feat(targets): re-namespace timestamp presets under field.temporal
jkomyno May 6, 2026
b9a2caf
feat(psl)!: remove @updatedAt attribute, add migration hint
jkomyno May 6, 2026
889ba98
docs(authoring): update READMEs for temporal preset surface
jkomyno May 6, 2026
6ec547a
refactor(sql,family): share temporal.* preset registration across tar…
jkomyno May 6, 2026
fa2c8d2
refactor(sql,runtime): drop applyOnEmptyUpdate opt-in
jkomyno May 6, 2026
2a01ae5
refactor(sql,authoring): tighten nullable+executionDefaults rejection
jkomyno May 6, 2026
93be243
docs(plan): record YAGNI cuts and temporal preset consolidation
jkomyno May 6, 2026
13e24d0
Merge branch 'main' into feat/created-updated-at-authoring
jkomyno May 7, 2026
94a7ee2
fix(contract,authoring): validate resolved literal defaults before ca…
jkomyno May 7, 2026
c835e8f
refactor(family-sql): isolate runtime generator from control-plane mo…
jkomyno May 7, 2026
dbdb336
test(orm-client): assert Date identity for stableAcrossRows
jkomyno May 7, 2026
58d7f5a
feat(sql): support id-less sql models
jkomyno May 5, 2026
014ea5c
fix(psl): tighten @id and @@id identity validation (todos 067, 068, 0…
jkomyno May 5, 2026
a44bf5a
refactor(psl): collapse primary-key reconciliation and document ORM P…
jkomyno May 5, 2026
d54fcc1
chore(psl): unify entityLabel parameter and hoist model-attribute lab…
jkomyno May 5, 2026
405ea06
fix(psl): reject @@unique and @@index with duplicate fields
jkomyno May 5, 2026
6cba724
fix(ci): stabilize idless model checks
jkomyno May 7, 2026
ba6b20c
Merge remote-tracking branch 'origin/main' into feat/idless-models
jkomyno May 8, 2026
f8dd2df
docs(sql): clarify id-less ORM primary-key fallback
jkomyno May 8, 2026
116bc52
fix(sql): centralize storage constraint validation
jkomyno May 8, 2026
c1d6b15
chore(projects): close out id-less-sql-models
jkomyno May 8, 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
4 changes: 4 additions & 0 deletions docs/architecture docs/subsystems/3. Query Lanes.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,10 @@ const plan = o.user()

The ORM is optional and layered on top of the DSL.

### Implicit invariant: ORM ops assume a primary key

`storage.tables.<t>.primaryKey` is optional in the SQL contract — id-less SQL tables are emittable from PSL (`model X { ... }` without `@id`/`@@id`) and from introspection. ORM read/write operations (`findUnique`, `update(where)`, `delete(where)`) assume a single canonical identity to address rows; they have no fallback for id-less models. The current ORM lane has no code referring to `model.id` directly, so id-less models do not break composition today, but adding any ORM op that depends on a primary key against an id-less model will fail at runtime. The long-term fix is a per-model capability (e.g. `sql.ormRequiresPrimaryKey`) gated at composition time; until then, treat id-less models as DSL-only.
Comment thread
jkomyno marked this conversation as resolved.
Outdated
Comment thread
jkomyno marked this conversation as resolved.
Outdated

### Shared Collection interface across families

The ORM's `Collection` class with fluent chaining is a shared architectural pattern that works across database families — not just SQL. A comparative analysis with the MongoDB ORM (see [10. MongoDB Family](10.%20MongoDB%20Family.md)) revealed that the consumer-facing surface is fundamentally the same:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { EmissionSpi } from '@prisma-next/framework-components/emission';
import { timeouts } from '@prisma-next/test-utils';
import { ifDefined } from '@prisma-next/utils/defined';
import { notOk, ok } from '@prisma-next/utils/result';
import { describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('@prisma-next/emitter', () => ({
emit: vi.fn(
Expand All @@ -36,6 +36,20 @@ import type { ControlProgressEvent } from '../../src/control-api/types';

const mockEmit = vi.mocked(emitFn);

function createMockEmitResult(): EmitResult {
return {
storageHash: 'test-core-hash',
profileHash: 'test-profile-hash',
contractJson: '{"test": true}',
contractDts: 'export interface Contract {}',
};
}

beforeEach(() => {
mockEmit.mockReset();
mockEmit.mockResolvedValue(createMockEmitResult());
});

function createSourceProvider(
load: ContractSourceProvider['load'] = async () => ok({ test: true } as unknown as Contract),
inputs?: readonly string[],
Expand Down
1 change: 1 addition & 0 deletions packages/1-framework/3-tooling/cli/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default defineConfig({
pool: 'forks',
maxWorkers: 1,
isolate: false,
fileParallelism: false,
testTimeout: timeouts.vitestPackageDefault,
hookTimeout: timeouts.vitestPackageDefault,
setupFiles: ['./test/setup.ts'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,70 +171,74 @@ describe('emitter round-trip', () => {
timeouts.typeScriptCompilation,
);

it('round-trip with nullable fields', async () => {
const ir = createTestContract({
storage: {
tables: {
user: {
columns: {
id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
email: { codecId: 'pg/text@1', nativeType: 'text', nullable: true },
name: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
it(
'round-trip with nullable fields',
async () => {
const ir = createTestContract({
storage: {
tables: {
user: {
columns: {
id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
email: { codecId: 'pg/text@1', nativeType: 'text', nullable: true },
name: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
},
primaryKey: { columns: ['id'] },
uniques: [],
indexes: [],
foreignKeys: [],
},
primaryKey: { columns: ['id'] },
uniques: [],
indexes: [],
foreignKeys: [],
},
},
},
extensionPacks: {
postgres: { version: '0.0.1' },
pg: {},
},
});
extensionPacks: {
postgres: { version: '0.0.1' },
pg: {},
},
});

const codecTypeImports: TypesImportSpec[] = [];
const operationTypeImports: TypesImportSpec[] = [];
const extensionIds = ['postgres', 'pg'];
const options: EmitStackInput = {
codecTypeImports,
operationTypeImports,
extensionIds,
};
const codecTypeImports: TypesImportSpec[] = [];
const operationTypeImports: TypesImportSpec[] = [];
const extensionIds = ['postgres', 'pg'];
const options: EmitStackInput = {
codecTypeImports,
operationTypeImports,
extensionIds,
};

const result1 = await emit(ir, options, mockSqlHook);
const contractJson1 = JSON.parse(result1.contractJson) as Record<string, unknown>;
const result1 = await emit(ir, options, mockSqlHook);
const contractJson1 = JSON.parse(result1.contractJson) as Record<string, unknown>;

const ir2 = createTestContract({
targetFamily: contractJson1['targetFamily'] as string,
target: contractJson1['target'] as string,
roots: contractJson1['roots'] as Record<string, string>,
models: contractJson1['models'] as Record<string, unknown>,
storage: contractJson1['storage'] as Record<string, unknown>,
extensionPacks: contractJson1['extensionPacks'] as Record<string, unknown>,
capabilities:
(contractJson1['capabilities'] as Record<string, Record<string, boolean>>) || {},
meta: (contractJson1['meta'] as Record<string, unknown>) || {},
});
const ir2 = createTestContract({
targetFamily: contractJson1['targetFamily'] as string,
target: contractJson1['target'] as string,
roots: contractJson1['roots'] as Record<string, string>,
models: contractJson1['models'] as Record<string, unknown>,
storage: contractJson1['storage'] as Record<string, unknown>,
extensionPacks: contractJson1['extensionPacks'] as Record<string, unknown>,
capabilities:
(contractJson1['capabilities'] as Record<string, Record<string, boolean>>) || {},
meta: (contractJson1['meta'] as Record<string, unknown>) || {},
});

const result2 = await emit(ir2, options, mockSqlHook);
const result2 = await emit(ir2, options, mockSqlHook);

expect(result1.contractJson).toBe(result2.contractJson);
expect(result1.storageHash).toBe(result2.storageHash);
expect(result1.contractJson).toBe(result2.contractJson);
expect(result1.storageHash).toBe(result2.storageHash);

const parsed2 = JSON.parse(result2.contractJson) as Record<string, unknown>;
const storage = parsed2['storage'] as Record<string, unknown>;
const tables = storage['tables'] as Record<string, unknown>;
const user = tables['user'] as Record<string, unknown>;
const columns = user['columns'] as Record<string, unknown>;
const id = columns['id'] as Record<string, unknown>;
const email = columns['email'] as Record<string, unknown>;
const name = columns['name'] as Record<string, unknown>;
expect(id['nullable']).toBe(false);
expect(email['nullable']).toBe(true);
expect(name['nullable']).toBe(false);
});
const parsed2 = JSON.parse(result2.contractJson) as Record<string, unknown>;
const storage = parsed2['storage'] as Record<string, unknown>;
const tables = storage['tables'] as Record<string, unknown>;
const user = tables['user'] as Record<string, unknown>;
const columns = user['columns'] as Record<string, unknown>;
const id = columns['id'] as Record<string, unknown>;
const email = columns['email'] as Record<string, unknown>;
const name = columns['name'] as Record<string, unknown>;
expect(id['nullable']).toBe(false);
expect(email['nullable']).toBe(true);
expect(name['nullable']).toBe(false);
},
timeouts.typeScriptCompilation,
);

it('round-trip with capabilities', async () => {
const ir = createTestContract({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ export function prismaVitePlugin(
log(` → ${result.files.json}`, 'debug');
log(` → ${result.files.dts}`, 'debug');

if (server) {
server.moduleGraph.onFileChange(result.files.json);
server.moduleGraph.onFileChange(result.files.dts);
}

if (server && !hasQueuedEmit) {
server.ws.send({ type: 'full-reload' });
} else if (hasQueuedEmit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ function createMockServer() {
ssrLoadModule: vi.fn().mockResolvedValue({}),
moduleGraph: {
getModuleById: vi.fn().mockReturnValue(null),
onFileChange: vi.fn(),
},
};
}
Expand Down Expand Up @@ -415,6 +416,22 @@ describe('prismaVitePlugin', () => {
);
});

it('invalidates emitted artifacts after successful emit', async () => {
const plugin = prismaVitePlugin('prisma-next.config.ts', { logLevel: 'silent' });
const mockServer = createMockServer();

const configResolved = plugin.configResolved as unknown as (config: { root: string }) => void;
configResolved({ root: '/project' });

const configureServer = plugin.configureServer as unknown as (
server: ReturnType<typeof createMockServer>,
) => Promise<void>;
await configureServer(mockServer);

expect(mockServer.moduleGraph.onFileChange).toHaveBeenCalledWith('/out/contract.json');
expect(mockServer.moduleGraph.onFileChange).toHaveBeenCalledWith('/out/contract.d.ts');
});

it('loads config once on startup before the initial emit', async () => {
const plugin = prismaVitePlugin('prisma-next.config.ts', { logLevel: 'silent' });
const mockServer = createMockServer();
Expand Down
Loading
Loading