Skip to content

feat: migration handling#175

Open
WhyAsh5114 wants to merge 19 commits intomainfrom
migrations
Open

feat: migration handling#175
WhyAsh5114 wants to merge 19 commits intomainfrom
migrations

Conversation

@WhyAsh5114
Copy link
Copy Markdown
Member

@WhyAsh5114 WhyAsh5114 commented Mar 22, 2026

Description

Handle migrations

Fixes #21

Type of change

  • New feature (non-breaking change which adds functionality)
  • Documentation update

How Has This Been Tested?

  • tests/schema-gen/tests/migration/migrations.test.ts

Summary by CodeRabbit

Release Notes

  • New Features

    • Added database migration system with automatic schema version tracking and validation
    • Added automatic schema drift detection with configurable recovery behavior (error or database reset)
  • Chores

    • Improved test infrastructure for schema generation workflows
    • Refactored internal code organization

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
idb-client-generator-docs Ready Ready Preview, Comment Mar 22, 2026 2:23pm
pidb-kanban-example Ready Ready Preview, Comment Mar 22, 2026 2:23pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

This PR introduces schema versioning and migrations support to the Prisma IDB generator. It adds automatic schema hash computation and drift detection, enables version-based database initialization with sequential migration application, and refactors generator file creators to use options objects for improved maintainability.

Changes

Cohort / File(s) Summary
Schema Hash & Drift Detection
apps/pidb-kanban-example/src/lib/prisma-idb/client/idb-schema-hash.ts, apps/pidb-kanban-example/src/lib/prisma-idb/client/idb-interface.ts, apps/pidb-kanban-example/src/lib/prisma-idb/client/prisma-idb-client.ts, apps/usage/src/lib/prisma-idb/client/...
Added _idb_meta object store for persisting schema hash; added IDB_SCHEMA_HASH constant generation; implemented runtime schema drift detection with automatic DB recreation or error handling.
Generator: Migrations Infrastructure
packages/generator/src/fileCreators/migrations/create.ts, packages/generator/src/fileCreators/migrations/createDmmfSnapshot.ts, packages/generator/src/fileCreators/migrations/createMigrationFunction.ts, packages/generator/src/helpers/migrations.ts
New migration generation pipeline: list migration folders, extract snapshots, compute schema hashes and diffs, generate versioned dmmf.ts and migration.ts files with conditional outbox/meta store inclusion.
Generator: File Creator Refactoring
packages/generator/src/fileCreators/apply-pull/create.ts, packages/generator/src/fileCreators/batch-processor/create.ts, packages/generator/src/fileCreators/idb-interface/create.ts, packages/generator/src/fileCreators/idb-utils/create.ts, packages/generator/src/fileCreators/scoped-schema/create.ts, packages/generator/src/fileCreators/validators/create.ts
Converted function signatures from positional parameters to single options objects for consistency and maintainability.
Generator: Client & Index Generation
packages/generator/src/fileCreators/prisma-idb-client/create.ts, packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts, packages/generator/src/fileCreators/index/create-client-index.ts, packages/generator/src/fileCreators/idb-schema-hash/create.ts
Added migration-aware client initialization with version-based upgrade logic; new schema hash file generation; updated index exports to conditionally include migration and hash modules.
Generator: Configuration & Orchestration
packages/generator/src/helpers/parseGeneratorConfig.ts, packages/generator/src/helpers/utils.ts, packages/generator/src/generator.ts
Extended config parsing to capture dropDbOnSchemaVersionMismatch flag; added standalone TypeScript type mapping; refactored generator orchestration to invoke migrations generation and plumb migration info through file creators.
Test Infrastructure
tests/schema-gen/tests/migration/migrations.test.ts, tests/schema-gen/tests/migration/schemas/v0-base.prisma, tests/schema-gen/tests/migration/schemas/v1-new-field.prisma, tests/schema-gen/tests/migration/schemas/v2-new-model.prisma, tests/schema-gen/tests/projection/generator.test.ts
Added comprehensive end-to-end migration tests with versioned schema fixtures; updated projection tests to use new schema directory structure.
Workspace & Configuration Updates
package.json, pnpm-workspace.yaml, tests/.gitignore, tests/schema-gen/package.json, tests/schema-gen/.gitignore, tests/schema-gen/tests/projection/schemas/valid/user-board-todo.prisma
Updated Turbo test filter to target schema-gen tests; adjusted workspace globs; renamed test package to @prisma-idb/schema-gen-tests; added gitignore entries; updated schema generator output paths.

Sequence Diagram(s)

sequenceDiagram
    participant Client as PrismaIDBClient
    participant IDB as IndexedDB
    participant MetaStore as _idb_meta Store
    participant MigrationSvc as Migration Service
    
    Client->>IDB: openDB("prisma-idb")
    IDB-->>Client: db handle
    Client->>MetaStore: read "schemaHash"
    MetaStore-->>Client: stored hash (or null)
    
    alt Schema Hash Mismatch
        Client->>IDB: close db
        Client->>MigrationSvc: check DROP_DB_ON_SCHEMA_VERSION_MISMATCH
        alt Drop DB
            MigrationSvc->>IDB: deleteDB("prisma-idb")
            MigrationSvc->>Client: recreate client recursively
        else Throw Error
            MigrationSvc-->>Client: throw mismatch error with instructions
        end
    else Hash Match or No Prior Hash
        Client->>IDB: initialize (upgrade callback)
        IDB->>MigrationSvc: switchOnVersion(oldVersion)
        MigrationSvc->>MigrationSvc: call migrateV1(), migrateV2(), ... migrateVn()
        MigrationSvc-->>IDB: create/update stores via each migration
        IDB-->>Client: db ready
        Client->>MetaStore: write IDB_SCHEMA_HASH to "schemaHash"
        MetaStore-->>Client: stored
        Client->>Client: set instance and complete
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PR #124: Modifies generator configuration parsing and file emission pipeline, sharing the same entry points in packages/generator/src/generator.ts and config structures.
  • PR #127: Refactors multiple file creator function signatures to options-object patterns, directly overlapping with the generator API changes in this PR.
  • PR #154: Modifies IDB client code generation and outbox/version-meta store handling, affecting the same PrismaIDBClient class and idb-interface generation logic.

