diff --git a/internal/diff/function.go b/internal/diff/function.go index 1b7112fa..01c86659 100644 --- a/internal/diff/function.go +++ b/internal/diff/function.go @@ -223,6 +223,13 @@ func generateFunctionSQL(function *ir.Function, targetSchema string) string { } // Note: Don't output PARALLEL UNSAFE (it's the default) + // 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)) + } + // Add the function body if function.Definition != "" { // Check if this uses RETURN clause syntax (PG14+) @@ -393,6 +400,9 @@ func functionsEqual(old, new *ir.Function) bool { if old.Parallel != new.Parallel { return false } + if old.SearchPath != new.SearchPath { + return false + } if old.Comment != new.Comment { return false } @@ -439,6 +449,9 @@ func functionsEqualExceptComment(old, new *ir.Function) bool { if old.Parallel != new.Parallel { return false } + if old.SearchPath != new.SearchPath { + return false + } // Note: We intentionally do NOT compare Comment here oldInputParams := filterNonTableParameters(old.Parameters) diff --git a/ir/inspector.go b/ir/inspector.go index cc532a50..64f9507a 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -934,6 +934,12 @@ func (i *Inspector) buildFunctions(ctx context.Context, schema *IR, targetSchema // This signature includes all parameter information including modes, names, types, and defaults parameters := i.parseParametersFromSignature(signature, schemaName) + // Handle search_path + searchPath := "" + if fn.SearchPath.Valid { + searchPath = fn.SearchPath.String + } + function := &Function{ Schema: schemaName, Name: functionName, @@ -947,6 +953,7 @@ func (i *Inspector) buildFunctions(ctx context.Context, schema *IR, targetSchema IsSecurityDefiner: isSecurityDefiner, IsLeakproof: isLeakproof, Parallel: parallelMode, + SearchPath: searchPath, } // Use name(arguments) as key to support function overloading diff --git a/ir/ir.go b/ir/ir.go index 242fa65f..05d613d3 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -136,6 +136,7 @@ type Function struct { IsSecurityDefiner bool `json:"is_security_definer,omitempty"` // SECURITY DEFINER IsLeakproof bool `json:"is_leakproof,omitempty"` // LEAKPROOF Parallel string `json:"parallel,omitempty"` // SAFE, UNSAFE, RESTRICTED + SearchPath string `json:"search_path,omitempty"` // SET search_path value } // GetArguments returns the function arguments string (types only) for function identification. diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index fe2164ab..d331421a 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -979,7 +979,8 @@ SELECT p.proisstrict AS is_strict, p.prosecdef AS is_security_definer, p.proleakproof AS is_leakproof, - p.proparallel AS parallel_mode + p.proparallel AS parallel_mode, + (SELECT substring(cfg FROM 'search_path=(.*)') FROM unnest(p.proconfig) AS cfg WHERE cfg LIKE 'search_path=%') AS search_path FROM information_schema.routines r LEFT JOIN pg_proc p ON p.proname = r.routine_name AND p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = r.routine_schema) diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index 8fe65638..8e5f00f8 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -1335,7 +1335,8 @@ SELECT p.proisstrict AS is_strict, p.prosecdef AS is_security_definer, p.proleakproof AS is_leakproof, - p.proparallel AS parallel_mode + p.proparallel AS parallel_mode, + (SELECT substring(cfg FROM 'search_path=(.*)') FROM unnest(p.proconfig) AS cfg WHERE cfg LIKE 'search_path=%') AS search_path FROM information_schema.routines r LEFT JOIN pg_proc p ON p.proname = r.routine_name AND p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = r.routine_schema) @@ -1362,6 +1363,7 @@ type GetFunctionsForSchemaRow struct { IsSecurityDefiner bool `db:"is_security_definer" json:"is_security_definer"` IsLeakproof bool `db:"is_leakproof" json:"is_leakproof"` ParallelMode interface{} `db:"parallel_mode" json:"parallel_mode"` + SearchPath sql.NullString `db:"search_path" json:"search_path"` } // GetFunctionsForSchema retrieves all user-defined functions for a specific schema @@ -1389,6 +1391,7 @@ func (q *Queries) GetFunctionsForSchema(ctx context.Context, dollar_1 sql.NullSt &i.IsSecurityDefiner, &i.IsLeakproof, &i.ParallelMode, + &i.SearchPath, ); err != nil { return nil, err } diff --git a/testdata/diff/create_function/add_function/diff.sql b/testdata/diff/create_function/add_function/diff.sql index 84ab1350..b7c08968 100644 --- a/testdata/diff/create_function/add_function/diff.sql +++ b/testdata/diff/create_function/add_function/diff.sql @@ -38,6 +38,7 @@ STRICT SECURITY DEFINER LEAKPROOF PARALLEL RESTRICTED +SET search_path = pg_catalog, public AS $$ DECLARE total numeric; diff --git a/testdata/diff/create_function/add_function/new.sql b/testdata/diff/create_function/add_function/new.sql index d5f4177d..8b5dd545 100644 --- a/testdata/diff/create_function/add_function/new.sql +++ b/testdata/diff/create_function/add_function/new.sql @@ -20,6 +20,7 @@ STRICT SECURITY DEFINER LEAKPROOF PARALLEL RESTRICTED +SET search_path = pg_catalog, public AS $$ DECLARE total numeric; diff --git a/testdata/diff/create_function/add_function/plan.json b/testdata/diff/create_function/add_function/plan.json index 20778f44..e3f52ebe 100644 --- a/testdata/diff/create_function/add_function/plan.json +++ b/testdata/diff/create_function/add_function/plan.json @@ -21,7 +21,7 @@ "path": "public.mask_sensitive_data" }, { - "sql": "CREATE OR REPLACE FUNCTION process_order(\n order_id integer,\n discount_percent numeric DEFAULT 0,\n priority_level integer DEFAULT 1,\n note varchar DEFAULT '',\n status text DEFAULT 'pending',\n apply_tax boolean DEFAULT true,\n is_priority boolean DEFAULT false,\n expiry_date date DEFAULT (CURRENT_DATE + '1 year'::interval)\n)\nRETURNS numeric\nLANGUAGE plpgsql\nVOLATILE\nSTRICT\nSECURITY DEFINER\nLEAKPROOF\nPARALLEL RESTRICTED\nAS $$\nDECLARE\n total numeric;\nBEGIN\n SELECT amount INTO total FROM orders WHERE id = order_id;\n RETURN total - (total * discount_percent / 100);\nEND;\n$$;", + "sql": "CREATE OR REPLACE FUNCTION process_order(\n order_id integer,\n discount_percent numeric DEFAULT 0,\n priority_level integer DEFAULT 1,\n note varchar DEFAULT '',\n status text DEFAULT 'pending',\n apply_tax boolean DEFAULT true,\n is_priority boolean DEFAULT false,\n expiry_date date DEFAULT (CURRENT_DATE + '1 year'::interval)\n)\nRETURNS numeric\nLANGUAGE plpgsql\nVOLATILE\nSTRICT\nSECURITY DEFINER\nLEAKPROOF\nPARALLEL RESTRICTED\nSET search_path = pg_catalog, public\nAS $$\nDECLARE\n total numeric;\nBEGIN\n SELECT amount INTO total FROM orders WHERE id = order_id;\n RETURN total - (total * discount_percent / 100);\nEND;\n$$;", "type": "function", "operation": "create", "path": "public.process_order" diff --git a/testdata/diff/create_function/add_function/plan.sql b/testdata/diff/create_function/add_function/plan.sql index 84ab1350..b7c08968 100644 --- a/testdata/diff/create_function/add_function/plan.sql +++ b/testdata/diff/create_function/add_function/plan.sql @@ -38,6 +38,7 @@ STRICT SECURITY DEFINER LEAKPROOF PARALLEL RESTRICTED +SET search_path = pg_catalog, public AS $$ DECLARE total numeric; diff --git a/testdata/diff/create_function/add_function/plan.txt b/testdata/diff/create_function/add_function/plan.txt index 40b91823..25da216f 100644 --- a/testdata/diff/create_function/add_function/plan.txt +++ b/testdata/diff/create_function/add_function/plan.txt @@ -51,6 +51,7 @@ STRICT SECURITY DEFINER LEAKPROOF PARALLEL RESTRICTED +SET search_path = pg_catalog, public AS $$ DECLARE total numeric;