📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-06-29 16:09:10 +00:00
parent 90c6c04c3f
commit f7f50d9fea
314 changed files with 31913 additions and 351 deletions
@@ -0,0 +1,179 @@
---
name: drizzle-migration-conflict
description: "Diagnose, repair, and prevent Drizzle Kit migration conflicts involving generated SQL, snapshots, journals, merge queues, and team workflows."
category: databases
risk: critical
source: community
source_repo: chaunsin/agent-skills
source_type: community
date_added: "2026-06-29"
author: chaunsin
tags: [drizzle, migrations, database, ci, merge-conflicts]
tools: [git, python, rg]
license: "Apache-2.0"
license_source: "https://github.com/chaunsin/agent-skills/blob/master/LICENSE"
---
# Drizzle Migration Conflict
Use this skill to help a user diagnose, repair, and prevent Drizzle Kit migration conflicts in a
multi-developer repository. Drizzle migrations encode both SQL and migration snapshots, so the safe
answer depends on the current migration directory shape, the Drizzle Kit version, and the git state.
## When to Use This Skill
- Use when Drizzle migration files, `_journal.json`, or `snapshot.json` conflict after a pull, merge, rebase, or PR update.
- Use when `drizzle-kit check` reports non-commutative migrations or migration folder conflicts.
- Use when a team wants a safe repair flow for generated Drizzle migrations after schema changes converge.
- Use when designing CI or merge-queue policy to prevent repeated Drizzle migration conflicts.
## Safety rules
- Start in read-only diagnosis mode unless the user explicitly asks to fix files.
- Do not run `drizzle-kit migrate`, `drizzle-kit push`, database seed scripts, or any command that
connects to a live database unless the user explicitly requests it and the target is clear.
- Treat `drizzle-kit check`, project typechecks, and tests as command execution that may load project
config, environment variables, or scripts. Inspect scripts/config first, and require an explicit
non-production or disposable target before any DB-backed validation.
- Do not delete migration files, rewrite `_journal.json`, or run `git checkout --ours`,
`git checkout --theirs`, `git restore`, or `rm` unless the user has confirmed the exact side and
files to change.
- Do not recommend `drizzle-kit push` as the production solution for migration conflicts; it skips
the auditable migration history that teams need.
- Treat `--ignore-conflicts` as an exception for a known false positive, not as the normal fix.
- Preserve schema source code changes unless the user explicitly asks to discard them. Conflict
repair normally discards generated migrations and regenerates them from the merged schema.
- If `ours` and `theirs` could mean different branches depending on merge direction, ask the user to
identify the parent branch before suggesting checkout commands.
## Required references
- Read `references/sources.md` when the answer depends on current Drizzle behavior, official
guidance, or one of the preserved external links.
- Read `references/conflict-resolution.md` before recommending a repair flow.
- Read `references/ci-policy.md` before proposing CI, merge queue, or team workflow changes.
- Read `references/report-template.md` before writing a diagnostic report.
## Source references
The full list of official docs, Drizzle GitHub discussions, community scripts, and merge-queue
references lives in `references/sources.md` with trust levels and caveats. Read that file whenever
the answer depends on current Drizzle behavior. Re-verify the official docs and the most relevant
discussion when the project's `drizzle-kit` major version changes, since migration internals
(snapshot format, journal shape, `drizzle-kit check` semantics) have shifted between releases.
## Mode selection
Classify the task first:
1. **Diagnose** - The user has a conflict or failed `drizzle-kit check` and wants to understand it.
2. **Repair** - The user explicitly asks to fix or regenerate migration files.
3. **CI hardening** - The user wants to prevent future conflicts in PRs or merge queues.
4. **Explain** - The user wants a conceptual answer or a team playbook.
When the mode is not explicit, choose Diagnose.
Each mode unlocks a specific set of actions. Do not cross these boundaries without an explicit upgrade:
- **Diagnose** - read-only only. Run `git status`, `git ls-files -u`, the helper script, and file
inspection. Do not run `drizzle-kit check`, typechecks, tests, or any write command. Report
findings and the proposed repair path, but do not execute it.
- **Repair** - adds file writes and `drizzle-kit generate`/`check` execution, each gated by the
Safety rules and explicit confirmation of the exact files and side (`ours`/`theirs`) to change.
- **CI hardening** - adds proposing or editing CI/workflow files. Do not run migration commands
against the user's database to validate the workflow; validate the workflow syntax and logic only.
- **Explain** - conceptual only. No commands against the repo beyond optional read-only inspection.
## Repository discovery
Collect repo facts before giving commands:
```bash
git status --short
git rev-parse --show-toplevel
git rev-parse --abbrev-ref HEAD
git ls-files -u
rg --files -g 'drizzle.config.*' -g 'package.json' -g 'pnpm-lock.yaml' -g 'yarn.lock' -g 'package-lock.json'
```
Then inspect the relevant files:
- `drizzle.config.*` for `out`, `schema`, dialect, and config shape.
- `package.json` scripts for the project-approved `generate`, `check`, and `migrate` commands.
- `package.json` dependencies or lockfile snippets for `drizzle-kit` and `drizzle-orm` versions.
- The migration output directory, either from config or common names like `drizzle/`, `migrations/`,
or `src/db/migrations/`.
If this skill's helper script is available, run it in read-only mode:
```bash
python3 <skill-dir>/scripts/check_drizzle_migrations.py --root .
```
Resolve `<skill-dir>` to the installed skill directory before running. Check these locations in order
and use the first that contains `scripts/check_drizzle_migrations.py`:
1. The target repository's vendored copy: `<repo-root>/skills/drizzle-migration-conflict`.
2. The Claude Code skills directory: `~/.claude/skills/drizzle-migration-conflict`.
3. Any other install location reported by the user's environment.
If none of these resolve, fall back to the manual `git`/`rg` inspection commands above and tell the
user the helper script was not found. Use `--config <file>` and `--migrations-dir <dir>` when the
project has multiple Drizzle configs or outputs. The script never connects to a database and never
writes files; it only reads migration directories and reports structural issues.
## Migration structure decision
Identify the structure before proposing a fix:
- **Legacy structure**: `<out>/meta/_journal.json`, `<out>/meta/*_snapshot.json`, and root-level
migration SQL files such as `<out>/0003_name.sql`.
- **Folder-based structure**: each migration is a directory containing `migration.sql` and
`snapshot.json`.
- **Unknown or mixed structure**: stop and report ambiguity. Do not guess a destructive repair.
## Recommended repair principles
- Resolve schema source conflicts first. The regenerated migration must reflect the merged schema,
not one side's stale snapshot.
- Treat the parent or target branch migration history as the source of truth when repairing a feature
branch after updating from that branch.
- Prefer discarding and regenerating generated migration artifacts over hand-editing journal or
snapshot files.
- After regeneration, validate in tiers: database-free structural checks first; then `drizzle-kit
check` only after confirming its config/env cannot point at production; then project tests only
after inspecting the scripts and any database targets.
- If the user asks to apply changes, state exactly which files will be changed before performing the
write.
## Output rules
- Use the user's language when practical, but keep command snippets and file paths literal.
- State the detected migration structure and selected mode.
- Separate confirmed conflicts from assumptions and missing evidence.
- Give a safe default path first, then optional automation or CI hardening.
- For destructive steps, label them as "requires confirmation" and explain what will be lost.
- Never echo secrets. When inspecting `drizzle.config.*`, `.env`, or environment variables, do not
include database URLs, passwords, tokens, or connection strings in the report. Reference them as
`<redacted>` or describe only whether they point at a production-like target.
- Use the conclusion values from `references/report-template.md` for diagnostic reports:
`NO_CONFLICT_FOUND`, `SAFE_TO_REGENERATE`, `NEEDS_USER_CONFIRMATION`, or `BLOCKED_BY_AMBIGUITY`.
## Limitations
- This skill cannot guarantee that a regenerated migration is production-safe without review against the target database state and deployment process.
- It does not run DB-backed migration commands unless the user explicitly confirms the target and the command.
- It is focused on Drizzle Kit migration conflicts, not general schema design or application-query optimization.
## Test prompts
Use these prompts to validate the skill behavior:
- "My Drizzle `_journal.json` and `0003_snapshot.json` conflict during merge. Tell me what to do."
- "We upgraded to the migration folder layout and `drizzle-kit check` reports a non-commutative conflict."
- "Design CI so our team stops merging broken Drizzle migrations."
- "Can I solve this production Drizzle migration conflict with `drizzle-kit push`?"
- "Use the links in the skill to re-check the current official Drizzle migration conflict guidance."
- "We're halfway through moving from the legacy flat layout to folder-based migrations. How do we handle a conflict during the transition?"
- "Our `drizzle.config.ts` sets `out` from `process.env.MIGRATIONS_DIR`, and the helper says no out directory was found. What now?"
- "`drizzle-kit check` keeps failing on a migration we know commutes. Can we just always pass `--ignore-conflicts`?"
@@ -0,0 +1,87 @@
# CI and Team Policy
Use this reference when the user wants to prevent Drizzle migration conflicts in pull requests,
protected branches, or GitHub merge queues.
## Recommended layers
1. **Local developer habit**
- Pull or merge the parent branch before generating a migration.
- Generate migrations once schema source conflicts are resolved.
- Run `drizzle-kit check` only after confirming its config/env do not target production.
2. **Pull request check**
- Run the project's normal static checks.
- Run `drizzle-kit check` or the package script that wraps it with explicit non-production config.
- Run the read-only helper script to catch legacy journal/snapshot mismatches.
3. **Merge queue check**
- If GitHub merge queue is enabled, run the same check on `merge_group` events.
- Do not assume a successful PR check means the queued merge result is still conflict-free.
## GitHub Actions skeleton
Adapt package manager, config path, migration directory, and script location to the target
repository. The helper script must be vendored or copied into the repository before CI can run it.
Never point CI migration checks at production credentials.
```yaml
name: drizzle-migration-check
on:
pull_request:
merge_group:
jobs:
drizzle-migration-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- uses: pnpm/action-setup@v4
with:
version: 9
- run: pnpm install --frozen-lockfile
# Run only with a non-production or disposable DATABASE_URL if the config requires one.
- run: pnpm exec drizzle-kit check --config drizzle.config.ts
# Example assumes the helper was copied to scripts/check_drizzle_migrations.py.
- run: python3 scripts/check_drizzle_migrations.py --root . --config drizzle.config.ts --migrations-dir drizzle
```
If the repository does not vendor this skill, copy the helper script into the repo or run an
equivalent read-only check from the CI tooling repository. In multi-config repositories, pass the
same config and matching migration directory to both Drizzle Kit and the helper script.
The helper script exits with: `0` when all checked directories are clean, `1` when any error or
warning issue is found, and `2` when no migration directory was discovered at all. A CI step that
runs the script should fail the job on a non-zero exit, but treat exit `2` as "nothing to check"
only if the repo is expected to have no Drizzle migrations; otherwise exit `2` usually means
detection missed the migration directory and the config should be passed explicitly.
## What merge queue does and does not solve
Merge queue can serialize the final merge order and test a temporary merge result. It does not
rewrite Drizzle migrations, re-run `drizzle-kit generate`, or choose which branch's snapshots are
correct. The check should fail when generated migration history is inconsistent, then the developer
updates the branch and regenerates migrations.
## Policy recommendations
- Require one migration-generation point per PR after schema conflicts are resolved.
- Treat migration artifacts as generated but reviewable files: do not silently rewrite them in CI.
- Require `drizzle-kit check` or an equivalent conflict check before merge.
- In legacy projects, reject duplicate migration numbers and journal/snapshot drift.
- In folder-based projects, reject incomplete migration directories and failed commutativity checks.
- Keep production migration execution separate from PR validation.
## When CI should fail
Fail the job when any of these are true:
- `_journal.json` contains duplicate `idx` or `tag` values.
- A journal entry references a missing SQL file or snapshot.
- Root SQL or snapshot files exist but are not referenced by the journal in a legacy output.
- Migration files contain Git conflict markers.
- A folder-based migration directory is missing `migration.sql` or `snapshot.json`.
- `drizzle-kit check` reports a non-commutative migration conflict.
@@ -0,0 +1,163 @@
# Conflict Resolution Playbook
Use this playbook after collecting repo facts. The goal is to preserve schema intent while replacing
stale generated migration artifacts with a migration generated from the merged schema.
## Decision tree
1. Is the repository currently in a merge or rebase?
- Check `git status --short` and `git ls-files -u`.
- If yes, identify whether the user is merging the parent branch into a feature branch, rebasing a
feature branch, or merging a feature branch into the parent branch.
2. Which migration structure is present?
- Legacy: `meta/_journal.json`, `meta/*_snapshot.json`, root SQL files.
- Folder-based: migration directories with `migration.sql` and `snapshot.json`.
- Mixed or unknown: stop and ask for the intended migration output path.
- Transitioning (legacy artifacts plus a partial move to folder-based): do not repair until the
user confirms the target structure. Treat the legacy artifacts and the folder-based artifacts
as one logical history only after the intended end state is clear; otherwise a repair could
discard the wrong side.
3. Are schema source files already resolved?
- If not, resolve those first or tell the user the migration cannot be regenerated safely yet.
4. Is the user asking for diagnosis or repair?
- Diagnosis stays read-only.
- Repair can include file changes only after the exact generated files to discard are understood.
## Read-only inspection commands
```bash
git status --short
git ls-files -u
rg --files -g 'drizzle.config.*' -g 'package.json'
rg -n "drizzle-kit|drizzle-orm|db:generate|db:check|migrate" package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null
python3 <skill-dir>/scripts/check_drizzle_migrations.py --root .
```
If `rg` is not available, use `find` and `grep` equivalents. Resolve `<skill-dir>` to the installed
skill directory before running the helper. Check in order and use the first match that contains
`scripts/check_drizzle_migrations.py`: the target repo's vendored
`skills/drizzle-migration-conflict`, then `~/.claude/skills/drizzle-migration-conflict`, then any
user-reported install location. If none resolve, fall back to the `git`/`rg` inspection commands
above and tell the user the helper was not found.
## Legacy structure repair
Legacy Drizzle output usually looks like this:
```text
drizzle/
0000_initial.sql
0001_add_user.sql
meta/
_journal.json
0000_snapshot.json
0001_snapshot.json
```
Safe flow for a feature branch updated from the parent branch:
1. Resolve schema source conflicts first.
2. Keep the parent branch's migration history as the baseline.
3. Discard generated migration files created on the feature branch after it diverged from the parent
branch.
4. Re-run the project-approved `drizzle-kit generate` script from `package.json`.
5. Validate the regenerated history.
Do not hand-edit `_journal.json` or snapshot JSON unless the user explicitly asks for an emergency
manual repair and accepts the risk. The next generated migration depends on those snapshots.
### Ours/theirs warning
`ours` and `theirs` change meaning with merge direction:
| Situation | `ours` usually means | `theirs` usually means | Safe guidance |
| --- | --- | --- | --- |
| On feature branch, merging parent branch into it | current feature branch | parent branch being merged in | Parent branch is often `theirs`, but verify before checkout. |
| On parent branch, merging feature branch into it | current parent branch | feature branch | Parent branch is often `ours`, but verify before checkout. |
| Rebase | meaning can be unintuitive | meaning can be unintuitive | Avoid shorthand; use explicit branch/path restore if possible. |
When in doubt, ask which branch should be the migration-history source of truth. Do not guess.
## Folder-based structure repair
Folder-based Drizzle output usually looks like this:
```text
drizzle/
20260618120000_add_user/
migration.sql
snapshot.json
```
Safe flow:
1. Inspect the Drizzle config and env first, then run `drizzle-kit check` or the project script
wrapping it only with a non-production target.
2. If it reports a non-commutative migration conflict, identify the conflicting migration and any
later migrations based on it.
3. Remove or regenerate only the generated migration artifacts that are downstream of the conflict,
after user confirmation.
4. Re-run `drizzle-kit generate` from the merged schema.
5. Re-run the helper script, and re-run `drizzle-kit check` only after confirming the config/env
target is still non-production.
Use `--ignore-conflicts` only for a known false positive after reviewing why the migrations commute
or why the check is wrong. Include that decision in the report.
## Validation after regeneration
Run validation in tiers so the agent does not accidentally touch a live database or run arbitrary
project scripts.
### Database-free checks
```bash
python3 <skill-dir>/scripts/check_drizzle_migrations.py --root . --migrations-dir <migration-dir>
```
### Loads project config or environment
Run `drizzle-kit check` only after inspecting `drizzle.config.*`, package scripts, and relevant env
variables. Confirm that any database URL or credentials point to a non-production or disposable
target before executing it. Work through this checklist before running the command:
1. Read `drizzle.config.*` and note any `url`, `dbCredentials`, `credentials`, or connection fields.
Determine whether they are literal, read from `process.env`, or loaded via `dotenv`.
2. Identify which env vars feed those fields (common names: `DATABASE_URL`, `DB_URL`,
`POSTGRES_URL`, `DRIZZLE_DATABASE_URL`). Check `.env`, `.env.local`, and the package script's
environment for their values without echoing secrets.
3. If a value points at a production host (named `prod`/`production`, a managed cluster endpoint,
or a host the user identifies as live), stop and ask for a disposable target. Do not run the check.
4. If `drizzle-kit check` needs a real connection for the configured dialect, prefer overriding the
URL inline with a disposable/local database, or use a config that disables connection (some
dialects allow a schema-only check). If neither is possible, fall back to the database-free
helper script and report that `drizzle-kit check` could not be run safely.
5. Only after the target is confirmed non-production, run the project-approved check command.
```bash
# Project script names vary; inspect package.json first.
# Override with a disposable DATABASE_URL only if the config requires a connection.
DATABASE_URL=postgres://localhost/disposable pnpm exec drizzle-kit check --config <drizzle-config>
```
### Project tests
Run typechecks or tests only after inspecting the script definitions. Tests may run migrations,
connect to databases, mutate fixtures, or start services.
```bash
pnpm typecheck
pnpm test
```
Avoid live database commands unless the user names a disposable database or explicitly requests a
migration run.
## Anti-patterns
- Running `drizzle-kit push` to bypass migration history in production.
- Keeping both sides' generated migrations and manually renumbering files without regenerating from
the merged schema.
- Resolving `_journal.json` by accepting both sides without verifying SQL and snapshot pairs.
- Using `git checkout --theirs drizzle/` without understanding merge direction.
- Ignoring `drizzle-kit check` with `--ignore-conflicts` as the default team workflow.
@@ -0,0 +1,69 @@
# Report Template
Use this template for diagnosis and repair recommendations. Keep reports short and evidence-based.
## Conclusion values
- `NO_CONFLICT_FOUND` - No migration conflict or structural inconsistency was found from available
evidence.
- `SAFE_TO_REGENERATE` - The conflict is understood, schema source is resolved, and the recommended
next step is to discard generated artifacts and regenerate migrations.
- `NEEDS_USER_CONFIRMATION` - A repair path exists, but a destructive step or branch-side decision
requires confirmation.
- `BLOCKED_BY_AMBIGUITY` - The migration structure, source-of-truth branch, schema state, or
migration directory cannot be determined safely.
## Template
````markdown
# Drizzle Migration Conflict Report
Conclusion: <NO_CONFLICT_FOUND | SAFE_TO_REGENERATE | NEEDS_USER_CONFIRMATION | BLOCKED_BY_AMBIGUITY>
Mode: <diagnose | repair | ci-hardening | explain>
## Detected Structure
- Migration directory: `<path>`
- Structure: <legacy | folder-based | mixed | unknown>
- Drizzle Kit version: <version or unable to verify>
- Git state: <clean | dirty | active merge | active rebase | unable to verify>
## Conflict State
- <confirmed conflict or inconsistency with file paths>
- <journal/snapshot/SQL mismatch, non-commutative check, or conflict marker evidence>
## Recommended Path
- <safe next step>
- <why this path preserves schema intent and migration history>
## Commands
```bash
# Read-only commands first.
<commands>
# Destructive commands only if confirmed by the user.
<commands requiring confirmation>
```
## Files At Risk
- `<path>` - <why it may be discarded or regenerated>
## Validation
- <drizzle-kit check or project script>
- <helper script command>
- <typecheck/test command if relevant>
## Unable To Verify
- <missing version, unavailable branch, unknown migration path, or external docs not refreshed>
````
## Reporting rules
- Put destructive commands in a clearly labeled block.
- Do not output `--ours` or `--theirs` commands unless the merge/rebase direction, source-of-truth
branch, and exact file paths are confirmed. Otherwise use `BLOCKED_BY_AMBIGUITY`.
- If the project has multiple Drizzle configs, report each output independently.
- If no conflict is found but the worktree is dirty, state that uncommitted files were not repaired.
- Do not include clean checklist categories that are irrelevant to the user's conflict.
- Redact secrets. Never include database URLs, passwords, tokens, or connection strings in the
report. When a config or env value matters, describe only whether it points at a production-like
target and write the value as `<redacted>`.
@@ -0,0 +1,51 @@
# Source References
Last verified: 2026-06-18.
Use this file when an answer depends on upstream Drizzle behavior, community scripts, or CI platform
behavior. Drizzle Kit migration internals can change, so prefer current official docs and the
project's installed `drizzle-kit` version over memory when resolving a real conflict.
## Official and semi-official Drizzle sources
| Source | Link | Use | Trust level |
| --- | --- | --- | --- |
| Discussion 1104 | https://github.com/drizzle-team/drizzle-orm/discussions/1104 | Original team-collaboration conflict thread for legacy `_journal.json` and snapshot conflicts. Useful for understanding why parallel generated migrations diverge. | Drizzle GitHub discussion; useful but may include outdated comments. |
| Discussion 2832 | https://github.com/drizzle-team/drizzle-orm/discussions/2832 | Migration folder structure redesign and reasoning. Use to understand why the old flat structure is git-hostile. | Drizzle GitHub discussion; design context may predate current release behavior. |
| Discussion 5005 | https://github.com/drizzle-team/drizzle-orm/discussions/5005 | Commutative migration checking, `drizzle-kit check`, and conflict behavior in newer Drizzle Kit versions. | High value for current direction; verify against installed version. |
| Discussion 5581 | https://github.com/drizzle-team/drizzle-orm/discussions/5581 | Practical parent-branch-as-source-of-truth repair workflow. | Community workflow; good playbook, still verify against repo state. |
| Generate docs | https://orm.drizzle.team/docs/drizzle-kit-generate | How Drizzle Kit derives migrations from schema and snapshots. | Official docs. |
| Check docs | https://orm.drizzle.team/docs/drizzle-kit-check | Migration consistency checking for team workflows. | Official docs. |
| Migration overview | https://orm.drizzle.team/docs/migrations | General migration concepts and current official migration overview. | Official docs. |
## Community scripts
These scripts are reference material only. Do not copy their destructive behavior into a generic
agent workflow without dry-run mode and explicit user confirmation.
| Source | Link | Use | Caveat |
| --- | --- | --- | --- |
| Legacy undo script | https://gist.github.com/anthonyjoeseph/102c0e3ea8496fe111029a8b8a95cc3a | Shows a merge-time undo workflow for legacy Drizzle migration artifacts. | Assumes legacy structure and uses git/file operations that can discard local generated files. |
| Legacy repair script | https://gist.github.com/anthonyjoeseph/6b99beb34d494acd1dfc83a192ed9388 | Detects duplicate legacy migration numbers and can repair by removing orphaned generated files. | `FORCE_FIX` is destructive; adapt only the read-only checks unless the user confirms. |
| Earlier repair variant | https://gist.github.com/gburtini/7e34842c567dd80ee834de74e7b79edd | Useful for historical context and comparing conflict-detection logic. | Earlier variant had caveats fixed by later forks; do not rely on it alone. |
## CI and merge queue sources
| Source | Link | Use | Caveat |
| --- | --- | --- | --- |
| GitHub merge queue docs | https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue | Explains merge queue behavior and why required checks must also run for `merge_group` events. | Merge queue serializes merging; it does not regenerate Drizzle migrations by itself. |
## Version-sensitive guidance
Before giving high-confidence advice for a live repository:
1. Check the local `drizzle-kit` version from `package.json` and the lockfile first.
2. Check whether the migration output uses the legacy flat structure or the folder-based structure.
3. If command execution is acceptable and dependencies are already installed, use a local-only
package-manager command. Prefer `pnpm exec drizzle-kit --version`,
`yarn exec drizzle-kit --version`, or `npm exec --no-install drizzle-kit -- --version`. Do not
use plain `npx` for version probing because it can download or resolve a different package.
4. If online browsing is available and the user asks for current guidance, re-open the official docs
and the discussion most relevant to the installed version.
5. If a local result conflicts with these sources, trust the local repository state and report the
mismatch explicitly.
@@ -0,0 +1,721 @@
#!/usr/bin/env python3
"""Read-only structural checks for Drizzle migration outputs.
This helper never connects to a database, never imports project code, and never writes
files. It only reads migration directories, parses `_journal.json`/snapshot JSON, and
reports structural inconsistencies.
Exit codes:
0 All checked migration directories are clean (no errors or warnings).
1 At least one error or warning issue was found.
2 No migration directories were discovered (pass --config or --migrations-dir).
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Iterable
CONFIG_NAME_PATTERN = re.compile(r"^drizzle(?:[.-].+)?\.config\.(?:ts|js|mjs|cjs|mts|cts)$")
COMMON_DIRS = (
"drizzle",
"migrations",
"src/db/migrations",
"db/migrations",
)
SKIP_DIR_NAMES = {
".git",
".hg",
".svn",
"node_modules",
".next",
".nuxt",
"dist",
"build",
"coverage",
"target",
"vendor",
"__pycache__",
}
CONFLICT_MARKERS = ("<<<<<<<", "=======", ">>>>>>>")
TEXT_SUFFIXES = {".sql", ".json", ".ts", ".js", ".mts", ".mjs", ".cts", ".cjs"}
@dataclass
class Issue:
severity: str
code: str
path: str
message: str
@dataclass
class DirectoryReport:
path: str
structure: str
issues: list[Issue]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Check Drizzle migration directories for read-only structural conflicts."
)
parser.add_argument("--root", default=".", help="Repository root or package root. Default: .")
parser.add_argument(
"--config",
action="append",
default=[],
help="Drizzle config file to inspect for an out directory. May be passed more than once.",
)
parser.add_argument(
"--migrations-dir",
action="append",
default=[],
help="Migration output directory. May be passed more than once.",
)
parser.add_argument(
"--allow-outside-root",
action="store_true",
help=(
"Allow explicit config/out or migration directories outside --root. "
"Only use when the user has named the exact path and you have confirmed it "
"contains no sensitive content; the script will still skip known vendored "
"directories but cannot guarantee what lives under an arbitrary root."
),
)
parser.add_argument("--json", action="store_true", help="Print JSON output.")
return parser.parse_args()
def strip_json_comments(text: str) -> str:
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
text = re.sub(r"(^|\s)//.*$", r"\1", text, flags=re.M)
return text
def read_json(path: Path) -> tuple[Any | None, str | None]:
try:
return json.loads(strip_json_comments(path.read_text(encoding="utf-8"))), None
except Exception as exc: # noqa: BLE001 - error text is reported to the caller.
return None, str(exc)
def path_in_root(path: Path, root: Path) -> bool:
try:
path.resolve().relative_to(root.resolve())
return True
except ValueError:
return False
def normalize_dir(root: Path, value: str) -> Path:
candidate = Path(value.strip())
if not candidate.is_absolute():
candidate = root / candidate
return candidate.resolve()
def relative(path: Path, root: Path) -> str:
try:
return str(path.relative_to(root))
except ValueError:
return str(path)
def make_issue(severity: str, code: str, path: Path | str, root: Path, message: str) -> Issue:
if isinstance(path, Path):
issue_path = relative(path, root)
else:
issue_path = path
return Issue(severity=severity, code=code, path=issue_path, message=message)
def add_issue(issues: list[Issue], severity: str, code: str, path: Path, root: Path, message: str) -> None:
issues.append(make_issue(severity, code, path, root, message))
def iter_config_files(
root: Path, explicit_configs: Iterable[str], allow_outside_root: bool
) -> tuple[list[Path], list[Issue]]:
issues: list[Issue] = []
configs: list[Path] = []
seen: set[Path] = set()
for value in explicit_configs:
path = normalize_dir(root, value)
if not allow_outside_root and not path_in_root(path, root):
issues.append(
make_issue(
"error",
"config-outside-root",
path,
root,
"Config path is outside --root. Pass --allow-outside-root only after verifying it is intended.",
)
)
continue
if not path.exists():
issues.append(make_issue("error", "missing-config", path, root, "Config file does not exist."))
continue
if path not in seen:
seen.add(path)
configs.append(path)
if explicit_configs:
return configs, issues
for current_root, dirnames, filenames in os.walk(root):
dirnames[:] = [name for name in dirnames if name not in SKIP_DIR_NAMES]
base = Path(current_root)
for filename in filenames:
if CONFIG_NAME_PATTERN.match(filename):
path = (base / filename).resolve()
if path not in seen:
seen.add(path)
configs.append(path)
return configs, issues
def parse_config_out_dirs(root: Path, configs: list[Path], allow_outside_root: bool) -> tuple[list[Path], list[Issue]]:
dirs: list[Path] = []
issues: list[Issue] = []
seen: set[Path] = set()
for config in configs:
try:
text = config.read_text(encoding="utf-8")
except UnicodeDecodeError as exc:
issues.append(make_issue("warning", "unreadable-config", config, root, f"Cannot read config as UTF-8: {exc}"))
continue
matches = list(re.finditer(r'''\bout\s*:\s*['"`]([^'"`]+)['"`]''', text))
if not matches:
issues.append(
make_issue(
"warning",
"config-out-not-found",
config,
root,
"No literal out directory found in config. If `out` is computed "
"(e.g. process.env.MIGRATIONS_DIR), pass --migrations-dir explicitly "
"so the migration directory is not missed.",
)
)
continue
for match in matches:
path = normalize_dir(config.parent, match.group(1))
if not allow_outside_root and not path_in_root(path, root):
issues.append(
make_issue(
"error",
"migrations-dir-outside-root",
path,
root,
"Config out directory is outside --root; refusing to scan it by default.",
)
)
continue
if path not in seen:
seen.add(path)
dirs.append(path)
return dirs, issues
def discover_dirs(args: argparse.Namespace, root: Path) -> tuple[list[Path], list[Issue]]:
issues: list[Issue] = []
dirs: list[Path] = []
seen: set[Path] = set()
for value in args.migrations_dir:
path = normalize_dir(root, value)
if not args.allow_outside_root and not path_in_root(path, root):
issues.append(
make_issue(
"error",
"migrations-dir-outside-root",
path,
root,
"Migration directory is outside --root; refusing to scan it by default.",
)
)
continue
if path not in seen:
seen.add(path)
dirs.append(path)
configs, config_issues = iter_config_files(root, args.config, args.allow_outside_root)
issues.extend(config_issues)
if not args.migrations_dir and configs:
if not args.config and len(configs) > 1:
issue_paths = ", ".join(relative(config, root) for config in configs)
issues.append(
make_issue(
"error",
"multiple-drizzle-configs",
root,
root,
f"Multiple Drizzle config files found ({issue_paths}); pass --config or --migrations-dir explicitly.",
)
)
return [], issues
config_dirs, out_issues = parse_config_out_dirs(root, configs, args.allow_outside_root)
issues.extend(out_issues)
for path in config_dirs:
if path.exists() and path not in seen:
seen.add(path)
dirs.append(path)
if dirs or issues:
return dirs, issues
# Only use common fallbacks when there are no Drizzle configs to disambiguate the output.
for value in COMMON_DIRS:
path = normalize_dir(root, value)
if path.exists() and path not in seen:
seen.add(path)
dirs.append(path)
return dirs, issues
def iter_text_files(directory: Path) -> Iterable[Path]:
for current_root, dirnames, filenames in os.walk(directory):
dirnames[:] = [name for name in dirnames if name not in SKIP_DIR_NAMES]
base = Path(current_root)
for filename in filenames:
path = base / filename
if path.suffix in TEXT_SUFFIXES:
yield path
def has_conflict_markers(path: Path) -> bool:
try:
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
if line.startswith(CONFLICT_MARKERS):
return True
except OSError:
return False
return False
def scan_conflict_markers(directory: Path, root: Path, issues: list[Issue]) -> None:
for path in iter_text_files(directory):
if has_conflict_markers(path):
add_issue(
issues,
"error",
"conflict-marker",
path,
root,
"File contains Git conflict markers.",
)
def structure_signals(directory: Path) -> tuple[bool, bool, list[Path]]:
journal = (directory / "meta" / "_journal.json").exists()
root_sql = any(path.is_file() for path in directory.glob("*.sql"))
meta_snapshots = any(path.is_file() for path in (directory / "meta").glob("*_snapshot.json"))
child_dirs = [path for path in directory.iterdir() if path.is_dir() and path.name != "meta"]
child_migration_files = any(
(child / "migration.sql").exists() or (child / "snapshot.json").exists() for child in child_dirs
)
legacy_signal = journal or root_sql or meta_snapshots
folder_signal = child_migration_files or (bool(child_dirs) and not legacy_signal)
return legacy_signal, folder_signal, child_dirs
def detect_structure(directory: Path) -> str:
if not directory.exists():
return "missing"
legacy_signal, folder_signal, _ = structure_signals(directory)
if legacy_signal and folder_signal:
return "mixed"
if legacy_signal:
return "legacy"
if folder_signal:
return "folder-based"
return "unknown"
def migration_number(stem: str) -> str | None:
match = re.match(r"^(\d+)(?:[_-].*)?$", stem)
return match.group(1) if match else None
def snapshot_names_for_entry(entry: dict[str, Any]) -> set[str]:
names: set[str] = set()
idx = entry.get("idx")
tag = entry.get("tag")
if isinstance(idx, int):
names.add(f"{idx:04d}_snapshot.json")
elif isinstance(idx, str) and idx.isdigit():
names.add(f"{int(idx):04d}_snapshot.json")
if isinstance(tag, str):
prefix = tag.split("_", 1)[0].split("-", 1)[0]
if prefix.isdigit():
names.add(f"{int(prefix):04d}_snapshot.json")
names.add(f"{prefix}_snapshot.json")
return names
def check_duplicate_values(
entries: list[dict[str, Any]], key: str, journal: Path, root: Path, issues: list[Issue]
) -> None:
values: dict[Any, int] = {}
for entry in entries:
value = entry.get(key)
if value is None:
continue
values[value] = values.get(value, 0) + 1
for value, count in values.items():
if count > 1:
add_issue(
issues,
"error",
f"duplicate-{key}",
journal,
root,
f"_journal.json contains duplicate {key} value {value!r} ({count} entries).",
)
def check_idx_gap(entries: list[dict[str, Any]], journal: Path, root: Path, issues: list[Issue]) -> None:
"""Warn when journal `idx` values are not contiguous starting from 0."""
idx_values: list[int] = []
for entry in entries:
idx = entry.get("idx")
if isinstance(idx, bool):
continue
if isinstance(idx, int):
idx_values.append(idx)
elif isinstance(idx, str) and idx.isdigit():
idx_values.append(int(idx))
if not idx_values:
return
sorted_idx = sorted(set(idx_values))
expected = list(range(sorted_idx[0], sorted_idx[0] + len(sorted_idx)))
if sorted_idx != expected or sorted_idx[0] != 0:
missing = sorted(set(expected) - set(sorted_idx))
gap_text = f"missing indices {missing}" if missing else f"starts at {sorted_idx[0]} instead of 0"
add_issue(
issues,
"warning",
"idx-gap",
journal,
root,
f"_journal.json idx sequence is not contiguous from 0 ({gap_text}). This can indicate a "
"conflict or a manually deleted migration.",
)
def check_snapshot_chain(
snapshots: list[tuple[Path, Any]], directory: Path, root: Path, issues: list[Issue]
) -> None:
"""Validate that snapshot `prevId` links form a chain over known snapshot `id` values."""
id_to_paths: dict[str, list[Path]] = {}
parsed: list[tuple[Path, str | None, str | None]] = []
for path, data in snapshots:
if not isinstance(data, dict):
continue
snap_id = data.get("id")
prev_id = data.get("prevId")
if isinstance(snap_id, str) and snap_id:
id_to_paths.setdefault(snap_id, []).append(path)
parsed.append((path, snap_id, prev_id if isinstance(prev_id, str) else None))
else:
parsed.append((path, None, prev_id if isinstance(prev_id, str) else None))
for snap_id, paths in id_to_paths.items():
if len(paths) > 1:
joined = ", ".join(relative(path, root) for path in paths)
add_issue(
issues,
"error",
"duplicate-snapshot-id",
paths[0],
root,
f"Multiple snapshot files share id {snap_id!r}: {joined}. Drizzle uses snapshot ids to "
"chain migrations; duplicates usually mean a generated file was copied instead of regenerated.",
)
known_ids = set(id_to_paths.keys())
for path, snap_id, prev_id in parsed:
if prev_id is None or prev_id == "":
continue
if prev_id not in known_ids:
add_issue(
issues,
"warning",
"broken-snapshot-chain",
path,
root,
f"Snapshot prevId {prev_id!r} does not match any snapshot id in {relative(directory, root)}. "
"The migration chain may be broken by a conflict or a partial repair.",
)
def validate_snapshot_json(path: Path, root: Path, issues: list[Issue]) -> Any | None:
data, error = read_json(path)
if error:
add_issue(issues, "error", "invalid-snapshot-json", path, root, f"Cannot parse snapshot JSON: {error}")
return None
return data
def check_legacy(directory: Path, root: Path) -> DirectoryReport:
issues: list[Issue] = []
journal = directory / "meta" / "_journal.json"
data, error = read_json(journal)
if error:
add_issue(issues, "error", "invalid-journal", journal, root, f"Cannot parse _journal.json: {error}")
scan_conflict_markers(directory, root, issues)
return DirectoryReport(str(relative(directory, root)), "legacy", issues)
if not isinstance(data, dict) or not isinstance(data.get("entries"), list):
add_issue(
issues,
"error",
"invalid-journal-shape",
journal,
root,
"_journal.json must be an object with an entries array.",
)
entries: list[dict[str, Any]] = []
else:
entries = [entry for entry in data["entries"] if isinstance(entry, dict)]
check_duplicate_values(entries, "idx", journal, root, issues)
check_duplicate_values(entries, "tag", journal, root, issues)
check_idx_gap(entries, journal, root, issues)
expected_sql: set[str] = set()
expected_snapshots: set[str] = set()
for entry in entries:
tag = entry.get("tag")
if isinstance(tag, str) and tag:
expected_sql.add(f"{tag}.sql")
sql_path = directory / f"{tag}.sql"
if not sql_path.exists():
add_issue(
issues,
"error",
"missing-sql",
sql_path,
root,
f"Journal entry tag {tag!r} does not have a matching SQL file.",
)
snapshots = snapshot_names_for_entry(entry)
expected_snapshots.update(snapshots)
if snapshots and not any((directory / "meta" / name).exists() for name in snapshots):
add_issue(
issues,
"error",
"missing-snapshot",
directory / "meta" / sorted(snapshots)[0],
root,
f"Journal entry {entry!r} does not have a matching snapshot file.",
)
sql_files = sorted(path for path in directory.glob("*.sql") if path.is_file())
by_number: dict[str, list[Path]] = {}
for path in sql_files:
number = migration_number(path.stem)
if number:
by_number.setdefault(number, []).append(path)
if path.name not in expected_sql:
add_issue(
issues,
"warning",
"orphan-sql",
path,
root,
"SQL migration is not referenced by _journal.json.",
)
for number, paths in by_number.items():
if len(paths) > 1:
joined = ", ".join(relative(path, root) for path in paths)
add_issue(
issues,
"error",
"duplicate-migration-number",
paths[0],
root,
f"Multiple SQL migrations share number {number}: {joined}.",
)
snapshot_files = sorted((directory / "meta").glob("*_snapshot.json"))
parsed_snapshots: list[tuple[Path, Any | None]] = []
for path in snapshot_files:
data = validate_snapshot_json(path, root, issues)
parsed_snapshots.append((path, data))
if path.name not in expected_snapshots:
add_issue(
issues,
"warning",
"orphan-snapshot",
path,
root,
"Snapshot file is not referenced by _journal.json.",
)
check_snapshot_chain(parsed_snapshots, directory, root, issues)
scan_conflict_markers(directory, root, issues)
return DirectoryReport(str(relative(directory, root)), "legacy", issues)
def check_folder_based(directory: Path, root: Path) -> DirectoryReport:
issues: list[Issue] = []
names: dict[str, list[Path]] = {}
child_dirs = [path for path in directory.iterdir() if path.is_dir() and path.name != "meta"]
for child in sorted(child_dirs):
names.setdefault(child.name.lower(), []).append(child)
migration_sql = child / "migration.sql"
snapshot_json = child / "snapshot.json"
if not migration_sql.exists():
add_issue(
issues,
"error",
"missing-migration-sql",
migration_sql,
root,
"Folder-based migration is missing migration.sql.",
)
if not snapshot_json.exists():
add_issue(
issues,
"error",
"missing-snapshot-json",
snapshot_json,
root,
"Folder-based migration is missing snapshot.json.",
)
else:
validate_snapshot_json(snapshot_json, root, issues)
for lower_name, paths in names.items():
if len(paths) > 1:
joined = ", ".join(relative(path, root) for path in paths)
add_issue(
issues,
"error",
"duplicate-migration-directory",
paths[0],
root,
f"Migration directory name differs only by case for {lower_name!r}: {joined}.",
)
scan_conflict_markers(directory, root, issues)
return DirectoryReport(str(relative(directory, root)), "folder-based", issues)
def check_mixed(directory: Path, root: Path) -> DirectoryReport:
issues: list[Issue] = []
add_issue(
issues,
"error",
"mixed-structure",
directory,
root,
"Legacy journal/root SQL signals and folder-based migration signals coexist; choose the intended migration structure before repair.",
)
scan_conflict_markers(directory, root, issues)
return DirectoryReport(str(relative(directory, root)), "mixed", issues)
def check_directory(directory: Path, root: Path) -> DirectoryReport:
if not directory.exists():
return DirectoryReport(
str(relative(directory, root)),
"missing",
[
Issue(
severity="error",
code="missing-migrations-dir",
path=relative(directory, root),
message="Migration directory does not exist.",
)
],
)
structure = detect_structure(directory)
if structure == "mixed":
return check_mixed(directory, root)
if structure == "legacy":
return check_legacy(directory, root)
if structure == "folder-based":
return check_folder_based(directory, root)
issues: list[Issue] = []
add_issue(
issues,
"warning",
"unknown-structure",
directory,
root,
"Could not identify a legacy or folder-based Drizzle migration structure; skipping recursive scan.",
)
return DirectoryReport(str(relative(directory, root)), "unknown", issues)
def report_as_json(root: Path, reports: list[DirectoryReport]) -> str:
return json.dumps(
{
"root": str(root),
"checked_dirs": [asdict(report) for report in reports],
"issue_count": sum(len(report.issues) for report in reports),
"note": "This helper is structural only and does not replace drizzle-kit check.",
},
indent=2,
sort_keys=True,
)
def report_as_text(root: Path, reports: list[DirectoryReport]) -> str:
lines = [f"Drizzle migration check root: {root}"]
lines.append("Note: this helper is structural only and does not replace drizzle-kit check.")
if not reports:
lines.append("No migration directories found. Pass --config or --migrations-dir if detection missed one.")
return "\n".join(lines)
for report in reports:
lines.append(f"\nDirectory: {report.path}")
lines.append(f"Structure: {report.structure}")
if not report.issues:
lines.append("Issues: none")
continue
lines.append("Issues:")
for issue in report.issues:
lines.append(f"- [{issue.severity}] {issue.code}: {issue.path} - {issue.message}")
return "\n".join(lines)
def main() -> int:
args = parse_args()
root = Path(args.root).resolve()
dirs, discovery_issues = discover_dirs(args, root)
reports: list[DirectoryReport] = []
if discovery_issues:
reports.append(DirectoryReport(".", "discovery", discovery_issues))
reports.extend(check_directory(path, root) for path in dirs)
if args.json:
print(report_as_json(root, reports))
else:
print(report_as_text(root, reports))
if not reports:
return 2
if any(issue.severity == "error" for report in reports for issue in report.issues):
return 1
if any(issue.severity == "warning" for report in reports for issue in report.issues):
return 1
return 0
if __name__ == "__main__":
sys.exit(main())