Skip to content

Implement ORM migrator#896

Merged
kibertoad merged 12 commits intomainfrom
feat/drizzle-migrator
Mar 28, 2026
Merged

Implement ORM migrator#896
kibertoad merged 12 commits intomainfrom
feat/drizzle-migrator

Conversation

@kibertoad
Copy link
Copy Markdown
Collaborator

Changes

Provide a clear path for migration from Prisma to Drizzle

Checklist

  • Apply one of following labels; major, minor, patch or skip-release
  • I've updated the documentation, or no changes were necessary
  • I've updated the tests, or no changes were necessary

AI Assistance Tracking

We're running a metric to understand where AI assists our engineering work. Please select exactly one of the options below:

Mark "Yes" if AI helped in any part of this work, for example: generating code, refactoring, debugging support,
explaining something, reviewing an idea, or suggesting an approach.

  • Yes, AI assisted with this PR
  • No, AI did not assist with this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: lokalise/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 01784329-fb96-4ce9-acc6-9e4ca69bdb6b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds MySQL support and introduces a new markMigrationsApplied feature to the Drizzle utilities library. The feature enables baseline population of Drizzle's migrations tracking table for existing databases by reading migration metadata from meta/_journal.json, computing SHA-256 hashes of migration SQL files, and inserting tracking records into the database. Changes include a new TypeScript module with core functions (computeMigrationHash, readMigrationJournal, readMigrationEntries, markMigrationsApplied), comprehensive test coverage with unit and integration tests for PostgreSQL and MySQL, test fixtures and infrastructure (Docker Compose MySQL service, environment configuration, dependencies), README documentation, and public API exports.

Sequence Diagram

sequenceDiagram
    actor Caller
    participant FileSystem as File System
    participant DB as Database
    
    Caller->>Caller: markMigrationsApplied(options)
    Caller->>FileSystem: Read meta/_journal.json
    FileSystem-->>Caller: Journal metadata + entries
    Caller->>FileSystem: Read migration SQL files<br/>(.sql for each entry)
    FileSystem-->>Caller: SQL content
    Caller->>Caller: Compute SHA-256 hash<br/>for each migration
    Caller->>DB: Query existing<br/>tracked migration hashes
    DB-->>Caller: Existing hashes
    Caller->>Caller: Determine missing<br/>migrations
    Caller->>DB: Insert missing migrations<br/>with timestamps
    DB-->>Caller: Insert complete
    Caller-->>Caller: Return result with<br/>applied/skipped counts
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete. It provides a brief purpose statement but lacks detailed explanation of changes, doesn't mark documentation/tests as updated despite substantial additions, and the checklist is partially unfilled despite clear documentation and test additions. Complete the description with a detailed 'Changes' section explaining the new markMigrationsApplied feature, and mark documentation/tests checkboxes as completed since README and comprehensive test suite were added.
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Implement ORM migrator' is somewhat vague and doesn't clearly convey the specific purpose of adding a utility to help migrate from Prisma to Drizzle. While it references a real component being added, it lacks clarity about the migration path context. Consider a more descriptive title like 'Add markMigrationsApplied utility for Prisma-to-Drizzle migration' or 'Implement baseline migration tracker for Drizzle migrations' to better reflect the actual change.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/drizzle-migrator

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

Copy link
Copy Markdown

@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: 3

🧹 Nitpick comments (6)
packages/app/drizzle-utils/src/markMigrationsApplied.ts (3)

96-101: Identifier quoting does not escape special characters.

The quoteIdentifier function wraps identifiers in quotes but doesn't escape embedded quote characters. If migrationsTable or migrationsSchema contain backticks (MySQL) or double quotes (PostgreSQL), it could break the SQL or enable injection.

While this is a migration utility (not a web API) and input comes from trusted configuration, consider escaping for defense in depth.

🛡️ Suggested defensive fix
 function quoteIdentifier(name: string, dialect: Dialect): string {
   if (dialect === 'mysql') {
-    return `\`${name}\``
+    return `\`${name.replace(/`/g, '``')}\``
   }
-  return `"${name}"`
+  return `"${name.replace(/"/g, '""')}"`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/drizzle-utils/src/markMigrationsApplied.ts` around lines 96 -
101, The quoteIdentifier function currently wraps names but doesn't escape
embedded quote characters, so update quoteIdentifier(name: string, dialect:
Dialect) to first replace any internal quote characters with the SQL-appropriate
escaped form (for mysql replace backtick ` with double backticks ``; for
postgres/other dialects replace double-quote " with two double-quotes ""), then
wrap the result in the chosen quote character; ensure callers that pass
migrationsTable and migrationsSchema use this updated function so identifiers
containing quotes are safely escaped.

