Skip to content

Reconcile foreign objects in deploy#50

Merged
gmr merged 3 commits into
mainfrom
feature/deploy-foreign-objects
Jun 16, 2026
Merged

Reconcile foreign objects in deploy#50
gmr merged 3 commits into
mainfrom
feature/deploy-foreign-objects

Conversation

@gmr

@gmr gmr commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Summary

Close the deploy-side gap for foreign objects: deploy now reconciles changes to foreign data wrappers, servers, user mappings, and foreign tables, not just creates and drops them.

Problem

deploy could create foreign objects when missing and drop them when project-absent, but it never reconciled changes:

  • Foreign data wrappers, servers, and user mappings were not in the diff's modeled() set, so a change to (say) a server's options or a wrapper's handler was classified Undiffable and left untouched.
  • Foreign-table option changes fell back to drop+recreate (gated), even though PostgreSQL can alter them in place.
  • The generic drop_sql emitted DROP USER MAPPING IF EXISTS <user>, which is invalid SQL — user mappings drop per server (FOR <user> SERVER <srv>) — and database-only foreign objects were dropped unordered.

Solution

  • Diffable (diff.rs): add FDW, server, and user mapping to modeled() and database_index(), and retain removed objects' definitions so drops can render correctly.
  • In-place ALTER renderers (alter.rs):
    • FDWHANDLER/VALIDATOR (incl. NO HANDLER/NO VALIDATOR), OPTIONS, comment.
    • ServerVERSION, OPTIONS, comment; a wrapper or TYPE change (neither alterable) rebuilds.
    • User mapping — per-server OPTIONS, with CREATE/DROP for servers the project adds or removes.
    • Foreign tableOPTIONS and comment in place; a server or column change rebuilds.
    • A shared OPTIONS (ADD/SET/DROP …) delta renderer backs all four.
  • Redacted-password safety: a password option the project does not carry is left untouched, so a redacted pull (the default) never strips a live credential.
  • Correct DROP: entry_key now orders removed foreign objects (user-mapping/foreign-table → server → FDW), and user mappings render the per-server drop form.

Impact

deploy plans are now complete for foreign objects. No change to objects that already matched (verified by an empty-plan test). Drops remain gated behind --allow-drop as before.

Testing

cargo fmt, cargo clippy --all-targets -- -D warnings, cargo test (140 lib + integration tests, +8 new). New coverage: alter.rs unit tests for each renderer and the redacted-password case; two tests/deploy.rs integration tests over synthesized foreign archives (in-place ALTERs + gated/allowed server drop, and a matching-foreign empty plan). bin/deploy-gates is extended to drive wrapper/server/foreign-table option drift end-to-end against compose PG17 (run locally with just deploy-gates).

Summary by CodeRabbit

  • New Features
    • Expanded deploy reconciliation to include PostgreSQL foreign data wrappers, foreign servers, user mappings, and foreign tables, with in-place updates for OPTIONS and comments.
  • Improvements
    • More accurate handling of foreign-object drift (including redeploy verification), and safer handling of user-mapping credentials so redacted passwords aren’t accidentally changed.
    • Foreign-object drops are better qualified and gated behind explicit --allow-drop where required.
    • Improved generation of USER MAPPING ... FOR subjects, treating PUBLIC as a special-case.
  • Documentation
    • Updated deploy change-reconciliation rules for the additional foreign-object categories and password redaction behavior.
  • Tests
    • Added integration tests covering foreign-object drift, credential preservation, gated drops, and no-op deploy detection.

deploy could create and drop foreign data wrappers, servers, user
mappings, and foreign tables, but never reconciled changes to them:
the three schemaless types were existence-only (left untouched when
they differed) and foreign-table option changes forced a drop+recreate.

- Make foreign data wrappers, servers, and user mappings diffable
  (diff.rs `modeled` + `database_index`), so a change is detected and
  resolved instead of being reported as undiffable.
