-
Notifications
You must be signed in to change notification settings - Fork 41
fix: plan fails with schema-qualified references in function bodies (#399) #400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -131,6 +131,15 @@ func (ed *ExternalDatabase) ApplySchema(ctx context.Context, schema string, sql | |
| return fmt.Errorf("failed to set search_path: %w", err) | ||
| } | ||
|
|
||
| // 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) | ||
| } | ||
|
Comment on lines
+134
to
+141
|
||
|
|
||
| // Strip schema qualifications from SQL before applying to temporary schema | ||
| // This ensures that objects are created in the temporary schema via search_path | ||
| // rather than being explicitly qualified with the original schema name | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| CREATE OR REPLACE FUNCTION role_has_cap( | ||
| p_role role_type, | ||
| p_cap text | ||
| ) | ||
| RETURNS boolean | ||
| LANGUAGE sql | ||
| STABLE | ||
| AS $$ | ||
| SELECT EXISTS ( | ||
| SELECT 1 | ||
| FROM public.role_caps rc | ||
| WHERE rc.role = p_role | ||
| AND rc.capability = p_cap | ||
| ); | ||
| $$; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| CREATE TYPE public.role_type AS ENUM ('OWNER', 'MEMBER'); | ||
|
|
||
| CREATE TABLE public.role_caps ( | ||
| role public.role_type NOT NULL, | ||
| capability text NOT NULL, | ||
| PRIMARY KEY (role, capability) | ||
| ); | ||
|
|
||
| CREATE OR REPLACE FUNCTION public.role_has_cap( | ||
| p_role public.role_type, | ||
| p_cap text | ||
| ) RETURNS boolean | ||
| LANGUAGE sql | ||
| STABLE | ||
| AS $$ | ||
| SELECT EXISTS ( | ||
| SELECT 1 | ||
| FROM public.role_caps rc | ||
| WHERE rc.role = p_role | ||
| AND rc.capability = p_cap | ||
| ); | ||
| $$; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| CREATE TYPE role_type AS ENUM ('OWNER', 'MEMBER'); | ||
|
|
||
| CREATE TABLE role_caps ( | ||
| role role_type NOT NULL, | ||
| capability text NOT NULL, | ||
| PRIMARY KEY (role, capability) | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "version": "1.0.0", | ||
| "pgschema_version": "1.9.0", | ||
| "created_at": "1970-01-01T00:00:00Z", | ||
| "source_fingerprint": { | ||
| "hash": "eb148b37b7b6325bdd5f0c1c120dfe0bd71a062ce69951aa946c452aff2dc662" | ||
| }, | ||
| "groups": [ | ||
| { | ||
| "steps": [ | ||
| { | ||
| "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$$;", | ||
| "type": "function", | ||
| "operation": "create", | ||
| "path": "public.role_has_cap" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| CREATE OR REPLACE FUNCTION role_has_cap( | ||
| p_role role_type, | ||
| p_cap text | ||
| ) | ||
| RETURNS boolean | ||
| LANGUAGE sql | ||
| STABLE | ||
| AS $$ | ||
| SELECT EXISTS ( | ||
| SELECT 1 | ||
| FROM public.role_caps rc | ||
| WHERE rc.role = p_role | ||
| AND rc.capability = p_cap | ||
| ); | ||
| $$; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| Plan: 1 to add. | ||
|
|
||
| Summary by type: | ||
| functions: 1 to add | ||
|
|
||
| Functions: | ||
| + role_has_cap | ||
|
|
||
| DDL to be executed: | ||
| -------------------------------------------------- | ||
|
|
||
| CREATE OR REPLACE FUNCTION role_has_cap( | ||
| p_role role_type, | ||
| p_cap text | ||
| ) | ||
| RETURNS boolean | ||
| LANGUAGE sql | ||
| STABLE | ||
| AS $$ | ||
| SELECT EXISTS ( | ||
| SELECT 1 | ||
| FROM public.role_caps rc | ||
| WHERE rc.role = p_role | ||
| AND rc.capability = p_cap | ||
| ); | ||
| $$; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SET check_function_bodies = offpersists on the session. Because this usesdb.Conn()(connection pool) and thenconn.Close(), that connection may be reused later withcheck_function_bodiesstill disabled. Consider usingBEGIN; SET LOCAL check_function_bodies = off; ... COMMIT;around the desired-state SQL, or resetting the GUC (RESET check_function_bodies) in adeferso pooled connections are returned to a clean state even on failures.There was a problem hiding this comment.
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
EmbeddedPostgresinstance owns its own private*sql.DBconnected to a temporary embedded PostgreSQL instance that gets destroyed after plan generation. The connection pool is never shared. The existingSET search_pathtwo lines above follows the same pattern with no cleanup, so adding a defer only for this setting would be inconsistent.