Poem

🐰 Hops through versions with care,
Schemas tracked with hash so fair,
Migrations dance from V to V,
Drift detected, deftly free!
Meta stores now mark the way,
IndexedDB grows each day! 🌱✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: migration handling' clearly and concisely describes the main feature added in this changeset—implementing migration handling functionality.
Linked Issues check ✅ Passed The PR implements all three core objectives from issue #21: schema change detection via hash-based drift detection in PrismaIDBClient, version management through MigrationInfo and currentVersion tracking, and data transformation support via generated migrate functions.
Out of Scope Changes check ✅ Passed All changes align with migration feature scope. Refactored function signatures to options-based parameters, restructured test layout, added schema snapshot/diff helpers, migration file generators, and hash-based version tracking—all directly supporting migration handling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch migrations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (9)
packages/generator/src/fileCreators/idb-interface/create.ts (1)

8-24: The hasMigrations = false branch is no longer safe.

The paired client template now always accesses _idb_meta for schema-hash checks, so this helper can still emit a PrismaIDBSchema without that store. That leaves an easy path for future callers or tests to generate a schema type that diverges from the client template. Prefer generating _idb_meta unconditionally here, or derive the flag from the exact same condition used by the client generator.

Also applies to: 57-62

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/generator/src/fileCreators/idb-interface/create.ts` around lines 8 -
24, The current conditional default hasMigrations = false allows
createPrismaIDBSchema to emit a PrismaIDBSchema without the _idb_meta store
while the paired client template always expects _idb_meta; update create.ts so
that _idb_meta is generated unconditionally (remove or ignore hasMigrations when
deciding to include _idb_meta) or else compute hasMigrations using the exact
same condition the client generator uses so both sides match; refer to the
options variable and the hasMigrations binding in this file and ensure the logic
that builds the schema models always includes a model named "_idb_meta" (or
derives hasMigrations from the client generator condition) to keep schema and
client in sync.
packages/generator/src/generator.ts (1)

84-92: Consider an options bag for generateMigrations too.

This is now the only orchestration call here still carrying seven positional arguments, several of them same-type strings/booleans. Converting it to an object would make future migration additions much harder to misorder.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/generator/src/generator.ts` around lines 84 - 92, Refactor the
generateMigrations API to accept a single options object instead of seven
positional args: replace the call that currently passes (options.schemaPath,
outputPath, filteredModels, options.dmmf.datamodel.enums, outboxSync,
outboxModelName, versionMetaModelName) with a single object literal e.g. {
schemaPath: options.schemaPath, outputPath, models: filteredModels, enums:
options.dmmf.datamodel.enums, outboxSync, outboxModelName, versionMetaModelName
}, then update the generateMigrations function signature and its type
definitions to destructure those named properties (schemaPath, outputPath,
models, enums, outboxSync, outboxModelName, versionMetaModelName) and update any
other call sites to the new object-shape to avoid misordering of same-typed
params.
packages/generator/src/helpers/migrations.ts (1)

68-80: Minor: Redundant filter condition.

Line 77 explicitly excludes migration_lock.toml, but this is already excluded by the entry.isDirectory() check since migration_lock.toml is a file. The explicit check is harmless but unnecessary.

♻️ Simplified filter
   return fs
     .readdirSync(migrationsDir, { withFileTypes: true })
-    .filter((entry) => entry.isDirectory() && entry.name !== "migration_lock.toml")
+    .filter((entry) => entry.isDirectory())
     .map((entry) => entry.name)
     .sort();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/generator/src/helpers/migrations.ts` around lines 68 - 80, In
listMigrationFolders, remove the redundant check excluding
"migration_lock.toml": the filter should only test entry.isDirectory() (in the
readdirSync(...).filter callback) since files like migration_lock.toml are
already excluded; update the filter used in listMigrationFolders to only keep
directories and leave the subsequent map/sort logic unchanged.
packages/generator/src/fileCreators/migrations/createMigrationFunction.ts (1)

64-135: Add exhaustiveness check for MigrationOp switch.

The switch statement covers all 8 current MigrationOp types, but lacks a default case. If a new operation type is added to MigrationOp, this will silently skip it without warning.

♻️ Add exhaustiveness check
     case "removeEnumValue":
       writer.writeLine(`// [auto] removed enum value: ${op.enumName}.${op.value}`);
       writer.writeLine(`// TODO: Existing records using "${op.value}" may need to be updated.`);
       // ... existing code ...
       writer.blankLine();
       break;
