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
34 changes: 34 additions & 0 deletions ir/inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -1555,6 +1556,39 @@ func (i *Inspector) normalizeTriggerCondition(rawCondition string) string {
}
}

// Use pg_query to normalize the expression for consistent formatting
// This handles complex expressions, case normalization, etc.
normalizedCondition := normalizeExpressionWithPgQuery(condition)
Copy link

Copilot AI Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function normalizeExpressionWithPgQuery is called but not defined in this file or imported. This will cause a compilation error.

Copilot uses AI. Check for mistakes.
if normalizedCondition != "" {
condition = normalizedCondition
}

// Post-process to handle PostgreSQL's "NOT x IS DISTINCT FROM y" → "x IS NOT DISTINCT FROM y"
condition = i.normalizeNotDistinctFromPattern(condition)

return condition
}

// normalizeNotDistinctFromPattern converts "NOT x IS DISTINCT FROM y" to "x IS NOT DISTINCT FROM y"
// This handles the specific case where pg_query.Deparse converts PostgreSQL's internal
// "NOT (x IS DISTINCT FROM y)" representation to "NOT x IS DISTINCT FROM y"
func (i *Inspector) normalizeNotDistinctFromPattern(condition string) string {
// Use regex to match: "NOT <expr> IS DISTINCT FROM <expr>"
// and convert to: "<expr> IS NOT DISTINCT FROM <expr>"

// Pattern explanation:
// ^NOT\s+ - starts with "NOT" followed by whitespace
// (.+?) - non-greedy capture of left expression
// \s+IS\s+DISTINCT\s+FROM\s+ - " IS DISTINCT FROM " with flexible whitespace
// (.+)$ - capture the right expression to end of string
re := regexp.MustCompile(`^NOT\s+(.+?)\s+IS\s+DISTINCT\s+FROM\s+(.+)$`)
Copy link

Copilot AI Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex is compiled on every function call. Consider moving the compilation to package level or use a sync.Once to compile it once for better performance.

Copilot uses AI. Check for mistakes.

if matches := re.FindStringSubmatch(condition); matches != nil {
left := strings.TrimSpace(matches[1])
right := strings.TrimSpace(matches[2])
return fmt.Sprintf("%s IS NOT DISTINCT FROM %s", left, right)
}

return condition
}

Expand Down
14 changes: 14 additions & 0 deletions ir/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,20 @@ func (p *Parser) parseAExpr(expr *pg_query.A_Expr) string {
return fmt.Sprintf("%s IN %s", left, right)
}

// Handle DISTINCT FROM expressions
if expr.Kind == pg_query.A_Expr_Kind_AEXPR_DISTINCT {
left := p.extractExpressionText(expr.Lexpr)
right := p.extractExpressionText(expr.Rexpr)
return fmt.Sprintf("%s IS DISTINCT FROM %s", left, right)
}

// Handle NOT DISTINCT FROM expressions
if expr.Kind == pg_query.A_Expr_Kind_AEXPR_NOT_DISTINCT {
left := p.extractExpressionText(expr.Lexpr)
right := p.extractExpressionText(expr.Rexpr)
return fmt.Sprintf("%s IS NOT DISTINCT FROM %s", left, right)
}

