Skip to content

feat: production-readiness improvements (PK tracking, transactions, CONCURRENTLY, rename detection)#13

Merged
rekurt merged 5 commits into
masterfrom
feat/features
Apr 8, 2026
Merged

feat: production-readiness improvements (PK tracking, transactions, CONCURRENTLY, rename detection)#13
rekurt merged 5 commits into
masterfrom
feat/features

Conversation

@rekurt
Copy link
Copy Markdown
Owner

@rekurt rekurt commented Apr 8, 2026

Summary

  • fix(loader): resolve pg_sequences panic by casting regtype to text and handling nullable fields with permission warnings
  • feat(model): add PrimaryKey variant to ConstraintKind, track PKs across all loaders (PostgreSQL, MySQL, SQLite, SQL files)
  • feat(cli): add --no-transaction, --concurrently, --detect-renames flags
  • feat(migration): wrap migrations in BEGIN/COMMIT by default; support CREATE INDEX CONCURRENTLY (PostgreSQL only, auto-disables transactions)
  • feat(diff): column/table rename detection heuristic (opt-in via --detect-renames, experimental)
    • High confidence: type + nullability + default match → executable SQL
    • Medium confidence: type + nullability match → SQL commented out for safety
  • refactor: extract command handlers from main.rs into src/commands/ module; split migration.rs into migration/{mod,sql}.rs
  • fix(ci): DROP COLUMN now correctly reported as blocking; renamed tables/columns included in CI report
  • fix(migration): rollback renames execute before constraint re-addition; MySQL PK uses ADD PRIMARY KEY (no named constraint); medium-confidence rollback renames commented out
  • fix(output): print_diff and DiffSummary now display renamed tables/columns
  • fix(error): replace unwrap_or_default with proper error propagation for JSON/YAML serialization; SQLite error propagation instead of silent filter_map(ok())
  • chore: remove unused Trigger/Function model types; add RenameConfidence Copy/Display derives
  • test: add 22 new tests covering PK tracking, rename detection, CONCURRENTLY, and transaction wrapping

Test plan

  • cargo test — 166 tests pass (was 144)
  • cargo clippy — no warnings
  • Manual test against real PostgreSQL databases (accounts vs fd_accounting)
  • Verify --detect-renames with a real column rename scenario
  • Verify --concurrently generates correct SQL and --no-transaction omits BEGIN/COMMIT
  • Verify --ci --format json includes renames in report

rekurt added 2 commits April 8, 2026 15:21
…s, CONCURRENTLY, and rename detection

- fix(loader): resolve pg_sequences panic by casting regtype to text and handling nullable fields
- feat(model): add PrimaryKey variant to ConstraintKind, track PKs across all loaders (PG, MySQL, SQLite, SQL files)
- feat(cli): add --no-transaction, --concurrently, --detect-renames flags
- feat(migration): wrap migrations in BEGIN/COMMIT by default, support CREATE INDEX CONCURRENTLY (PG only)
- feat(diff): column/table rename detection heuristic (opt-in, experimental)
  - High confidence: type+nullability+default match
  - Medium confidence: type+nullability match (SQL commented out for safety)
