From facbad5214f75c3fd6b46d2a4a867bb8245c8d56 Mon Sep 17 00:00:00 2001 From: Hemcharan Varma Date: Sun, 11 Oct 2026 11:46:26 +0530 Subject: [PATCH 1/3] fix(sqlite-adapter): leave out Prisma 7 migration ledger during introspection SqliteControlAdapter now leaves out _prisma_migrations during introspection unless explicitly declared in the contract, matching PostgresControlAdapter parity (PR #30671). Signed-off-by: Hemcharan Varma --- packages/1-framework/3-tooling/cli/README.md | 4 +- .../sqlite/src/core/control-adapter.ts | 36 ++++++++- .../test/control-adapter.tool-tables.test.ts | 80 +++++++++++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 packages/3-targets/6-adapters/sqlite/test/control-adapter.tool-tables.test.ts diff --git a/packages/1-framework/3-tooling/cli/README.md b/packages/1-framework/3-tooling/cli/README.md index a830ade9b831..fc3e6523e1f2 100644 --- a/packages/1-framework/3-tooling/cli/README.md +++ b/packages/1-framework/3-tooling/cli/README.md @@ -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 @@ -503,7 +503,7 @@ prisma migration ref set db _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 diff --git a/packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts b/packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts index 0a8f1e2ddb98..0220a5475088 100644 --- a/packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts +++ b/packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts @@ -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 = new Set(['_prisma_migrations']); + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function extractContractDeclaredTableNames(contract: unknown): ReadonlySet { + 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(); + 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; @@ -529,8 +559,9 @@ export class SqliteControlAdapter implements SqlControlAdapter<'sqlite'> { async introspect( driver: SqlControlDriverInstance<'sqlite'>, - _contract?: unknown, + contract?: unknown, ): Promise { + 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 @@ -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( diff --git a/packages/3-targets/6-adapters/sqlite/test/control-adapter.tool-tables.test.ts b/packages/3-targets/6-adapters/sqlite/test/control-adapter.tool-tables.test.ts new file mode 100644 index 000000000000..232d85c66995 --- /dev/null +++ b/packages/3-targets/6-adapters/sqlite/test/control-adapter.tool-tables.test.ts @@ -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>(sql: string, params?: readonly unknown[]) { + const stmt = db.prepare(sql); + const rows = stmt.all(...((params ?? []) as Array)) 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(); + }); +}); From 1f397a85875a6a412ec24b49cd1f454370d330fa Mon Sep 17 00:00:00 2001 From: Hemcharan Varma Date: Tue, 13 Oct 2026 04:57:29 +0530 Subject: [PATCH 2/3] fix(sqlite-adapter): add docstrings for new introspect helpers Signed-off-by: Hemcharan Varma --- .../3-targets/6-adapters/sqlite/src/core/control-adapter.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts b/packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts index 0220a5475088..f3c2e251b4ba 100644 --- a/packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts +++ b/packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts @@ -72,10 +72,16 @@ const SQLITE_LEDGER_TABLE = '_prisma_ledger'; */ const OTHER_TOOL_MIGRATION_TABLES: ReadonlySet = new Set(['_prisma_migrations']); +/** Returns true when `value` is a non-null object, narrowing it to `Record`. */ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null; } +/** + * Collects every table name declared across all storage namespaces in `contract`. + * Returns an empty set when `contract` is absent or malformed so the caller can + * treat every name as undeclared. + */ function extractContractDeclaredTableNames(contract: unknown): ReadonlySet { if (!isRecord(contract)) return new Set(); const storage = contract['storage']; From 81511fef2d762b16e80fbc7edf5b68fa9d888391 Mon Sep 17 00:00:00 2001 From: Hemcharan Varma Date: Tue, 13 Oct 2026 05:17:41 +0530 Subject: [PATCH 3/3] fix(sqlite-adapter): reject arrays in isRecord to treat array namespaces as malformed Signed-off-by: Hemcharan Varma --- .../6-adapters/sqlite/src/core/control-adapter.ts | 4 ++-- .../sqlite/test/control-adapter.tool-tables.test.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts b/packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts index f3c2e251b4ba..b1ed13d908af 100644 --- a/packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts +++ b/packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts @@ -72,9 +72,9 @@ const SQLITE_LEDGER_TABLE = '_prisma_ledger'; */ const OTHER_TOOL_MIGRATION_TABLES: ReadonlySet = new Set(['_prisma_migrations']); -/** Returns true when `value` is a non-null object, narrowing it to `Record`. */ +/** Returns true when `value` is a non-null object, narrowing it to `Record`. Arrays are excluded so array-shaped contract fields are treated as malformed. */ function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null; + return typeof value === 'object' && value !== null && !Array.isArray(value); } /** diff --git a/packages/3-targets/6-adapters/sqlite/test/control-adapter.tool-tables.test.ts b/packages/3-targets/6-adapters/sqlite/test/control-adapter.tool-tables.test.ts index 232d85c66995..b766ad086d5c 100644 --- a/packages/3-targets/6-adapters/sqlite/test/control-adapter.tool-tables.test.ts +++ b/packages/3-targets/6-adapters/sqlite/test/control-adapter.tool-tables.test.ts @@ -77,4 +77,14 @@ describe('SqliteControlAdapter introspection of migration tool tables', () => { expect(Object.keys(result.tables)).toEqual(['_prisma_migrations', 'user']); await driver.close(); }); + + it('leaves out _prisma_migrations when contract namespaces is an array (malformed)', async () => { + const driver = createMemoryDriver(INIT_SQL); + const result = await adapter.introspect(driver, { + storage: { namespaces: [{ entries: { table: { _prisma_migrations: {} } } }] }, + }); + + expect(Object.keys(result.tables)).toEqual(['user']); + await driver.close(); + }); });