176-179: Journal file is read twice.

readMigrationJournal is called at line 177, then readMigrationEntries internally calls it again at line 179. Consider refactoring to pass the already-parsed journal to avoid redundant file I/O.

♻️ Suggested optimization

Add an internal helper that accepts a pre-parsed journal:

function readMigrationEntriesFromJournal(
  folder: string,
  journal: MigrationJournal,
): MigrationEntry[] {
  return journal.entries.map((entry) => {
    const sqlPath = join(folder, `${entry.tag}.sql`)
    const sqlContent = readFileSync(sqlPath, 'utf-8')
    return {
      tag: entry.tag,
      hash: computeMigrationHash(sqlContent),
      createdAt: entry.when,
    }
  })
}

Then use it in markMigrationsApplied:

   const journal = readMigrationJournal(folder)
   const dialect = resolveDialect(journal, options.dialect)
-  const entries = readMigrationEntries(folder)
+  const entries = readMigrationEntriesFromJournal(folder, journal)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/drizzle-utils/src/markMigrationsApplied.ts` around lines 176 -
179, The code currently calls readMigrationJournal(folder) in
markMigrationsApplied and then calls readMigrationEntries(folder), which itself
re-reads and parses the journal causing duplicate I/O; refactor by adding a
helper like readMigrationEntriesFromJournal(folder, journal) (or modify
readMigrationEntries to accept an optional MigrationJournal) and use the
already-parsed journal returned by readMigrationJournal inside
markMigrationsApplied to build entries (compute hash, read SQL files, map
createdAt/tag) so the journal file is read only once; update callers to pass the
pre-parsed journal and remove the redundant readMigrationJournal call inside
readMigrationEntries.

115-119: Synchronous file I/O may block the event loop.

readFileSync is used for reading the journal and SQL files. For typical migration scenarios with a small number of files, this is acceptable. However, if this utility might be used in contexts with many migrations or performance-sensitive environments, consider offering an async variant.

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

In `@packages/app/drizzle-utils/src/markMigrationsApplied.ts` around lines 115 -
119, The current readMigrationJournal function (and other places using
readFileSync to load SQL/journal files) performs synchronous file I/O which can
block the event loop; add an asynchronous variant (e.g.,
readMigrationJournalAsync) that uses fs.promises.readFile (or util.promisify)
and returns Promise<MigrationJournal>, and update any helpers that read SQL
files to provide async counterparts (e.g., readSqlFileAsync) so callers can opt
into non-blocking behavior while keeping the existing sync implementations for
backward compatibility; ensure the async functions preserve the same path
resolution logic (join(resolve(migrationsFolder), 'meta', '_journal.json')) and
JSON.parse behavior and export them alongside the current sync functions.
packages/app/drizzle-utils/README.md (1)

94-114: Add language specifier to fenced code block.

The workflow steps are in a fenced code block without a language specifier. Consider using text or plaintext as the language, or reformatting as a numbered markdown list for better accessibility and linting compliance.

📝 Suggested fix
-```
+```text
 1. npm install drizzle-orm drizzle-kit
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/drizzle-utils/README.md` around lines 94 - 114, The fenced code
block containing the numbered Drizzle workflow (starting with "1. npm install
drizzle-orm drizzle-kit") lacks a language specifier; update that
triple-backtick block to include a language such as text or plaintext (e.g.,
```text) or alternatively convert the block into a proper Markdown numbered list
so linters and accessibility tools recognize it; ensure the visible steps (npm
install..., Create your Drizzle schema, npx drizzle-kit generate, Run
markMigrationsApplied(...), npx drizzle-kit migrate, Remove the old ORM...)
remain unchanged in content.
packages/app/drizzle-utils/src/markMigrationsApplied.test.ts (2)

280-290: Consider using afterEach for MySQL connection cleanup.

The current pattern closes connection inside individual tests (lines 311, 339), which works but is inconsistent. If a test fails before calling connection.end(), the connection may leak. Consider moving cleanup to afterEach for robustness.

♻️ Suggested refactor
   beforeEach(async () => {
     connection = await mysql.createConnection(getMysqlDatabaseUrl())
     await connection.execute(`DROP TABLE IF EXISTS \`${testTable}\``)
   })

+  afterEach(async () => {
+    if (connection) {
+      await connection.end()
+    }
+  })
+
   afterAll(async () => {
     // Clean up - connection may have been closed by a test, create fresh one
     const conn = await mysql.createConnection(getMysqlDatabaseUrl())
     await conn.execute(`DROP TABLE IF EXISTS \`${testTable}\``)
     await conn.end()
   })

Then remove await connection.end() from individual tests (lines 311, 339).

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

