Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions ir/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
60 changes: 60 additions & 0 deletions ir/normalize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down