+
+    default: {
+      const _exhaustive: never = op;
+      throw new Error(`Unhandled migration op type: ${(_exhaustive as MigrationOp).type}`);
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/generator/src/fileCreators/migrations/createMigrationFunction.ts`
around lines 64 - 135, The switch in writeOp over MigrationOp is missing an
exhaustive/default branch; add a default case that forces a compile-time
exhaustiveness check and runtime error for unknown op types (e.g., default: {
const _exhaustive: never = op; throw new Error(`Unhandled MigrationOp type:
${(op as any)?.type}`); } or call an assertUnreachable(op) helper) so new
MigrationOp variants fail fast at build/runtime; update the writeOp function to
include this default branch referencing the MigrationOp union.
packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts (2)

136-157: Fallthrough comment placement in generated switch.

The generated switch uses intentional fallthrough for sequential migrations, but the fallthrough comment (line 147) appears only once after all cases. For clarity in the generated code, consider adding a fallthrough comment after each case.

♻️ Improved fallthrough comments
           writer.write(`switch (oldVersion) `).block(() => {
             for (let i = 0; i < migrationInfo.currentVersion; i++) {
               writer.writeLine(`// eslint-disable-next-line `@typescript-eslint/no-explicit-any``);
               writer.writeLine(`case ${i}: await migrateV${i + 1}(db as any, tx as any);`);
+              writer.writeLine(`// falls through`);
             }
-            writer.writeLine(`// fallthrough intentional — runs all pending migrations in sequence`);
           });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts`
around lines 136 - 157, The generated switch in the upgrade callback (inside
openDB<PrismaIDBSchema>("prisma-idb", CURRENT_VERSION, ...)) places a single
fallthrough comment after all cases which obscures intent; modify the loop that
emits migration cases in PrismaIDBClient.ts so that after each emitted case line
(case ${i}: await migrateV${i + 1}(...);) you also emit a per-case fallthrough
comment (e.g., // fallthrough intentional — continue to next migration) so every
migration case documents the intentional fallthrough; update the code that
writes the cases (the for loop that writes `case ${i}: await migrateV${i + 1}(db
as any, tx as any);`) to append a writer.writeLine(...) immediately after each
case emission instead of a single comment after the loop.

614-621: resetDatabase may leave stale schema hash.

The resetDatabase method (lines 614-621) deletes the database and re-initializes, but does not update the schemaHash in _idb_meta. After reset, the next createClient() call will find no stored hash (undefined), which is handled correctly by line 101's storedHash !== undefined check.

However, this means the hash won't be persisted until createClient() is called again. Consider whether this is the intended behavior or if resetDatabase should also persist the hash.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts`
around lines 614 - 621, The resetDatabase method currently deletes the DB and
re-initializes but does not persist the schema hash into _idb_meta, leaving a
stale/absent schemaHash for subsequent createClient() calls; update the
addResetDatabaseMethod implementation for the resetDatabase method to, after
calling PrismaIDBClient.instance.initialize(), also write the current schema
hash into the _idb_meta entry (e.g., set _idb_meta.schemaHash to the active
schema hash and persist it via the same internal helper used elsewhere to store
meta) so that subsequent createClient() sees a persisted schemaHash; reference
resetDatabase, _idb_meta, schemaHash and PrismaIDBClient.instance.initialize()
when making the change.
packages/generator/src/fileCreators/migrations/createDmmfSnapshot.ts (1)

41-58: Minor: Consider simplifying type wrapping for cleaner output.

The current parentheses wrapping (lines 55-56) produces valid but verbose types like ((string)[]) | null. For cleaner generated code, you could conditionally add parentheses only when necessary.

♻️ Optional simplification
-  if (isList) tsType = `(${tsType})[]`;
-  if (!isRequired) tsType = `(${tsType}) | null`;
+  if (isList) tsType = `${tsType}[]`;
+  if (!isRequired) tsType = `${tsType} | null`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/generator/src/fileCreators/migrations/createDmmfSnapshot.ts` around
lines 41 - 58, prismaTypeToTs currently always wraps the computed tsType when
making lists and nullable types, leading to redundant parentheses like
((string)[]) | null; change the wrapping logic in prismaTypeToTs so parentheses
are only added when necessary: when creating a list, only wrap tsType as
`(${tsType})[]` if the base tsType contains a union (`' | '`), otherwise use
`${tsType}[]`; when marking nullable, only wrap the type in parentheses if it
contains a union (`' | '`), otherwise use `${tsType} | null`. Update the logic
in prismaTypeToTs to apply these conditional parentheses rules (refer to the
isList and isRequired handling in prismaTypeToTs).
packages/generator/src/fileCreators/migrations/create.ts (2)

30-38: Consider using an options object for better maintainability.

The function has 7 parameters, which can make call sites harder to read and maintain. Given that the PR commits mention "options-style function signatures," consider aligning this function with that pattern.

♻️ Suggested refactor
+export interface GenerateMigrationsOptions {
+  schemaPath: string;
+  outputPath: string;
+  filteredModels: readonly DMMF.Model[];
+  enums: readonly DMMF.DatamodelEnum[];
+  outboxSync: boolean;
+  outboxModelName: string;
+  versionMetaModelName: string;
+}
+
-export async function generateMigrations(
-  schemaPath: string,
-  outputPath: string,
-  filteredModels: readonly DMMF.Model[],
-  enums: readonly DMMF.DatamodelEnum[],
-  outboxSync: boolean,
-  outboxModelName: string,
-  versionMetaModelName: string
-): Promise<MigrationGenerationResult> {
+export async function generateMigrations(
+  options: GenerateMigrationsOptions
+): Promise<MigrationGenerationResult> {
+  const {
+    schemaPath,
+    outputPath,
+    filteredModels,
+    enums,
+    outboxSync,
+    outboxModelName,
+    versionMetaModelName,
+  } = options;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/generator/src/fileCreators/migrations/create.ts` around lines 30 -
38, The generateMigrations function has a long positional parameter list;
refactor it to accept a single options object (e.g. define a
MigrationOptions/type with properties schemaPath, outputPath, filteredModels,
enums, outboxSync, outboxModelName, versionMetaModelName) and update the
generateMigrations signature to take that options object and use destructuring
inside the function; update all call sites to pass an object instead of
positional args (or add an overload/compat shim if needed for backward
compatibility) and keep the Promise<MigrationGenerationResult> return type
unchanged.