In `@packages/app/drizzle-utils/src/markMigrationsApplied.test.ts` around lines
280 - 290, Tests currently rely on closing the MySQL connection inside
individual tests which can leak if a test fails; add an afterEach hook to always
drop the test table and call connection.end() (mirror the setup in beforeEach
that creates connection using mysql.createConnection and getMysqlDatabaseUrl()),
and remove the explicit await connection.end() calls from the individual tests
(references: beforeEach, afterAll, and the tests that currently call
connection.end()). This ensures connection cleanup runs after every test and
prevents leaks while preserving the existing beforeEach table-drop/setup logic.

165-183: Consider cleaning up the temporary directory.

The test creates a temporary directory but doesn't clean it up afterward. While the OS will eventually clear tmpdir(), explicitly removing it ensures a cleaner test environment.

♻️ Suggested improvement
+    const { mkdtempSync, writeFileSync, mkdirSync, rmSync } = await import('node:fs')
     const { tmpdir } = await import('node:os')
     const tempDir = mkdtempSync(join(tmpdir(), 'drizzle-test-'))
     mkdirSync(join(tempDir, 'meta'), { recursive: true })
     writeFileSync(
       join(tempDir, 'meta', '_journal.json'),
       JSON.stringify({ version: '7', dialect: 'postgresql', entries: [] }),
     )

     const result = await markMigrationsApplied({
       migrationsFolder: tempDir,
       executor: noopExecutor,
       dialect: 'postgresql',
     })

     expect(result).toEqual({ total: 0, applied: 0, skipped: 0, entries: [] })
+
+    // Cleanup
+    rmSync(tempDir, { recursive: true })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/drizzle-utils/src/markMigrationsApplied.test.ts` around lines
165 - 183, The test creates a temporary directory (tempDir via mkdtempSync) for
markMigrationsApplied but never removes it; update the test to remove the
tempDir after the assertion (e.g., in a finally block or afterEach) using
fs.rmSync or fs.promises.rm with recursive: true and force: true so the created
meta/_journal.json and tempDir are cleaned up; reference the tempDir variable
and the markMigrationsApplied invocation (and noopExecutor) when adding the
cleanup to ensure the temp files are deleted regardless of test success or
failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app/drizzle-utils/package.json`:
- Line 30: The pretest:ci npm script uses the Docker Compose flag --quiet-pull
which requires Docker Compose v2.0.0+ (or legacy v1.20.0+); update the
"pretest:ci" script to remove or make the flag conditional so it doesn't break
older CI runners: either drop --quiet-pull from the "pretest:ci" entry in
package.json, or replace the single command with a small shell-safe sequence
that checks docker compose version (or attempts a plain docker compose pull/up
fallback) before running docker compose up -d --wait; ensure references to
"pretest:ci" and the --quiet-pull token are updated accordingly.

In `@packages/app/drizzle-utils/src/markMigrationsApplied.test.ts`:
- Around line 43-45: The TS2532 arises because TypeScript can't guarantee
journal.entries is defined after expect(journal.entries).toHaveLength(2); fix by
asserting or guarding before indexing: either add an explicit non-null assertion
when accessing items (e.g. use journal.entries![0] and journal.entries![1]) or
add an assertion like expect(journal.entries).toBeDefined() /
expect(journal.entries).not.toBeUndefined() before the indexed expects so the
type narrows; update the assertions referencing journal.entries in
markMigrationsApplied.test.ts accordingly.

In `@packages/app/drizzle-utils/test/getMysqlDatabaseUrl.ts`:
- Around line 4-10: The current check uses
process.env[MYSQL_DATABASE_URL_ENVVAR] and throws only when it's undefined,
allowing whitespace-only values to slip through; update the logic in
getMysqlDatabaseUrl (use the MYSQL_DATABASE_URL_ENVVAR and databaseUrl
variables) to trim the environment value first (e.g., const trimmed =
(process.env[MYSQL_DATABASE_URL_ENVVAR] ?? "").trim()), then throw an Error if
trimmed is empty, and finally return the trimmed value so blank-but-present DB
URLs are rejected early with a clear message.

---

