Skip to content

Commit 0352fc1

Browse files
tianzhouclaude
andauthored
fix: plan fails with schema-qualified references in function bodies (#399) (#400)
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>
1 parent 194f72e commit 0352fc1

9 files changed

Lines changed: 128 additions & 0 deletions

File tree

internal/postgres/desired_state.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ func stripSchemaQualifications(sql string, schemaName string) string {
104104
// Schema qualifiers inside function/procedure bodies (dollar-quoted blocks)
105105
// must be preserved — the user may need them when search_path doesn't include
106106
// the function's schema (e.g., SET search_path = ''). (Issue #354)
107+
//
108+
// To avoid type-identity mismatches between stripped parameter types and
109+
// unstripped body references (Issue #399), callers should disable function
110+
// body validation with SET check_function_bodies = off before executing
111+
// the resulting SQL.
107112
segments := splitDollarQuotedSegments(sql)
108113
var result strings.Builder
109114
result.Grow(len(sql))

internal/postgres/embedded.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,15 @@ func (ep *EmbeddedPostgres) ApplySchema(ctx context.Context, schema string, sql
220220
return fmt.Errorf("failed to set search_path: %w", err)
221221
}
222222

223+
// Disable function body validation to avoid type-identity mismatches (issue #399).
224+
// Schema qualifications inside dollar-quoted function bodies are preserved (issue #354),
225+
// but parameter types are stripped. For SQL-language functions, PostgreSQL validates the
226+
// body at creation time, which can fail when body references use the original schema's
227+
// types while parameters reference the temporary schema's types.
228+
if _, err := util.ExecContextWithLogging(ctx, conn, "SET check_function_bodies = off", "disable function body validation for desired state"); err != nil {
229+
return fmt.Errorf("failed to disable check_function_bodies: %w", err)
230+
}
231+
223232
// Strip schema qualifications from SQL before applying to temporary schema
224233
// This ensures that objects are created in the temporary schema via search_path
225234
// rather than being explicitly qualified with the original schema name

internal/postgres/external.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,15 @@ func (ed *ExternalDatabase) ApplySchema(ctx context.Context, schema string, sql
131131
return fmt.Errorf("failed to set search_path: %w", err)
132132
}
133133

134+
// Disable function body validation to avoid type-identity mismatches (issue #399).
135+
// Schema qualifications inside dollar-quoted function bodies are preserved (issue #354),
136+
// but parameter types are stripped. For SQL-language functions, PostgreSQL validates the
137+
// body at creation time, which can fail when body references use the original schema's
138+
// types while parameters reference the temporary schema's types.
139+
if _, err := util.ExecContextWithLogging(ctx, conn, "SET check_function_bodies = off", "disable function body validation for desired state"); err != nil {
140+
return fmt.Errorf("failed to disable check_function_bodies: %w", err)
141+
}
142+
134143
// Strip schema qualifications from SQL before applying to temporary schema
135144
// This ensures that objects are created in the temporary schema via search_path
136145
// rather than being explicitly qualified with the original schema name
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
CREATE OR REPLACE FUNCTION role_has_cap(
2+
p_role role_type,
3+
p_cap text
4+
)
5+
RETURNS boolean
6+
LANGUAGE sql
7+
STABLE
8+
AS $$
9+
SELECT EXISTS (
10+
SELECT 1
11+
FROM public.role_caps rc
12+
WHERE rc.role = p_role
13+
AND rc.capability = p_cap
14+
);
15+
$$;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
CREATE TYPE public.role_type AS ENUM ('OWNER', 'MEMBER');
2+
3+
CREATE TABLE public.role_caps (
4+
role public.role_type NOT NULL,
5+
capability text NOT NULL,
6+
PRIMARY KEY (role, capability)
7+
);
8+
9+
CREATE OR REPLACE FUNCTION public.role_has_cap(
10+
p_role public.role_type,
11+
p_cap text
12+
) RETURNS boolean
13+
LANGUAGE sql
14+
STABLE
15+
AS $$
16+
SELECT EXISTS (
17+
SELECT 1
18+
FROM public.role_caps rc
19+
WHERE rc.role = p_role
20+
AND rc.capability = p_cap
21+
);
22+
$$;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE TYPE role_type AS ENUM ('OWNER', 'MEMBER');
2+
3+
CREATE TABLE role_caps (
4+
role role_type NOT NULL,
5+
capability text NOT NULL,
6+
PRIMARY KEY (role, capability)
7+
);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"version": "1.0.0",
3+
"pgschema_version": "1.9.0",
4+
"created_at": "1970-01-01T00:00:00Z",
5+
"source_fingerprint": {
6+
"hash": "eb148b37b7b6325bdd5f0c1c120dfe0bd71a062ce69951aa946c452aff2dc662"
7+
},
8+
"groups": [
9+
{
10+
"steps": [
11+
{
12+
"sql": "CREATE OR REPLACE FUNCTION role_has_cap(\n p_role role_type,\n p_cap text\n)\nRETURNS boolean\nLANGUAGE sql\nSTABLE\nAS $$\n SELECT EXISTS (\n SELECT 1\n FROM public.role_caps rc\n WHERE rc.role = p_role\n AND rc.capability = p_cap\n );\n$$;",
13+
"type": "function",
14+
"operation": "create",
15+
"path": "public.role_has_cap"
16+
}
17+
]
18+
}
19+
]
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
CREATE OR REPLACE FUNCTION role_has_cap(
2+
p_role role_type,
3+
p_cap text
4+
)
5+
RETURNS boolean
6+
LANGUAGE sql
7+
STABLE
8+
AS $$
9+
SELECT EXISTS (
10+
SELECT 1
11+
FROM public.role_caps rc
12+
WHERE rc.role = p_role
13+
AND rc.capability = p_cap
14+
);
15+
$$;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Plan: 1 to add.
2+
3+
Summary by type:
4+
functions: 1 to add
5+
6+
Functions:
7+
+ role_has_cap
8+
9+
DDL to be executed:
10+
--------------------------------------------------
11+
12+
CREATE OR REPLACE FUNCTION role_has_cap(
13+
p_role role_type,
14+
p_cap text
15+
)
16+
RETURNS boolean
17+
LANGUAGE sql
18+
STABLE
19+
AS $$
20+
SELECT EXISTS (
21+
SELECT 1
22+
FROM public.role_caps rc
23+
WHERE rc.role = p_role
24+
AND rc.capability = p_cap
25+
);
26+
$$;

0 commit comments

Comments
 (0)