84-100: Simplify redundant option-setting logic.

The current flow sets initial values at lines 85-91, then overrides them at lines 94-100. Tracing through both branches shows:

  • dmmfOptions.outboxSync ends up as outboxSync regardless of isFirstMigration
  • dmmfOptions.includeMetaStore ends up as true for all versions (V1 via line 85/90, V2+ via line 99)

This can be simplified:

♻️ Simplified logic
     const isFirstMigration = version === 1;
-    const includeMetaStore = isFirstMigration;
     const dmmfOptions = {
-      outboxSync: outboxSync && isFirstMigration,
+      outboxSync,
       outboxModelName,
       versionMetaModelName,
-      includeMetaStore,
+      includeMetaStore: true, // Meta store exists for all versions
     };

-    // For V2+, outbox/meta stores already exist from V1 — include them in schema
-    if (outboxSync && !isFirstMigration) {
-      dmmfOptions.outboxSync = true;
-    }
-    // _idb_meta always exists after V1
-    if (!isFirstMigration) {
-      dmmfOptions.includeMetaStore = true;
-    }

Note: If you apply this, you'll need to update line 120 to pass isFirstMigration directly instead of includeMetaStore.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/generator/src/fileCreators/migrations/create.ts` around lines 84 -
100, The dmmfOptions construction is redundant: set dmmfOptions.outboxSync to
the incoming outboxSync value (no conditional on isFirstMigration) and set
dmmfOptions.includeMetaStore to true (it is always true after V1 or for V1),
keep outboxModelName and versionMetaModelName as-is, and remove the later
conditional overrides that reassign those fields; also update the downstream
call that currently passes includeMetaStore to instead pass the original
isFirstMigration value so callers still receive the migration-version flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/pidb-kanban-example/src/lib/prisma-idb/client/prisma-idb-client.ts`:
- Around line 456-468: The IndexedDB upgrade callback adds the "_idb_meta" store
but the file still uses the legacy hardcoded IDB_VERSION (IDB_VERSION = 1), so
existing DBs won't trigger upgrade; fix by bumping the IDB version constant
(IDB_VERSION) to a higher integer or switch the generated client to use the
migration-based CURRENT_VERSION path used by the generator, ensuring the
openDB(...) call's upgrade() runs and creates "_idb_meta" (referencing openDB,
upgrade, IDB_VERSION and the "_idb_meta" store) and update any schema-drift
check that reads client._db.get("_idb_meta", "schemaHash") to match the new
versioning approach.

In `@apps/usage/src/lib/prisma-idb/client/prisma-idb-client.ts`:
- Around line 105-109: When the blocking() callback closes the underlying
connection it must also clear the cached singleton so future createClient()
calls don't return a stale PrismaIDBClient with a closed _db; update the
blocking() implementation on the PrismaIDBClient to set the static instance
(e.g., PrismaIDBClient.instance or whatever singleton variable is used) to
undefined/null after calling db.close(), ensuring subsequent createClient()
constructs a fresh client with a valid _db.
- Around line 56-67: The generated PrismaIDB client currently reads from the
"_idb_meta" store immediately after initialize() and caches the singleton before
blocking() can close the connection, causing missing-store and closed-instance
bugs; update the generator template (PrismaIDBClient.ts) to (1) bump IDB_VERSION
and add an upgrade migration that ensures the "_idb_meta" object store is
created on existing databases so the stored schemaHash lookup succeeds, and (2)
move singleton caching to occur only after the drift check and after setting up
blocking(), or detect when client._db.close() runs inside blocking() and
invalidate the cached singleton (e.g., clear the cached PrismaIDBClient) so
subsequent PrismaIDBClient.createClient() returns a fresh, open connection;
ensure the logic still respects DROP_DB_ON_SCHEMA_VERSION_MISMATCH and the
IDB_SCHEMA_HASH comparison.

In `@packages/generator/src/fileCreators/migrations/create.ts`:
- Around line 58-64: Wrap the existing read/parse logic that reads
snapshotJsonPath and pushes into snapshots in a try-catch around
fs.readFileSync(...) and JSON.parse(...); on JSON.parse failure catch the error,
throw or surface a new error that includes snapshotJsonPath and the original
error message (or log it) so callers see "corrupted snapshot.json at
{snapshotJsonPath}: {originalError}" and avoid an unhelpful stacktrace; keep the
fallback branch that pushes { ...currentSnapshot, version: i + 1 } unchanged for
the non-existent-file case.

---

Nitpick comments:
In `@packages/generator/src/fileCreators/idb-interface/create.ts`:
- Around line 8-24: The current conditional default hasMigrations = false allows
createPrismaIDBSchema to emit a PrismaIDBSchema without the _idb_meta store
while the paired client template always expects _idb_meta; update create.ts so
that _idb_meta is generated unconditionally (remove or ignore hasMigrations when
deciding to include _idb_meta) or else compute hasMigrations using the exact
same condition the client generator uses so both sides match; refer to the
options variable and the hasMigrations binding in this file and ensure the logic
that builds the schema models always includes a model named "_idb_meta" (or
derives hasMigrations from the client generator condition) to keep schema and
client in sync.

In `@packages/generator/src/fileCreators/migrations/create.ts`:
- Around line 30-38: The generateMigrations function has a long positional
parameter list; refactor it to accept a single options object (e.g. define a
MigrationOptions/type with properties schemaPath, outputPath, filteredModels,
enums, outboxSync, outboxModelName, versionMetaModelName) and update the
generateMigrations signature to take that options object and use destructuring
inside the function; update all call sites to pass an object instead of
positional args (or add an overload/compat shim if needed for backward
compatibility) and keep the Promise<MigrationGenerationResult> return type
unchanged.
- Around line 84-100: The dmmfOptions construction is redundant: set
dmmfOptions.outboxSync to the incoming outboxSync value (no conditional on
isFirstMigration) and set dmmfOptions.includeMetaStore to true (it is always
true after V1 or for V1), keep outboxModelName and versionMetaModelName as-is,
and remove the later conditional overrides that reassign those fields; also
update the downstream call that currently passes includeMetaStore to instead
pass the original isFirstMigration value so callers still receive the
migration-version flag.

In `@packages/generator/src/fileCreators/migrations/createDmmfSnapshot.ts`:
- Around line 41-58: prismaTypeToTs currently always wraps the computed tsType
when making lists and nullable types, leading to redundant parentheses like
((string)[]) | null; change the wrapping logic in prismaTypeToTs so parentheses
are only added when necessary: when creating a list, only wrap tsType as
`(${tsType})[]` if the base tsType contains a union (`' | '`), otherwise use
`${tsType}[]`; when marking nullable, only wrap the type in parentheses if it
contains a union (`' | '`), otherwise use `${tsType} | null`. Update the logic
in prismaTypeToTs to apply these conditional parentheses rules (refer to the
isList and isRequired handling in prismaTypeToTs).

In `@packages/generator/src/fileCreators/migrations/createMigrationFunction.ts`:
- Around line 64-135: The switch in writeOp over MigrationOp is missing an
exhaustive/default branch; add a default case that forces a compile-time
exhaustiveness check and runtime error for unknown op types (e.g., default: {
const _exhaustive: never = op; throw new Error(`Unhandled MigrationOp type:
${(op as any)?.type}`); } or call an assertUnreachable(op) helper) so new
MigrationOp variants fail fast at build/runtime; update the writeOp function to
include this default branch referencing the MigrationOp union.

In
`@packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts`:
- Around line 136-157: The generated switch in the upgrade callback (inside
openDB<PrismaIDBSchema>("prisma-idb", CURRENT_VERSION, ...)) places a single
fallthrough comment after all cases which obscures intent; modify the loop that
emits migration cases in PrismaIDBClient.ts so that after each emitted case line
(case ${i}: await migrateV${i + 1}(...);) you also emit a per-case fallthrough
comment (e.g., // fallthrough intentional — continue to next migration) so every
migration case documents the intentional fallthrough; update the code that
writes the cases (the for loop that writes `case ${i}: await migrateV${i + 1}(db
as any, tx as any);`) to append a writer.writeLine(...) immediately after each
case emission instead of a single comment after the loop.
- Around line 614-621: The resetDatabase method currently deletes the DB and
re-initializes but does not persist the schema hash into _idb_meta, leaving a
stale/absent schemaHash for subsequent createClient() calls; update the
addResetDatabaseMethod implementation for the resetDatabase method to, after
calling PrismaIDBClient.instance.initialize(), also write the current schema
hash into the _idb_meta entry (e.g., set _idb_meta.schemaHash to the active
schema hash and persist it via the same internal helper used elsewhere to store
meta) so that subsequent createClient() sees a persisted schemaHash; reference
resetDatabase, _idb_meta, schemaHash and PrismaIDBClient.instance.initialize()
when making the change.

In `@packages/generator/src/generator.ts`:
- Around line 84-92: Refactor the generateMigrations API to accept a single
options object instead of seven positional args: replace the call that currently
passes (options.schemaPath, outputPath, filteredModels,
options.dmmf.datamodel.enums, outboxSync, outboxModelName, versionMetaModelName)
with a single object literal e.g. { schemaPath: options.schemaPath, outputPath,
models: filteredModels, enums: options.dmmf.datamodel.enums, outboxSync,
outboxModelName, versionMetaModelName }, then update the generateMigrations
function signature and its type definitions to destructure those named
properties (schemaPath, outputPath, models, enums, outboxSync, outboxModelName,
versionMetaModelName) and update any other call sites to the new object-shape to
avoid misordering of same-typed params.

In `@packages/generator/src/helpers/migrations.ts`:
- Around line 68-80: In listMigrationFolders, remove the redundant check
excluding "migration_lock.toml": the filter should only test entry.isDirectory()
(in the readdirSync(...).filter callback) since files like migration_lock.toml
are already excluded; update the filter used in listMigrationFolders to only
keep directories and leave the subsequent map/sort logic unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c9317fa9-a427-4deb-8816-11e68b768954

📥 Commits

Reviewing files that changed from the base of the PR and between 926754c and 900fe5d.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • tests/schema-gen/tests/projection/__snapshots__/generator.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (48)
  • apps/pidb-kanban-example/src/lib/prisma-idb/client/idb-interface.ts
  • apps/pidb-kanban-example/src/lib/prisma-idb/client/idb-schema-hash.ts
  • apps/pidb-kanban-example/src/lib/prisma-idb/client/index.ts
  • apps/pidb-kanban-example/src/lib/prisma-idb/client/prisma-idb-client.ts
  • apps/usage/src/lib/prisma-idb/client/idb-interface.ts
  • apps/usage/src/lib/prisma-idb/client/idb-schema-hash.ts
  • apps/usage/src/lib/prisma-idb/client/index.ts
  • apps/usage/src/lib/prisma-idb/client/prisma-idb-client.ts
  • package.json
  • packages/generator/src/fileCreators/apply-pull/create.ts
  • packages/generator/src/fileCreators/batch-processor/create.ts
  • packages/generator/src/fileCreators/idb-interface/create.ts
  • packages/generator/src/fileCreators/idb-schema-hash/create.ts
  • packages/generator/src/fileCreators/idb-utils/create.ts
  • packages/generator/src/fileCreators/index/create-client-index.ts
  • packages/generator/src/fileCreators/migrations/create.ts
  • packages/generator/src/fileCreators/migrations/createDmmfSnapshot.ts
  • packages/generator/src/fileCreators/migrations/createMigrationFunction.ts
  • packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts
  • packages/generator/src/fileCreators/prisma-idb-client/create.ts
  • packages/generator/src/fileCreators/scoped-schema/create.ts
  • packages/generator/src/fileCreators/validators/create.ts
  • packages/generator/src/generator.ts
  • packages/generator/src/helpers/migrations.ts
  • packages/generator/src/helpers/parseGeneratorConfig.ts
  • packages/generator/src/helpers/utils.ts
  • pnpm-workspace.yaml
  • tests/.gitignore
  • tests/schema-gen/.gitignore
  • tests/schema-gen/package.json
  • tests/schema-gen/tests/migration/migrations.test.ts
  • tests/schema-gen/tests/migration/schemas/v0-base.prisma
  • tests/schema-gen/tests/migration/schemas/v1-new-field.prisma
  • tests/schema-gen/tests/migration/schemas/v2-new-model.prisma
  • tests/schema-gen/tests/projection/generator.test.ts
  • tests/schema-gen/tests/projection/schemas/invalid/autoincrement-id.prisma
  • tests/schema-gen/tests/projection/schemas/invalid/changelog-manually-included.prisma
  • tests/schema-gen/tests/projection/schemas/invalid/extra-change-operation-enum-value.prisma
  • tests/schema-gen/tests/projection/schemas/invalid/extra-changelog-field.prisma
  • tests/schema-gen/tests/projection/schemas/invalid/invalid-change-operation-enum-value.prisma
  • tests/schema-gen/tests/projection/schemas/invalid/invalid-changelog-operation-field-type.prisma
  • tests/schema-gen/tests/projection/schemas/invalid/missing-change-operation-enum.prisma
  • tests/schema-gen/tests/projection/schemas/invalid/missing-changelog.prisma
  • tests/schema-gen/tests/projection/schemas/invalid/no-root-model-with-sync.prisma
  • tests/schema-gen/tests/projection/schemas/invalid/ownership-cycle-ambiguous.prisma
  • tests/schema-gen/tests/projection/schemas/invalid/ownership-cycle-self.prisma
  • tests/schema-gen/tests/projection/schemas/invalid/wrong-changelog-id-default.prisma
  • tests/schema-gen/tests/projection/schemas/valid/user-board-todo.prisma
💤 Files with no reviewable changes (1)
  • tests/.gitignore

Comment on lines +456 to 468
const db = await openDB<PrismaIDBSchema>("prisma-idb", IDB_VERSION, {
upgrade(db) {
db.createObjectStore("Board", { keyPath: ["id"] });
db.createObjectStore("Todo", { keyPath: ["id"] });
const UserStore = db.createObjectStore("User", { keyPath: ["id"] });
UserStore.createIndex("emailIndex", ["email"], { unique: true });
db.createObjectStore("OutboxEvent", { keyPath: ["id"] });
db.createObjectStore("VersionMeta", { keyPath: ["model", "key"] });
db.createObjectStore("_idb_meta");
},
blocking() {
db.close();
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  apps/pidb-kanban-example/src/lib/prisma-idb/client/prisma-idb-client.ts
)

if [ -f packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts ]; then
  files+=(packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts)
fi

rg -n -C2 'const IDB_VERSION =|CURRENT_VERSION|get\("_idb_meta"|put\("_idb_meta"|createObjectStore\("_idb_meta"' "${files[@]}"

Repository: prisma-idb/idb-client-generator

Length of output: 5112


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for version-related constants and logic in the generator
rg -n 'IDB_VERSION|CURRENT_VERSION|version.*=|increment.*version' \
  packages/generator/src/fileCreators/prisma-idb-client/ -A 2 -B 2 | head -100

Repository: prisma-idb/idb-client-generator

Length of output: 3503


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the full PrismaIDBClient.ts generator to understand version handling
wc -l packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts

Repository: prisma-idb/idb-client-generator

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -eulo pipefail

# Search for where IDB_VERSION or version constants are written in the generator
rg -n 'writeLine.*VERSION|IDB_VERSION|const.*VERSION' \
  packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts

Repository: prisma-idb/idb-client-generator

Length of output: 403


Fix IndexedDB schema upgrade path: bump version when introducing _idb_meta store.

The generated code adds _idb_meta store in the upgrade() callback (line 464) but uses the legacy path with hardcoded IDB_VERSION = 1 (line 19). Existing users with a v1 database will not trigger the upgrade callback, so _idb_meta is never created. The subsequent schema-drift check at line 57 (await client._db.get("_idb_meta", "schemaHash")) fails instead of detecting drift.

The generator has two code paths: a migration-based path using CURRENT_VERSION (when migration info is available) and a legacy path using hardcoded IDB_VERSION = 1. The generated example uses the legacy path. When adding new object stores, ensure the version is incremented in the generator's version declaration logic, or migrate to the migration-based upgrade callback in packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/pidb-kanban-example/src/lib/prisma-idb/client/prisma-idb-client.ts`
around lines 456 - 468, The IndexedDB upgrade callback adds the "_idb_meta"
store but the file still uses the legacy hardcoded IDB_VERSION (IDB_VERSION =
1), so existing DBs won't trigger upgrade; fix by bumping the IDB version
constant (IDB_VERSION) to a higher integer or switch the generated client to use
the migration-based CURRENT_VERSION path used by the generator, ensuring the
openDB(...) call's upgrade() runs and creates "_idb_meta" (referencing openDB,
upgrade, IDB_VERSION and the "_idb_meta" store) and update any schema-drift
check that reads client._db.get("_idb_meta", "schemaHash") to match the new
versioning approach.

