Skip to content

fix: handle empty search_path and preserve schema qualifiers in function bodies (#354)#357

Merged
tianzhou merged 2 commits intomainfrom
fix/issue-354-empty-search-path
Mar 13, 2026
Merged

fix: handle empty search_path and preserve schema qualifiers in function bodies (#354)#357
tianzhou merged 2 commits intomainfrom
fix/issue-354-empty-search-path

Conversation

@tianzhou
Copy link
Contributor

@tianzhou tianzhou commented Mar 13, 2026

Summary

  • Bug 1: SET search_path = '' was rendered as SET search_path = "" (invalid SQL). PostgreSQL stores this as search_path="" in pg_proc.proconfig; the extracted literal "" is now correctly rendered as ''.
  • Bug 2: Schema qualifiers (e.g., public.test) were unconditionally stripped from function/procedure bodies during normalization. This broke functions with restrictive search_path settings ('', pg_temp, etc.) where qualified references are required. Fix: move stripping from normalization time to comparison time, and make SQL preprocessing skip dollar-quoted blocks.

Fixes #354

Test plan

  • Added diff test create_function/issue_354_empty_search_path covering both bugs
  • Added unit tests for splitDollarQuotedSegments (7 cases including parameter references, nested tags)
  • Updated existing test expectations for preserved qualifiers (dependency/table_to_function, issue_252, issue_320)
  • All function/procedure diff tests pass
  • All integration tests pass (TestPlanAndApply)
  • All dump tests pass

🤖 Generated with Claude Code

PostgreSQL stores SET search_path = '' as search_path="" in proconfig.
The extracted value is literal "" (two double-quote chars), which was
rendered verbatim as invalid SQL. Now correctly renders as ''.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 13, 2026 09:16
@greptile-apps
Copy link

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR fixes a specific, well-understood PostgreSQL serialization quirk: when a function is created with SET search_path = '', PostgreSQL stores search_path="" (two literal double-quote chars) in pg_proc.proconfig. The SQL query that extracts the value returns "" verbatim, which was previously rendered as SET search_path = "" — invalid SQL, since PostgreSQL interprets "" as a quoted (empty) identifier rather than an empty string. The fix adds a targeted special-case in generateFunctionSQL to translate that sentinel value back to the correct single-quoted empty string ''.

Key observations:

  • Correctness: The round-trip is stable — after the fixed SQL is applied, PostgreSQL stores search_path="" again, which the inspector reads as "", and the renderer correctly produces '' on every subsequent diff pass.
  • replaceSchemaInSearchPath interaction verified: The desired_state.go tokenizer handles '' in the search_path value correctly (no false schema-name matches), so there is no regression in the temp-schema substitution path.
  • plan.go replaceString interaction verified: Calling replaceString("") on the raw IR value leaves "" unchanged, so the downstream plan rewriting is not affected.
  • Test coverage: The added diff test case covers the create path (old state has no function; new state adds one with SET search_path = ''). The alter path (function already exists, search_path transitions from non-empty to '') is not explicitly tested, but is covered by the same generateFunctionSQL code path.
  • Procedure struct has no SearchPath field: Procedures can also carry SET search_path in PostgreSQL. That is a pre-existing gap and out of scope for this PR.
  • Minor style point: A comment noting that only the whole-value "" case is handled (not "" embedded in a composite path like pg_catalog, "") would prevent future confusion.

Confidence Score: 5/5

  • This PR is safe to merge — it fixes a concrete SQL generation bug with no side-effects on other code paths.
  • The change is minimal and targeted (8 lines in one function), the bug and fix are clearly explained, all touched code paths are verified correct (round-trip through proconfig, replaceSchemaInSearchPath, and plan.go replaceString), and the golden-file test suite validates the expected output. The only minor gap is a missing test for the alter/modify path with empty search_path, but that path calls the same generateFunctionSQL function so the fix naturally covers it.
  • No files require special attention; internal/diff/function.go is the only changed logic file and the change is straightforward.

Important Files Changed

Filename Overview
internal/diff/function.go Adds a special-case branch in generateFunctionSQL to render function.SearchPath == ""`` as SET search_path = '' instead of the raw `""` value extracted from `pg_proc.proconfig`. Logic is correct but the `""` check is tied to PostgreSQL's internal proconfig serialization format; a comment documents this coupling.
testdata/diff/create_function/issue_354_empty_search_path/new.sql New desired-state SQL for the test case: table + function with SET search_path = ''. Well-formed input for the diff test.
testdata/diff/create_function/issue_354_empty_search_path/diff.sql Expected diff output — CREATE OR REPLACE FUNCTION with SET search_path = '' (single-quoted). Confirms the bug fix produces correct SQL.

Sequence Diagram

sequenceDiagram
    participant PG as PostgreSQL (pg_proc.proconfig)
    participant Inspector as ir/Inspector
    participant IR as ir.Function.SearchPath
    participant DiffGen as generateFunctionSQL
    participant SQL as Generated SQL

    PG->>Inspector: search_path="" (proconfig entry for SET search_path = '')
    Inspector->>IR: SearchPath = `""`
    IR->>DiffGen: function.SearchPath == `""`?
    alt SearchPath is `""`
        DiffGen->>SQL: SET search_path = '' ✅
    else SearchPath is non-empty (e.g. pg_catalog, public)
        DiffGen->>SQL: SET search_path = pg_catalog, public ✅
    else SearchPath is ""
        Note over DiffGen,SQL: Before fix: SET search_path = "" ❌ (invalid SQL)
    end
Loading

Last reviewed commit: e46e036

Comment on lines 248 to 256
if function.SearchPath != "" {
stmt.WriteString(fmt.Sprintf("\nSET search_path = %s", function.SearchPath))
// PostgreSQL stores SET search_path = '' as search_path="" in proconfig.
// The extracted value is "" (two double-quote chars). Render as '' (single-quoted empty string).
if function.SearchPath == `""` {
stmt.WriteString("\nSET search_path = ''")
} else {
stmt.WriteString(fmt.Sprintf("\nSET search_path = %s", function.SearchPath))
}
}
Copy link

Choose a reason for hiding this comment

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

Consider also covering partial "" in a composite path

The current check only fires when the entire SearchPath value equals "" (i.e., search_path="" was the only proconfig entry, representing a fully empty path). If PostgreSQL ever stores a composite value such as pg_catalog, "" — where the empty element appears alongside real schemas — the literal "" in that string would fall through to the else branch and be rendered verbatim as a double-quoted identifier, rather than ''.

In practice this is very unlikely, but since the fix already has a comment explaining the proconfig format, it might be worth a brief note here that only the whole-value case is handled:

Suggested change
if function.SearchPath != "" {
stmt.WriteString(fmt.Sprintf("\nSET search_path = %s", function.SearchPath))
// PostgreSQL stores SET search_path = '' as search_path="" in proconfig.
// The extracted value is "" (two double-quote chars). Render as '' (single-quoted empty string).
if function.SearchPath == `""` {
stmt.WriteString("\nSET search_path = ''")
} else {
stmt.WriteString(fmt.Sprintf("\nSET search_path = %s", function.SearchPath))
}
}
if function.SearchPath != "" {
// PostgreSQL stores SET search_path = '' as search_path="" in proconfig.
// The extracted value is "" (two double-quote chars). Render as '' (single-quoted empty string).
// Note: only the whole-value empty case is handled; mixed paths (e.g. pg_catalog, "") are not expected.
if function.SearchPath == `""` {
stmt.WriteString("\nSET search_path = ''")
} else {
stmt.WriteString(fmt.Sprintf("\nSET search_path = %s", function.SearchPath))
}
}

Copy link
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 function DDL rendering for SET search_path = '' by translating PostgreSQL’s pg_proc.proconfig encoding (search_path="") into valid SQL (SET search_path = '') during diff/plan generation.

Changes:

  • Special-case rendering of function.SearchPath == "\"\"" to emit SET search_path = '' in generated function SQL.
  • Added a new golden diff fixture covering empty search_path for functions (create_function/issue_354_empty_search_path).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
internal/diff/function.go Adjusts function SQL generation to render empty search_path correctly.
testdata/diff/create_function/issue_354_empty_search_path/plan.txt Adds expected human plan output for the new empty search_path case.
testdata/diff/create_function/issue_354_empty_search_path/plan.sql Adds expected SQL plan output for the new empty search_path case.
testdata/diff/create_function/issue_354_empty_search_path/plan.json Adds expected JSON plan output for the new empty search_path case.
testdata/diff/create_function/issue_354_empty_search_path/old.sql Adds old-state SQL for the golden diff fixture.
testdata/diff/create_function/issue_354_empty_search_path/new.sql Adds new-state SQL for the golden diff fixture.
testdata/diff/create_function/issue_354_empty_search_path/diff.sql Adds expected diff SQL for the golden diff fixture.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 245 to +252
// Add SET search_path if specified
// Note: Output without outer quotes to handle multi-schema paths correctly
// e.g., "SET search_path = pg_catalog, public" not "SET search_path = 'pg_catalog, public'"
if function.SearchPath != "" {
stmt.WriteString(fmt.Sprintf("\nSET search_path = %s", function.SearchPath))
// PostgreSQL stores SET search_path = '' as search_path="" in proconfig.
// The extracted value is "" (two double-quote chars). Render as '' (single-quoted empty string).
if function.SearchPath == `""` {
stmt.WriteString("\nSET search_path = ''")
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
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 function DDL rendering for the special case where PostgreSQL stores SET search_path = '' as search_path="" in pg_proc.proconfig, ensuring emitted SQL uses valid '' instead of invalid "".

Changes:

  • Special-case Function.SearchPath == "\"\"" when generating function SQL to emit SET search_path = ''.
  • Add a new function diff fixture covering empty search_path rendering (issue_354_empty_search_path).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
internal/diff/function.go Renders the search_path="" stored form as SET search_path = '' to avoid invalid SQL.
testdata/diff/create_function/issue_354_empty_search_path/plan.txt Adds expected plan output for the empty search_path function case.
testdata/diff/create_function/issue_354_empty_search_path/plan.sql Adds expected generated SQL for the empty search_path function case.
testdata/diff/create_function/issue_354_empty_search_path/plan.json Adds expected JSON plan output verifying the emitted SQL contains SET search_path = ''.
testdata/diff/create_function/issue_354_empty_search_path/old.sql Adds the “before” schema for the new diff fixture.
testdata/diff/create_function/issue_354_empty_search_path/new.sql Adds the “after” schema including a function with SET search_path = ''.
testdata/diff/create_function/issue_354_empty_search_path/diff.sql Adds the expected diff SQL output for the new fixture.

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

You can also share your feedback on Copilot code review. Take the survey.

@tianzhou tianzhou merged commit a79a7e1 into main Mar 13, 2026
5 checks passed
@tianzhou tianzhou changed the title fix: render empty search_path as '' instead of "" (#354) fix: handle empty search_path and preserve schema qualifiers in function bodies (#354) Mar 13, 2026
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.

SET search_path not showing correct on plan or apply if you use ''

2 participants