Skip to content

feat: prepare safe-migrate v0.4.4 - #8

Merged
dsecurity49 merged 5 commits into
mainfrom
feat/v0.4.4-enum-value-rename
Aug 2, 2026
Merged

feat: prepare safe-migrate v0.4.4#8
dsecurity49 merged 5 commits into
mainfrom
feat/v0.4.4-enum-value-rename

Conversation

@dsecurity49

@dsecurity49 dsecurity49 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • model PostgreSQL enum value renames, including search-path resolution, transaction rollback, and deterministic conflict handling
  • add V4 cache provenance and role-sensitive migration semantics for SET ROLE, session authorization, ownership, and PostgreSQL 16+ SET OPTION
  • extend cache inspection, differential fixtures, package safeguards, and user/maintainer documentation for v0.4.4

Compatibility

  • v0.4.4 writes headered V4 caches
  • headered V3 caches remain readable with limited role provenance
  • unsupported older cache formats report generic guidance to run safe-migrate sync

User impact

  • migration chains now model enum renames and role/session state more faithfully
  • cache inspect reports a redacted role count without exposing role names or membership edges
  • the README now documents command-specific options, sync --out, global --no-color, and the GitHub Action's explicit config behavior
  • published crate packages exclude the synthetic frozen cache

Validation

  • cargo test --locked
  • cargo clippy --all-targets --locked -- -D warnings
  • cargo fmt -- --check
  • cargo package --locked --allow-dirty
  • installed the packaged source and verified safe-migrate 0.4.4
  • bash scripts/test-install-dry-run
  • scripts/fuzz: 413 inputs, zero crashes, timeouts, operational errors, or invalid reports
  • scripts/live-auto-sync against PostgreSQL 18.2
  • scripts/live-cache-encryption against PostgreSQL 18.2
  • live_tests/run.sh -v: 517/517 passed
  • scripts/live-differential -v: 301/301 matched PostgreSQL 18.2 in 416.45 seconds
  • all local Markdown links resolve

Summary by CodeRabbit

  • New Features

    • Added role-aware PostgreSQL analysis, including SET ROLE, session authorization, ownership changes, and role-sensitive permissions.
    • Added support for enum value renaming and improved enum handling.
    • Added expanded string-literal, owner, and search-path parsing.
  • Bug Fixes

    • Improved transaction rollback and search-path restoration for role-related changes.
    • Added validation for PostgreSQL authorization and conflict scenarios.
  • Documentation

    • Updated installation, configuration, cache compatibility, and release guidance.
  • Chores

    • Released version 0.4.4 with version 4 cache support and compatible handling for version 3 caches.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@dsecurity49, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 38c1c2a3-ceba-4408-88e9-e4d34d9bc0c2

📥 Commits

Reviewing files that changed from the base of the PR and between e91f803 and 71c40bf.