Comment on lines +56 to +67
const storedHash = await client._db.get("_idb_meta", "schemaHash");
if (storedHash !== undefined && storedHash !== IDB_SCHEMA_HASH) {
client._db.close();
if (!DROP_DB_ON_SCHEMA_VERSION_MISMATCH) {
throw new Error(
`IDB schema mismatch: stored hash "${storedHash}" does not match expected "${IDB_SCHEMA_HASH}". Set dropDbOnSchemaVersionMismatch = true in your generator config to automatically reset the database.`
);
}
await deleteDB("prisma-idb");
return PrismaIDBClient.createClient();
}
await client._db.put("_idb_meta", IDB_SCHEMA_HASH, "schemaHash");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect generated clients/templates for DB versioning and meta-store upgrade handling.
fd 'prisma-idb-client\.ts$' . -t f | while read -r file; do
  echo "=== $file ==="
  rg -n -C2 'const IDB_VERSION|openDB<|upgrade\(|oldVersion|_idb_meta|schemaHash' "$file"
done

Repository: prisma-idb/idb-client-generator

Length of output: 2586


🏁 Script executed:

# Get the full blocking() callback implementation and singleton handling
sed -n '105,120p' apps/usage/src/lib/prisma-idb/client/prisma-idb-client.ts

Repository: prisma-idb/idb-client-generator