Nitpick comments:
In `@packages/app/drizzle-utils/README.md`:
- Around line 94-114: The fenced code block containing the numbered Drizzle
workflow (starting with "1. npm install drizzle-orm drizzle-kit") lacks a
language specifier; update that triple-backtick block to include a language such
as text or plaintext (e.g., ```text) or alternatively convert the block into a
proper Markdown numbered list so linters and accessibility tools recognize it;
ensure the visible steps (npm install..., Create your Drizzle schema, npx
drizzle-kit generate, Run markMigrationsApplied(...), npx drizzle-kit migrate,
Remove the old ORM...) remain unchanged in content.

In `@packages/app/drizzle-utils/src/markMigrationsApplied.test.ts`:
- Around line 280-290: Tests currently rely on closing the MySQL connection
inside individual tests which can leak if a test fails; add an afterEach hook to
always drop the test table and call connection.end() (mirror the setup in
beforeEach that creates connection using mysql.createConnection and
getMysqlDatabaseUrl()), and remove the explicit await connection.end() calls
from the individual tests (references: beforeEach, afterAll, and the tests that
currently call connection.end()). This ensures connection cleanup runs after
every test and prevents leaks while preserving the existing beforeEach
table-drop/setup logic.
- Around line 165-183: The test creates a temporary directory (tempDir via
mkdtempSync) for markMigrationsApplied but never removes it; update the test to
remove the tempDir after the assertion (e.g., in a finally block or afterEach)
using fs.rmSync or fs.promises.rm with recursive: true and force: true so the
created meta/_journal.json and tempDir are cleaned up; reference the tempDir
variable and the markMigrationsApplied invocation (and noopExecutor) when adding
the cleanup to ensure the temp files are deleted regardless of test success or
failure.

In `@packages/app/drizzle-utils/src/markMigrationsApplied.ts`:
- Around line 96-101: The quoteIdentifier function currently wraps names but
doesn't escape embedded quote characters, so update quoteIdentifier(name:
string, dialect: Dialect) to first replace any internal quote characters with
the SQL-appropriate escaped form (for mysql replace backtick ` with double
backticks ``; for postgres/other dialects replace double-quote " with two
double-quotes ""), then wrap the result in the chosen quote character; ensure
callers that pass migrationsTable and migrationsSchema use this updated function
so identifiers containing quotes are safely escaped.
- Around line 176-179: The code currently calls readMigrationJournal(folder) in
markMigrationsApplied and then calls readMigrationEntries(folder), which itself
re-reads and parses the journal causing duplicate I/O; refactor by adding a
helper like readMigrationEntriesFromJournal(folder, journal) (or modify
readMigrationEntries to accept an optional MigrationJournal) and use the
already-parsed journal returned by readMigrationJournal inside
markMigrationsApplied to build entries (compute hash, read SQL files, map
createdAt/tag) so the journal file is read only once; update callers to pass the
pre-parsed journal and remove the redundant readMigrationJournal call inside
readMigrationEntries.
- Around line 115-119: The current readMigrationJournal function (and other
places using readFileSync to load SQL/journal files) performs synchronous file
I/O which can block the event loop; add an asynchronous variant (e.g.,
readMigrationJournalAsync) that uses fs.promises.readFile (or util.promisify)
and returns Promise<MigrationJournal>, and update any helpers that read SQL
files to provide async counterparts (e.g., readSqlFileAsync) so callers can opt
into non-blocking behavior while keeping the existing sync implementations for
backward compatibility; ensure the async functions preserve the same path
resolution logic (join(resolve(migrationsFolder), 'meta', '_journal.json')) and
JSON.parse behavior and export them alongside the current sync functions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: lokalise/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: acfa4b10-bdfd-4197-831f-6583e3146b38

📥 Commits

Reviewing files that changed from the base of the PR and between 42609d3 and 64e8cf4.

📒 Files selected for processing (13)
  • packages/app/drizzle-utils/.env.test
  • packages/app/drizzle-utils/README.md
  • packages/app/drizzle-utils/docker-compose.yml
  • packages/app/drizzle-utils/package.json
  • packages/app/drizzle-utils/src/index.ts
  • packages/app/drizzle-utils/src/markMigrationsApplied.test.ts
  • packages/app/drizzle-utils/src/markMigrationsApplied.ts
  • packages/app/drizzle-utils/test/fixtures/migrations-mysql/0000_init.sql
  • packages/app/drizzle-utils/test/fixtures/migrations-mysql/meta/_journal.json
  • packages/app/drizzle-utils/test/fixtures/migrations/0000_init.sql
  • packages/app/drizzle-utils/test/fixtures/migrations/0001_add_users.sql
  • packages/app/drizzle-utils/test/fixtures/migrations/meta/_journal.json
  • packages/app/drizzle-utils/test/getMysqlDatabaseUrl.ts

Copy link
Copy Markdown
Collaborator

@CarlosGamero CarlosGamero left a comment

Choose a reason for hiding this comment

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

Looks great! just left some small comments 🚀 The only relevant one is about Prisma updatedAt prop

@kibertoad kibertoad merged commit 39c0e4a into main Mar 28, 2026
6 checks passed
@kibertoad kibertoad deleted the feat/drizzle-migrator branch March 28, 2026 13:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants