📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -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.
|
||||
+163
@@ -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.
|
||||
+69
@@ -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.
|
||||
Executable
+721
@@ -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())
|
||||
Reference in New Issue
Block a user