Length of output: 841


🏁 Script executed:

# Check for generator templates that produce this file
fd -t f 'prisma-idb-client' packages/generator/src --type f

Repository: prisma-idb/idb-client-generator

Length of output: 57


🏁 Script executed:

# Verify if this is indeed a generated file by checking for generation markers
head -20 apps/usage/src/lib/prisma-idb/client/prisma-idb-client.ts

Repository: prisma-idb/idb-client-generator

Length of output: 881


🏁 Script executed:

# Search for the generator template that produces prisma-idb-client.ts
fd -t f 'prisma-idb-client' packages/generator/src

Repository: prisma-idb/idb-client-generator

Length of output: 57


🏁 Script executed:

# Look for the template file by pattern
find packages/generator/src -name '*prisma-idb-client*' -o -name '*client.ts*' | head -20

Repository: prisma-idb/idb-client-generator

Length of output: 128


🏁 Script executed:

# Check the generator directory structure
tree -L 3 packages/generator/src

Repository: prisma-idb/idb-client-generator

Length of output: 1180


🏁 Script executed:

# Inspect the generator template for prisma-idb-client
cat -n packages/generator/src/fileCreators/prisma-idb-client/create.ts | head -150

Repository: prisma-idb/idb-client-generator

Length of output: 5287


🏁 Script executed:

