Repository navigation
feat: add rowx migrate command - #160
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ffd98a395d
ℹ️ 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".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #145
What
rowx migrate add | run | revert | infoand the[tool.rowx.migrate]config.check/config.py:[tool.rowx.migrate]parsed intoConfig.migrate(MigrateConfig:dir,table,create_schemas,versioning,kind).Nonewhen the table is absent, and an empty table counts as configured. Unknown keys get the existing did-you-mean error, and badversioningorkindvalues are config errors with a suggestion (exit 2). A newdatabase_url(config, url)helper is the--database-url/database_url_envlookup, now shared withcheck.check/migrate.py(new): parsers and handlers.addwrites sqlx's placeholder content (-- Add migration script here,-- Add up migration script here,-- Add down migration script here), creates the directory, and infers versioning (sqlx's rule; sequential is{:04}) and kind (latest migration) unless a flag or the config says otherwise. Timestamps are UTCYYYYMMDDHHMMSS. It creates files exclusively (open("x")) and refuses an existing version before writing anything.run,revertandinfouseMigratoroverrowx.postgres.connect. They printApplied|Reverted|Would apply|Would revert <version>/<migrate|revert> <description> (<time>), andinfoprints<version>/<state> <description>. Every error iserror: ...on stderr with the DSN and password redacted (sameredactascheck), exit 2.--dry-runcallsMigrator.run/undowithdry_run=True: the Rust plan runs under the lock as for a real call, but nothing is executed and no schema or history table is created. Errors are the real run's, from the same code.revertis a singleMigrator.undocall, so the versions are chosen and reverted under one lock. Without--target-versionit passestarget=None, which reverts only the latest applied migration (chosen under that lock). EachRevertedline's time is the status'sexecution_time, whichundonow sets to how long the revert took.addrejects a simple migration whose description ends in.upor.down(the file would read back as half of a reversible pair and break the directory), a description with a NUL byte, and reports a directory it can't create or write aserror: ...(exit 2).check/cli.py: registers themigrategroup and dispatches it with the same exit-2 handling ascheck.Changes to the merged Migrator (wiring edits)
Migrator.run(..., dry_run=False)andMigrator.undo(..., dry_run=False), as api.md specifies. Rust:rowx_postgres::migrate::{run_dry, undo_dry}list the history rows under the lock (creating nothing), run the core plan, and return the statuses the call would produce (appliedfor run,pendingfor undo, noinstalled_on/execution_time). The binding'sOp::Run/Op::Undocarry the flag, and a dry run never bumps the pool epoch or clears the statement cache.Migrator.undo(target=None):targetstays a required keyword but may beNone, meaning "only the latest applied migration".rowx_core::migrate::latest_undo_target(pure, unit-tested) picks it inside the executor's lock.rowx_postgres::migrate::undo/undo_drytakeOption<i64>. Design change to propose: api.md typesundo's target asint; this widens it toint | Noneso the CLI's targetless revert is one claim.undonow returns each reverted status withexecution_time= how long its revert took (the version has no history row afterwards, so the field was alwaysNonebefore). Design change to propose: document this in api.md/migrations.md._core/postgres.pyi,rowx/postgres/_migrate.pyand the typing cases updated for both.New tests
unit/check/test_config.py:test_migrate_table_is_absent_by_default,test_empty_migrate_table_is_configured_with_defaults,test_migrate_table_parses_every_key,test_migrate_unknown_key_suggests,test_migrate_unknown_key_without_suggestion,test_misspelled_migrate_table_suggests,test_migrate_bad_values(bad enums, empty and non-string values),test_migrate_must_be_a_table.unit/test_cli.py:test_add_creates_the_directory_with_sqlx_placeholder_content,test_add_replaces_spaces_in_the_description,test_add_timestamp_is_utc_to_the_second,test_add_with_no_migrations_infers_a_timestamp,test_add_infers_versioning_by_sqlxs_rule(parametrized),test_add_reversible_pairs_use_the_up_version_for_the_rule,test_add_flags_override_inference,test_add_reversible_writes_a_pair,test_add_kind_is_inferred_from_the_latest_migration,test_add_simple_flag_beats_a_reversible_history,test_add_refuses_to_overwrite,test_add_refuses_a_version_that_exists_before_writing_a_pair,test_add_reads_dir_versioning_and_kind_from_config,test_add_source_is_relative_to_the_current_directory,test_add_without_a_pyproject_defaults_to_migrations,test_add_rejects_a_bad_description,test_add_reports_a_bad_existing_directory,test_add_reports_a_bad_config,test_add_conflicting_flags_are_a_usage_error,test_database_commands_need_a_url,test_database_url_env_comes_from_the_config,test_an_unreachable_database_exits_two_without_the_password,test_a_bad_migration_directory_exits_two,test_a_missing_config_file_exits_two,test_format_duration.rowx-corethe_latest_undo_target_reverts_only_the_latest_applied_version;rowx-postgres/tests/migrate.rsundo_without_a_target_reverts_only_the_latest_and_times_each_revert,a_dry_run_plans_like_the_real_one_and_changes_nothing.integration/postgres/test__migrate.py:test_undo_without_a_target_reverts_the_latest_only_and_times_it,test_a_dry_run_returns_the_planned_statuses_and_changes_nothing(includingcreate_schemasnever being created).unit/typing/cases.py:dry_runandtarget=Nonereveals, plusnegative_migrator_arguments.unit/test_cli.py(review fixes):test_add_rejects_a_simple_description_that_reads_back_as_half_of_a_pair,test_add_a_reversible_description_may_end_in_up,test_add_reports_a_directory_it_cannot_create, and a NUL case intest_add_rejects_a_bad_description.integration/check/test_migrate_cli.py(db, goldens inintegration/check/golden/migrate_*.txt, elapsed times normalised):test_run_info_revert_info_match_goldens,test_run_dry_run_changes_nothing,test_revert_dry_run_changes_nothing,test_revert_dry_run_on_a_fresh_database_creates_nothing,test_dry_run_reports_the_same_errors_as_a_real_run,test_target_version,test_revert_to_zero_reverts_everything,test_revert_without_a_down_file_exits_two_and_reverts_nothing,test_out_of_order_exits_two_unless_allowed,test_missing_migrations_exit_two_unless_ignored,test_info_shows_both_checksums_of_a_modified_migration,test_a_failing_migration_exits_two_with_the_server_message,test_config_table_and_create_schemas_are_used,test_a_password_is_never_printed(a role with a random password, covering success, a failing migration and a refused login).Wiring edits
check/cli.py:migratesubcommand registration, dispatch, and use of the shareddatabase_urlhelper.check/config.py:"migrate"added to the[tool.rowx]known keys.dry_run,undo(target=None)and the revertexecution_time(listed under "Changes to the merged Migrator").Spec ambiguities
dry_runonMigrator. api.md showsdry_runonrun/undobut feat: add rowx.postgres.migrator #159 didn't add it; it is added here (see above) so the plan stays in rowx-core.infoand applied checksums.MigrationStatushas no applied checksum, so for amodifiedrowinforeadsversion, checksumfrom the history table with an{table:ident}query (schema-qualified names split on the last.). The line isN/modified <description> (local abcd1234, applied ef567890), short hex being the first 4 bytes. Amissingrow prints no description.runline is given in the design.infoprints<version>/<state> <description> (<time>), with the time only for applied rows that have one.revert. Resolved byundoreturning the revert'sexecution_time(see above);revertis one call under one lock.Migrator.runreturns nothing on error, sorunprints noAppliedlines for migrations that committed before a failing one (the same holds forrevert). The error is on stderr and the earlier ones stay applied. Fixing it needsMigrationFailedError(and the connection-error path) to carry the versions done so far, which is a new public error attribute the design doesn't have; proposed as a design change rather than done here.--reversibleor--simpleflag always wins, whereas sqlx ignores--reversibleonce migrations exist.dir. Without the table,dirismigrationsrelative to the directory of the foundpyproject.toml(or the current directory when there is none).--sourceis relative to the current directory.addrefuses when the new version already exists in the directory, which can only happen with a timestamp collision in the same second; it reports this as "refusing to overwrite"._(as sqlx), and an empty description or one containing a path separator is an error (exit 2).--database-urlis not accepted byadd, which needs no database.--reversible --simple) exit 2 through argparse's usage error.🤖 Generated with Claude Code