⛔ Files ignored due to path filters (1)
  • live_tests/differential_manifest.json is excluded by !live_tests/**
📒 Files selected for processing (7)
  • src/ast/visitor.rs
  • src/ast/visitor_tests.rs
  • src/model/role.rs
  • tests/cli_tests.rs
  • tests/live_auto_sync.rs
  • tests/live_differential_harness.rs
  • tests/state_mutation.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@dsecurity49
dsecurity49 marked this pull request as ready for review August 2, 2026 16:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
tests/live_auto_sync.rs (1)

99-106: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Query session_user as its own oracle.

The test uses current_user as the expected value for both source_role and source_session_role. The two differ when the connection sets a role, for example through options=-c role=.... Read both values to keep the assertion exact.

♻️ Proposed change
-    let expected_role: String = client
-        .query_one("SELECT current_user", &[])
-        .expect("query current_user")
-        .get(0);
-    run_auto_sync_case(&database_url, &expected_role, "lint");
-    run_auto_sync_case(&database_url, &expected_role, "lint-chain");
+    let row = client
+        .query_one("SELECT current_user, session_user", &[])
+        .expect("query role oracle");
+    let expected_role: String = row.get(0);
+    let expected_session_role: String = row.get(1);
+    run_auto_sync_case(&database_url, &expected_role, &expected_session_role, "lint");
+    run_auto_sync_case(
+        &database_url,
+        &expected_role,
+        &expected_session_role,
+        "lint-chain",
+    );

run_auto_sync_case then takes expected_session_role and asserts it against cache.metadata.source_session_role.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/live_auto_sync.rs` around lines 99 - 106, Update the test setup around
run_auto_sync_case to query session_user separately from current_user and retain
both expected values. Extend run_auto_sync_case to accept expected_session_role,
and assert cache.metadata.source_session_role against it while continuing to use
expected_role for source_role in both lint cases.
tests/live_differential_harness.rs (1)

1055-1107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the role expectations into the manifest and classify missing roles correctly.

Two points on this check:

  1. The function hardcodes rule_26_chain-conflict and four role names inside the generic harness. The manifest already carries declarative per-rule expectations such as required_relations and fixture_scopes. A required_role_edges manifest field would keep fixture data in differential_manifest.json and keep the harness rule-agnostic.
  2. Every failure is reported as RootCauseClassification::SimulatorBug. A missing role means differential_baseline.sql did not create it, or the sync role cannot read pg_roles. That is an environment or baseline problem. Classify an absent role separately from an incorrect can_set_role_to edge so the mismatch report points at the real cause.
♻️ Proposed change for point 2
-    match role("sm_set_member") {
-        Some(member) if edge(&member.can_set_role_to, "sm_set_bridge") => {}
-        _ => errors.push("sm_set_member is missing its SET edge to sm_set_bridge"),
-    }
+    let mut absent = Vec::new();
+    for name in ["sm_set_member", "sm_set_bridge"] {
+        if role(name).is_none() {
+            absent.push(format!("{name} is absent from the synced role catalog"));
+        }
+    }

Map entries in absent to MismatchCategory::BaselineObjectAbsent with RootCauseClassification::EnvironmentIssue, and keep RoleMembershipMismatch/SimulatorBug for present roles with wrong edges.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/live_differential_harness.rs` around lines 1055 - 1107, Update
check_role_membership_cache to load declarative required_role_edges expectations
from differential_manifest.json instead of hardcoding rule_26_chain-conflict and
role names, keeping the harness rule-agnostic. Track roles absent from cache
separately from roles with incorrect edges; report absent roles as
BaselineObjectAbsent with EnvironmentIssue, while retaining
RoleMembershipMismatch and SimulatorBug for present roles whose edges are wrong.
src/sync.rs (1)

944-965: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Align member_of with its current meaning.

member_of currently records all pg_auth_members rows, while its doc comment says it is only inherited roles. On PostgreSQL 16+, pg_auth_members is independent for set_option and inherit_option, so a membership without the SET option (for example WITH SET FALSE) still populates member_of. Either split this into “membership edges” and “inherited privileges” or update the doc comment to “roles this role is a member of”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/sync.rs` around lines 944 - 965, Update the documentation for the role
field member_of to describe it as the roles this role is a member of, rather
than inherited roles. Keep the existing membership_query processing and
can_set_role_to behavior unchanged.
src/ast/visitor.rs (1)

2924-2939: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse resolve_string_literal for the session-authorization literal.

This block strips single quotes manually. It handles only plain STRING tokens. SET SESSION AUTHORIZATION E'app\x5fuser'; and other prefixed string forms return None, so the statement produces no fact. The new resolve_string_literal helper already decodes every supported literal kind and removes the expect call.

♻️ Proposed refactor
-        let role_from_literal = node.literal().and_then(|lit| {
-            let text = lit.syntax().text().to_string();
-            let unescaped = text
-                .strip_prefix('\'')
-                .and_then(|inner| inner.strip_suffix('\''))
-                .map(|inner| inner.replace("''", "'"));
-            if unescaped.as_deref().is_none_or(str::is_empty) {
-                None
-            } else {
-                Some(crate::analysis::facts::RoleFact::Named {
-                    name: unescaped.expect("checked above"),
-                    via_legacy_group_syntax: false,
-                })
-            }
-        });
+        let role_from_literal = node
+            .literal()
+            .and_then(|lit| Self::resolve_string_literal(&lit))
+            .filter(|name| !name.is_empty())
+            .map(|name| crate::analysis::facts::RoleFact::Named {
+                name,
+                via_legacy_group_syntax: false,
+            });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ast/visitor.rs` around lines 2924 - 2939, Update the
session-authorization literal handling in the visitor to call the existing
resolve_string_literal helper instead of manually stripping quotes and
unescaping text. Preserve the current empty-string filtering and construct
RoleFact::Named with the resolved value, removing the expect-based extraction so
prefixed literals such as E strings are supported.
src/analysis/state.rs (1)

1571-1591: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract one relation-owner helper. Both sites resolve the RoleFact identity, taint confidence for an unknown identity, snapshot the relation, assign ObjectId::new("", owner), and report the same conflict text. The duplicated logic can diverge when the owner rules change.

  • src/analysis/state.rs#L1571-L1591: replace the inline AlterTableActionMutation::OwnerTo body with a call to the shared helper.
  • src/analysis/state.rs#L2283-L2302: replace the inline Mutation::ChangeRelationOwner body with a call to the same helper.
♻️ Proposed helper
fn change_relation_owner(
    &mut self,
    id: &ObjectId,
    new_owner: &crate::analysis::facts::RoleFact,
) -> MutationResult {
    let Some((owner, known)) = self.role_fact_identity(new_owner) else {
        self.snapshot_confidence();
        self.local.confidence = Confidence::Tainted;
        return MutationResult::Skipped;
    };
    if !known {
        self.snapshot_confidence();
        self.local.confidence = Confidence::Tainted;
    }
    self.snapshot_relation(id);
    match self.local.relations.get_mut(id) {
        Some(RelationOverlay::Present(relation)) => {
            relation.owner = ObjectId::new("", owner);
            MutationResult::Applied
        }
        _ => MutationResult::Conflict {
            reason: format!("relation '{}' does not exist", id),
        },
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/analysis/state.rs` around lines 1571 - 1591, Extract a shared
change_relation_owner helper in src/analysis/state.rs that performs role
identity resolution, confidence tainting, relation snapshotting, owner
assignment, and the existing conflict result. Replace the inline OwnerTo logic
at src/analysis/state.rs lines 1571-1591 and the ChangeRelationOwner logic at
lines 2283-2302 with calls to this helper.
tests/state_mutation.rs (1)

706-734: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the test name with the condition under test.

The test never sets a cache version. It only omits cache.metadata.source_role. A V4 cache with absent role provenance reaches the same state. Rename the test to describe missing role provenance, so the name does not imply version gating that the test does not cover.

Consider also asserting that public.mood keeps old after the tainted resolution. The rename statement currently has no assertion.

♻️ Proposed rename
-    fn v3_without_role_provenance_taints_explicit_user_search_path() {
+    fn cache_without_role_provenance_taints_explicit_user_search_path() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/state_mutation.rs` around lines 706 - 734, Rename
v3_without_role_provenance_taints_explicit_user_search_path to describe missing
role provenance rather than cache-version behavior. In the same test, add an
assertion after analyze confirming that public.mood still contains the old enum
value, while preserving the existing tainted-resolution assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ast/visitor.rs`:
- Around line 1740-1750: Update the AddValue handling in the AST visitor to
derive before from the parsed AddValue placement accessor instead of searching
add_value.syntax().text(). Preserve the resulting AlterTypeActionFact::AddValue
fields while ensuring enum labels containing “before” do not override an AFTER
placement.

In `@tests/cli_tests.rs`:
- Around line 30-33: Ensure the published crate contains the
live_tests/.safe-migrate.cache fixture required by the write_fresh_cache test.
Update the root manifest’s exclude/include configuration to retain this file in
the package, or modify the test setup to use a fallback that does not depend on
the frozen fixture while preserving existing test behavior.

In `@tests/state_mutation.rs`:
- Around line 1941-1948: Update the engine.analyze call in the affected role
test to unwrap its result instead of binding it to _. Match the existing pattern
used by the other role tests, while leaving the SQL and subsequent assertions
unchanged.

---

Nitpick comments:
In `@src/analysis/state.rs`:
- Around line 1571-1591: Extract a shared change_relation_owner helper in
src/analysis/state.rs that performs role identity resolution, confidence
tainting, relation snapshotting, owner assignment, and the existing conflict
result. Replace the inline OwnerTo logic at src/analysis/state.rs lines
1571-1591 and the ChangeRelationOwner logic at lines 2283-2302 with calls to
this helper.

In `@src/ast/visitor.rs`:
- Around line 2924-2939: Update the session-authorization literal handling in
the visitor to call the existing resolve_string_literal helper instead of
manually stripping quotes and unescaping text. Preserve the current empty-string
filtering and construct RoleFact::Named with the resolved value, removing the
expect-based extraction so prefixed literals such as E strings are supported.

In `@src/sync.rs`:
- Around line 944-965: Update the documentation for the role field member_of to
describe it as the roles this role is a member of, rather than inherited roles.
Keep the existing membership_query processing and can_set_role_to behavior
unchanged.

In `@tests/live_auto_sync.rs`:
- Around line 99-106: Update the test setup around run_auto_sync_case to query
session_user separately from current_user and retain both expected values.
Extend run_auto_sync_case to accept expected_session_role, and assert
cache.metadata.source_session_role against it while continuing to use
expected_role for source_role in both lint cases.

In `@tests/live_differential_harness.rs`:
- Around line 1055-1107: Update check_role_membership_cache to load declarative
required_role_edges expectations from differential_manifest.json instead of
hardcoding rule_26_chain-conflict and role names, keeping the harness
rule-agnostic. Track roles absent from cache separately from roles with
incorrect edges; report absent roles as BaselineObjectAbsent with
EnvironmentIssue, while retaining RoleMembershipMismatch and SimulatorBug for
present roles whose edges are wrong.

In `@tests/state_mutation.rs`:
- Around line 706-734: Rename
v3_without_role_provenance_taints_explicit_user_search_path to describe missing
role provenance rather than cache-version behavior. In the same test, add an
assertion after analyze confirming that public.mood still contains the old enum
value, while preserving the existing tainted-resolution assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 03d368f9-9202-427f-a15b-9c9d6d7def12

📥 Commits

Reviewing files that changed from the base of the PR and between 77464b6 and e91f803.

⛔ Files ignored due to path filters (22)
  • Cargo.lock is excluded by !**/*.lock
  • live_tests/.safe-migrate.cache is excluded by !live_tests/**
  • live_tests/README.md is excluded by !live_tests/**
  • live_tests/differential_baseline.sql is excluded by !live_tests/**
  • live_tests/differential_manifest.json is excluded by !live_tests/**
  • live_tests/rule_20_alter-type-add-value-txn/safe_010_rename_value_search_path.sql is excluded by !live_tests/**
  • live_tests/rule_20_alter-type-add-value-txn/safe_011_rename_value_quoted.sql is excluded by !live_tests/**
  • live_tests/rule_20_alter-type-add-value-txn/safe_012_create_then_rename_value.sql is excluded by !live_tests/**
  • live_tests/rule_22_opaque-dynamic-sql/013_invalid_set_role_current_user.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/012_rename_enum_missing_label.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/013_rename_enum_duplicate_label.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/014_rename_value_non_enum.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/015_rename_value_missing_type.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/016_missing_set_role.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/017_unauthorized_set_role.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/safe_010_role_transaction_semantics.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/safe_011_session_authorization.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/safe_012_role_search_path_and_owner.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/safe_013_quoted_session_authorization.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/safe_014_session_authorization_rollback.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/safe_015_session_authorization_default.sql is excluded by !live_tests/**
  • live_tests/rule_26_chain-conflict/safe_016_transitive_set_role_membership.sql is excluded by !live_tests/**
📒 Files selected for processing (26)
  • .gitignore
  • CHANGELOG.md
  • Cargo.toml
  • README.md
  • docs/CONTRACT.md
  • docs/internal/ARCHITECTURE.md
  • docs/internal/CACHE.md
  • docs/internal/TESTING.md
  • src/analysis/facts.rs
  • src/analysis/mutations.rs
  • src/analysis/resolver.rs
  • src/analysis/state.rs
  • src/analysis/transaction.rs
  • src/ast/visitor.rs
  • src/ast/visitor_tests.rs
  • src/db/cache.rs
  • src/main.rs
  • src/model/role.rs
  • src/sync.rs
  • src/sync_tests.rs
  • tests/cli_tests.rs
  • tests/live_auto_sync.rs
  • tests/live_cache_encryption.rs
  • tests/live_differential_harness.rs
  • tests/rule_catalog.rs
  • tests/state_mutation.rs

Comment thread src/ast/visitor.rs Outdated
Comment thread tests/cli_tests.rs Outdated
Comment thread tests/state_mutation.rs Outdated
@dsecurity49
dsecurity49 merged commit 7597ccf into main Aug 2, 2026
8 checks passed
@dsecurity49
dsecurity49 deleted the feat/v0.4.4-enum-value-rename branch August 2, 2026 16:57
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