- refactor: extract command handlers from main.rs into src/commands/ module
- refactor: split migration.rs into migration/mod.rs + migration/sql.rs
- fix(ci): DROP COLUMN now correctly reported as blocking in CI report
- fix(ci): renamed tables/columns now included in CI report changes
- fix(migration): rollback renames now execute before constraint re-addition
- fix(migration): MySQL PK uses ADD PRIMARY KEY without named constraint
- fix(output): print_diff now displays renamed tables and columns
- fix(error): replace unwrap_or_default with proper error propagation for JSON/YAML serialization
- fix(loader): SQLite error propagation instead of silent filter_map(ok())
- fix(loader): emit warning when sequence metadata is NULL (permissions)
- chore: remove unused Trigger/Function model types
- chore: add RenameConfidence Copy/Display derives
- test: add 22 new tests for PK, renames, CONCURRENTLY, transactions
- fix(migration): rollback medium-confidence renames now commented out (matching forward migration)
- fix(output): DiffSummary now includes tables_renamed and columns_renamed counts
- fix(commands): emit warning when --concurrently used with non-PostgreSQL dialect
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d2f3c9aed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/migration/mod.rs
Comment on lines +330 to 334
statements.push(MigrationStatement {
sql: create_index_sql(idx, dialect, concurrently),
warnings,
is_blocking: !concurrently, // CONCURRENTLY is non-blocking
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve blocking status when CONCURRENTLY is unsupported

This marks every added index as non-blocking whenever concurrently is true, but create_index_sql only emits CONCURRENTLY for PostgreSQL. On MySQL/SQLite, --concurrently still produces plain CREATE INDEX while is_blocking becomes false, so CI/reporting can incorrectly treat a blocking index build as safe (notably with --fail-on-blocking).

Useful? React with 👍 / 👎.

Comment thread src/diff.rs Outdated
Comment on lines +380 to +384
// High confidence: identical column definitions
if rem.columns == add.columns {
return Some((ai, RenameConfidence::High));
}
// Medium confidence: same column count, ≥80% column name overlap
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep non-column table changes when detecting renames

High-confidence table rename matching is based only on columns, so a removed/added pair can be converted into a rename even when indexes or constraints differ. Because matched tables are then removed from added/removed sets in this function, those index/constraint deltas never reach modified_tables, and migration generation can emit only RENAME TABLE while silently skipping required schema changes.

Useful? React with 👍 / 👎.

Table rename detection removed matched tables from added/removed sets
without diffing non-column properties, causing index and constraint
changes to be silently dropped from migration output.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

dbdiff/src/ci.rs

Lines 50 to 51 in 06be941

pub fn from_diff_reversed(diff: &SchemaDiff, statements: &[MigrationStatement]) -> Self {
Self::build(diff, statements, true)

P1 Badge Reverse all object types in rollback CI classification

from_diff_reversed delegates to build(..., true), but build only flips ADD/DROP semantics for tables; column/index/constraint branches still use forward matching (e.g. added_columns looks for ADD COLUMN). In --direction down, rollback SQL contains DROP COLUMN/DROP INDEX, so lookups miss and default is_blocking values are wrong, which can underreport blocking rollback steps and let --ci --fail-on-blocking return EXIT_DRIFT instead of EXIT_UNSAFE.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/diff.rs Outdated
Comment on lines +412 to +416
if candidates.len() == 1 {
let (ai, confidence) = &candidates[0];
matched_removed.push(ri);
matched_added.push(*ai);
renames.push(TableRename {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require bijective matching before inferring table renames

The rename detector accepts a rename whenever a removed table has exactly one unmatched added candidate, but it does not verify the reverse uniqueness (that this added table is uniquely claimed by only one removed table). With --detect-renames, if two removed tables share the same column shape and one added table matches both, iteration order picks one arbitrary rename and drops the other table, which can preserve/drop the wrong dataset while still producing “high confidence” rename SQL.

Useful? React with 👍 / 👎.

…re bijective table rename matching

- CI report now correctly flips SQL patterns and blocking defaults for
  columns, indexes, and constraints in reverse (rollback) mode, not just
  tables. Previously, rollback lookups missed statements and used wrong
  default is_blocking values, underreporting blocking rollback steps.

- Table rename detector now requires bijective (1-to-1) matching: a rename
  is only accepted when the removed table has exactly one added candidate
  AND that added table is claimed by only one removed table. Previously,
  iteration order could pick an arbitrary rename when two removed tables
  shared the same column shape.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c760ee86d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/diff.rs
Comment on lines +353 to +356
if candidates.len() == 1 {
let (ai, confidence) = &candidates[0];
matched_removed.push(ri);
matched_added.push(*ai);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require bijection for column rename inference

The column rename matcher accepts a rename whenever a removed column currently has one unmatched candidate, but it never enforces reverse uniqueness. With --detect-renames, if two removed columns have the same type/nullability signature and only one added column fits, iteration order will rename one arbitrarily and leave the other as a drop, which can preserve/drop the wrong data column. Please require that the chosen added column is claimed by exactly one removed column (bijective matching), as you already do for table renames.

Useful? React with 👍 / 👎.

Comment thread src/diff.rs
Comment on lines +184 to +186
for (old_table, new_table) in &rename_pairs {
let table_diff =
diff_tables(&new_table.name, old_table, new_table, detect_renames);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Normalize renamed-table diffs to new table names

For detected table renames, diff_tables(&new_table.name, old_table, new_table, ...) builds removed_constraints/removed_indexes from old_table objects, so they keep the old table_name. Migration generation renames the table first, then emits drop statements from those removed objects; that makes DROP CONSTRAINT target a non-existent table after rename (and DROP INDEX ... ON old_table on MySQL). This breaks forward migrations whenever renamed tables also have constraint/index deltas, so removed objects should be rebased to the new table name before being added to modified_tables.

Useful? React with 👍 / 👎.

…e detection

When a table rename is detected with concurrent index/constraint changes,
removed_indexes and removed_constraints retained the old table name. This
caused DROP statements to target the pre-rename name, breaking forward
migrations on MySQL (DROP INDEX ... ON old_table) and PostgreSQL
(ALTER TABLE old_table DROP CONSTRAINT ...).
@rekurt rekurt merged commit 2c2b6cb into master Apr 8, 2026
@rekurt rekurt deleted the feat/features branch April 8, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant