From de06964f19dea35aeb4ae5ccf6d42dcec0c10fbe Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Mon, 23 Feb 2026 07:07:23 -0800 Subject: [PATCH] fix: normalize view definitions to strip same-schema qualifiers (#314) pg_get_viewdef() can produce different schema qualification for extension functions depending on search_path differences between embedded postgres (desired state) and target database (current state). For example, ltree's nlevel() might appear as "public.nlevel()" on one side and "nlevel()" on the other, causing an infinite plan/apply loop. Apply the existing stripSchemaPrefixFromBody() to view definitions during normalization, matching what already happens for function and procedure bodies. Co-Authored-By: Claude Opus 4.6 --- ir/normalize.go | 13 ++++++---- ir/normalize_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) 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