- Add in-place ALTER renderers (alter.rs):
  - FDW: HANDLER/VALIDATOR (incl. NO HANDLER/VALIDATOR), OPTIONS, comment
  - SERVER: VERSION, OPTIONS, comment; a wrapper/TYPE change rebuilds
  - USER MAPPING: per-server OPTIONS, with CREATE/DROP for added/removed
    servers
  - FOREIGN TABLE: OPTIONS and comment in place; server/column changes
    rebuild
- Share an `OPTIONS (ADD/SET/DROP ...)` delta renderer across the four.
- Never drop a user-mapping `password` the project does not carry, so a
  redacted pull (the default) cannot strip a live credential.
- Fix DROP of database-only foreign objects: order them via entry_key
  (so user-mapping/foreign-table drop before server before FDW) and
  render the per-server `DROP USER MAPPING ... FOR x SERVER y` form
  (the generic drop_sql emitted invalid SQL for user mappings).

Covered by alter.rs unit tests, two deploy integration tests over
synthesized foreign archives, and an extension of bin/deploy-gates that
drives the wrapper/server/foreign-table option drift end-to-end against
compose PG17. Documented in docs/commands.md, including the redacted-
password safety.

Co-authored-by: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 52d37c19-ff2f-46bb-8e61-28b7724336c2

📥 Commits

Reviewing files that changed from the base of the PR and between 544a69a and e84dfd4.

📒 Files selected for processing (1)
  • src/deploy/alter.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/deploy/alter.rs

📝 Walkthrough

Walkthrough

Adds full deploy reconciliation support for PostgreSQL foreign-data-wrapper objects (FDW, foreign server, user mapping, foreign table). Changes span the diff struct (Diff.removed becomes BTreeMap<ObjectKey, Definition>), drop SQL generation, entry_key mapping, new ALTER rendering functions, a user mapping subject utility, unit tests, integration test fixtures, deployment gate script extensions, and documentation.

Changes

FDW Deploy Reconciliation

Layer / File(s) Summary
Diff struct and FDW indexing
src/deploy/diff.rs
Diff.removed changes from Vec<ObjectKey> to BTreeMap<ObjectKey, Definition>. modeled() and database_index() are extended to include ForeignDataWrapper, Server, and UserMapping, enabling normalized diff and keyed drop rendering for those types.
Definition-aware DROP SQL and entry_key mapping
src/deploy/mod.rs
drop_sql gains Option<&Definition> and emits per-server DROP USER MAPPING IF EXISTS for user mappings. entry_key maps new FDW-related libpgdump::ObjectType variants and expands the schemaless object set. wanted construction and fallback DROP generation pass definitions through.
FDW/server/user_mapping/foreign_table ALTER reconciliation
src/deploy/alter.rs
resolve() gains arms for ForeignDataWrapper, Server, and UserMapping. table() detects foreign tables and delegates to foreign_table(). New functions fdw, server, user_mapping, and foreign_table implement in-place OPTIONS/handler/validator/version/comment ALTER SQL with fallback to replace for incompatible changes; helpers implement option deltas, options-clause rendering, and string literal escaping.
User mapping subject utility
src/utils.rs
New user_mapping_subject() function returns PUBLIC unquoted (case-insensitive) while quoting all other identifiers, with test coverage for the special case and standard identifiers.
Build module user mapping rendering
src/build/mod.rs
Uses user_mapping_subject() when rendering CREATE USER MAPPING ... FOR <user> and DROP USER MAPPING ... FOR <user> entries.
Unit tests, integration fixtures, gates, and documentation
src/deploy/alter.rs, tests/common/mod.rs, tests/deploy.rs, bin/deploy-gates, docs/commands.md
Unit tests cover all new ALTER reconciliation paths. mutated_foreign_archive test helper constructs a diverged FDW archive with option drift and an orphan server. Integration tests verify in-place ALTERs for drifted objects, redacted-password exclusion, gated server DROP, and empty plan when objects match. The gate script creates FDW objects and adds option drift to the convergence mutation phase. Documentation adds reconciliation rules for all four new object types including password redaction behavior.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as pglifecycle deploy
    participant Diff as diff::diff()
    participant Alter as alter::resolve()
    participant Mod as mod::drop_sql()

    CLI->>Diff: compute diff(repo, database_snapshot)
    Diff->>Diff: database_index() — index FDW/server/user_mapping
    Diff->>Diff: modeled() — classify ForeignDataWrapper/Server/UserMapping
    Diff-->>CLI: Diff { removed: BTreeMap ObjectKey Definition, changed: [...] }

    loop each changed FDW object
        CLI->>Alter: resolve(repo_def, db_def)
        alt ForeignDataWrapper
            Alter->>Alter: fdw(repo, db) — ALTER handler/validator/options/comment
        else Server
            Alter->>Alter: server(repo, db) — ALTER version/options or replace
        else UserMapping
            Alter->>Alter: user_mapping(repo, db) — per-server CREATE/ALTER/DROP
        else Table with server
            Alter->>Alter: foreign_table(repo, db) — ALTER OPTIONS/comment or replace
        end
        Alter-->>CLI: Resolution::Alter(sql) or Resolution::Replace
    end

    loop each removed object
        CLI->>Mod: drop_sql(key, Some(definition))
        alt UserMapping definition
            Mod-->>CLI: DROP USER MAPPING IF EXISTS ... FOR server per server
        else other
            Mod-->>CLI: DROP object_type IF EXISTS qualified_name
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • gmr/pglifecycle#45: Adds Table.server and Table.options propagation and forces Resolution::Replace in src/deploy/alter.rs when those fields differ — the direct predecessor to this PR's foreign_table in-place reconciliation logic.
  • gmr/pglifecycle#25: Establishes the core src/deploy/alter.rs/diff.rs/deploy-planning reconciliation framework that this PR extends with FDW/server/user-mapping/foreign-table resolvers.
  • gmr/pglifecycle#26: Introduces the bin/deploy-gates convergence gate infrastructure and alter.rs in-place rendering paths that this PR builds upon for FDW objects.