# Search for upgrade and blocking callback in the template
rg -n -A 5 -B 2 'upgrade\(|blocking\(' packages/generator/src/fileCreators/prisma-idb-client/

Repository: prisma-idb/idb-client-generator

Length of output: 4279


🏁 Script executed:

# Check for any version bump logic or migration handling in the generator
rg -n 'IDB_VERSION|oldVersion' packages/generator/src/

Repository: prisma-idb/idb-client-generator

Length of output: 686


🏁 Script executed:

# Check the complete initialization and singleton caching logic
sed -n '40,75p' apps/usage/src/lib/prisma-idb/client/prisma-idb-client.ts

Repository: prisma-idb/idb-client-generator

Length of output: 1622


🏁 Script executed:

# Verify the learning about generated code and confirm the fix location
head -5 packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts

Repository: prisma-idb/idb-client-generator

Length of output: 358


Fix _idb_meta store initialization for existing databases and singleton caching after blocking() closes the connection.

Lines 56–67 access _idb_meta immediately after initialize(), but the store is only created in the upgrade() callback. Since IDB_VERSION remains 1, existing user databases won't re-run the upgrade and will lack the _idb_meta store, causing a missing-store error before the drift check executes.

Additionally, the singleton is cached (line 69) before blocking() fires. When another tab requests the database, blocking() closes the connection, leaving the cached instance in a closed state. Subsequent createClient() calls return a closed database.

