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
3 changes: 2 additions & 1 deletion internal/diff/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ func normalizeSQL(sql string) string {
var normalizedLines []string

for _, line := range lines {
trimmed := strings.TrimSpace(line)
// Preserve leading whitespace (indentation) but trim trailing whitespace
trimmed := strings.TrimRight(line, " \t")
if trimmed != "" {
normalizedLines = append(normalizedLines, trimmed)
}
Expand Down
1 change: 1 addition & 0 deletions internal/diff/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ func generateViewSQL(view *ir.View, targetSchema string) string {
createClause = "CREATE OR REPLACE VIEW"
}

// Use the view definition as-is - it has already been normalized
return fmt.Sprintf("%s %s AS\n%s;", createClause, viewName, view.Definition)
}

Expand Down
9 changes: 7 additions & 2 deletions ir/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ func (f *postgreSQLFormatter) formatSelectStmt(stmt *pg_query.SelectStmt) {
func (f *postgreSQLFormatter) formatTargetList(targets []*pg_query.Node) {
for i, target := range targets {
if i == 0 {
f.buffer.WriteString("\n ") // First column indentation
f.buffer.WriteString(" ") // First column on same line as SELECT
} else {
f.buffer.WriteString(",\n ") // Subsequent columns
f.buffer.WriteString(",\n ") // Subsequent columns indented
}

if resTarget := target.GetResTarget(); resTarget != nil {
Expand Down Expand Up @@ -424,6 +424,11 @@ func (f *postgreSQLFormatter) formatFuncCall(funcCall *pg_query.FuncCall) {
// Format arguments
f.buffer.WriteString("(")

// Handle DISTINCT for aggregate functions
if funcCall.AggDistinct {
f.buffer.WriteString("DISTINCT ")
}

// Handle aggregate functions with star (like COUNT(*))
if funcCall.AggStar {
f.buffer.WriteString("*")
Expand Down
18 changes: 6 additions & 12 deletions ir/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -1066,29 +1066,23 @@ func normalizeViewWithAST(definition string, viewSchema string) string {
}

// Step 1: Normalize ORDER BY clauses (modify AST if needed)
orderByModified := false
if len(selectStmt.SortClause) > 0 {
// Build reverse alias map (expression -> alias) from target list
exprToAliasMap := buildExpressionToAliasMap(selectStmt.TargetList)

// Transform ORDER BY clauses: replace complex expressions with aliases when possible
for _, sortItem := range selectStmt.SortClause {
if sortBy := sortItem.GetSortBy(); sortBy != nil {
if wasModified := normalizeOrderByExpressionToAlias(sortBy, exprToAliasMap); wasModified {
orderByModified = true
}
normalizeOrderByExpressionToAlias(sortBy, exprToAliasMap)
}
}
}

// Step 2: Check if we need to use custom formatter
// Use custom formatter if:
// a) The view definition contains "= ANY" (needs conversion to IN)
// b) ORDER BY was modified
needsCustomFormatter := strings.Contains(definition, "= ANY") || orderByModified

if needsCustomFormatter {
// Use custom formatter to format the entire query
// Step 2: Check if we need to use custom formatter for normalization
// Use custom formatter only if the view definition contains "= ANY" (needs conversion to IN)
// For other cases, preserve the original definition to avoid breaking complex expressions
if strings.Contains(definition, "= ANY") {
// Use custom formatter to normalize the query
// The formatter will handle:
// - Converting "= ANY (ARRAY[...])" to "IN (...)"
// - Proper formatting of all expressions
Expand Down
2 changes: 1 addition & 1 deletion testdata/diff/create_table/add_check/diff.sql
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ALTER TABLE code
ADD CONSTRAINT code_check CHECK (code > 0 AND code < 255);
ADD CONSTRAINT code_check CHECK (code > 0 AND code < 255);
25 changes: 17 additions & 8 deletions testdata/diff/create_view/add_view/diff.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
CREATE OR REPLACE VIEW employee_department_view AS
SELECT e.id,
e.name AS employee_name,
d.name AS department_name,
d.manager_id
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.name IS NOT NULL AND d.manager_id IS NOT NULL;
CREATE OR REPLACE VIEW array_operators_view AS
SELECT id,
priority,
CASE WHEN priority IN (10, 20, 30) THEN 'matched' ELSE 'not_matched' END AS equal_any_test,
CASE WHEN priority > ANY (ARRAY[10, 20, 30]) THEN 'high' ELSE 'low' END AS greater_any_test,
CASE WHEN priority < ANY (ARRAY[5, 15, 25]) THEN 'found_lower' ELSE 'all_higher' END AS less_any_test,
CASE WHEN priority <> ANY (ARRAY[1, 2, 3]) THEN 'different' ELSE 'same' END AS not_equal_any_test
FROM employees;

CREATE OR REPLACE VIEW text_search_view AS
SELECT id,
COALESCE((first_name::text || ' '::text) || last_name::text, 'Anonymous'::text) AS display_name,
COALESCE(email, ''::character varying) AS email,
COALESCE(bio, 'No description available'::text) AS description,
to_tsvector('english'::regconfig, (((COALESCE(first_name, ''::character varying)::text || ' '::text) || COALESCE(last_name, ''::character varying)::text) || ' '::text) || COALESCE(bio, ''::text)) AS search_vector
FROM employees
WHERE status::text = 'active'::text;
37 changes: 28 additions & 9 deletions testdata/diff/create_view/add_view/new.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
CREATE TABLE public.employees (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
department_id INTEGER
first_name VARCHAR(50),
last_name VARCHAR(50),
email VARCHAR(100),
bio TEXT,
status VARCHAR(20) NOT NULL,
department_id INTEGER,
priority INTEGER
);

CREATE TABLE public.departments (
Expand All @@ -10,12 +16,25 @@ CREATE TABLE public.departments (
manager_id INTEGER
);

CREATE VIEW public.employee_department_view AS
-- View testing array operators: all ANY/ALL operators are preserved
CREATE VIEW public.array_operators_view AS
SELECT
e.id,
e.name AS employee_name,
d.name AS department_name,
d.manager_id
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.name IS NOT NULL AND d.manager_id IS NOT NULL;
id,
priority,
-- All ANY operations preserve the ANY syntax
CASE WHEN priority = ANY(ARRAY[10, 20, 30]) THEN 'matched' ELSE 'not_matched' END AS equal_any_test,
CASE WHEN priority > ANY(ARRAY[10, 20, 30]) THEN 'high' ELSE 'low' END AS greater_any_test,
CASE WHEN priority < ANY(ARRAY[5, 15, 25]) THEN 'found_lower' ELSE 'all_higher' END AS less_any_test,
CASE WHEN priority <> ANY(ARRAY[1, 2, 3]) THEN 'different' ELSE 'same' END AS not_equal_any_test
FROM employees;

-- View testing COALESCE, string concatenation, and to_tsvector for full text search
CREATE VIEW public.text_search_view AS
SELECT
id,
COALESCE(first_name || ' ' || last_name, 'Anonymous') AS display_name,
COALESCE(email, '') AS email,
COALESCE(bio, 'No description available') AS description,
to_tsvector('english', COALESCE(first_name, '') || ' ' || COALESCE(last_name, '') || ' ' || COALESCE(bio, '')) AS search_vector
FROM employees
WHERE status = 'active';
8 changes: 7 additions & 1 deletion testdata/diff/create_view/add_view/old.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
CREATE TABLE public.employees (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
department_id INTEGER
first_name VARCHAR(50),
last_name VARCHAR(50),
email VARCHAR(100),
bio TEXT,
status VARCHAR(20) NOT NULL,
department_id INTEGER,
priority INTEGER
);

CREATE TABLE public.departments (
Expand Down
12 changes: 9 additions & 3 deletions testdata/diff/create_view/add_view/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
"pgschema_version": "1.4.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "95ad22fa390833179c9661028a1d5b17d27f87223dac3481576654941198336c"
"hash": "e937536446c155860f69ab4a38fc142791a9a3636ada3e9113057afd9d875190"
},
"groups": [
{
"steps": [
{
"sql": "CREATE OR REPLACE VIEW employee_department_view AS\n SELECT e.id,\n e.name AS employee_name,\n d.name AS department_name,\n d.manager_id\n FROM employees e\n JOIN departments d ON e.department_id = d.id\n WHERE e.name IS NOT NULL AND d.manager_id IS NOT NULL;",
"sql": "CREATE OR REPLACE VIEW array_operators_view AS\n SELECT id,\n priority,\n CASE WHEN priority IN (10, 20, 30) THEN 'matched' ELSE 'not_matched' END AS equal_any_test,\n CASE WHEN priority > ANY (ARRAY[10, 20, 30]) THEN 'high' ELSE 'low' END AS greater_any_test,\n CASE WHEN priority < ANY (ARRAY[5, 15, 25]) THEN 'found_lower' ELSE 'all_higher' END AS less_any_test,\n CASE WHEN priority <> ANY (ARRAY[1, 2, 3]) THEN 'different' ELSE 'same' END AS not_equal_any_test\n FROM employees;",
"type": "view",
"operation": "create",
"path": "public.employee_department_view"
"path": "public.array_operators_view"
},
{
"sql": "CREATE OR REPLACE VIEW text_search_view AS\n SELECT id,\n COALESCE((first_name::text || ' '::text) || last_name::text, 'Anonymous'::text) AS display_name,\n COALESCE(email, ''::character varying) AS email,\n COALESCE(bio, 'No description available'::text) AS description,\n to_tsvector('english'::regconfig, (((COALESCE(first_name, ''::character varying)::text || ' '::text) || COALESCE(last_name, ''::character varying)::text) || ' '::text) || COALESCE(bio, ''::text)) AS search_vector\n FROM employees\n WHERE status::text = 'active'::text;",
"type": "view",
"operation": "create",
"path": "public.text_search_view"
}
]
}
Expand Down
25 changes: 17 additions & 8 deletions testdata/diff/create_view/add_view/plan.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
CREATE OR REPLACE VIEW employee_department_view AS
SELECT e.id,
e.name AS employee_name,
d.name AS department_name,
d.manager_id
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.name IS NOT NULL AND d.manager_id IS NOT NULL;
CREATE OR REPLACE VIEW array_operators_view AS
SELECT id,
priority,
CASE WHEN priority IN (10, 20, 30) THEN 'matched' ELSE 'not_matched' END AS equal_any_test,
CASE WHEN priority > ANY (ARRAY[10, 20, 30]) THEN 'high' ELSE 'low' END AS greater_any_test,
CASE WHEN priority < ANY (ARRAY[5, 15, 25]) THEN 'found_lower' ELSE 'all_higher' END AS less_any_test,
CASE WHEN priority <> ANY (ARRAY[1, 2, 3]) THEN 'different' ELSE 'same' END AS not_equal_any_test
FROM employees;

CREATE OR REPLACE VIEW text_search_view AS
SELECT id,
COALESCE((first_name::text || ' '::text) || last_name::text, 'Anonymous'::text) AS display_name,
COALESCE(email, ''::character varying) AS email,
COALESCE(bio, 'No description available'::text) AS description,
to_tsvector('english'::regconfig, (((COALESCE(first_name, ''::character varying)::text || ' '::text) || COALESCE(last_name, ''::character varying)::text) || ' '::text) || COALESCE(bio, ''::text)) AS search_vector
FROM employees
WHERE status::text = 'active'::text;
32 changes: 21 additions & 11 deletions testdata/diff/create_view/add_view/plan.txt
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
Plan: 1 to add.
Plan: 2 to add.

Summary by type:
views: 1 to add
views: 2 to add

Views:
+ employee_department_view
+ array_operators_view
+ text_search_view

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

CREATE OR REPLACE VIEW employee_department_view AS
SELECT e.id,
e.name AS employee_name,
d.name AS department_name,
d.manager_id
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE e.name IS NOT NULL AND d.manager_id IS NOT NULL;
CREATE OR REPLACE VIEW array_operators_view AS
SELECT id,
priority,
CASE WHEN priority IN (10, 20, 30) THEN 'matched' ELSE 'not_matched' END AS equal_any_test,
CASE WHEN priority > ANY (ARRAY[10, 20, 30]) THEN 'high' ELSE 'low' END AS greater_any_test,
CASE WHEN priority < ANY (ARRAY[5, 15, 25]) THEN 'found_lower' ELSE 'all_higher' END AS less_any_test,
CASE WHEN priority <> ANY (ARRAY[1, 2, 3]) THEN 'different' ELSE 'same' END AS not_equal_any_test
FROM employees;

CREATE OR REPLACE VIEW text_search_view AS
SELECT id,
COALESCE((first_name::text || ' '::text) || last_name::text, 'Anonymous'::text) AS display_name,
COALESCE(email, ''::character varying) AS email,
COALESCE(bio, 'No description available'::text) AS description,
to_tsvector('english'::regconfig, (((COALESCE(first_name, ''::character varying)::text || ' '::text) || COALESCE(last_name, ''::character varying)::text) || ' '::text) || COALESCE(bio, ''::text)) AS search_vector
FROM employees
WHERE status::text = 'active'::text;
9 changes: 0 additions & 9 deletions testdata/diff/create_view/add_view_array_operators/diff.sql

This file was deleted.

21 changes: 0 additions & 21 deletions testdata/diff/create_view/add_view_array_operators/new.sql

This file was deleted.

5 changes: 0 additions & 5 deletions testdata/diff/create_view/add_view_array_operators/old.sql

This file was deleted.

20 changes: 0 additions & 20 deletions testdata/diff/create_view/add_view_array_operators/plan.json

This file was deleted.

9 changes: 0 additions & 9 deletions testdata/diff/create_view/add_view_array_operators/plan.sql

This file was deleted.

20 changes: 0 additions & 20 deletions testdata/diff/create_view/add_view_array_operators/plan.txt

This file was deleted.

8 changes: 0 additions & 8 deletions testdata/diff/create_view/add_view_coalesce/diff.sql

This file was deleted.

18 changes: 0 additions & 18 deletions testdata/diff/create_view/add_view_coalesce/new.sql

This file was deleted.

8 changes: 0 additions & 8 deletions testdata/diff/create_view/add_view_coalesce/old.sql

This file was deleted.

Loading