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
4 changes: 2 additions & 2 deletions packages/1-framework/3-tooling/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ The SQL family provides this via `@internal/family-sql/control`. The `verify()`

Inspect the live database schema and display it as a human-readable tree or machine-consumable JSON. This command is read-only and never writes files.

On Postgres it leaves out `_prisma_migrations`, the table Prisma 7 records its applied migrations in.
On Postgres and SQLite it leaves out `_prisma_migrations`, the table Prisma 7 records its applied migrations in.

**Command:**
```bash
Expand Down Expand Up @@ -503,7 +503,7 @@ prisma migration ref set db <timestamp>_baseline

Inspect the live database schema and write an inferred PSL contract to disk. Use this for brownfield adoption when you want a starting `contract.prisma` before running `contract emit` and `db sign`.

On Postgres it writes no model for `_prisma_migrations`, the table Prisma 7 records its applied migrations in.
On Postgres and SQLite it writes no model for `_prisma_migrations`, the table Prisma 7 records its applied migrations in.

**Command:**
```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,36 @@ import type { SqliteContract } from './types';
const SQLITE_MARKER_TABLE = '_prisma_marker';
const SQLITE_LEDGER_TABLE = '_prisma_ledger';

/**
* Tables another migration tool, not Prisma 8, keeps in the database: Prisma 7's ledger. `introspect` leaves them out unless the contract declares a table of that name.
*/
const OTHER_TOOL_MIGRATION_TABLES: ReadonlySet<string> = new Set(['_prisma_migrations']);

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}

function extractContractDeclaredTableNames(contract: unknown): ReadonlySet<string> {
if (!isRecord(contract)) return new Set();
const storage = contract['storage'];
if (!isRecord(storage)) return new Set();
const namespaces = storage['namespaces'];
if (!isRecord(namespaces)) return new Set();
const declared = new Set<string>();
for (const namespace of Object.values(namespaces)) {
if (!isRecord(namespace)) continue;
const entries = namespace['entries'];
if (!isRecord(entries)) continue;
const tables = entries['table'];
if (isRecord(tables)) {
for (const name of Object.keys(tables)) {
declared.add(name);
}
}
}
return declared;
}

type SqliteLedgerRow = {
readonly space: string;
readonly migration_name: string;
Expand Down Expand Up @@ -529,8 +559,9 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {

async introspect(
driver: SqlControlDriverInstance<'sqlite'>,
_contract?: unknown,
contract?: unknown,
): Promise<SqlSchemaIR> {
const declaredTables = extractContractDeclaredTableNames(contract);
// Filter out runner-managed control tables (`_prisma_marker`,
// `_prisma_ledger`) — they're an implementation detail of the migration
// runner, not part of the user-authored contract, so they must not
Expand All @@ -548,6 +579,9 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> {

for (const tableRow of tablesResult.rows) {
const tableName = tableRow.name;
if (OTHER_TOOL_MIGRATION_TABLES.has(tableName) && !declaredTables.has(tableName)) {
continue;
}

// SQLite's synchronous driver serializes reads — no benefit from Promise.all
const columnsResult = await driver.query<PragmaTableInfoRow>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { DatabaseSync } from 'node:sqlite';
import { UNBOUND_NAMESPACE_ID } from '@internal/framework-components/ir';
import type { SqlControlDriverInstance } from '@internal/sql-contract/types';
import { createSqliteBuiltinCodecLookup } from '@internal/target-sqlite/codecs';
import { describe, expect, it } from 'vitest';
import { SqliteControlAdapter } from '../src/core/control-adapter';

function createMemoryDriver(initSql: string): SqlControlDriverInstance<'sqlite'> & {
readonly db: DatabaseSync;
} {
const db = new DatabaseSync(':memory:');
db.exec('PRAGMA foreign_keys = ON');
db.exec(initSql);
return {
familyId: 'sql' as const,
targetId: 'sqlite' as const,
async query<Row = Record<string, unknown>>(sql: string, params?: readonly unknown[]) {
const stmt = db.prepare(sql);
const rows = stmt.all(...((params ?? []) as Array<string | number | null>)) as Row[];
return { rows };
},
async close() {
db.close();
},
db,
};
}

function contractDeclaringTables(tables: readonly string[]): unknown {
return {
storage: {
namespaces: {
[UNBOUND_NAMESPACE_ID]: {
entries: { table: Object.fromEntries(tables.map((name) => [name, {}])) },
},
},
},
};
}

const adapter = new SqliteControlAdapter(createSqliteBuiltinCodecLookup());

const INIT_SQL = `
CREATE TABLE "user" (id INTEGER PRIMARY KEY, email TEXT NOT NULL);
CREATE TABLE "_prisma_migrations" (
id TEXT PRIMARY KEY NOT NULL,
checksum TEXT NOT NULL
);
CREATE TABLE "_prisma_marker" (space TEXT PRIMARY KEY);
CREATE TABLE "_prisma_ledger" (id INTEGER PRIMARY KEY);
`;

describe('SqliteControlAdapter introspection of migration tool tables', () => {
it('leaves out _prisma_migrations when no contract is given', async () => {
const driver = createMemoryDriver(INIT_SQL);
const result = await adapter.introspect(driver);

expect(Object.keys(result.tables)).toEqual(['user']);
await driver.close();
});

it('leaves out _prisma_migrations when the contract does not declare it', async () => {
const driver = createMemoryDriver(INIT_SQL);
const result = await adapter.introspect(driver, contractDeclaringTables(['user']));

expect(Object.keys(result.tables)).toEqual(['user']);
await driver.close();
});

it('keeps _prisma_migrations when the contract declares it', async () => {
const driver = createMemoryDriver(INIT_SQL);
const result = await adapter.introspect(
driver,
contractDeclaringTables(['_prisma_migrations', 'user']),
);

expect(Object.keys(result.tables)).toEqual(['_prisma_migrations', 'user']);
await driver.close();
});
});