-
Notifications
You must be signed in to change notification settings - Fork 29
fix: defer functions with view dependencies to after view creation (#300) #302
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 | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1541,8 +1541,26 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto | |||||||||||
| // Create tables WITHOUT function/domain dependencies first (functions may reference these) | ||||||||||||
| deferredPolicies1, deferredConstraints1 := generateCreateTablesSQL(tablesWithoutDeps, targetSchema, collector, existingTables, shouldDeferPolicy) | ||||||||||||
|
|
||||||||||||
| // Create functions (functions may depend on tables created above) | ||||||||||||
| generateCreateFunctionsSQL(d.addedFunctions, targetSchema, collector) | ||||||||||||
| // Build view lookup - needed for detecting functions that depend on views | ||||||||||||
| newViewLookup := buildViewLookup(d.addedViews) | ||||||||||||
|
|
||||||||||||
| // Separate functions into those with/without view dependencies | ||||||||||||
| // Functions that reference views in their return type or parameters must be created after views | ||||||||||||
| functionsWithoutViewDeps := d.addedFunctions | ||||||||||||
| var functionsWithViewDeps []*ir.Function | ||||||||||||
| if len(newViewLookup) > 0 { | ||||||||||||
| functionsWithoutViewDeps = nil | ||||||||||||
| for _, fn := range d.addedFunctions { | ||||||||||||
| if functionReferencesNewView(fn, newViewLookup) { | ||||||||||||
| functionsWithViewDeps = append(functionsWithViewDeps, fn) | ||||||||||||
| } else { | ||||||||||||
| functionsWithoutViewDeps = append(functionsWithoutViewDeps, fn) | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Create functions WITHOUT view dependencies (functions may depend on tables created above) | ||||||||||||
| generateCreateFunctionsSQL(functionsWithoutViewDeps, targetSchema, collector) | ||||||||||||
|
|
||||||||||||
| // Create domains WITH function dependencies (now that functions exist) | ||||||||||||
| // These domains have CHECK constraints that reference functions | ||||||||||||
|
|
@@ -1572,6 +1590,10 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto | |||||||||||
| // Create views | ||||||||||||
| generateCreateViewsSQL(d.addedViews, targetSchema, collector) | ||||||||||||
|
|
||||||||||||
| // Create functions WITH view dependencies (now that views exist) | ||||||||||||
| // These functions reference views in their return type or parameter types (issue #300) | ||||||||||||
| generateCreateFunctionsSQL(functionsWithViewDeps, targetSchema, collector) | ||||||||||||
|
|
||||||||||||
| // Revoke default grants on new tables that the user explicitly didn't include | ||||||||||||
| // This must happen AFTER tables are created but BEFORE explicit grants | ||||||||||||
| // See https://github.com/pgplex/pgschema/issues/253 | ||||||||||||
|
|
@@ -1828,30 +1850,113 @@ func columnExistsInTables(tables map[string]*ir.Table, schema, tableName, column | |||||||||||
| return false | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // buildFunctionLookup returns case-insensitive lookup keys for newly added functions. | ||||||||||||
| // Keys include both unqualified (function name only) and schema-qualified identifiers. | ||||||||||||
| func buildFunctionLookup(functions []*ir.Function) map[string]struct{} { | ||||||||||||
| if len(functions) == 0 { | ||||||||||||
| // buildSchemaNameLookup builds a case-insensitive lookup map from schema/name pairs. | ||||||||||||
| // Keys include both unqualified (name only) and schema-qualified identifiers. | ||||||||||||
| func buildSchemaNameLookup(names []struct{ schema, name string }) map[string]struct{} { | ||||||||||||
| if len(names) == 0 { | ||||||||||||
| return nil | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| lookup := make(map[string]struct{}, len(functions)*2) | ||||||||||||
| for _, fn := range functions { | ||||||||||||
| if fn == nil || fn.Name == "" { | ||||||||||||
| lookup := make(map[string]struct{}, len(names)*2) | ||||||||||||
| for _, n := range names { | ||||||||||||
| name := strings.ToLower(n.name) | ||||||||||||
| if name == "" { | ||||||||||||
| continue | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| name := strings.ToLower(fn.Name) | ||||||||||||
| lookup[name] = struct{}{} | ||||||||||||
|
|
||||||||||||
| if fn.Schema != "" { | ||||||||||||
| qualified := fmt.Sprintf("%s.%s", strings.ToLower(fn.Schema), name) | ||||||||||||
| lookup[qualified] = struct{}{} | ||||||||||||
| if n.schema != "" { | ||||||||||||
| lookup[strings.ToLower(n.schema)+"."+name] = struct{}{} | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| return lookup | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // buildFunctionLookup returns case-insensitive lookup keys for newly added functions. | ||||||||||||
| func buildFunctionLookup(functions []*ir.Function) map[string]struct{} { | ||||||||||||
| names := make([]struct{ schema, name string }, len(functions)) | ||||||||||||
| for i, fn := range functions { | ||||||||||||
| names[i] = struct{ schema, name string }{fn.Schema, fn.Name} | ||||||||||||
| } | ||||||||||||
| return buildSchemaNameLookup(names) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // buildViewLookup returns case-insensitive lookup keys for newly added views. | ||||||||||||
| func buildViewLookup(views []*ir.View) map[string]struct{} { | ||||||||||||
| names := make([]struct{ schema, name string }, len(views)) | ||||||||||||
| for i, v := range views { | ||||||||||||
| names[i] = struct{ schema, name string }{v.Schema, v.Name} | ||||||||||||
| } | ||||||||||||
| return buildSchemaNameLookup(names) | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+1884
to
+1891
|
||||||||||||
|
|
||||||||||||
| // functionReferencesNewView determines if a function references any newly added views | ||||||||||||
| // in its return type or parameter types. This handles cases where functions use | ||||||||||||
| // view composite types (e.g., RETURNS SETOF view_name or parameter of view_name type). | ||||||||||||
| func functionReferencesNewView(fn *ir.Function, newViews map[string]struct{}) bool { | ||||||||||||
| if len(newViews) == 0 || fn == nil { | ||||||||||||
| return false | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Check return type (e.g., "SETOF public.actor", "actor", "SETOF actor") | ||||||||||||
| if fn.ReturnType != "" { | ||||||||||||
| typeName := extractBaseTypeName(fn.ReturnType) | ||||||||||||
| if typeMatchesLookup(typeName, fn.Schema, newViews) { | ||||||||||||
| return true | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Check parameter types | ||||||||||||
| for _, param := range fn.Parameters { | ||||||||||||
|
||||||||||||
| for _, param := range fn.Parameters { | |
| for _, param := range fn.Parameters { | |
| if param == nil { | |
| continue | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| CREATE TABLE IF NOT EXISTS activity ( | ||
| id uuid, | ||
| author_id uuid, | ||
| CONSTRAINT activity_pkey PRIMARY KEY (id) | ||
| ); | ||
|
|
||
| CREATE TABLE IF NOT EXISTS contact ( | ||
| id uuid, | ||
| name text NOT NULL, | ||
| CONSTRAINT contact_pkey PRIMARY KEY (id) | ||
| ); | ||
|
|
||
| CREATE OR REPLACE VIEW actor AS | ||
| SELECT id, | ||
| name | ||
| FROM contact; | ||
|
|
||
| CREATE OR REPLACE FUNCTION get_actor( | ||
| activity activity | ||
| ) | ||
| RETURNS SETOF actor | ||
| LANGUAGE sql | ||
| STABLE | ||
| AS $$ SELECT actor.* FROM actor WHERE actor.id = activity.author_id | ||
| $$; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| -- Table whose composite type is used as a function parameter | ||
| CREATE TABLE public.activity ( | ||
| id uuid PRIMARY KEY, | ||
| author_id uuid | ||
| ); | ||
|
|
||
| -- Table used by the view | ||
| CREATE TABLE public.contact ( | ||
| id uuid PRIMARY KEY, | ||
| name text NOT NULL | ||
| ); | ||
|
|
||
| -- View referenced in the function's return type | ||
| CREATE OR REPLACE VIEW public.actor AS | ||
| SELECT id, name FROM public.contact; | ||
|
|
||
| -- Function that uses the table composite type as a parameter | ||
| -- and references a view in its return type. | ||
| -- This function must be created AFTER: | ||
| -- 1. The activity table (for the composite type parameter) | ||
| -- 2. The actor view (for RETURNS SETOF actor) | ||
| CREATE OR REPLACE FUNCTION public.get_actor(activity activity) | ||
| RETURNS SETOF actor ROWS 1 | ||
| LANGUAGE sql STABLE | ||
| AS $$ SELECT actor.* FROM actor WHERE actor.id = activity.author_id $$; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| -- Empty schema (no objects) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| { | ||
| "version": "1.0.0", | ||
| "pgschema_version": "1.7.1", | ||
| "created_at": "1970-01-01T00:00:00Z", | ||
| "source_fingerprint": { | ||
| "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" | ||
| }, | ||
| "groups": [ | ||
| { | ||
| "steps": [ | ||
| { | ||
| "sql": "CREATE TABLE IF NOT EXISTS activity (\n id uuid,\n author_id uuid,\n CONSTRAINT activity_pkey PRIMARY KEY (id)\n);", | ||
| "type": "table", | ||
| "operation": "create", | ||
| "path": "public.activity" | ||
| }, | ||
| { | ||
| "sql": "CREATE TABLE IF NOT EXISTS contact (\n id uuid,\n name text NOT NULL,\n CONSTRAINT contact_pkey PRIMARY KEY (id)\n);", | ||
| "type": "table", | ||
| "operation": "create", | ||
| "path": "public.contact" | ||
| }, | ||
| { | ||
| "sql": "CREATE OR REPLACE VIEW actor AS\n SELECT id,\n name\n FROM contact;", | ||
| "type": "view", | ||
| "operation": "create", | ||
| "path": "public.actor" | ||
| }, | ||
| { | ||
| "sql": "CREATE OR REPLACE FUNCTION get_actor(\n activity activity\n)\nRETURNS SETOF actor\nLANGUAGE sql\nSTABLE\nAS $$ SELECT actor.* FROM actor WHERE actor.id = activity.author_id\n$$;", | ||
| "type": "function", | ||
| "operation": "create", | ||
| "path": "public.get_actor" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| CREATE TABLE IF NOT EXISTS activity ( | ||
| id uuid, | ||
| author_id uuid, | ||
| CONSTRAINT activity_pkey PRIMARY KEY (id) | ||
| ); | ||
|
|
||
| CREATE TABLE IF NOT EXISTS contact ( | ||
| id uuid, | ||
| name text NOT NULL, | ||
| CONSTRAINT contact_pkey PRIMARY KEY (id) | ||
| ); | ||
|
|
||
| CREATE OR REPLACE VIEW actor AS | ||
| SELECT id, | ||
| name | ||
| FROM contact; | ||
|
|
||
| CREATE OR REPLACE FUNCTION get_actor( | ||
| activity activity | ||
| ) | ||
| RETURNS SETOF actor | ||
| LANGUAGE sql | ||
| STABLE | ||
| AS $$ SELECT actor.* FROM actor WHERE actor.id = activity.author_id | ||
| $$; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| Plan: 4 to add. | ||
|
|
||
| Summary by type: | ||
| functions: 1 to add | ||
| tables: 2 to add | ||
| views: 1 to add | ||
|
|
||
| Functions: | ||
| + get_actor | ||
|
|
||
| Tables: | ||
| + activity | ||
| + contact | ||
|
|
||
| Views: | ||
| + actor | ||
|
|
||
| DDL to be executed: | ||
| -------------------------------------------------- | ||
|
|
||
| CREATE TABLE IF NOT EXISTS activity ( | ||
| id uuid, | ||
| author_id uuid, | ||
| CONSTRAINT activity_pkey PRIMARY KEY (id) | ||
| ); | ||
|
|
||
| CREATE TABLE IF NOT EXISTS contact ( | ||
| id uuid, | ||
| name text NOT NULL, | ||
| CONSTRAINT contact_pkey PRIMARY KEY (id) | ||
| ); | ||
|
|
||
| CREATE OR REPLACE VIEW actor AS | ||
| SELECT id, | ||
| name | ||
| FROM contact; | ||
|
|
||
| CREATE OR REPLACE FUNCTION get_actor( | ||
| activity activity | ||
| ) | ||
| RETURNS SETOF actor | ||
| LANGUAGE sql | ||
| STABLE | ||
| AS $$ SELECT actor.* FROM actor WHERE actor.id = activity.author_id | ||
| $$; |
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.
The refactored buildFunctionLookup has lost the nil check that existed in the previous implementation. The old code checked
if fn == nil || fn.Name == ""before accessing fn.Name. While the current implementation may work if functions are never nil in practice, this removes a defensive programming safeguard that was previously in place. Consider adding a nil check in buildSchemaNameLookup or in the callers before constructing the names slice.