Skip to content

fix: plan fails with schema-qualified references in function bodies (#399)#400

Merged
tianzhou merged 1 commit intomainfrom
fix/issue-399-schema-qualified-body
Apr 11, 2026
Merged

fix: plan fails with schema-qualified references in function bodies (#399)#400
tianzhou merged 1 commit intomainfrom
fix/issue-399-schema-qualified-body

Conversation

@tianzhou
Copy link
Copy Markdown
Contributor

Summary

  • When a SQL-language function has schema-qualified references inside its $$-delimited body (e.g., FROM public.role_caps), pgschema plan fails with operator does not exist because parameter types are stripped to the temporary schema while body references still point to the original schema
  • Fix: disable function body validation (SET check_function_bodies = off) when applying desired state SQL to temporary schemas during plan generation — the body is only needed for IR extraction, not execution
  • This preserves the existing behavior of keeping dollar-quoted bodies intact (SET search_path not showing correct on plan or apply if you use '' #354) while avoiding the type-identity mismatch

Fixes #399

Test plan

  • Added test case testdata/diff/create_function/issue_399_schema_qualified_body/ reproducing the bug
  • Verified test fails before fix, passes after
  • All existing tests pass including issue_354_empty_search_path (no regression)
  • Full test suite passes (go test ./...)

🤖 Generated with Claude Code

…399)

Disable function body validation (SET check_function_bodies = off) when
applying desired state SQL to temporary schemas. This prevents type-identity
mismatches where parameter types are stripped to the temp schema but body
references still point to the original schema.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 11, 2026 10:47
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 11, 2026

Greptile Summary

This PR fixes a plan failure that occurred when SQL-language functions had schema-qualified references inside their $$-delimited bodies (e.g., FROM public.role_caps). The root cause was a type-identity mismatch: parameter types were stripped of the public. prefix (to resolve in the temporary schema via search_path), but body references kept the original public. qualification, causing PostgreSQL's function-body validator to see them as different types.

The fix adds SET check_function_bodies = off before applying desired-state SQL to the temporary schema in both embedded.go and external.go. Since the temporary schema is only used for IR extraction (not function execution), skipping body validation at creation time is safe and correct. The fix is symmetric across both providers and is paired with a targeted test case that reproduces the original failure.

Confidence Score: 5/5

Safe to merge — the fix is minimal, well-scoped, and correct for both database providers.

The fix uses a well-known PostgreSQL GUC (check_function_bodies = off) applied only to the dedicated, short-lived connection used for IR extraction. Disabling body validation does not affect IR quality (the inspector reads from pg_catalog, not by executing functions), and any real errors in function bodies will still be caught when the generated migration is applied to the target database. The fix is symmetric across both providers, accompanied by a targeted regression test, and the existing test for issue #354 continues to pass. No P0 or P1 issues found.

No files require special attention.

Important Files Changed

Filename Overview
internal/postgres/embedded.go Adds SET check_function_bodies = off on the dedicated connection before executing desired-state SQL; correctly placed after search_path is set and before the SQL execution block.
internal/postgres/external.go Mirrors the same fix from embedded.go for the external database provider; symmetric and correct.
internal/postgres/desired_state.go Adds an explanatory comment in stripSchemaQualifications linking issue #399 to the check_function_bodies workaround and issue #354; no logic changes.
testdata/diff/create_function/issue_399_schema_qualified_body/new.sql Desired-state SQL with schema-qualified body references that previously triggered the failure; correctly represents the reported issue.
testdata/diff/create_function/issue_399_schema_qualified_body/diff.sql Expected migration output; parameter types are correctly schema-stripped while the dollar-quoted body preserves public.role_caps, matching the issue #354 requirement.

Sequence Diagram

sequenceDiagram
    participant User as User SQL Files
    participant AP as ApplySchema
    participant PG as Temp PostgreSQL Schema
    participant IR as IR Inspector

    User->>AP: SQL with public.role_type params and public.role_caps in body
    AP->>PG: SET search_path TO pgschema_tmp_xxx, public
    AP->>PG: SET check_function_bodies = off (NEW #399)
    AP->>AP: stripSchemaQualifications() - params stripped, body preserved (#354)
    AP->>PG: CREATE FUNCTION role_has_cap(p_role role_type...) AS $$ ... FROM public.role_caps ... $$
    Note over PG: Body NOT validated at creation time (check_function_bodies = off)
    PG-->>AP: Function created successfully
    AP->>IR: Inspect pg_catalog for function definition
    IR-->>AP: IR with body intact
Loading

Reviews (1): Last reviewed commit: "fix: plan fails with schema-qualified re..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes pgschema plan failures when SQL-language function bodies contain schema-qualified references by disabling function body validation while applying desired-state SQL to the temporary planning schema.

Changes:

  • Set check_function_bodies = off during desired-state application to temp schemas (embedded + external plan DB paths).
  • Document the rationale/requirement alongside schema-qualification stripping logic.
  • Add a regression test fixture reproducing issue #399 (schema-qualified reference inside $$ function body).

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/postgres/external.go Disables function body validation prior to executing rewritten desired-state SQL in the external plan DB.
internal/postgres/embedded.go Disables function body validation prior to executing rewritten desired-state SQL in the embedded planner DB.
internal/postgres/desired_state.go Documents the interaction between body-preservation and function-body validation (issues #354 / #399).
testdata/diff/create_function/issue_399_schema_qualified_body/plan.txt Expected human-readable plan output for the new regression case.
testdata/diff/create_function/issue_399_schema_qualified_body/plan.sql Expected SQL plan step output for the new regression case.
testdata/diff/create_function/issue_399_schema_qualified_body/plan.json Expected JSON plan output for the new regression case.
testdata/diff/create_function/issue_399_schema_qualified_body/old.sql Baseline SQL state for the regression fixture.
testdata/diff/create_function/issue_399_schema_qualified_body/new.sql Desired SQL state reproducing the schema-qualified body reference bug.
testdata/diff/create_function/issue_399_schema_qualified_body/diff.sql Expected diff SQL for the regression fixture.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +134 to +141
// Disable function body validation to avoid type-identity mismatches (issue #399).
// Schema qualifications inside dollar-quoted function bodies are preserved (issue #354),
// but parameter types are stripped. For SQL-language functions, PostgreSQL validates the
// body at creation time, which can fail when body references use the original schema's
// types while parameters reference the temporary schema's types.
if _, err := util.ExecContextWithLogging(ctx, conn, "SET check_function_bodies = off", "disable function body validation for desired state"); err != nil {
return fmt.Errorf("failed to disable check_function_bodies: %w", err)
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

SET check_function_bodies = off is session-scoped. Since this uses db.Conn() (a pooled connection) and conn.Close() returns it to the pool, the setting can leak into later uses of the same underlying connection. To avoid cross-call side effects, wrap the desired-state application in an explicit transaction and use SET LOCAL check_function_bodies = off, or ensure the setting is reset (e.g., RESET check_function_bodies) via defer, including on error paths.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The *sql.DB pool here is private to the ExternalDatabase instance, which is short-lived (created for plan generation, then Stop()'d). No other code shares this pool. The same session-scoped SET search_path on the line right above also has no cleanup/reset — adding a defer for check_function_bodies but not for search_path would be inconsistent. Both are fine as-is given the instance lifecycle.

// types while parameters reference the temporary schema's types.
if _, err := util.ExecContextWithLogging(ctx, conn, "SET check_function_bodies = off", "disable function body validation for desired state"); err != nil {
return fmt.Errorf("failed to disable check_function_bodies: %w", err)
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

SET check_function_bodies = off persists on the session. Because this uses db.Conn() (connection pool) and then conn.Close(), that connection may be reused later with check_function_bodies still disabled. Consider using BEGIN; SET LOCAL check_function_bodies = off; ... COMMIT; around the desired-state SQL, or resetting the GUC (RESET check_function_bodies) in a defer so pooled connections are returned to a clean state even on failures.

Suggested change
}
}
defer func() {
_, _ = conn.ExecContext(context.Background(), "RESET check_function_bodies")
}()

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as the other comment — the EmbeddedPostgres instance owns its own private *sql.DB connected to a temporary embedded PostgreSQL instance that gets destroyed after plan generation. The connection pool is never shared. The existing SET search_path two lines above follows the same pattern with no cleanup, so adding a defer only for this setting would be inconsistent.

@tianzhou tianzhou merged commit 0352fc1 into main Apr 11, 2026
6 checks passed
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.

v1.9.0: plan fails with "operator does not exist" when function body contains qualified references to the target schema

2 participants