Both issues originate in the generator template at packages/generator/src/fileCreators/prisma-idb-client/classes/PrismaIDBClient.ts and must be fixed there:

  • Add a version bump and migration to safely create _idb_meta in existing databases.
  • Detect and invalidate the singleton when blocking() closes the database, or defer singleton caching until after the drift check and blocking() setup is complete.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/usage/src/lib/prisma-idb/client/prisma-idb-client.ts` around lines 56 -
67, The generated PrismaIDB client currently reads from the "_idb_meta" store
immediately after initialize() and caches the singleton before blocking() can
close the connection, causing missing-store and closed-instance bugs; update the
generator template (PrismaIDBClient.ts) to (1) bump IDB_VERSION and add an
upgrade migration that ensures the "_idb_meta" object store is created on
existing databases so the stored schemaHash lookup succeeds, and (2) move
singleton caching to occur only after the drift check and after setting up
blocking(), or detect when client._db.close() runs inside blocking() and
invalidate the cached singleton (e.g., clear the cached PrismaIDBClient) so
subsequent PrismaIDBClient.createClient() returns a fresh, open connection;
ensure the logic still respects DROP_DB_ON_SCHEMA_VERSION_MISMATCH and the
IDB_SCHEMA_HASH comparison.

Comment on lines +105 to +109
blocking() {
db.close();
},
});
this._db = db;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether any close/blocking path clears the cached singleton.
fd 'prisma-idb-client\.ts$' . -t f | while read -r file; do
  echo "=== $file ==="
  rg -n -C3 'private static instance|createClient\(|blocking\(|close\(|instance\s*=' "$file"
done

Repository: prisma-idb/idb-client-generator

Length of output: 3804


Clear the cached singleton instance when blocking() closes the database.

The blocking() callback closes the underlying connection but doesn't clear the static instance variable. The next createClient() call returns the same stale PrismaIDBClient with a closed _db, causing operations to fail.

Suggested fix
-  private static instance: PrismaIDBClient;
+  private static instance: PrismaIDBClient | undefined;
...
       blocking() {
         db.close();
+        if (PrismaIDBClient.instance?._db === db) {
+          PrismaIDBClient.instance = undefined;
+        }
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/usage/src/lib/prisma-idb/client/prisma-idb-client.ts` around lines 105 -
109, When the blocking() callback closes the underlying connection it must also
clear the cached singleton so future createClient() calls don't return a stale
PrismaIDBClient with a closed _db; update the blocking() implementation on the
PrismaIDBClient to set the static instance (e.g., PrismaIDBClient.instance or
whatever singleton variable is used) to undefined/null after calling db.close(),
ensuring subsequent createClient() constructs a fresh client with a valid _db.

Comment on lines +58 to +64
if (fs.existsSync(snapshotJsonPath)) {
const json = fs.readFileSync(snapshotJsonPath, "utf-8");
snapshots.push(JSON.parse(json) as Snapshot);
} else {
// No existing snapshot — use current DMMF with this version number
snapshots.push({ ...currentSnapshot, version: i + 1 });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling for corrupted snapshot files.

If snapshot.json exists but contains invalid JSON, JSON.parse will throw an unhelpful error. Consider wrapping with try-catch to provide actionable feedback.

🛡️ Suggested fix
     if (fs.existsSync(snapshotJsonPath)) {
       const json = fs.readFileSync(snapshotJsonPath, "utf-8");
-      snapshots.push(JSON.parse(json) as Snapshot);
+      try {
+        snapshots.push(JSON.parse(json) as Snapshot);
+      } catch (e) {
+        throw new Error(
+          `Failed to parse snapshot.json for migration "${folderName}": ${e instanceof Error ? e.message : e}`
+        );
+      }
     } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (fs.existsSync(snapshotJsonPath)) {
const json = fs.readFileSync(snapshotJsonPath, "utf-8");
snapshots.push(JSON.parse(json) as Snapshot);
} else {
// No existing snapshot — use current DMMF with this version number
snapshots.push({ ...currentSnapshot, version: i + 1 });
}
if (fs.existsSync(snapshotJsonPath)) {
const json = fs.readFileSync(snapshotJsonPath, "utf-8");
try {
snapshots.push(JSON.parse(json) as Snapshot);
} catch (e) {
throw new Error(
`Failed to parse snapshot.json for migration "${folderName}": ${e instanceof Error ? e.message : e}`
);
}
} else {
// No existing snapshot — use current DMMF with this version number
snapshots.push({ ...currentSnapshot, version: i + 1 });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/generator/src/fileCreators/migrations/create.ts` around lines 58 -
64, Wrap the existing read/parse logic that reads snapshotJsonPath and pushes
into snapshots in a try-catch around fs.readFileSync(...) and JSON.parse(...);
on JSON.parse failure catch the error, throw or surface a new error that
includes snapshotJsonPath and the original error message (or log it) so callers
see "corrupted snapshot.json at {snapshotJsonPath}: {originalError}" and avoid
an unhelpful stacktrace; keep the fallback branch that pushes {
...currentSnapshot, version: i + 1 } unchanged for the non-existent-file case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrations

1 participant