// Simplified implementation for basic expressions
if len(expr.Name) > 0 {
if str := expr.Name[0].GetString_(); str != nil {
Expand Down
11 changes: 11 additions & 0 deletions testdata/diff/create_trigger/add_trigger_when_distinct/diff.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE OR REPLACE TRIGGER products_description_trigger
BEFORE UPDATE ON products
FOR EACH ROW
WHEN (NEW.description IS DISTINCT FROM OLD.description)
EXECUTE FUNCTION log_description_change();

CREATE OR REPLACE TRIGGER products_status_trigger
BEFORE UPDATE ON products
FOR EACH ROW
WHEN (NEW.status IS NOT DISTINCT FROM OLD.status)
EXECUTE FUNCTION skip_status_change();
34 changes: 34 additions & 0 deletions testdata/diff/create_trigger/add_trigger_when_distinct/new.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
CREATE TABLE public.products (
id serial PRIMARY KEY,
name text NOT NULL,
description text,
status text,
updated_at timestamp DEFAULT CURRENT_TIMESTAMP
);

CREATE OR REPLACE FUNCTION public.log_description_change()
RETURNS trigger AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION public.skip_status_change()
RETURNS trigger AS $$
BEGIN
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER products_description_trigger
BEFORE UPDATE ON public.products
FOR EACH ROW
WHEN (NEW.description IS DISTINCT FROM OLD.description)
EXECUTE FUNCTION public.log_description_change();

CREATE TRIGGER products_status_trigger
BEFORE UPDATE ON public.products
FOR EACH ROW
WHEN (NEW.status IS NOT DISTINCT FROM OLD.status)
EXECUTE FUNCTION public.skip_status_change();
22 changes: 22 additions & 0 deletions testdata/diff/create_trigger/add_trigger_when_distinct/old.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
CREATE TABLE public.products (
id serial PRIMARY KEY,
name text NOT NULL,
description text,
status text,
updated_at timestamp DEFAULT CURRENT_TIMESTAMP
);

CREATE OR REPLACE FUNCTION public.log_description_change()
RETURNS trigger AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION public.skip_status_change()
RETURNS trigger AS $$
BEGIN
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
26 changes: 26 additions & 0 deletions testdata/diff/create_trigger/add_trigger_when_distinct/plan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"version": "1.0.0",
"pgschema_version": "1.1.1",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "98db11096a7a86d2175ff6821924a2b64dddbf240681f23079c0d912d3ea22b5"
},
"groups": [
{
"steps": [
{
"sql": "CREATE OR REPLACE TRIGGER products_description_trigger\n BEFORE UPDATE ON products\n FOR EACH ROW\n WHEN (NEW.description IS DISTINCT FROM OLD.description)\n EXECUTE FUNCTION log_description_change();",
"type": "table.trigger",
"operation": "create",
"path": "public.products.products_description_trigger"
},
{
"sql": "CREATE OR REPLACE TRIGGER products_status_trigger\n BEFORE UPDATE ON products\n FOR EACH ROW\n WHEN (NEW.status IS NOT DISTINCT FROM OLD.status)\n EXECUTE FUNCTION skip_status_change();",
"type": "table.trigger",
"operation": "create",
"path": "public.products.products_status_trigger"
}
]
}
]
}
11 changes: 11 additions & 0 deletions testdata/diff/create_trigger/add_trigger_when_distinct/plan.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE OR REPLACE TRIGGER products_description_trigger
BEFORE UPDATE ON products
FOR EACH ROW
WHEN (NEW.description IS DISTINCT FROM OLD.description)
EXECUTE FUNCTION log_description_change();

CREATE OR REPLACE TRIGGER products_status_trigger
BEFORE UPDATE ON products
FOR EACH ROW
WHEN (NEW.status IS NOT DISTINCT FROM OLD.status)
EXECUTE FUNCTION skip_status_change();
24 changes: 24 additions & 0 deletions testdata/diff/create_trigger/add_trigger_when_distinct/plan.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Plan: 1 to modify.

Summary by type:
tables: 1 to modify

Tables:
~ products
+ products_description_trigger (trigger)
+ products_status_trigger (trigger)

DDL to be executed:
--------------------------------------------------

CREATE OR REPLACE TRIGGER products_description_trigger
BEFORE UPDATE ON products
FOR EACH ROW
WHEN (NEW.description IS DISTINCT FROM OLD.description)
EXECUTE FUNCTION log_description_change();

CREATE OR REPLACE TRIGGER products_status_trigger
BEFORE UPDATE ON products
FOR EACH ROW
WHEN (NEW.status IS NOT DISTINCT FROM OLD.status)
EXECUTE FUNCTION skip_status_change();