Poem

🐇 Hop, hop through the foreign lands,
Where wrappers and servers join paws and hands,
User mappings altered, passwords kept tight,
No orphaned server escapes my sight!
OPTIONS in place, the diff comes up clean —
The tidiest deploy a rabbit has seen. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Reconcile foreign objects in deploy' directly and concisely summarizes the main change: extending the deploy command to handle reconciliation of foreign data objects.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/deploy-foreign-objects

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

@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: 2

🧹 Nitpick comments (1)
src/deploy/mod.rs (1)

375-400: 💤 Low value

Consider documenting the invariant that UserMapping keys always have definitions.

If a UserMapping key ever reaches this function with definition: None, the fallback path generates invalid SQL (DROP USER MAPPING IF EXISTS name; without the required FOR user SERVER server clause). The current code flow guarantees definitions are present, but the invariant is implicit.

A defensive assertion or comment would prevent future refactoring from accidentally violating this:

♻️ Suggested defensive check
 fn drop_sql(key: &ObjectKey, definition: Option<&Definition>) -> String {
     if let Some(Definition::UserMapping(mapping)) = definition {
         return mapping
             .servers
             .iter()
             .map(|server| {
                 format!(
                     "DROP USER MAPPING IF EXISTS FOR {} SERVER {};\n",
                     quote_ident(&mapping.name),
                     quote_ident(&server.name),
                 )
             })
             .collect();
     }
+    // user mappings require the definition to render per-server drops;
+    // the diff guarantees removed keys have definitions, so this is
+    // unreachable for UserMapping—assert to catch future refactoring
+    debug_assert!(
+        key.desc != constants::ObjectType::UserMapping,
+        "UserMapping drop requires a definition"
+    );
     // function keys are identity signatures and must not be quoted
🤖 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/deploy/mod.rs` around lines 375 - 400, The drop_sql function has an
implicit invariant that UserMapping keys must always have a
Definition::UserMapping provided, but if a UserMapping key reaches the fallback
path with definition: None, it would generate invalid SQL missing the required
FOR user SERVER server clause. Add a defensive assertion or comment at the
beginning of the function to explicitly document and enforce this invariant:
when key.desc indicates a UserMapping object type, the definition parameter must
always be Some(Definition::UserMapping), ensuring future refactoring doesn't
accidentally violate this contract.
🤖 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 `@docs/commands.md`:
- Around line 89-90: The Foreign servers bullet point in the documentation
currently describes wrapper and TYPE changes, but does not document the fallback
behavior when an existing server VERSION is cleared or removed. Add
documentation to the Foreign servers section explaining that removing an
existing VERSION also triggers a rebuild, so the documented behavior matches the
actual implementation behavior and sets proper operator expectations.

In `@src/deploy/alter.rs`:
- Around line 770-806: PostgreSQL requires PUBLIC to be used as a keyword (not
quoted) in CREATE/ALTER/DROP USER MAPPING statements, but the current
implementation unconditionally quotes all identifiers using quote_ident().
Create a new helper function that returns PUBLIC unquoted and all other names
quoted, then replace the quote_ident(&repo.name) call in the user_mapping
function in src/deploy/alter.rs:771 with this helper function, and also apply
the same helper function at src/deploy/mod.rs:383 where the user mapping name is
being quoted.

---

Nitpick comments:
In `@src/deploy/mod.rs`:
- Around line 375-400: The drop_sql function has an implicit invariant that
UserMapping keys must always have a Definition::UserMapping provided, but if a
UserMapping key reaches the fallback path with definition: None, it would
generate invalid SQL missing the required FOR user SERVER server clause. Add a
defensive assertion or comment at the beginning of the function to explicitly
document and enforce this invariant: when key.desc indicates a UserMapping
object type, the definition parameter must always be
Some(Definition::UserMapping), ensuring future refactoring doesn't accidentally
violate this contract.
🪄 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: 0fb9994e-7048-41d1-85be-84028dbdf7ac

📥 Commits

Reviewing files that changed from the base of the PR and between deb10a9 and 9112dca.

📒 Files selected for processing (7)
  • bin/deploy-gates
  • docs/commands.md
  • src/deploy/alter.rs
  • src/deploy/diff.rs
  • src/deploy/mod.rs
  • tests/common/mod.rs
  • tests/deploy.rs

Comment thread docs/commands.md Outdated
Comment thread src/deploy/alter.rs
gmr and others added 2 commits June 16, 2026 14:20
- add utils::user_mapping_subject, leaving PUBLIC as a keyword and
  quoting every other user name
- apply it to the CREATE/DROP USER MAPPING statements in build so a
  public mapping no longer emits invalid FOR "PUBLIC" SQL
- document the foreign-server VERSION-clearing rebuild fallback in
  docs/commands.md to match deploy behavior

Addresses CodeRabbit review on PR #50.

Co-authored-by: Claude <noreply@anthropic.com>
The deploy ALTER path (deploy::alter::user_mapping) also quoted the
mapping name unconditionally, so a public mapping emitted invalid
CREATE/ALTER/DROP USER MAPPING FOR "PUBLIC" SQL. Apply
utils::user_mapping_subject there too and cover it with a test.

Addresses CodeRabbit review on PR #50.

Co-authored-by: Claude <noreply@anthropic.com>
@gmr

gmr commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

🤖 This comment was posted by Claude on behalf of @gmr

Addressed both CodeRabbit review comments:

  • USER MAPPING PUBLIC quoting (Major): Added utils::user_mapping_subject, which leaves PUBLIC as an unquoted keyword and quotes all other user names. Applied it to both the build path (build::dump_user_mapping) and the deploy ALTER path (deploy::alter::user_mapping), so CREATE/ALTER/DROP USER MAPPING no longer emit invalid FOR "PUBLIC" SQL. Covered with unit tests in both modules. Commits 544a69a, e84dfd4.
  • Foreign-server VERSION fallback docs (Minor): Documented in docs/commands.md that clearing an existing VERSION cannot be done in place and falls back to drop+recreate. Commit 544a69a.

Quality gates pass locally: cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test (142 + integration tests green). Both review threads resolved.

@gmr gmr merged commit c6d87fd into main Jun 16, 2026
2 checks passed
@gmr gmr deleted the feature/deploy-foreign-objects branch June 16, 2026 18:47
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