diff --git a/ir/normalize.go b/ir/normalize.go index 909654f3..1d95ce4e 100644 --- a/ir/normalize.go +++ b/ir/normalize.go @@ -267,16 +267,19 @@ func normalizePolicyExpression(expr string, tableSchema string) string { // normalizeView normalizes view definition. // -// Since both desired state (from embedded postgres) and current state (from target database) -// now come from the same PostgreSQL version via pg_get_viewdef(), they produce identical -// output and no normalization is needed. +// While both desired state (from embedded postgres) and current state (from target database) +// come from pg_get_viewdef(), they may differ in schema qualification of functions and tables. +// This happens when extension functions (e.g., ltree's nlevel()) or search_path differences +// cause one side to produce "public.func()" and the other "func()". +// Stripping same-schema qualifiers ensures the definitions compare as equal. (Issue #314) func normalizeView(view *View) { if view == nil { return } - // View definition needs no normalization - both IR forms come from database inspection - // at the same PostgreSQL version, so pg_get_viewdef() output is identical. + // Strip same-schema qualifiers from view definition for consistent comparison. + // This uses the same logic as function/procedure body normalization. + view.Definition = stripSchemaPrefixFromBody(view.Definition, view.Schema) // Normalize triggers on the view (e.g., INSTEAD OF triggers) for _, trigger := range view.Triggers { diff --git a/ir/normalize_test.go b/ir/normalize_test.go index b4f4ef77..78760e3a 100644 --- a/ir/normalize_test.go +++ b/ir/normalize_test.go @@ -101,6 +101,66 @@ func TestStripSchemaPrefixFromBody(t *testing.T) { } } +func TestNormalizeViewStripsSchemaPrefixFromDefinition(t *testing.T) { + tests := []struct { + name string + schema string + definition string + expected string + }{ + { + name: "strips same-schema function qualification", + schema: "public", + definition: " SELECT id,\n created_at\n FROM categories c\n WHERE public.nlevel(path) = 8", + expected: " SELECT id,\n created_at\n FROM categories c\n WHERE nlevel(path) = 8", + }, + { + name: "preserves cross-schema function qualification", + schema: "public", + definition: " SELECT id\n FROM t\n WHERE other_schema.some_func(x) = 1", + expected: " SELECT id\n FROM t\n WHERE other_schema.some_func(x) = 1", + }, + { + name: "strips same-schema table reference", + schema: "public", + definition: " SELECT id\n FROM public.categories c\n WHERE nlevel(path) = 8", + expected: " SELECT id\n FROM categories c\n WHERE nlevel(path) = 8", + }, + { + name: "no-op when no schema prefix present", + schema: "public", + definition: " SELECT id,\n created_at\n FROM categories c\n WHERE nlevel(path) = 8", + expected: " SELECT id,\n created_at\n FROM categories c\n WHERE nlevel(path) = 8", + }, + { + name: "strips multiple occurrences", + schema: "myschema", + definition: " SELECT myschema.func1(x), myschema.func2(y)\n FROM myschema.tbl", + expected: " SELECT func1(x), func2(y)\n FROM tbl", + }, + { + name: "preserves string literals containing schema prefix", + schema: "public", + definition: " SELECT 'public.data' AS label\n FROM public.categories", + expected: " SELECT 'public.data' AS label\n FROM categories", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + view := &View{ + Schema: tt.schema, + Name: "test_view", + Definition: tt.definition, + } + normalizeView(view) + if view.Definition != tt.expected { + t.Errorf("normalizeView() definition = %q, want %q", view.Definition, tt.expected) + } + }) + } +} + func TestNormalizeCheckClause(t *testing.T) { tests := []struct { name string