diff --git a/__fixtures__/generated/generated.json b/__fixtures__/generated/generated.json index e5815db1..a2a04ff9 100644 --- a/__fixtures__/generated/generated.json +++ b/__fixtures__/generated/generated.json @@ -53,6 +53,13 @@ "pretty/selects-13.sql": "SELECT\n id,\n name\nFROM users\nWHERE\n id IN (SELECT\n user_id\nFROM orders\nWHERE\n total > 100)", "pretty/selects-14.sql": "SELECT\n id,\n name,\n email\nFROM users\nWHERE\n active = true", "pretty/selects-15.sql": "SELECT\n u.id,\n u.name,\n u.email,\n p.title\nFROM users AS u\nJOIN profiles AS p ON u.id = p.user_id\nWHERE\n u.active = true\n AND u.created_at > '2023-01-01'\nGROUP BY\n u.id,\n u.name,\n u.email,\n p.title\nHAVING\n count(*) > 1\nORDER BY\n u.created_at DESC,\n u.name ASC\nLIMIT 10\nOFFSET 5", + "pretty/quoting-1.sql": "CREATE FUNCTION faker.float(min double precision DEFAULT 0, max double precision DEFAULT 100) RETURNS double precision AS $EOFCODE$\nBEGIN\n RETURN min + random() * (max - min);\nEND;\n$EOFCODE$ LANGUAGE plpgsql", + "pretty/quoting-2.sql": "CREATE FUNCTION faker.\"float\"(min double precision DEFAULT 0, max double precision DEFAULT 100) RETURNS double precision AS $EOFCODE$\nBEGIN\n RETURN min + random() * (max - min);\nEND;\n$EOFCODE$ LANGUAGE plpgsql", + "pretty/quoting-3.sql": "CREATE FUNCTION faker.interval(min int, max int) RETURNS interval AS $EOFCODE$\nBEGIN\n RETURN make_interval(secs => (min + floor(random() * (max - min + 1)))::int);\nEND;\n$EOFCODE$ LANGUAGE plpgsql", + "pretty/quoting-4.sql": "CREATE FUNCTION faker.\"interval\"(min int, max int) RETURNS interval AS $EOFCODE$\nBEGIN\n RETURN make_interval(secs => (min + floor(random() * (max - min + 1)))::int);\nEND;\n$EOFCODE$ LANGUAGE plpgsql", + "pretty/quoting-5.sql": "CREATE FUNCTION faker.boolean() RETURNS boolean AS $EOFCODE$\nBEGIN\n RETURN random() < 0.5;\nEND;\n$EOFCODE$ LANGUAGE plpgsql", + "pretty/quoting-6.sql": "CREATE FUNCTION faker.\"boolean\"() RETURNS boolean AS $EOFCODE$\nBEGIN\n RETURN random() < 0.5;\nEND;\n$EOFCODE$ LANGUAGE plpgsql", + "pretty/quoting-7.sql": "CREATE DOMAIN origin AS text CHECK (value = pg_catalog.\"substring\"(value, '^(https?://[^/]*)'))", "pretty/procedures-1.sql": "SELECT handle_insert('TYPE_A')", "pretty/procedures-2.sql": "SELECT \"HandleInsert\"('TYPE_A', 'Region-1')", "pretty/procedures-3.sql": "SELECT compute_score(42, TRUE)", @@ -80,6 +87,15 @@ "pretty/misc-15.sql": "ALTER DEFAULT PRIVILEGES IN SCHEMA dashboard_jobs \n GRANT EXECUTE ON FUNCTIONS TO administrator", "pretty/misc-16.sql": "GRANT EXECUTE ON FUNCTION dashboard_private.uuid_generate_seeded_uuid TO PUBLIC", "pretty/misc-17.sql": "SELECT CAST(t.date AT TIME ZONE $$America/New_York$$ AS text)::date FROM tbl t", + "pretty/formatting-1.sql": "EXPLAIN (COSTS OFF) SELECT * FROM onek2 WHERE unique2 = 11 AND stringu1 = 'ATAAAA'", + "pretty/formatting-2.sql": "EXPLAIN SELECT * FROM onek2 WHERE unique2 = 11 AND stringu1 = 'ATAAAA'", + "pretty/formatting-3.sql": "INSERT INTO objects.object (name, val, active, hash)\nVALUES ('name', 'val', 't'::boolean, 'abcdefg'),\n ('name', 'val', 't'::boolean, 'abcdefg'),\n ('name', 'val', 't'::boolean, 'abcdefg')", + "pretty/formatting-4.sql": "INSERT INTO objects.object (name, val, active, hash)\nVALUES ('name', 'val', TRUE, 'abcdefg'),\n ('name', 'val', TRUE, 'abcdefg'),\n ('name', 'val', TRUE, 'abcdefg')", + "pretty/formatting-5.sql": "INSERT INTO objects.object (name, val, active, hash)\nVALUES ('name', 'val', FALSE, 'abcdefg')", + "pretty/formatting-6.sql": "INSERT INTO objects.object (name, val, active, hash)\nVALUES ('name', 'val', 'f'::boolean, 'abcdefg')", + "pretty/formatting-7.sql": "CREATE FUNCTION test_func(IN p1 pos_int) RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql", + "pretty/formatting-8.sql": "CREATE FUNCTION test_func2(IN p1 pos_int, IN p2 text) RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql", + "pretty/formatting-9.sql": "CREATE FUNCTION test_func3(IN p1 integer, OUT p2 text, INOUT p3 boolean) RETURNS record AS $$ BEGIN p2 := 'test'; END; $$ LANGUAGE plpgsql", "pretty/cte-1.sql": "WITH regional_sales AS (SELECT region, SUM(sales_amount) as total_sales FROM sales GROUP BY region) SELECT * FROM regional_sales", "pretty/cte-2.sql": "WITH regional_sales AS (SELECT region, SUM(sales_amount) as total_sales FROM sales GROUP BY region), top_regions AS (SELECT region FROM regional_sales WHERE total_sales > 1000000) SELECT * FROM top_regions", "pretty/cte-3.sql": "WITH RECURSIVE employee_hierarchy AS (SELECT id, name, manager_id, 1 as level FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.id, e.name, e.manager_id, eh.level + 1 FROM employees e JOIN employee_hierarchy eh ON e.manager_id = eh.id) SELECT * FROM employee_hierarchy", diff --git a/__fixtures__/kitchen-sink/pretty/formatting.sql b/__fixtures__/kitchen-sink/pretty/formatting.sql new file mode 100644 index 00000000..fc0303d1 --- /dev/null +++ b/__fixtures__/kitchen-sink/pretty/formatting.sql @@ -0,0 +1,40 @@ +-- 1. EXPLAIN with COSTS OFF - tests optional keyword preservation +-- Some deparsers may omit optional keywords like COSTS OFF +-- This documents that EXPLAIN (COSTS OFF) may deparse differently than EXPLAIN alone +EXPLAIN (COSTS OFF) SELECT * FROM onek2 WHERE unique2 = 11 AND stringu1 = 'ATAAAA'; + +-- 2. EXPLAIN without COSTS (default behavior) +EXPLAIN SELECT * FROM onek2 WHERE unique2 = 11 AND stringu1 = 'ATAAAA'; + +-- 3. Boolean literal formatting - INPUT FORM with 't'::boolean cast +-- The deparser may normalize this to TRUE +INSERT INTO objects.object (name, val, active, hash) +VALUES ('name', 'val', 't'::boolean, 'abcdefg'), + ('name', 'val', 't'::boolean, 'abcdefg'), + ('name', 'val', 't'::boolean, 'abcdefg'); + +-- 4. Boolean literal formatting - CANONICAL FORM with TRUE +INSERT INTO objects.object (name, val, active, hash) +VALUES ('name', 'val', TRUE, 'abcdefg'), + ('name', 'val', TRUE, 'abcdefg'), + ('name', 'val', TRUE, 'abcdefg'); + +-- 5. Boolean literal formatting - CANONICAL FORM with FALSE +INSERT INTO objects.object (name, val, active, hash) +VALUES ('name', 'val', FALSE, 'abcdefg'); + +-- 6. Boolean literal formatting - INPUT FORM with 'f'::boolean cast +-- The deparser may normalize this to FALSE +INSERT INTO objects.object (name, val, active, hash) +VALUES ('name', 'val', 'f'::boolean, 'abcdefg'); + +-- 7. Parenthesization / argument formatting with IN parameter mode +-- Tests formatting of function parameters with IN mode and custom types +-- Input form: ( IN p1 pos_int ) +CREATE FUNCTION test_func(IN p1 pos_int) RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql; + +-- 8. Parenthesization with multiple IN parameters +CREATE FUNCTION test_func2(IN p1 pos_int, IN p2 text) RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql; + +-- 9. Mixed parameter modes (IN, OUT, INOUT) +CREATE FUNCTION test_func3(IN p1 integer, OUT p2 text, INOUT p3 boolean) RETURNS record AS $$ BEGIN p2 := 'test'; END; $$ LANGUAGE plpgsql; diff --git a/__fixtures__/kitchen-sink/pretty/quoting.sql b/__fixtures__/kitchen-sink/pretty/quoting.sql new file mode 100644 index 00000000..869e5fb7 --- /dev/null +++ b/__fixtures__/kitchen-sink/pretty/quoting.sql @@ -0,0 +1,47 @@ +-- 1. Unquoted function name "float" (reserved keyword) - INPUT FORM +-- The deparser should quote this reserved keyword +CREATE FUNCTION faker.float(min double precision DEFAULT 0, max double precision DEFAULT 100) RETURNS double precision AS $EOFCODE$ +BEGIN + RETURN min + random() * (max - min); +END; +$EOFCODE$ LANGUAGE plpgsql; + +-- 2. Quoted function name "float" - CANONICAL FORM (idempotence check) +CREATE FUNCTION faker."float"(min double precision DEFAULT 0, max double precision DEFAULT 100) RETURNS double precision AS $EOFCODE$ +BEGIN + RETURN min + random() * (max - min); +END; +$EOFCODE$ LANGUAGE plpgsql; + +-- 3. Unquoted function name "interval" (reserved keyword) - INPUT FORM +CREATE FUNCTION faker.interval(min int, max int) RETURNS interval AS $EOFCODE$ +BEGIN + RETURN make_interval(secs => (min + floor(random() * (max - min + 1)))::int); +END; +$EOFCODE$ LANGUAGE plpgsql; + +-- 4. Quoted function name "interval" - CANONICAL FORM (idempotence check) +CREATE FUNCTION faker."interval"(min int, max int) RETURNS interval AS $EOFCODE$ +BEGIN + RETURN make_interval(secs => (min + floor(random() * (max - min + 1)))::int); +END; +$EOFCODE$ LANGUAGE plpgsql; + +-- 5. Unquoted function name "boolean" (reserved keyword) - INPUT FORM +CREATE FUNCTION faker.boolean() RETURNS boolean AS $EOFCODE$ +BEGIN + RETURN random() < 0.5; +END; +$EOFCODE$ LANGUAGE plpgsql; + +-- 6. Quoted function name "boolean" - CANONICAL FORM (idempotence check) +CREATE FUNCTION faker."boolean"() RETURNS boolean AS $EOFCODE$ +BEGIN + RETURN random() < 0.5; +END; +$EOFCODE$ LANGUAGE plpgsql; + +-- 7. pg_catalog.substring with quoted identifier - CANONICAL FORM +-- Note: SUBSTRING(value FROM 'pattern') SQL syntax gets deparsed to pg_catalog."substring"(value, 'pattern') +-- The SQL syntax form cannot be tested here due to AST round-trip differences (COERCE_SQL_SYNTAX vs COERCE_EXPLICIT_CALL) +CREATE DOMAIN origin AS text CHECK (value = pg_catalog."substring"(value, '^(https?://[^/]*)')); diff --git a/packages/deparser/__tests__/pretty/__snapshots__/formatting-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/formatting-pretty.test.ts.snap new file mode 100644 index 00000000..9f466abe --- /dev/null +++ b/packages/deparser/__tests__/pretty/__snapshots__/formatting-pretty.test.ts.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`non-pretty: pretty/formatting-1.sql 1`] = `"EXPLAIN (COSTS OFF) SELECT * FROM onek2 WHERE unique2 = 11 AND stringu1 = 'ATAAAA'"`; + +exports[`non-pretty: pretty/formatting-2.sql 1`] = `"EXPLAIN SELECT * FROM onek2 WHERE unique2 = 11 AND stringu1 = 'ATAAAA'"`; + +exports[`non-pretty: pretty/formatting-3.sql 1`] = `"INSERT INTO objects.object (name, val, active, hash) VALUES ('name', 'val', CAST('t' AS boolean), 'abcdefg'), ('name', 'val', CAST('t' AS boolean), 'abcdefg'), ('name', 'val', CAST('t' AS boolean), 'abcdefg')"`; + +exports[`non-pretty: pretty/formatting-4.sql 1`] = `"INSERT INTO objects.object (name, val, active, hash) VALUES ('name', 'val', true, 'abcdefg'), ('name', 'val', true, 'abcdefg'), ('name', 'val', true, 'abcdefg')"`; + +exports[`non-pretty: pretty/formatting-5.sql 1`] = `"INSERT INTO objects.object (name, val, active, hash) VALUES ('name', 'val', false, 'abcdefg')"`; + +exports[`non-pretty: pretty/formatting-6.sql 1`] = `"INSERT INTO objects.object (name, val, active, hash) VALUES ('name', 'val', CAST('f' AS boolean), 'abcdefg')"`; + +exports[`non-pretty: pretty/formatting-7.sql 1`] = `"CREATE FUNCTION test_func(IN p1 pos_int) RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql"`; + +exports[`non-pretty: pretty/formatting-8.sql 1`] = `"CREATE FUNCTION test_func2(IN p1 pos_int, IN p2 text) RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql"`; + +exports[`non-pretty: pretty/formatting-9.sql 1`] = `"CREATE FUNCTION test_func3(IN p1 int, OUT p2 text, INOUT p3 boolean) RETURNS record AS $$ BEGIN p2 := 'test'; END; $$ LANGUAGE plpgsql"`; + +exports[`pretty: pretty/formatting-1.sql 1`] = ` +"EXPLAIN (COSTS OFF) SELECT * +FROM onek2 +WHERE + unique2 = 11 + AND stringu1 = 'ATAAAA'" +`; + +exports[`pretty: pretty/formatting-2.sql 1`] = ` +"EXPLAIN SELECT * +FROM onek2 +WHERE + unique2 = 11 + AND stringu1 = 'ATAAAA'" +`; + +exports[`pretty: pretty/formatting-3.sql 1`] = ` +"INSERT INTO objects.object ( + name, + val, + active, + hash +) VALUES + ('name', 'val', CAST('t' AS boolean), 'abcdefg'), + ('name', 'val', CAST('t' AS boolean), 'abcdefg'), + ('name', 'val', CAST('t' AS boolean), 'abcdefg')" +`; + +exports[`pretty: pretty/formatting-4.sql 1`] = ` +"INSERT INTO objects.object ( + name, + val, + active, + hash +) VALUES + ('name', 'val', true, 'abcdefg'), + ('name', 'val', true, 'abcdefg'), + ('name', 'val', true, 'abcdefg')" +`; + +exports[`pretty: pretty/formatting-5.sql 1`] = ` +"INSERT INTO objects.object ( + name, + val, + active, + hash +) VALUES + ('name', 'val', false, 'abcdefg')" +`; + +exports[`pretty: pretty/formatting-6.sql 1`] = ` +"INSERT INTO objects.object ( + name, + val, + active, + hash +) VALUES + ('name', 'val', CAST('f' AS boolean), 'abcdefg')" +`; + +exports[`pretty: pretty/formatting-7.sql 1`] = `"CREATE FUNCTION test_func(IN p1 pos_int) RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql"`; + +exports[`pretty: pretty/formatting-8.sql 1`] = `"CREATE FUNCTION test_func2(IN p1 pos_int, IN p2 text) RETURNS void AS $$ BEGIN NULL; END; $$ LANGUAGE plpgsql"`; + +exports[`pretty: pretty/formatting-9.sql 1`] = `"CREATE FUNCTION test_func3(IN p1 int, OUT p2 text, INOUT p3 boolean) RETURNS record AS $$ BEGIN p2 := 'test'; END; $$ LANGUAGE plpgsql"`; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/quoting-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/quoting-pretty.test.ts.snap new file mode 100644 index 00000000..2a7f41dc --- /dev/null +++ b/packages/deparser/__tests__/pretty/__snapshots__/quoting-pretty.test.ts.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`non-pretty: pretty/quoting-1.sql 1`] = ` +"CREATE FUNCTION faker."float"(min double precision DEFAULT 0, max double precision DEFAULT 100) RETURNS double precision AS $$ +BEGIN + RETURN min + random() * (max - min); +END; +$$ LANGUAGE plpgsql" +`; + +exports[`non-pretty: pretty/quoting-2.sql 1`] = ` +"CREATE FUNCTION faker."float"(min double precision DEFAULT 0, max double precision DEFAULT 100) RETURNS double precision AS $$ +BEGIN + RETURN min + random() * (max - min); +END; +$$ LANGUAGE plpgsql" +`; + +exports[`non-pretty: pretty/quoting-3.sql 1`] = ` +"CREATE FUNCTION faker."interval"(min int, max int) RETURNS interval AS $$ +BEGIN + RETURN make_interval(secs => (min + floor(random() * (max - min + 1)))::int); +END; +$$ LANGUAGE plpgsql" +`; + +exports[`non-pretty: pretty/quoting-4.sql 1`] = ` +"CREATE FUNCTION faker."interval"(min int, max int) RETURNS interval AS $$ +BEGIN + RETURN make_interval(secs => (min + floor(random() * (max - min + 1)))::int); +END; +$$ LANGUAGE plpgsql" +`; + +exports[`non-pretty: pretty/quoting-5.sql 1`] = ` +"CREATE FUNCTION faker."boolean"() RETURNS boolean AS $$ +BEGIN + RETURN random() < 0.5; +END; +$$ LANGUAGE plpgsql" +`; + +exports[`non-pretty: pretty/quoting-6.sql 1`] = ` +"CREATE FUNCTION faker."boolean"() RETURNS boolean AS $$ +BEGIN + RETURN random() < 0.5; +END; +$$ LANGUAGE plpgsql" +`; + +exports[`non-pretty: pretty/quoting-7.sql 1`] = `"CREATE DOMAIN origin AS text CHECK (value = pg_catalog."substring"(value, '^(https?://[^/]*)'))"`; + +exports[`pretty: pretty/quoting-1.sql 1`] = ` +"CREATE FUNCTION faker."float"(min double precision DEFAULT 0, max double precision DEFAULT 100) RETURNS double precision AS $$ +BEGIN + RETURN min + random() * (max - min); +END; +$$ LANGUAGE plpgsql" +`; + +exports[`pretty: pretty/quoting-2.sql 1`] = ` +"CREATE FUNCTION faker."float"(min double precision DEFAULT 0, max double precision DEFAULT 100) RETURNS double precision AS $$ +BEGIN + RETURN min + random() * (max - min); +END; +$$ LANGUAGE plpgsql" +`; + +exports[`pretty: pretty/quoting-3.sql 1`] = ` +"CREATE FUNCTION faker."interval"(min int, max int) RETURNS interval AS $$ +BEGIN + RETURN make_interval(secs => (min + floor(random() * (max - min + 1)))::int); +END; +$$ LANGUAGE plpgsql" +`; + +exports[`pretty: pretty/quoting-4.sql 1`] = ` +"CREATE FUNCTION faker."interval"(min int, max int) RETURNS interval AS $$ +BEGIN + RETURN make_interval(secs => (min + floor(random() * (max - min + 1)))::int); +END; +$$ LANGUAGE plpgsql" +`; + +exports[`pretty: pretty/quoting-5.sql 1`] = ` +"CREATE FUNCTION faker."boolean"() RETURNS boolean AS $$ +BEGIN + RETURN random() < 0.5; +END; +$$ LANGUAGE plpgsql" +`; + +exports[`pretty: pretty/quoting-6.sql 1`] = ` +"CREATE FUNCTION faker."boolean"() RETURNS boolean AS $$ +BEGIN + RETURN random() < 0.5; +END; +$$ LANGUAGE plpgsql" +`; + +exports[`pretty: pretty/quoting-7.sql 1`] = ` +"CREATE DOMAIN origin AS text + CHECK (value = pg_catalog."substring"(value, '^(https?://[^/]*)'))" +`; diff --git a/packages/deparser/__tests__/pretty/formatting-pretty.test.ts b/packages/deparser/__tests__/pretty/formatting-pretty.test.ts new file mode 100644 index 00000000..aee7c727 --- /dev/null +++ b/packages/deparser/__tests__/pretty/formatting-pretty.test.ts @@ -0,0 +1,14 @@ +import { PrettyTest } from '../../test-utils/PrettyTest'; +const prettyTest = new PrettyTest([ + 'pretty/formatting-1.sql', + 'pretty/formatting-2.sql', + 'pretty/formatting-3.sql', + 'pretty/formatting-4.sql', + 'pretty/formatting-5.sql', + 'pretty/formatting-6.sql', + 'pretty/formatting-7.sql', + 'pretty/formatting-8.sql', + 'pretty/formatting-9.sql', +]); + +prettyTest.generateTests(); diff --git a/packages/deparser/__tests__/pretty/quoting-pretty.test.ts b/packages/deparser/__tests__/pretty/quoting-pretty.test.ts new file mode 100644 index 00000000..1d663374 --- /dev/null +++ b/packages/deparser/__tests__/pretty/quoting-pretty.test.ts @@ -0,0 +1,12 @@ +import { PrettyTest } from '../../test-utils/PrettyTest'; +const prettyTest = new PrettyTest([ + 'pretty/quoting-1.sql', + 'pretty/quoting-2.sql', + 'pretty/quoting-3.sql', + 'pretty/quoting-4.sql', + 'pretty/quoting-5.sql', + 'pretty/quoting-6.sql', + 'pretty/quoting-7.sql', +]); + +prettyTest.generateTests();