diff --git a/__fixtures__/generated/generated.json b/__fixtures__/generated/generated.json index 016fa6e0..c564ef78 100644 --- a/__fixtures__/generated/generated.json +++ b/__fixtures__/generated/generated.json @@ -1,10 +1,68 @@ { - "pretty/select_statements-1.sql": "SELECT id, name, email FROM users WHERE active = true", - "pretty/select_statements-2.sql": "SELECT \n u.id,\n u.name,\n u.email,\n p.title as profile_title\nFROM users u\nJOIN profiles p ON u.id = p.user_id\nWHERE u.active = true\n AND u.created_at > '2023-01-01'\nGROUP BY u.id, u.name, u.email, p.title\nHAVING COUNT(*) > 1\nORDER BY u.created_at DESC, u.name ASC\nLIMIT 10\nOFFSET 5", - "pretty/select_statements-3.sql": "SELECT id, name FROM users WHERE id IN (\n SELECT user_id FROM orders WHERE total > 100\n)", - "pretty/select_statements-4.sql": "SELECT name FROM customers\nUNION ALL\nSELECT name FROM suppliers\nORDER BY name", - "pretty/select_statements-5.sql": "SELECT name, email FROM users WHERE status = 'active'", - "pretty/select_statements-6.sql": "SELECT u.name, o.total FROM users u, orders o WHERE u.id = o.user_id", + "pretty/types-1.sql": "CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')", + "pretty/types-2.sql": "CREATE TYPE \"AlertLevel\" AS ENUM ('Low', 'MEDIUM', 'High', 'CRITICAL')", + "pretty/types-3.sql": "CREATE TYPE address AS (\n street text,\n city text,\n zip_code int\n)", + "pretty/types-4.sql": "CREATE TYPE \"PostalInfo\" AS (\n \"Street\" text,\n \"City\" text,\n \"ZipCode\" integer\n)", + "pretty/types-5.sql": "CREATE TYPE public.user_metadata AS (\n key text,\n value jsonb\n)", + "pretty/types-6.sql": "CREATE TYPE tsrange_custom AS RANGE (\n subtype = timestamp with time zone,\n subtype_diff = timestamp_diff,\n canonical = normalize_tsrange\n)", + "pretty/types-7.sql": "CREATE TYPE version_enum AS ENUM ('1.0', '1.1', '2.0')", + "pretty/types-8.sql": "CREATE TYPE full_location AS (\n address address,\n region_code char(2)\n)", + "pretty/types-9.sql": "CREATE TYPE \"Workflow-State\" AS ENUM ('draft', 'in-review', 'needs-fix', 'finalized')", + "pretty/triggers-1.sql": "CREATE TRIGGER audit_insert_trigger\n AFTER INSERT\n ON public.users\n FOR EACH ROW\n EXECUTE PROCEDURE log_user_insert()", + "pretty/triggers-2.sql": "CREATE TRIGGER \"AuditTrigger\"\n AFTER DELETE\n ON \"SensitiveData\"\n FOR EACH ROW\n EXECUTE PROCEDURE public.log_deletion()", + "pretty/triggers-3.sql": "CREATE TRIGGER archive_if_inactive\n BEFORE UPDATE\n ON accounts\n FOR EACH ROW\n WHEN (OLD.active = false)\n EXECUTE PROCEDURE \"ArchiveFunction\"()", + "pretty/triggers-4.sql": "CREATE TRIGGER update_stats_on_change\n AFTER UPDATE OR INSERT\n ON metrics.stats\n FOR EACH ROW\n EXECUTE PROCEDURE metrics.update_stats('user', true)", + "pretty/triggers-5.sql": "CREATE TRIGGER \"TrickyTrigger\"\n BEFORE DELETE\n ON \"weirdSchema\".\"ComplexTable\"\n FOR EACH ROW\n WHEN (OLD.\"status\" = 'pending')\n EXECUTE PROCEDURE \"weirdSchema\".\"ComplexFn\"('arg1', 42)", + "pretty/triggers-6.sql": "CREATE TRIGGER user_activity_log\n AFTER INSERT OR DELETE OR UPDATE\n ON users\n FOR EACH ROW\n EXECUTE PROCEDURE audit.activity_log()", + "pretty/triggers-7.sql": "CREATE TRIGGER no_schema\n BEFORE INSERT\n ON log_table\n FOR EACH ROW\n EXECUTE PROCEDURE update_log()", + "pretty/triggers-8.sql": "CREATE TRIGGER flag_special_updates\n AFTER UPDATE\n ON profiles\n FOR EACH ROW\n WHEN (NEW.\"accessLevel\" = 'admin')\n EXECUTE PROCEDURE flag_admin_change()", + "pretty/triggers-9.sql": "CREATE TRIGGER \"TriggerMixedCase\"\n BEFORE INSERT\n ON dataPoints\n FOR EACH ROW\n EXECUTE PROCEDURE \"HandleInsert\"('TYPE_A', 'Region-1')", + "pretty/triggers-10.sql": "CREATE TRIGGER cascade_on_partition\n AFTER DELETE\n ON events_log_partition\n FOR EACH ROW\n EXECUTE PROCEDURE propagate_deletion()", + "pretty/tables-1.sql": "CREATE TABLE public.users (\n id serial PRIMARY KEY,\n name text NOT NULL\n)", + "pretty/tables-2.sql": "CREATE TABLE \"App\".\"User Data\" (\n \"User ID\" uuid PRIMARY KEY,\n \"Full Name\" text NOT NULL\n)", + "pretty/tables-3.sql": "CREATE TABLE system.settings (\n setting_key text PRIMARY KEY,\n setting_value text,\n CONSTRAINT \"Default Setting Check\" CHECK (setting_value IS NOT NULL)\n)", + "pretty/tables-4.sql": "CREATE TABLE \"Inventory\".\"StockItems\" (\n \"ItemID\" int PRIMARY KEY,\n \"Tags\" text[]\n)", + "pretty/tables-5.sql": "CREATE TABLE \"Orders\".\"OrderLines\" (\n id serial PRIMARY KEY,\n order_id int,\n CONSTRAINT \"FK Order Reference\" FOREIGN KEY (order_id) REFERENCES \"Orders\".\"Order\"(\"OrderID\")\n)", + "pretty/tables-6.sql": "CREATE TABLE contact_info (\n id int PRIMARY KEY,\n location address -- assumed composite type\n)", + "pretty/tables-7.sql": "CREATE TABLE \"Archive\".\"OldUsers\" (\n archived_at timestamptz DEFAULT now()\n) INHERITS (\"Users\".\"User Data\")", + "pretty/tables-8.sql": "CREATE TABLE logging.audit_trail (\n log_id int GENERATED BY DEFAULT AS IDENTITY,\n message text,\n CONSTRAINT \"PK_Audit\" PRIMARY KEY (log_id)\n)", + "pretty/tables-9.sql": "CREATE TABLE finance.transactions (\n amount numeric,\n tax_rate numeric,\n total numeric GENERATED ALWAYS AS (amount * (1 + tax_rate)) STORED\n)", + "pretty/tables-10.sql": "CREATE TABLE metrics.monthly_stats (\n stat_id serial,\n recorded_at date\n) PARTITION BY RANGE (recorded_at)", + "pretty/tables-11.sql": "CREATE TABLE school.attendance (\n \"Student ID\" uuid,\n \"Class ID\" uuid,\n attended_on date DEFAULT CURRENT_DATE,\n PRIMARY KEY (\"Student ID\", \"Class ID\")\n)", + "pretty/tables-12.sql": "CREATE TABLE secure.sessions (\n session_id uuid PRIMARY KEY,\n user_id uuid,\n CONSTRAINT \"fk-user->session\" FOREIGN KEY (user_id) REFERENCES users(id)\n)", + "pretty/tables-13.sql": "CREATE TABLE public.\"API Keys\" (\n \"KeyID\" uuid PRIMARY KEY,\n \"ClientName\" text,\n \"KeyValue\" text UNIQUE,\n CONSTRAINT \"Unique_ClientName\" UNIQUE (\"ClientName\")\n)", + "pretty/tables-14.sql": "CREATE TABLE alerts (\n alert_id serial PRIMARY KEY,\n level \"AlertLevel\" NOT NULL -- assumed enum type\n)", + "pretty/tables-15.sql": "CREATE TABLE \"Billing\".\"Invoices\" (\n invoice_id uuid PRIMARY KEY,\n \"Client ID\" uuid,\n CONSTRAINT \"FK_Client\" FOREIGN KEY (\"Client ID\") REFERENCES \"Clients\".\"ClientBase\"(\"Client ID\")\n)", + "pretty/tables-16.sql": "CREATE TABLE media.assets (\n id uuid PRIMARY KEY,\n url text,\n CONSTRAINT \"Check-URL-NonEmpty\" CHECK (url <> '')\n)", + "pretty/tables-17.sql": "CREATE TABLE data.snapshots (\n id serial PRIMARY KEY,\n metadata jsonb,\n context address\n)", + "pretty/tables-18.sql": "CREATE TABLE \"x-Schema\".\"z-Table\" (\n \"Z-ID\" int PRIMARY KEY,\n \"Z-Name\" text,\n CONSTRAINT \"z-Name-Check\" CHECK (\"Z-Name\" ~ '^[A-Z]')\n)", + "pretty/tables-19.sql": "CREATE TABLE users.details (\n \"first_name\" text NOT NULL,\n \"last_name\" text,\n CONSTRAINT \"first_name_required\" CHECK (\"first_name\" <> '')\n)", + "pretty/tables-20.sql": "CREATE TABLE \"Calculated\".\"Metrics\" (\n base int,\n adjustment int DEFAULT 0,\n \"Total\" int GENERATED ALWAYS AS (base + adjustment) STORED\n)", + "pretty/selects-1.sql": "SELECT 1", + "pretty/selects-2.sql": "SELECT 'abc'::text", + "pretty/selects-3.sql": "SELECT now() AT TIME ZONE 'UTC'", + "pretty/selects-4.sql": "SELECT\n 1,\n 2", + "pretty/selects-5.sql": "SELECT\n id,\n name,\n email\nFROM users", + "pretty/selects-6.sql": "SELECT DISTINCT id FROM users", + "pretty/selects-7.sql": "SELECT DISTINCT\n id,\n name\nFROM users", + "pretty/selects-8.sql": "SELECT\n id,\n upper(name) AS name_upper,\n created_at + interval '1 day' AS expires_at\nFROM accounts", + "pretty/selects-9.sql": "SELECT (SELECT max(score) FROM results)", + "pretty/selects-10.sql": "SELECT\n count(*) OVER (),\n u.id\nFROM users u", + "pretty/selects-11.sql": "SELECT\n name\nFROM customers\nUNION\nALL\nSELECT\n name\nFROM suppliers\nORDER BY\n name", + "pretty/selects-12.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\nLEFT JOIN orders AS o ON u.id = o.user_id\nRIGHT JOIN addresses AS a ON u.id = a.user_id\nWHERE\n u.active = true", + "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/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)", + "pretty/procedures-4.sql": "SELECT metrics.get_total('2025-01-01', '2025-01-31')", + "pretty/procedures-5.sql": "SELECT * FROM users WHERE is_active(user_id)", + "pretty/procedures-6.sql": "SELECT * FROM get_user_details(1001)", + "pretty/procedures-7.sql": "SELECT * FROM get_recent_events('login') AS events", + "pretty/procedures-8.sql": "SELECT \"Analytics\".\"RunQuery\"('Q-123', '2025-06')", + "pretty/procedures-9.sql": "SELECT calculate_discount(price * quantity, customer_tier)", + "pretty/procedures-10.sql": "SELECT perform_backup('daily', FALSE)", "pretty/misc-1.sql": "WITH recent_orders AS (\n SELECT o.id, o.user_id, o.created_at\n FROM orders o\n WHERE o.created_at > NOW() - INTERVAL '30 days'\n), high_value_orders AS (\n SELECT r.user_id, COUNT(*) AS order_count, SUM(oi.price * oi.quantity) AS total_spent\n FROM recent_orders r\n JOIN order_items oi ON r.id = oi.order_id\n GROUP BY r.user_id\n)\nSELECT u.id, u.name, h.total_spent\nFROM users u\nJOIN high_value_orders h ON u.id = h.user_id\nWHERE h.total_spent > 1000\nORDER BY h.total_spent DESC", "pretty/misc-2.sql": "SELECT\n department,\n employee_id,\n COUNT(*) FILTER (WHERE status = 'active') OVER (PARTITION BY department) AS active_count,\n RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_rank\nFROM employee_status\nGROUP BY GROUPING SETS ((department), (department, employee_id))", "pretty/misc-3.sql": "SELECT u.id, u.name, j.key, j.value\nFROM users u,\nLATERAL jsonb_each_text(u.preferences) AS j(key, value)\nWHERE j.key LIKE 'notif_%' AND j.value::boolean = true", @@ -18,19 +76,75 @@ "pretty/misc-11.sql": "SELECT *\nFROM users u,\nLATERAL (\n SELECT \n (CASE \n WHEN u.is_admin THEN 'admin_dashboard'\n ELSE 'user_dashboard'\n END) AS dashboard_view\n) AS derived", "pretty/misc-12.sql": "SELECT \n id,\n (SELECT \n CASE \n WHEN COUNT(*) > 5 THEN 'frequent'\n ELSE 'occasional'\n END\n FROM purchases p WHERE p.user_id = u.id) AS purchase_freq\nFROM users u", "pretty/misc-13.sql": "SELECT \n id,\n CASE \n WHEN rank() OVER (ORDER BY score DESC) = 1 THEN 'top'\n ELSE 'normal'\n END AS tier\nFROM players", + "pretty/misc-14.sql": "CREATE TRIGGER decrease_job_queue_count_on_delete \n AFTER DELETE ON dashboard_jobs.jobs \n FOR EACH ROW\n WHEN ( OLD.queue_name IS NOT NULL ) \n EXECUTE PROCEDURE dashboard_jobs.tg_decrease_job_queue_count ()", + "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/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", + "pretty/cte-4.sql": "WITH sales_summary AS (SELECT region, product_category, SUM(amount) as total FROM sales GROUP BY region, product_category), regional_totals AS (SELECT region, SUM(total) as region_total FROM sales_summary GROUP BY region) SELECT s.region, s.product_category, s.total, r.region_total FROM sales_summary s JOIN regional_totals r ON s.region = r.region", "pretty/create_table-1.sql": "CREATE TABLE users (\n id SERIAL PRIMARY KEY,\n name TEXT NOT NULL,\n email TEXT UNIQUE\n)", "pretty/create_table-2.sql": "CREATE TABLE products (\n id SERIAL PRIMARY KEY,\n name VARCHAR(255) NOT NULL,\n price DECIMAL(10,2) CHECK (price > 0),\n category_id INTEGER,\n description TEXT,\n created_at TIMESTAMP DEFAULT now(),\n updated_at TIMESTAMP,\n UNIQUE (name, category_id),\n FOREIGN KEY (category_id) REFERENCES categories(id)\n)", "pretty/create_table-3.sql": "CREATE TABLE orders (\n id SERIAL PRIMARY KEY,\n subtotal DECIMAL(10,2) NOT NULL,\n tax_rate DECIMAL(5,4) DEFAULT 0.0825,\n tax_amount DECIMAL(10,2) GENERATED ALWAYS AS (subtotal * tax_rate) STORED,\n total DECIMAL(10,2) GENERATED ALWAYS AS (subtotal + tax_amount) STORED\n)", "pretty/create_table-4.sql": "CREATE TABLE sales (\n id SERIAL,\n sale_date DATE NOT NULL,\n amount DECIMAL(10,2),\n region VARCHAR(50)\n) PARTITION BY RANGE (sale_date)", "pretty/create_table-5.sql": "CREATE TEMPORARY TABLE temp_calculations (\n id INTEGER,\n value DECIMAL(15,5),\n result TEXT\n)", + "pretty/create_table-6.sql": "CREATE TABLE orders (\n id SERIAL PRIMARY KEY,\n user_id INTEGER NOT NULL,\n total DECIMAL(10,2) CHECK (total > 0),\n status VARCHAR(20) DEFAULT 'pending',\n created_at TIMESTAMP DEFAULT now(),\n FOREIGN KEY (user_id) REFERENCES users(id)\n)", "pretty/create_policy-1.sql": "CREATE POLICY user_policy ON users FOR ALL TO authenticated_users USING (user_id = current_user_id())", "pretty/create_policy-2.sql": "CREATE POLICY admin_policy ON sensitive_data \n AS RESTRICTIVE \n FOR SELECT \n TO admin_role \n USING (department = current_user_department()) \n WITH CHECK (approved = true)", "pretty/create_policy-3.sql": "CREATE POLICY complex_policy ON documents \n FOR UPDATE \n TO document_editors \n USING (\n owner_id = current_user_id() OR \n (shared = true AND permissions @> '{\"edit\": true}')\n ) \n WITH CHECK (\n status != 'archived' AND \n last_modified > now() - interval '1 day'\n )", "pretty/create_policy-4.sql": "CREATE POLICY simple_policy ON posts FOR SELECT TO public USING (published = true)", - "pretty/constraints-1.sql": "CREATE TABLE orders (\n id SERIAL PRIMARY KEY,\n user_id INTEGER NOT NULL,\n total DECIMAL(10,2) CHECK (total > 0),\n status VARCHAR(20) DEFAULT 'pending',\n created_at TIMESTAMP DEFAULT now(),\n CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,\n CONSTRAINT unique_user_date UNIQUE (user_id, created_at),\n CONSTRAINT check_status CHECK (status IN ('pending', 'completed', 'cancelled'))\n)", - "pretty/constraints-2.sql": "ALTER TABLE products ADD CONSTRAINT fk_category \n FOREIGN KEY (category_id) \n REFERENCES categories(id) \n ON UPDATE CASCADE \n ON DELETE SET NULL \n DEFERRABLE INITIALLY DEFERRED", - "pretty/constraints-3.sql": "ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0)", - "pretty/constraints-4.sql": "ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email)", + "pretty/create_policy-5.sql": "CREATE POLICY \"simple_policy\" ON posts FOR SELECT TO public USING (published = true)", + "pretty/create_policy-6.sql": "CREATE POLICY \"Simple Policy\" ON posts FOR SELECT TO public USING (published = true)", + "pretty/create_policy-7.sql": "CREATE POLICY SimplePolicy ON posts FOR SELECT TO public USING (published = true)", + "pretty/constraints-1.sql": "ALTER TABLE public.users\n ADD CONSTRAINT users_pkey PRIMARY KEY (id)", + "pretty/constraints-2.sql": "ALTER TABLE \"App\".\"User Data\"\n ADD CONSTRAINT \"Unique_Full Name\" UNIQUE (\"Full Name\")", + "pretty/constraints-3.sql": "ALTER TABLE school.attendance\n ADD CONSTRAINT attendance_unique UNIQUE (\"Student ID\", \"Class ID\")", + "pretty/constraints-4.sql": "ALTER TABLE \"Orders\".\"OrderLines\"\n ADD CONSTRAINT \"FK_Order_Ref\" FOREIGN KEY (order_id)\n REFERENCES \"Orders\".\"Order\"(\"OrderID\")", + "pretty/constraints-5.sql": "ALTER TABLE \"x-Schema\".\"z-Table\"\n ADD CONSTRAINT \"zNameFormatCheck\" CHECK (\"Z-Name\" ~ '^[A-Z]')", + "pretty/constraints-6.sql": "ALTER TABLE data.snapshots\n ADD CONSTRAINT metadata_has_key CHECK (metadata ? 'type')", + "pretty/constraints-7.sql": "ALTER TABLE \"Billing\".\"Invoices\"\n ADD CONSTRAINT \"FK_Client_ID\"\n FOREIGN KEY (\"Client ID\") REFERENCES \"Clients\".\"ClientBase\"(\"Client ID\")", + "pretty/constraints-8.sql": "ALTER TABLE \"API Keys\"\n ADD CONSTRAINT \"PK_KeyID\" PRIMARY KEY (\"KeyID\")", + "pretty/constraints-9.sql": "ALTER TABLE finance.transactions\n ADD CONSTRAINT tax_rate_range CHECK (tax_rate >= 0 AND tax_rate <= 1)", + "pretty/constraints-10.sql": "ALTER TABLE school.enrollments\n ADD CONSTRAINT fk_student_course FOREIGN KEY (student_id, course_id)\n REFERENCES school.courses_students(student_id, course_id)", + "pretty/constraints-11.sql": "CREATE TABLE orders (\n id SERIAL PRIMARY KEY,\n user_id INTEGER NOT NULL,\n total DECIMAL(10,2) CHECK (total > 0),\n status VARCHAR(20) DEFAULT 'pending',\n created_at TIMESTAMP DEFAULT now(),\n CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,\n CONSTRAINT unique_user_date UNIQUE (user_id, created_at),\n CONSTRAINT check_status CHECK (status IN ('pending', 'completed', 'cancelled'))\n)", + "pretty/constraints-12.sql": "ALTER TABLE products ADD CONSTRAINT fk_category \n FOREIGN KEY (category_id) \n REFERENCES categories(id) \n ON UPDATE CASCADE \n ON DELETE SET NULL \n DEFERRABLE INITIALLY DEFERRED", + "pretty/constraints-13.sql": "ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0)", + "pretty/constraints-14.sql": "ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email)", + "pretty/constraints-15.sql": "ALTER TABLE school.enrollments\n ADD CONSTRAINT fk_student_course\n FOREIGN KEY (student_id, course_id)\n REFERENCES school.courses_students (student_id, course_id)", + "pretty/constraints-16.sql": "ALTER TABLE school.enrollments\n ADD CONSTRAINT chk_enrollment_date\n CHECK (\n enrollment_date <= CURRENT_DATE\n AND status IN ('active', 'completed', 'withdrawn')\n )", + "pretty/constraints-17.sql": "CREATE TABLE school.enrollments (\n student_id INT NOT NULL,\n course_id INT NOT NULL,\n enrollment_date DATE NOT NULL,\n status TEXT CHECK (\n status IN ('active', 'completed', 'withdrawn')\n ),\n CHECK (\n enrollment_date <= CURRENT_DATE\n )\n)", + "pretty/casing-1.sql": "INSERT INTO users (name) VALUES ('John Doe')", + "pretty/casing-2.sql": "INSERT INTO users (name) VALUES ('ADMINISTRATOR')", + "pretty/casing-3.sql": "INSERT INTO users (name) VALUES ('lowercase')", + "pretty/casing-4.sql": "INSERT INTO users (name) VALUES ('camelCaseString')", + "pretty/casing-5.sql": "INSERT INTO users (name) VALUES ('snake_case_string')", + "pretty/casing-6.sql": "INSERT INTO users (name) VALUES ('kebab-case-value')", + "pretty/casing-7.sql": "INSERT INTO data.snapshots (metadata) VALUES ('{\"Type\": \"Full\", \"Status\": \"OK\"}')", + "pretty/casing-8.sql": "INSERT INTO \"AppSchema\".\"User Data\" (\"Full Name\") VALUES ('Jane Smith')", + "pretty/casing-9.sql": "INSERT INTO logtable (message) VALUES ('Init')", + "pretty/casing-10.sql": "INSERT INTO metrics.logs (message) VALUES ('NOW()')", + "pretty/casing-11.sql": "INSERT INTO users (name) VALUES ('SELECT')", + "pretty/casing-12.sql": "INSERT INTO users (name) VALUES ('john_doe@example.com')", + "pretty/casing-13.sql": "SELECT 'MixedCase'", + "pretty/casing-14.sql": "SELECT 'UPPERCASE'", + "pretty/casing-15.sql": "SELECT 'lowercase'", + "pretty/casing-16.sql": "SELECT 'camelCase'", + "pretty/casing-17.sql": "SELECT 'snake_case'", + "pretty/casing-18.sql": "SELECT 'kebab-case'", + "pretty/casing-19.sql": "SELECT 'SELECT * FROM users'", + "pretty/casing-20.sql": "SELECT 'sum(a + b)'", + "pretty/casing-21.sql": "SELECT name AS \"UserLabel\" FROM users", + "pretty/casing-22.sql": "SELECT * FROM users WHERE name = 'camelCaseString'", + "pretty/casing-23.sql": "SELECT * FROM users WHERE name = 'lowercase'", + "pretty/casing-24.sql": "SELECT * FROM users WHERE name = 'ADMINISTRATOR'", + "pretty/casing-25.sql": "SELECT * FROM logs WHERE message LIKE 'Warn%'", + "pretty/casing-26.sql": "SELECT * FROM alerts WHERE level IN ('Low', 'MEDIUM', 'High', 'CRITICAL')", + "pretty/casing-27.sql": "SELECT 'It''s working'", + "pretty/casing-28.sql": "SELECT E'Line1\\\\nLine2'", + "pretty/casing-29.sql": "SELECT 'Status: ✅'", + "pretty/casing-30.sql": "SELECT 'ALERT' AS \"Level\"", + "pretty/casing-31.sql": "SELECT \"HandleInsert\"('TYPE_A', 'Region-1')", + "pretty/casing-32.sql": "SELECT * FROM \"dataPoints\"", "original/simple-1.sql": "SELECT\n *\nFROM\n table_name\nWHERE\n name = 'test' AND num > 7 AND\n last_name LIKE '%''test''%'", "original/simple-2.sql": "SELECT\n *\nFROM\n table_name\nWHERE\n name = 'test' AND num > 7 AND\n last_name NOT LIKE '%''test''%'", "original/simple-3.sql": "SELECT\n *\nFROM\n table_name\nWHERE\n name = 'test' AND num > 7 AND\n last_name ILIKE '%''test''%'", diff --git a/__fixtures__/kitchen-sink/pretty/casing.sql b/__fixtures__/kitchen-sink/pretty/casing.sql new file mode 100644 index 00000000..7b3f21ec --- /dev/null +++ b/__fixtures__/kitchen-sink/pretty/casing.sql @@ -0,0 +1,97 @@ +-- 1. Insert with simple mixed-case string +INSERT INTO users (name) VALUES ('John Doe'); + +-- 2. Insert with ALL CAPS +INSERT INTO users (name) VALUES ('ADMINISTRATOR'); + +-- 3. Insert with lowercase only +INSERT INTO users (name) VALUES ('lowercase'); + +-- 4. Insert with camelCase +INSERT INTO users (name) VALUES ('camelCaseString'); + +-- 5. Insert with snake_case +INSERT INTO users (name) VALUES ('snake_case_string'); + +-- 6. Insert with kebab-case (string literal) +INSERT INTO users (name) VALUES ('kebab-case-value'); + +-- 7. Insert with JSON-looking string +INSERT INTO data.snapshots (metadata) VALUES ('{"Type": "Full", "Status": "OK"}'); + +-- 8. Insert into quoted table and column +INSERT INTO "AppSchema"."User Data" ("Full Name") VALUES ('Jane Smith'); + +-- 9. Insert multiple values with mixed casing +-- FILE ISSUE FOR THIS upstream: +-- INSERT INTO logtable (message) VALUES ('Init'), ('Reboot'), ('ERROR'), ('Warning'), ('info'); +INSERT INTO logtable (message) VALUES ('Init'); + +-- 10. Insert a string that looks like a function +INSERT INTO metrics.logs (message) VALUES ('NOW()'); + +-- 11. Insert with exact keyword-looking string +INSERT INTO users (name) VALUES ('SELECT'); + +-- 12. Insert lowercase string with special characters +INSERT INTO users (name) VALUES ('john_doe@example.com'); + +-- 13. Select mixed-case string literal +SELECT 'MixedCase'; + +-- 14. Select all uppercase +SELECT 'UPPERCASE'; + +-- 15. Select lowercase +SELECT 'lowercase'; + +-- 16. Select camelCase +SELECT 'camelCase'; + +-- 17. Select snake_case +SELECT 'snake_case'; + +-- 18. Select kebab-case +SELECT 'kebab-case'; + +-- 19. Select string that looks like SQL +SELECT 'SELECT * FROM users'; + +-- 20. Select string that looks like a function +SELECT 'sum(a + b)'; + +-- 21. Select with alias and quoted output name +SELECT name AS "UserLabel" FROM users; + +-- 22. Select where literal is camelCase +SELECT * FROM users WHERE name = 'camelCaseString'; + +-- 23. Select where literal is lowercase +SELECT * FROM users WHERE name = 'lowercase'; + +-- 24. Select where literal is ALL CAPS +SELECT * FROM users WHERE name = 'ADMINISTRATOR'; + +-- 25. Select where message starts with capital W +SELECT * FROM logs WHERE message LIKE 'Warn%'; + +-- 26. Select with multiple casing in IN clause +SELECT * FROM alerts WHERE level IN ('Low', 'MEDIUM', 'High', 'CRITICAL'); + +-- 27. Select string with escaped quote +SELECT 'It''s working'; + +-- 28. Select with E-prefixed escape string +SELECT E'Line1\\nLine2'; + +-- 29. Select with Unicode emoji string +SELECT 'Status: ✅'; + +-- 30. Select into quoted alias +SELECT 'ALERT' AS "Level"; + +-- 31. Select with quoted function name +SELECT "HandleInsert"('TYPE_A', 'Region-1'); + +-- 32. Select with quoted table name +SELECT * FROM "dataPoints"; diff --git a/__fixtures__/kitchen-sink/pretty/constraints.sql b/__fixtures__/kitchen-sink/pretty/constraints.sql index 1ba8adee..18ce9a91 100644 --- a/__fixtures__/kitchen-sink/pretty/constraints.sql +++ b/__fixtures__/kitchen-sink/pretty/constraints.sql @@ -1,3 +1,47 @@ +-- 1. Add a named primary key constraint +ALTER TABLE public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + +-- 2. Add a quoted unique constraint on a mixed-case column +ALTER TABLE "App"."User Data" + ADD CONSTRAINT "Unique_Full Name" UNIQUE ("Full Name"); + +-- 3. Add a composite unique constraint with custom name +ALTER TABLE school.attendance + ADD CONSTRAINT attendance_unique UNIQUE ("Student ID", "Class ID"); + +-- 4. Add a foreign key with quoted constraint and schema-qualified reference +ALTER TABLE "Orders"."OrderLines" + ADD CONSTRAINT "FK_Order_Ref" FOREIGN KEY (order_id) + REFERENCES "Orders"."Order"("OrderID"); + +-- 5. Add a check constraint with a regex pattern +ALTER TABLE "x-Schema"."z-Table" + ADD CONSTRAINT "zNameFormatCheck" CHECK ("Z-Name" ~ '^[A-Z]'); + +-- 6. Add a check constraint on JSON key existence +ALTER TABLE data.snapshots + ADD CONSTRAINT metadata_has_key CHECK (metadata ? 'type'); + +-- 7. Add a foreign key referencing quoted schema.table.column +ALTER TABLE "Billing"."Invoices" + ADD CONSTRAINT "FK_Client_ID" + FOREIGN KEY ("Client ID") REFERENCES "Clients"."ClientBase"("Client ID"); + +-- 8. Add a primary key on a quoted identifier +ALTER TABLE "API Keys" + ADD CONSTRAINT "PK_KeyID" PRIMARY KEY ("KeyID"); + +-- 9. Add a check on numeric range +ALTER TABLE finance.transactions + ADD CONSTRAINT tax_rate_range CHECK (tax_rate >= 0 AND tax_rate <= 1); + +-- 10. Add a multi-column foreign key with custom name +ALTER TABLE school.enrollments + ADD CONSTRAINT fk_student_course FOREIGN KEY (student_id, course_id) + REFERENCES school.courses_students(student_id, course_id); + +-- 11. CREATE TABLE orders ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, @@ -9,6 +53,8 @@ CREATE TABLE orders ( CONSTRAINT check_status CHECK (status IN ('pending', 'completed', 'cancelled')) ); +-- 12. + ALTER TABLE products ADD CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES categories(id) @@ -16,6 +62,41 @@ ALTER TABLE products ADD CONSTRAINT fk_category ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED; +-- 13 + ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0); +-- 14 + ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email); + + +-- 15 + +ALTER TABLE school.enrollments + ADD CONSTRAINT fk_student_course + FOREIGN KEY (student_id, course_id) + REFERENCES school.courses_students (student_id, course_id); + +-- 16 + +ALTER TABLE school.enrollments + ADD CONSTRAINT chk_enrollment_date + CHECK ( + enrollment_date <= CURRENT_DATE + AND status IN ('active', 'completed', 'withdrawn') + ); + +-- 17 + +CREATE TABLE school.enrollments ( + student_id INT NOT NULL, + course_id INT NOT NULL, + enrollment_date DATE NOT NULL, + status TEXT CHECK ( + status IN ('active', 'completed', 'withdrawn') + ), + CHECK ( + enrollment_date <= CURRENT_DATE + ) +); diff --git a/__fixtures__/kitchen-sink/pretty/create_policy.sql b/__fixtures__/kitchen-sink/pretty/create_policy.sql index 57f4f788..dea801c3 100644 --- a/__fixtures__/kitchen-sink/pretty/create_policy.sql +++ b/__fixtures__/kitchen-sink/pretty/create_policy.sql @@ -20,3 +20,9 @@ CREATE POLICY complex_policy ON documents ); CREATE POLICY simple_policy ON posts FOR SELECT TO public USING (published = true); + +CREATE POLICY "simple_policy" ON posts FOR SELECT TO public USING (published = true); + +CREATE POLICY "Simple Policy" ON posts FOR SELECT TO public USING (published = true); + +CREATE POLICY SimplePolicy ON posts FOR SELECT TO public USING (published = true); diff --git a/__fixtures__/kitchen-sink/pretty/create_table.sql b/__fixtures__/kitchen-sink/pretty/create_table.sql index cbfff60c..5cc23ab0 100644 --- a/__fixtures__/kitchen-sink/pretty/create_table.sql +++ b/__fixtures__/kitchen-sink/pretty/create_table.sql @@ -37,3 +37,12 @@ CREATE TEMPORARY TABLE temp_calculations ( value DECIMAL(15,5), result TEXT ); + +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL, + total DECIMAL(10,2) CHECK (total > 0), + status VARCHAR(20) DEFAULT 'pending', + created_at TIMESTAMP DEFAULT now(), + FOREIGN KEY (user_id) REFERENCES users(id) +); \ No newline at end of file diff --git a/__fixtures__/kitchen-sink/pretty/cte.sql b/__fixtures__/kitchen-sink/pretty/cte.sql new file mode 100644 index 00000000..ee04f515 --- /dev/null +++ b/__fixtures__/kitchen-sink/pretty/cte.sql @@ -0,0 +1,7 @@ +WITH regional_sales AS (SELECT region, SUM(sales_amount) as total_sales FROM sales GROUP BY region) SELECT * FROM regional_sales; + +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; + +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; + +WITH sales_summary AS (SELECT region, product_category, SUM(amount) as total FROM sales GROUP BY region, product_category), regional_totals AS (SELECT region, SUM(total) as region_total FROM sales_summary GROUP BY region) SELECT s.region, s.product_category, s.total, r.region_total FROM sales_summary s JOIN regional_totals r ON s.region = r.region; diff --git a/__fixtures__/kitchen-sink/pretty/misc.sql b/__fixtures__/kitchen-sink/pretty/misc.sql index d8305540..41259005 100644 --- a/__fixtures__/kitchen-sink/pretty/misc.sql +++ b/__fixtures__/kitchen-sink/pretty/misc.sql @@ -212,3 +212,20 @@ SELECT ELSE 'normal' END AS tier FROM players; + +-- 14. A trigger + +CREATE TRIGGER decrease_job_queue_count_on_delete + AFTER DELETE ON dashboard_jobs.jobs + FOR EACH ROW + WHEN ( OLD.queue_name IS NOT NULL ) + EXECUTE PROCEDURE dashboard_jobs.tg_decrease_job_queue_count (); + +-- 15. default privileges + +ALTER DEFAULT PRIVILEGES IN SCHEMA dashboard_jobs + GRANT EXECUTE ON FUNCTIONS TO administrator; + +-- 16. grant execute on function + +GRANT EXECUTE ON FUNCTION dashboard_private.uuid_generate_seeded_uuid TO PUBLIC; diff --git a/__fixtures__/kitchen-sink/pretty/procedures.sql b/__fixtures__/kitchen-sink/pretty/procedures.sql new file mode 100644 index 00000000..756c6d82 --- /dev/null +++ b/__fixtures__/kitchen-sink/pretty/procedures.sql @@ -0,0 +1,29 @@ +-- 1. Simple function call with one string arg +SELECT handle_insert('TYPE_A'); + +-- 2. Function call with mixed-case literal (should preserve case) +SELECT "HandleInsert"('TYPE_A', 'Region-1'); + +-- 3. Function call with numeric and boolean args +SELECT compute_score(42, TRUE); + +-- 4. Schema-qualified function call +SELECT metrics.get_total('2025-01-01', '2025-01-31'); + +-- 5. Function call in WHERE clause +SELECT * FROM users WHERE is_active(user_id); + +-- 6. Function call returning composite type +SELECT * FROM get_user_details(1001); + +-- 7. Function call inside FROM clause (set-returning) +SELECT * FROM get_recent_events('login') AS events; + +-- 8. Function call with quoted identifiers and args +SELECT "Analytics"."RunQuery"('Q-123', '2025-06'); + +-- 9. Function call with nested expressions +SELECT calculate_discount(price * quantity, customer_tier); + +-- 10. Procedure-style call (PL/pgSQL do-nothing) +SELECT perform_backup('daily', FALSE); diff --git a/__fixtures__/kitchen-sink/pretty/select_statements.sql b/__fixtures__/kitchen-sink/pretty/select_statements.sql deleted file mode 100644 index 3d215de6..00000000 --- a/__fixtures__/kitchen-sink/pretty/select_statements.sql +++ /dev/null @@ -1,29 +0,0 @@ -SELECT id, name, email FROM users WHERE active = true; - -SELECT - u.id, - u.name, - u.email, - p.title as profile_title -FROM users u -JOIN profiles p ON u.id = p.user_id -WHERE u.active = true - AND u.created_at > '2023-01-01' -GROUP BY u.id, u.name, u.email, p.title -HAVING COUNT(*) > 1 -ORDER BY u.created_at DESC, u.name ASC -LIMIT 10 -OFFSET 5; - -SELECT id, name FROM users WHERE id IN ( - SELECT user_id FROM orders WHERE total > 100 -); - -SELECT name FROM customers -UNION ALL -SELECT name FROM suppliers -ORDER BY name; - -SELECT name, email FROM users WHERE status = 'active'; - -SELECT u.name, o.total FROM users u, orders o WHERE u.id = o.user_id; diff --git a/__fixtures__/kitchen-sink/pretty/selects.sql b/__fixtures__/kitchen-sink/pretty/selects.sql new file mode 100644 index 00000000..23b3a3fb --- /dev/null +++ b/__fixtures__/kitchen-sink/pretty/selects.sql @@ -0,0 +1,118 @@ +-- 1. Single simple target (no newline) +SELECT 1; + +-- 2. Single casted literal (no newline) +SELECT 'abc'::text; + +-- 3. Single function call with operator (no newline) +SELECT now() AT TIME ZONE 'UTC'; + +-- 4. Multiple literals (newline format) +SELECT + 1, + 2; + +-- 5. Multiple identifiers (newline format) +SELECT + id, + name, + email +FROM users; + +-- 6. SELECT with DISTINCT and single target (no newline) +SELECT DISTINCT id FROM users; + +-- 7. SELECT with DISTINCT and multiple targets (newline format) +SELECT DISTINCT + id, + name +FROM users; + +-- 8. SELECT with aliasing and expressions (newline format) +SELECT + id, + upper(name) AS name_upper, + created_at + interval '1 day' AS expires_at +FROM accounts; + +-- 9. SELECT with subselect (single target, no newline) +SELECT (SELECT max(score) FROM results); + +-- 10. SELECT with function and window (multiple targets, newline format) +SELECT + count(*) OVER (), + u.id +FROM users u; + + +-- 11. Union query combining customer and supplier names with ALL modifier +SELECT + name +FROM customers +UNION +ALL +SELECT + name +FROM suppliers +ORDER BY + name; + + +-- 12. Complex join query demonstrating multiple join types (INNER, LEFT, RIGHT) +SELECT + u.id, + u.name, + u.email, + p.title +FROM users AS u +JOIN profiles AS p ON u.id = p.user_id +LEFT JOIN orders AS o ON u.id = o.user_id +RIGHT JOIN addresses AS a ON u.id = a.user_id +WHERE + u.active = true; + +-- 13. Subquery in WHERE clause using IN operator +SELECT + id, + name +FROM users +WHERE + id IN (SELECT + user_id +FROM orders +WHERE + total > 100); + + +-- 14. Basic SELECT with WHERE clause filtering active users +SELECT + id, + name, + email +FROM users +WHERE + active = true; + +-- 15. Complex query with JOIN, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT and OFFSET +SELECT + u.id, + u.name, + u.email, + p.title +FROM users AS u +JOIN profiles AS p ON u.id = p.user_id +WHERE + u.active = true + AND u.created_at > '2023-01-01' +GROUP BY + u.id, + u.name, + u.email, + p.title +HAVING + count(*) > 1 +ORDER BY + u.created_at DESC, + u.name ASC +LIMIT 10 +OFFSET 5; \ No newline at end of file diff --git a/__fixtures__/kitchen-sink/pretty/tables.sql b/__fixtures__/kitchen-sink/pretty/tables.sql new file mode 100644 index 00000000..a5fd32eb --- /dev/null +++ b/__fixtures__/kitchen-sink/pretty/tables.sql @@ -0,0 +1,133 @@ +-- 1. Simple table with primary key +CREATE TABLE public.users ( + id serial PRIMARY KEY, + name text NOT NULL +); + +-- 2. Quoted table and column names +CREATE TABLE "App"."User Data" ( + "User ID" uuid PRIMARY KEY, + "Full Name" text NOT NULL +); + +-- 3. Table with default values and a quoted constraint name +CREATE TABLE system.settings ( + setting_key text PRIMARY KEY, + setting_value text, + CONSTRAINT "Default Setting Check" CHECK (setting_value IS NOT NULL) +); + +-- 4. Table with quoted composite column and array +CREATE TABLE "Inventory"."StockItems" ( + "ItemID" int PRIMARY KEY, + "Tags" text[] +); + +-- 5. Foreign key with a quoted constraint name and target +CREATE TABLE "Orders"."OrderLines" ( + id serial PRIMARY KEY, + order_id int, + CONSTRAINT "FK Order Reference" FOREIGN KEY (order_id) REFERENCES "Orders"."Order"("OrderID") +); + +-- 6. Table using composite type with mixed-case field names +CREATE TABLE contact_info ( + id int PRIMARY KEY, + location address -- assumed composite type +); + +-- 7. Inheritance with quoted base table +CREATE TABLE "Archive"."OldUsers" ( + archived_at timestamptz DEFAULT now() +) INHERITS ("Users"."User Data"); + +-- 8. Identity column with quoted constraint +CREATE TABLE logging.audit_trail ( + log_id int GENERATED BY DEFAULT AS IDENTITY, + message text, + CONSTRAINT "PK_Audit" PRIMARY KEY (log_id) +); + +-- 9. Generated column with expression +CREATE TABLE finance.transactions ( + amount numeric, + tax_rate numeric, + total numeric GENERATED ALWAYS AS (amount * (1 + tax_rate)) STORED +); + +-- 10. Range partitioned table +CREATE TABLE metrics.monthly_stats ( + stat_id serial, + recorded_at date +) PARTITION BY RANGE (recorded_at); + +-- 11. Composite multi-column primary key +CREATE TABLE school.attendance ( + "Student ID" uuid, + "Class ID" uuid, + attended_on date DEFAULT CURRENT_DATE, + PRIMARY KEY ("Student ID", "Class ID") +); + +-- 12. Table with special chars in constraint name +CREATE TABLE secure.sessions ( + session_id uuid PRIMARY KEY, + user_id uuid, + CONSTRAINT "fk-user->session" FOREIGN KEY (user_id) REFERENCES users(id) +); + +-- 13. Table with multiple unique constraints and quoted keys +CREATE TABLE public."API Keys" ( + "KeyID" uuid PRIMARY KEY, + "ClientName" text, + "KeyValue" text UNIQUE, + CONSTRAINT "Unique_ClientName" UNIQUE ("ClientName") +); + +-- 14. Enum field reference +CREATE TABLE alerts ( + alert_id serial PRIMARY KEY, + level "AlertLevel" NOT NULL -- assumed enum type +); + +-- 15. Table with foreign key referencing quoted table/column +CREATE TABLE "Billing"."Invoices" ( + invoice_id uuid PRIMARY KEY, + "Client ID" uuid, + CONSTRAINT "FK_Client" FOREIGN KEY ("Client ID") REFERENCES "Clients"."ClientBase"("Client ID") +); + +-- 16. Table with special-cased check +CREATE TABLE media.assets ( + id uuid PRIMARY KEY, + url text, + CONSTRAINT "Check-URL-NonEmpty" CHECK (url <> '') +); + +-- 17. Composite type and JSON field +CREATE TABLE data.snapshots ( + id serial PRIMARY KEY, + metadata jsonb, + context address +); + +-- 18. Table with quoted schema, table, and constraints +CREATE TABLE "x-Schema"."z-Table" ( + "Z-ID" int PRIMARY KEY, + "Z-Name" text, + CONSTRAINT "z-Name-Check" CHECK ("Z-Name" ~ '^[A-Z]') +); + +-- 19. Table with lowercase constraint on quoted column +CREATE TABLE users.details ( + "first_name" text NOT NULL, + "last_name" text, + CONSTRAINT "first_name_required" CHECK ("first_name" <> '') +); + +-- 20. Table using generated stored and quoted default +CREATE TABLE "Calculated"."Metrics" ( + base int, + adjustment int DEFAULT 0, + "Total" int GENERATED ALWAYS AS (base + adjustment) STORED +); diff --git a/__fixtures__/kitchen-sink/pretty/triggers.sql b/__fixtures__/kitchen-sink/pretty/triggers.sql new file mode 100644 index 00000000..384f6739 --- /dev/null +++ b/__fixtures__/kitchen-sink/pretty/triggers.sql @@ -0,0 +1,72 @@ +-- 1. Basic unquoted trigger +CREATE TRIGGER audit_insert_trigger + AFTER INSERT + ON public.users + FOR EACH ROW + EXECUTE PROCEDURE log_user_insert(); + +-- 2. Quoted trigger name and table +CREATE TRIGGER "AuditTrigger" + AFTER DELETE + ON "SensitiveData" + FOR EACH ROW + EXECUTE PROCEDURE public.log_deletion(); + +-- 3. Trigger with WHEN clause and quoted function name +CREATE TRIGGER archive_if_inactive + BEFORE UPDATE + ON accounts + FOR EACH ROW + WHEN (OLD.active = false) + EXECUTE PROCEDURE "ArchiveFunction"(); + +-- 4. Schema-qualified trigger with arguments +CREATE TRIGGER update_stats_on_change + AFTER UPDATE OR INSERT + ON metrics.stats + FOR EACH ROW + EXECUTE PROCEDURE metrics.update_stats('user', true); + +-- 5. Quoted everything: trigger, table, schema, function +CREATE TRIGGER "TrickyTrigger" + BEFORE DELETE + ON "weirdSchema"."ComplexTable" + FOR EACH ROW + WHEN (OLD."status" = 'pending') + EXECUTE PROCEDURE "weirdSchema"."ComplexFn"('arg1', 42); + +-- 6. Trigger with multiple events +CREATE TRIGGER user_activity_log + AFTER INSERT OR DELETE OR UPDATE + ON users + FOR EACH ROW + EXECUTE PROCEDURE audit.activity_log(); + +-- 7. Trigger with no schema qualification +CREATE TRIGGER no_schema + BEFORE INSERT + ON log_table + FOR EACH ROW + EXECUTE PROCEDURE update_log(); + +-- 8. Trigger with quoted column references in WHEN +CREATE TRIGGER flag_special_updates + AFTER UPDATE + ON profiles + FOR EACH ROW + WHEN (NEW."accessLevel" = 'admin') + EXECUTE PROCEDURE flag_admin_change(); + +-- 9. Mixed-casing and quoted function args +CREATE TRIGGER "TriggerMixedCase" + BEFORE INSERT + ON dataPoints + FOR EACH ROW + EXECUTE PROCEDURE "HandleInsert"('TYPE_A', 'Region-1'); + +-- 10. Trigger for partitioned table +CREATE TRIGGER cascade_on_partition + AFTER DELETE + ON events_log_partition + FOR EACH ROW + EXECUTE PROCEDURE propagate_deletion(); diff --git a/__fixtures__/kitchen-sink/pretty/types.sql b/__fixtures__/kitchen-sink/pretty/types.sql new file mode 100644 index 00000000..9b3266a3 --- /dev/null +++ b/__fixtures__/kitchen-sink/pretty/types.sql @@ -0,0 +1,44 @@ +-- 1. Basic enum type +CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy'); + +-- 2. Enum with mixed-case and quoted values +CREATE TYPE "AlertLevel" AS ENUM ('Low', 'MEDIUM', 'High', 'CRITICAL'); + +-- 3. Composite type (record-style) +CREATE TYPE address AS ( + street text, + city text, + zip_code int +); + +-- 4. Composite type with quoted field names and types +CREATE TYPE "PostalInfo" AS ( + "Street" text, + "City" text, + "ZipCode" integer +); + +-- 5. Schema-qualified composite type +CREATE TYPE public.user_metadata AS ( + key text, + value jsonb +); + +-- 6. Range type with canonical, subtype and collation +CREATE TYPE tsrange_custom AS RANGE ( + subtype = timestamp with time zone, + subtype_diff = timestamp_diff, + canonical = normalize_tsrange +); + +-- 7. Enum with numeric-looking labels (quoted) +CREATE TYPE version_enum AS ENUM ('1.0', '1.1', '2.0'); + +-- 8. Composite with nested types +CREATE TYPE full_location AS ( + address address, + region_code char(2) +); + +-- 9. Complex enum type with hyphens and special chars (quoted) +CREATE TYPE "Workflow-State" AS ENUM ('draft', 'in-review', 'needs-fix', 'finalized'); diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-casing.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-casing.test.ts new file mode 100644 index 00000000..4f41e74c --- /dev/null +++ b/packages/deparser/__tests__/kitchen-sink/pretty-casing.test.ts @@ -0,0 +1,40 @@ + +import { FixtureTestUtils } from '../../test-utils'; +const fixtures = new FixtureTestUtils(); + +it('pretty-casing', async () => { + await fixtures.runFixtureTests([ + "pretty/casing-1.sql", + "pretty/casing-2.sql", + "pretty/casing-3.sql", + "pretty/casing-4.sql", + "pretty/casing-5.sql", + "pretty/casing-6.sql", + "pretty/casing-7.sql", + "pretty/casing-8.sql", + "pretty/casing-9.sql", + "pretty/casing-10.sql", + "pretty/casing-11.sql", + "pretty/casing-12.sql", + "pretty/casing-13.sql", + "pretty/casing-14.sql", + "pretty/casing-15.sql", + "pretty/casing-16.sql", + "pretty/casing-17.sql", + "pretty/casing-18.sql", + "pretty/casing-19.sql", + "pretty/casing-20.sql", + "pretty/casing-21.sql", + "pretty/casing-22.sql", + "pretty/casing-23.sql", + "pretty/casing-24.sql", + "pretty/casing-25.sql", + "pretty/casing-26.sql", + "pretty/casing-27.sql", + "pretty/casing-28.sql", + "pretty/casing-29.sql", + "pretty/casing-30.sql", + "pretty/casing-31.sql", + "pretty/casing-32.sql" +]); +}); diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-constraints.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-constraints.test.ts index 059a839d..293f3159 100644 --- a/packages/deparser/__tests__/kitchen-sink/pretty-constraints.test.ts +++ b/packages/deparser/__tests__/kitchen-sink/pretty-constraints.test.ts @@ -7,6 +7,19 @@ it('pretty-constraints', async () => { "pretty/constraints-1.sql", "pretty/constraints-2.sql", "pretty/constraints-3.sql", - "pretty/constraints-4.sql" + "pretty/constraints-4.sql", + "pretty/constraints-5.sql", + "pretty/constraints-6.sql", + "pretty/constraints-7.sql", + "pretty/constraints-8.sql", + "pretty/constraints-9.sql", + "pretty/constraints-10.sql", + "pretty/constraints-11.sql", + "pretty/constraints-12.sql", + "pretty/constraints-13.sql", + "pretty/constraints-14.sql", + "pretty/constraints-15.sql", + "pretty/constraints-16.sql", + "pretty/constraints-17.sql" ]); }); diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-create_policy.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-create_policy.test.ts index 31999ece..7e381647 100644 --- a/packages/deparser/__tests__/kitchen-sink/pretty-create_policy.test.ts +++ b/packages/deparser/__tests__/kitchen-sink/pretty-create_policy.test.ts @@ -7,6 +7,9 @@ it('pretty-create_policy', async () => { "pretty/create_policy-1.sql", "pretty/create_policy-2.sql", "pretty/create_policy-3.sql", - "pretty/create_policy-4.sql" + "pretty/create_policy-4.sql", + "pretty/create_policy-5.sql", + "pretty/create_policy-6.sql", + "pretty/create_policy-7.sql" ]); }); diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-create_table.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-create_table.test.ts index 50449dd4..f4fe067f 100644 --- a/packages/deparser/__tests__/kitchen-sink/pretty-create_table.test.ts +++ b/packages/deparser/__tests__/kitchen-sink/pretty-create_table.test.ts @@ -8,6 +8,7 @@ it('pretty-create_table', async () => { "pretty/create_table-2.sql", "pretty/create_table-3.sql", "pretty/create_table-4.sql", - "pretty/create_table-5.sql" + "pretty/create_table-5.sql", + "pretty/create_table-6.sql" ]); }); diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-cte.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-cte.test.ts new file mode 100644 index 00000000..82c53157 --- /dev/null +++ b/packages/deparser/__tests__/kitchen-sink/pretty-cte.test.ts @@ -0,0 +1,12 @@ + +import { FixtureTestUtils } from '../../test-utils'; +const fixtures = new FixtureTestUtils(); + +it('pretty-cte', async () => { + await fixtures.runFixtureTests([ + "pretty/cte-1.sql", + "pretty/cte-2.sql", + "pretty/cte-3.sql", + "pretty/cte-4.sql" +]); +}); diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-misc.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-misc.test.ts index 8c93e54f..5a57f1e6 100644 --- a/packages/deparser/__tests__/kitchen-sink/pretty-misc.test.ts +++ b/packages/deparser/__tests__/kitchen-sink/pretty-misc.test.ts @@ -16,6 +16,9 @@ it('pretty-misc', async () => { "pretty/misc-10.sql", "pretty/misc-11.sql", "pretty/misc-12.sql", - "pretty/misc-13.sql" + "pretty/misc-13.sql", + "pretty/misc-14.sql", + "pretty/misc-15.sql", + "pretty/misc-16.sql" ]); }); diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-procedures.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-procedures.test.ts new file mode 100644 index 00000000..37b8bafa --- /dev/null +++ b/packages/deparser/__tests__/kitchen-sink/pretty-procedures.test.ts @@ -0,0 +1,18 @@ + +import { FixtureTestUtils } from '../../test-utils'; +const fixtures = new FixtureTestUtils(); + +it('pretty-procedures', async () => { + await fixtures.runFixtureTests([ + "pretty/procedures-1.sql", + "pretty/procedures-2.sql", + "pretty/procedures-3.sql", + "pretty/procedures-4.sql", + "pretty/procedures-5.sql", + "pretty/procedures-6.sql", + "pretty/procedures-7.sql", + "pretty/procedures-8.sql", + "pretty/procedures-9.sql", + "pretty/procedures-10.sql" +]); +}); diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-selects.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-selects.test.ts new file mode 100644 index 00000000..4e7efb06 --- /dev/null +++ b/packages/deparser/__tests__/kitchen-sink/pretty-selects.test.ts @@ -0,0 +1,23 @@ + +import { FixtureTestUtils } from '../../test-utils'; +const fixtures = new FixtureTestUtils(); + +it('pretty-selects', async () => { + await fixtures.runFixtureTests([ + "pretty/selects-1.sql", + "pretty/selects-2.sql", + "pretty/selects-3.sql", + "pretty/selects-4.sql", + "pretty/selects-5.sql", + "pretty/selects-6.sql", + "pretty/selects-7.sql", + "pretty/selects-8.sql", + "pretty/selects-9.sql", + "pretty/selects-10.sql", + "pretty/selects-11.sql", + "pretty/selects-12.sql", + "pretty/selects-13.sql", + "pretty/selects-14.sql", + "pretty/selects-15.sql" +]); +}); diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-tables.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-tables.test.ts new file mode 100644 index 00000000..0843c2e3 --- /dev/null +++ b/packages/deparser/__tests__/kitchen-sink/pretty-tables.test.ts @@ -0,0 +1,28 @@ + +import { FixtureTestUtils } from '../../test-utils'; +const fixtures = new FixtureTestUtils(); + +it('pretty-tables', async () => { + await fixtures.runFixtureTests([ + "pretty/tables-1.sql", + "pretty/tables-2.sql", + "pretty/tables-3.sql", + "pretty/tables-4.sql", + "pretty/tables-5.sql", + "pretty/tables-6.sql", + "pretty/tables-7.sql", + "pretty/tables-8.sql", + "pretty/tables-9.sql", + "pretty/tables-10.sql", + "pretty/tables-11.sql", + "pretty/tables-12.sql", + "pretty/tables-13.sql", + "pretty/tables-14.sql", + "pretty/tables-15.sql", + "pretty/tables-16.sql", + "pretty/tables-17.sql", + "pretty/tables-18.sql", + "pretty/tables-19.sql", + "pretty/tables-20.sql" +]); +}); diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-triggers.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-triggers.test.ts new file mode 100644 index 00000000..06837e60 --- /dev/null +++ b/packages/deparser/__tests__/kitchen-sink/pretty-triggers.test.ts @@ -0,0 +1,18 @@ + +import { FixtureTestUtils } from '../../test-utils'; +const fixtures = new FixtureTestUtils(); + +it('pretty-triggers', async () => { + await fixtures.runFixtureTests([ + "pretty/triggers-1.sql", + "pretty/triggers-2.sql", + "pretty/triggers-3.sql", + "pretty/triggers-4.sql", + "pretty/triggers-5.sql", + "pretty/triggers-6.sql", + "pretty/triggers-7.sql", + "pretty/triggers-8.sql", + "pretty/triggers-9.sql", + "pretty/triggers-10.sql" +]); +}); diff --git a/packages/deparser/__tests__/kitchen-sink/pretty-types.test.ts b/packages/deparser/__tests__/kitchen-sink/pretty-types.test.ts new file mode 100644 index 00000000..d54be4e2 --- /dev/null +++ b/packages/deparser/__tests__/kitchen-sink/pretty-types.test.ts @@ -0,0 +1,17 @@ + +import { FixtureTestUtils } from '../../test-utils'; +const fixtures = new FixtureTestUtils(); + +it('pretty-types', async () => { + await fixtures.runFixtureTests([ + "pretty/types-1.sql", + "pretty/types-2.sql", + "pretty/types-3.sql", + "pretty/types-4.sql", + "pretty/types-5.sql", + "pretty/types-6.sql", + "pretty/types-7.sql", + "pretty/types-8.sql", + "pretty/types-9.sql" +]); +}); diff --git a/packages/deparser/__tests__/misc/__snapshots__/castings.test.ts.snap b/packages/deparser/__tests__/misc/__snapshots__/castings.test.ts.snap new file mode 100644 index 00000000..7811384e --- /dev/null +++ b/packages/deparser/__tests__/misc/__snapshots__/castings.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should format foreign key constraint with pretty option enabled 1`] = `"SELECT '123'::int;"`; diff --git a/packages/deparser/__tests__/misc/__snapshots__/pg-catalog.test.ts.snap b/packages/deparser/__tests__/misc/__snapshots__/pg-catalog.test.ts.snap new file mode 100644 index 00000000..0916aeb6 --- /dev/null +++ b/packages/deparser/__tests__/misc/__snapshots__/pg-catalog.test.ts.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should format pg_catalog.char with pretty option enabled 1`] = ` +"CREATE TABLE dashboard_jobs.jobs ( + id bigserial PRIMARY KEY, + queue_name text DEFAULT public.gen_random_uuid()::text, + task_identifier text NOT NULL, + payload pg_catalog.json DEFAULT '{}'::json NOT NULL, + priority int DEFAULT 0 NOT NULL, + run_at timestamptz DEFAULT now() NOT NULL, + attempts int DEFAULT 0 NOT NULL, + max_attempts int DEFAULT 25 NOT NULL, + key text, + last_error text, + locked_at timestamptz, + locked_by text, + CHECK (length(key) < 513), + CHECK (length(task_identifier) < 127), + CHECK (max_attempts > 0), + CHECK (length(queue_name) < 127), + CHECK (length(locked_by) > 3), + UNIQUE (key) +);" +`; diff --git a/packages/deparser/__tests__/misc/castings.test.ts b/packages/deparser/__tests__/misc/castings.test.ts new file mode 100644 index 00000000..a5e09941 --- /dev/null +++ b/packages/deparser/__tests__/misc/castings.test.ts @@ -0,0 +1,7 @@ +import { expectParseDeparse } from '../../test-utils'; + +it('should format foreign key constraint with pretty option enabled', async () => { + const sql = `SELECT '123'::INTEGER;`; + const result = await expectParseDeparse(sql, { pretty: true }); + expect(result).toMatchSnapshot(); +}); diff --git a/packages/deparser/__tests__/misc/pg-catalog.test.ts b/packages/deparser/__tests__/misc/pg-catalog.test.ts new file mode 100644 index 00000000..404d1e61 --- /dev/null +++ b/packages/deparser/__tests__/misc/pg-catalog.test.ts @@ -0,0 +1,28 @@ +import { expectParseDeparse } from '../../test-utils'; + +it('should format pg_catalog.char with pretty option enabled', async () => { + const sql = ` +CREATE TABLE dashboard_jobs.jobs ( + id bigserial PRIMARY KEY, + queue_name text DEFAULT CAST(public.gen_random_uuid() AS text), + task_identifier text NOT NULL, + payload pg_catalog.json DEFAULT '{}'::json NOT NULL, + priority int DEFAULT 0 NOT NULL, + run_at timestamptz DEFAULT now() NOT NULL, + attempts int DEFAULT 0 NOT NULL, + max_attempts int DEFAULT 25 NOT NULL, + key text, + last_error text, + locked_at timestamptz, + locked_by text, + CHECK (length(key) < 513), + CHECK (length(task_identifier) < 127), + CHECK (max_attempts > 0), + CHECK (length(queue_name) < 127), + CHECK (length(locked_by) > 3), + UNIQUE (key) +); + `; + const result = await expectParseDeparse(sql, { pretty: true }); + expect(result).toMatchSnapshot(); +}); diff --git a/packages/deparser/__tests__/pretty/__snapshots__/casing-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/casing-pretty.test.ts.snap new file mode 100644 index 00000000..94398412 --- /dev/null +++ b/packages/deparser/__tests__/pretty/__snapshots__/casing-pretty.test.ts.snap @@ -0,0 +1,196 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`non-pretty: pretty/casing-1.sql 1`] = `"INSERT INTO users (name) VALUES ('John Doe')"`; + +exports[`non-pretty: pretty/casing-2.sql 1`] = `"INSERT INTO users (name) VALUES ('ADMINISTRATOR')"`; + +exports[`non-pretty: pretty/casing-3.sql 1`] = `"INSERT INTO users (name) VALUES ('lowercase')"`; + +exports[`non-pretty: pretty/casing-4.sql 1`] = `"INSERT INTO users (name) VALUES ('camelCaseString')"`; + +exports[`non-pretty: pretty/casing-5.sql 1`] = `"INSERT INTO users (name) VALUES ('snake_case_string')"`; + +exports[`non-pretty: pretty/casing-6.sql 1`] = `"INSERT INTO users (name) VALUES ('kebab-case-value')"`; + +exports[`non-pretty: pretty/casing-7.sql 1`] = `"INSERT INTO data.snapshots (metadata) VALUES ('{"Type": "Full", "Status": "OK"}')"`; + +exports[`non-pretty: pretty/casing-8.sql 1`] = `"INSERT INTO "AppSchema"."User Data" ("Full Name") VALUES ('Jane Smith')"`; + +exports[`non-pretty: pretty/casing-9.sql 1`] = `"INSERT INTO logtable (message) VALUES ('Init')"`; + +exports[`non-pretty: pretty/casing-10.sql 1`] = `"INSERT INTO metrics.logs (message) VALUES ('NOW()')"`; + +exports[`non-pretty: pretty/casing-11.sql 1`] = `"INSERT INTO users (name) VALUES ('SELECT')"`; + +exports[`non-pretty: pretty/casing-12.sql 1`] = `"INSERT INTO users (name) VALUES ('john_doe@example.com')"`; + +exports[`non-pretty: pretty/casing-13.sql 1`] = `"SELECT 'MixedCase'"`; + +exports[`non-pretty: pretty/casing-14.sql 1`] = `"SELECT 'UPPERCASE'"`; + +exports[`non-pretty: pretty/casing-15.sql 1`] = `"SELECT 'lowercase'"`; + +exports[`non-pretty: pretty/casing-16.sql 1`] = `"SELECT 'camelCase'"`; + +exports[`non-pretty: pretty/casing-17.sql 1`] = `"SELECT 'snake_case'"`; + +exports[`non-pretty: pretty/casing-18.sql 1`] = `"SELECT 'kebab-case'"`; + +exports[`non-pretty: pretty/casing-19.sql 1`] = `"SELECT 'SELECT * FROM users'"`; + +exports[`non-pretty: pretty/casing-20.sql 1`] = `"SELECT 'sum(a + b)'"`; + +exports[`non-pretty: pretty/casing-21.sql 1`] = `"SELECT name AS "UserLabel" FROM users"`; + +exports[`non-pretty: pretty/casing-22.sql 1`] = `"SELECT * FROM users WHERE name = 'camelCaseString'"`; + +exports[`non-pretty: pretty/casing-23.sql 1`] = `"SELECT * FROM users WHERE name = 'lowercase'"`; + +exports[`non-pretty: pretty/casing-24.sql 1`] = `"SELECT * FROM users WHERE name = 'ADMINISTRATOR'"`; + +exports[`non-pretty: pretty/casing-25.sql 1`] = `"SELECT * FROM logs WHERE message LIKE 'Warn%'"`; + +exports[`non-pretty: pretty/casing-26.sql 1`] = `"SELECT * FROM alerts WHERE level IN ('Low', 'MEDIUM', 'High', 'CRITICAL')"`; + +exports[`non-pretty: pretty/casing-27.sql 1`] = `"SELECT 'It''s working'"`; + +exports[`non-pretty: pretty/casing-28.sql 1`] = `"SELECT E'Line1\\\\nLine2'"`; + +exports[`non-pretty: pretty/casing-29.sql 1`] = `"SELECT 'Status: ✅'"`; + +exports[`non-pretty: pretty/casing-30.sql 1`] = `"SELECT 'ALERT' AS "Level""`; + +exports[`non-pretty: pretty/casing-31.sql 1`] = `"SELECT "HandleInsert"('TYPE_A', 'Region-1')"`; + +exports[`non-pretty: pretty/casing-32.sql 1`] = `"SELECT * FROM "dataPoints""`; + +exports[`pretty: pretty/casing-1.sql 1`] = ` +"INSERT INTO users (name) VALUES +('John Doe')" +`; + +exports[`pretty: pretty/casing-2.sql 1`] = ` +"INSERT INTO users (name) VALUES +('ADMINISTRATOR')" +`; + +exports[`pretty: pretty/casing-3.sql 1`] = ` +"INSERT INTO users (name) VALUES +('lowercase')" +`; + +exports[`pretty: pretty/casing-4.sql 1`] = ` +"INSERT INTO users (name) VALUES +('camelCaseString')" +`; + +exports[`pretty: pretty/casing-5.sql 1`] = ` +"INSERT INTO users (name) VALUES +('snake_case_string')" +`; + +exports[`pretty: pretty/casing-6.sql 1`] = ` +"INSERT INTO users (name) VALUES +('kebab-case-value')" +`; + +exports[`pretty: pretty/casing-7.sql 1`] = ` +"INSERT INTO data.snapshots (metadata) VALUES +('{"Type": "Full", "Status": "OK"}')" +`; + +exports[`pretty: pretty/casing-8.sql 1`] = ` +"INSERT INTO "AppSchema"."User Data" ("Full Name") VALUES +('Jane Smith')" +`; + +exports[`pretty: pretty/casing-9.sql 1`] = ` +"INSERT INTO logtable (message) VALUES +('Init')" +`; + +exports[`pretty: pretty/casing-10.sql 1`] = ` +"INSERT INTO metrics.logs (message) VALUES +('NOW()')" +`; + +exports[`pretty: pretty/casing-11.sql 1`] = ` +"INSERT INTO users (name) VALUES +('SELECT')" +`; + +exports[`pretty: pretty/casing-12.sql 1`] = ` +"INSERT INTO users (name) VALUES +('john_doe@example.com')" +`; + +exports[`pretty: pretty/casing-13.sql 1`] = `"SELECT 'MixedCase'"`; + +exports[`pretty: pretty/casing-14.sql 1`] = `"SELECT 'UPPERCASE'"`; + +exports[`pretty: pretty/casing-15.sql 1`] = `"SELECT 'lowercase'"`; + +exports[`pretty: pretty/casing-16.sql 1`] = `"SELECT 'camelCase'"`; + +exports[`pretty: pretty/casing-17.sql 1`] = `"SELECT 'snake_case'"`; + +exports[`pretty: pretty/casing-18.sql 1`] = `"SELECT 'kebab-case'"`; + +exports[`pretty: pretty/casing-19.sql 1`] = `"SELECT 'SELECT * FROM users'"`; + +exports[`pretty: pretty/casing-20.sql 1`] = `"SELECT 'sum(a + b)'"`; + +exports[`pretty: pretty/casing-21.sql 1`] = ` +"SELECT name AS "UserLabel" +FROM users" +`; + +exports[`pretty: pretty/casing-22.sql 1`] = ` +"SELECT * +FROM users +WHERE + name = 'camelCaseString'" +`; + +exports[`pretty: pretty/casing-23.sql 1`] = ` +"SELECT * +FROM users +WHERE + name = 'lowercase'" +`; + +exports[`pretty: pretty/casing-24.sql 1`] = ` +"SELECT * +FROM users +WHERE + name = 'ADMINISTRATOR'" +`; + +exports[`pretty: pretty/casing-25.sql 1`] = ` +"SELECT * +FROM logs +WHERE + message LIKE 'Warn%'" +`; + +exports[`pretty: pretty/casing-26.sql 1`] = ` +"SELECT * +FROM alerts +WHERE + level IN ('Low', 'MEDIUM', 'High', 'CRITICAL')" +`; + +exports[`pretty: pretty/casing-27.sql 1`] = `"SELECT 'It''s working'"`; + +exports[`pretty: pretty/casing-28.sql 1`] = `"SELECT E'Line1\\\\nLine2'"`; + +exports[`pretty: pretty/casing-29.sql 1`] = `"SELECT 'Status: ✅'"`; + +exports[`pretty: pretty/casing-30.sql 1`] = `"SELECT 'ALERT' AS "Level""`; + +exports[`pretty: pretty/casing-31.sql 1`] = `"SELECT "HandleInsert"('TYPE_A', 'Region-1')"`; + +exports[`pretty: pretty/casing-32.sql 1`] = ` +"SELECT * +FROM "dataPoints"" +`; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/constraints-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/constraints-pretty.test.ts.snap index 89615060..fcb5d85d 100644 --- a/packages/deparser/__tests__/pretty/__snapshots__/constraints-pretty.test.ts.snap +++ b/packages/deparser/__tests__/pretty/__snapshots__/constraints-pretty.test.ts.snap @@ -1,34 +1,166 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Pretty constraint formatting should format check constraint with pretty option enabled 1`] = `"ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0);"`; +exports[`non-pretty: pretty/constraints-1.sql 1`] = `"ALTER TABLE public.users ADD CONSTRAINT users_pkey PRIMARY KEY (id)"`; -exports[`Pretty constraint formatting should format complex table with constraints with pretty option enabled 1`] = ` +exports[`non-pretty: pretty/constraints-2.sql 1`] = `"ALTER TABLE "App"."User Data" ADD CONSTRAINT "Unique_Full Name" UNIQUE ("Full Name")"`; + +exports[`non-pretty: pretty/constraints-3.sql 1`] = `"ALTER TABLE school.attendance ADD CONSTRAINT attendance_unique UNIQUE ("Student ID", "Class ID")"`; + +exports[`non-pretty: pretty/constraints-4.sql 1`] = `"ALTER TABLE "Orders"."OrderLines" ADD CONSTRAINT "FK_Order_Ref" FOREIGN KEY (order_id) REFERENCES "Orders"."Order" ("OrderID")"`; + +exports[`non-pretty: pretty/constraints-5.sql 1`] = `"ALTER TABLE "x-Schema"."z-Table" ADD CONSTRAINT "zNameFormatCheck" CHECK ("Z-Name" ~ '^[A-Z]')"`; + +exports[`non-pretty: pretty/constraints-6.sql 1`] = `"ALTER TABLE data.snapshots ADD CONSTRAINT metadata_has_key CHECK (metadata ? 'type')"`; + +exports[`non-pretty: pretty/constraints-7.sql 1`] = `"ALTER TABLE "Billing"."Invoices" ADD CONSTRAINT "FK_Client_ID" FOREIGN KEY ("Client ID") REFERENCES "Clients"."ClientBase" ("Client ID")"`; + +exports[`non-pretty: pretty/constraints-8.sql 1`] = `"ALTER TABLE "API Keys" ADD CONSTRAINT "PK_KeyID" PRIMARY KEY ("KeyID")"`; + +exports[`non-pretty: pretty/constraints-9.sql 1`] = `"ALTER TABLE finance.transactions ADD CONSTRAINT tax_rate_range CHECK (tax_rate >= 0 AND tax_rate <= 1)"`; + +exports[`non-pretty: pretty/constraints-10.sql 1`] = `"ALTER TABLE school.enrollments ADD CONSTRAINT fk_student_course FOREIGN KEY (student_id, course_id) REFERENCES school.courses_students (student_id, course_id)"`; + +exports[`non-pretty: pretty/constraints-11.sql 1`] = `"CREATE TABLE orders (id serial PRIMARY KEY, user_id int NOT NULL, total numeric(10, 2) CHECK (total > 0), status varchar(20) DEFAULT 'pending', created_at pg_catalog.timestamp DEFAULT now(), CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, CONSTRAINT unique_user_date UNIQUE (user_id, created_at), CONSTRAINT check_status CHECK (status IN ('pending', 'completed', 'cancelled')))"`; + +exports[`non-pretty: pretty/constraints-12.sql 1`] = `"ALTER TABLE products ADD CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES categories (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED"`; + +exports[`non-pretty: pretty/constraints-13.sql 1`] = `"ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0)"`; + +exports[`non-pretty: pretty/constraints-14.sql 1`] = `"ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email)"`; + +exports[`non-pretty: pretty/constraints-15.sql 1`] = `"ALTER TABLE school.enrollments ADD CONSTRAINT fk_student_course FOREIGN KEY (student_id, course_id) REFERENCES school.courses_students (student_id, course_id)"`; + +exports[`non-pretty: pretty/constraints-16.sql 1`] = `"ALTER TABLE school.enrollments ADD CONSTRAINT chk_enrollment_date CHECK (enrollment_date <= CURRENT_DATE AND status IN ('active', 'completed', 'withdrawn'))"`; + +exports[`non-pretty: pretty/constraints-17.sql 1`] = `"CREATE TABLE school.enrollments (student_id int NOT NULL, course_id int NOT NULL, enrollment_date date NOT NULL, status text CHECK (status IN ('active', 'completed', 'withdrawn')), CHECK (enrollment_date <= CURRENT_DATE))"`; + +exports[`pretty: pretty/constraints-1.sql 1`] = ` +"ALTER TABLE public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id)" +`; + +exports[`pretty: pretty/constraints-2.sql 1`] = ` +"ALTER TABLE "App"."User Data" + ADD CONSTRAINT "Unique_Full Name" + UNIQUE ("Full Name")" +`; + +exports[`pretty: pretty/constraints-3.sql 1`] = ` +"ALTER TABLE school.attendance + ADD CONSTRAINT attendance_unique + UNIQUE ("Student ID", "Class ID")" +`; + +exports[`pretty: pretty/constraints-4.sql 1`] = ` +"ALTER TABLE "Orders"."OrderLines" + ADD CONSTRAINT "FK_Order_Ref" + FOREIGN KEY(order_id) + REFERENCES "Orders"."Order" ("OrderID")" +`; + +exports[`pretty: pretty/constraints-5.sql 1`] = ` +"ALTER TABLE "x-Schema"."z-Table" + ADD CONSTRAINT "zNameFormatCheck" + CHECK ("Z-Name" ~ '^[A-Z]')" +`; + +exports[`pretty: pretty/constraints-6.sql 1`] = ` +"ALTER TABLE data.snapshots + ADD CONSTRAINT metadata_has_key + CHECK (metadata ? 'type')" +`; + +exports[`pretty: pretty/constraints-7.sql 1`] = ` +"ALTER TABLE "Billing"."Invoices" + ADD CONSTRAINT "FK_Client_ID" + FOREIGN KEY("Client ID") + REFERENCES "Clients"."ClientBase" ("Client ID")" +`; + +exports[`pretty: pretty/constraints-8.sql 1`] = ` +"ALTER TABLE "API Keys" + ADD CONSTRAINT "PK_KeyID" PRIMARY KEY ("KeyID")" +`; + +exports[`pretty: pretty/constraints-9.sql 1`] = ` +"ALTER TABLE finance.transactions + ADD CONSTRAINT tax_rate_range + CHECK ( + tax_rate >= 0 + AND tax_rate <= 1 + )" +`; + +exports[`pretty: pretty/constraints-10.sql 1`] = ` +"ALTER TABLE school.enrollments + ADD CONSTRAINT fk_student_course + FOREIGN KEY(student_id, course_id) + REFERENCES school.courses_students (student_id, course_id)" +`; + +exports[`pretty: pretty/constraints-11.sql 1`] = ` "CREATE TABLE orders ( id serial PRIMARY KEY, user_id int NOT NULL, total numeric(10, 2) CHECK (total > 0), status varchar(20) DEFAULT 'pending', - CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) - ON DELETE CASCADE -);" + created_at pg_catalog.timestamp DEFAULT now(), + CONSTRAINT fk_user + FOREIGN KEY(user_id) + REFERENCES users (id) + ON DELETE CASCADE, + CONSTRAINT unique_user_date + UNIQUE (user_id, created_at), + CONSTRAINT check_status + CHECK (status IN ('pending', 'completed', 'cancelled')) +)" `; -exports[`Pretty constraint formatting should format foreign key constraint with pretty option enabled 1`] = ` -"ALTER TABLE products ADD CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES categories (id) - ON UPDATE CASCADE - ON DELETE SET NULL - DEFERRABLE - INITIALLY DEFERRED;" +exports[`pretty: pretty/constraints-12.sql 1`] = ` +"ALTER TABLE products + ADD CONSTRAINT fk_category + FOREIGN KEY(category_id) + REFERENCES categories (id) + ON UPDATE CASCADE + ON DELETE SET NULL + DEFERRABLE + INITIALLY DEFERRED" `; -exports[`Pretty constraint formatting should maintain single-line format for complex table when pretty disabled 1`] = `"CREATE TABLE orders (id serial PRIMARY KEY, user_id int NOT NULL, total numeric(10, 2) CHECK (total > 0), status varchar(20) DEFAULT 'pending', CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE);"`; +exports[`pretty: pretty/constraints-13.sql 1`] = ` +"ALTER TABLE products + ADD CONSTRAINT check_price + CHECK (price > 0)" +`; -exports[`Pretty constraint formatting should maintain single-line format when pretty option disabled 1`] = `"ALTER TABLE products ADD CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES categories (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED;"`; +exports[`pretty: pretty/constraints-14.sql 1`] = ` +"ALTER TABLE users + ADD CONSTRAINT unique_email + UNIQUE (email)" +`; -exports[`Pretty constraint formatting should use custom newline and tab characters in pretty mode 1`] = ` -"ALTER TABLE products ADD CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES categories (id) - ON UPDATE CASCADE - ON DELETE SET NULL - DEFERRABLE - INITIALLY DEFERRED;" +exports[`pretty: pretty/constraints-15.sql 1`] = ` +"ALTER TABLE school.enrollments + ADD CONSTRAINT fk_student_course + FOREIGN KEY(student_id, course_id) + REFERENCES school.courses_students (student_id, course_id)" +`; + +exports[`pretty: pretty/constraints-16.sql 1`] = ` +"ALTER TABLE school.enrollments + ADD CONSTRAINT chk_enrollment_date + CHECK ( + enrollment_date <= CURRENT_DATE + AND status IN ('active', 'completed', 'withdrawn') + )" +`; + +exports[`pretty: pretty/constraints-17.sql 1`] = ` +"CREATE TABLE school.enrollments ( + student_id int NOT NULL, + course_id int NOT NULL, + enrollment_date date NOT NULL, + status text CHECK (status IN ('active', 'completed', 'withdrawn')), + CHECK (enrollment_date <= CURRENT_DATE) +)" `; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/create-policy-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/create-policy-pretty.test.ts.snap index e2e29089..33aed8b4 100644 --- a/packages/deparser/__tests__/pretty/__snapshots__/create-policy-pretty.test.ts.snap +++ b/packages/deparser/__tests__/pretty/__snapshots__/create-policy-pretty.test.ts.snap @@ -1,18 +1,32 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Pretty CREATE POLICY formatting should format basic CREATE POLICY with pretty option enabled 1`] = ` -"CREATE POLICY "user_policy" +exports[`non-pretty: pretty/create_policy-1.sql 1`] = `"CREATE POLICY user_policy ON users AS PERMISSIVE FOR ALL TO authenticated_users USING (user_id = current_user_id())"`; + +exports[`non-pretty: pretty/create_policy-2.sql 1`] = `"CREATE POLICY admin_policy ON sensitive_data AS RESTRICTIVE FOR SELECT TO admin_role USING (department = current_user_department()) WITH CHECK (approved = true)"`; + +exports[`non-pretty: pretty/create_policy-3.sql 1`] = `"CREATE POLICY complex_policy ON documents AS PERMISSIVE FOR UPDATE TO document_editors USING (owner_id = current_user_id() OR (shared = true AND permissions @> '{"edit": true}')) WITH CHECK (status <> 'archived' AND last_modified > (now() - '1 day'::interval))"`; + +exports[`non-pretty: pretty/create_policy-4.sql 1`] = `"CREATE POLICY simple_policy ON posts AS PERMISSIVE FOR SELECT TO PUBLIC USING (published = true)"`; + +exports[`non-pretty: pretty/create_policy-5.sql 1`] = `"CREATE POLICY simple_policy ON posts AS PERMISSIVE FOR SELECT TO PUBLIC USING (published = true)"`; + +exports[`non-pretty: pretty/create_policy-6.sql 1`] = `"CREATE POLICY "Simple Policy" ON posts AS PERMISSIVE FOR SELECT TO PUBLIC USING (published = true)"`; + +exports[`non-pretty: pretty/create_policy-7.sql 1`] = `"CREATE POLICY simplepolicy ON posts AS PERMISSIVE FOR SELECT TO PUBLIC USING (published = true)"`; + +exports[`pretty: pretty/create_policy-1.sql 1`] = ` +"CREATE POLICY user_policy ON users AS PERMISSIVE FOR ALL TO authenticated_users USING ( user_id = current_user_id() - );" + )" `; -exports[`Pretty CREATE POLICY formatting should format complex CREATE POLICY with pretty option enabled 1`] = ` -"CREATE POLICY "admin_policy" +exports[`pretty: pretty/create_policy-2.sql 1`] = ` +"CREATE POLICY admin_policy ON sensitive_data AS RESTRICTIVE FOR SELECT @@ -22,52 +36,66 @@ exports[`Pretty CREATE POLICY formatting should format complex CREATE POLICY wit ) WITH CHECK ( approved = true - );" + )" +`; + +exports[`pretty: pretty/create_policy-3.sql 1`] = ` +"CREATE POLICY complex_policy + ON documents + AS PERMISSIVE + FOR UPDATE + TO document_editors + USING ( + owner_id = current_user_id() + OR (shared = true + AND permissions @> '{"edit": true}') + ) + WITH CHECK ( + status <> 'archived' + AND last_modified > (now() - '1 day'::interval) + )" `; -exports[`Pretty CREATE POLICY formatting should format simple CREATE POLICY with pretty option enabled 1`] = ` -"CREATE POLICY "simple_policy" +exports[`pretty: pretty/create_policy-4.sql 1`] = ` +"CREATE POLICY simple_policy ON posts AS PERMISSIVE FOR SELECT - TO public + TO PUBLIC USING ( published = true - );" + )" `; -exports[`Pretty CREATE POLICY formatting should format very complex CREATE POLICY with pretty option enabled 1`] = ` -"CREATE POLICY "complex_policy" - ON sensitive_data - AS RESTRICTIVE +exports[`pretty: pretty/create_policy-5.sql 1`] = ` +"CREATE POLICY simple_policy + ON posts + AS PERMISSIVE FOR SELECT - TO admin_role + TO PUBLIC USING ( - department = current_user_department() - AND EXISTS (SELECT - 1 - FROM user_permissions - WHERE - (user_id = current_user_id() - AND permission = 'read_sensitive')) - ) - WITH CHECK ( - approved = true - AND created_by = current_user_id() - );" + published = true + )" `; -exports[`Pretty CREATE POLICY formatting should maintain single-line format for complex policy when pretty disabled 1`] = `"CREATE POLICY "admin_policy" ON sensitive_data AS RESTRICTIVE FOR SELECT TO admin_role USING (department = current_user_department()) WITH CHECK (approved = true);"`; - -exports[`Pretty CREATE POLICY formatting should maintain single-line format when pretty option disabled 1`] = `"CREATE POLICY "user_policy" ON users AS PERMISSIVE FOR ALL TO authenticated_users USING (user_id = current_user_id());"`; +exports[`pretty: pretty/create_policy-6.sql 1`] = ` +"CREATE POLICY "Simple Policy" + ON posts + AS PERMISSIVE + FOR SELECT + TO PUBLIC + USING ( + published = true + )" +`; -exports[`Pretty CREATE POLICY formatting should use custom newline and tab characters in pretty mode 1`] = ` -"CREATE POLICY "user_policy" - ON users - AS PERMISSIVE - FOR ALL - TO authenticated_users - USING ( - user_id = current_user_id() - );" +exports[`pretty: pretty/create_policy-7.sql 1`] = ` +"CREATE POLICY simplepolicy + ON posts + AS PERMISSIVE + FOR SELECT + TO PUBLIC + USING ( + published = true + )" `; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/create-table-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/create-table-pretty.test.ts.snap index a4053291..f555172e 100644 --- a/packages/deparser/__tests__/pretty/__snapshots__/create-table-pretty.test.ts.snap +++ b/packages/deparser/__tests__/pretty/__snapshots__/create-table-pretty.test.ts.snap @@ -1,32 +1,75 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Pretty CREATE TABLE formatting should format basic CREATE TABLE with pretty option enabled 1`] = ` +exports[`non-pretty: pretty/create_table-1.sql 1`] = `"CREATE TABLE users (id serial PRIMARY KEY, name text NOT NULL, email text UNIQUE)"`; + +exports[`non-pretty: pretty/create_table-2.sql 1`] = `"CREATE TABLE products (id serial PRIMARY KEY, name varchar(255) NOT NULL, price numeric(10, 2) CHECK (price > 0), category_id int, description text, created_at pg_catalog.timestamp DEFAULT now(), updated_at pg_catalog.timestamp, UNIQUE (name, category_id), FOREIGN KEY (category_id) REFERENCES categories (id))"`; + +exports[`non-pretty: pretty/create_table-3.sql 1`] = `"CREATE TABLE orders (id serial PRIMARY KEY, subtotal numeric(10, 2) NOT NULL, tax_rate numeric(5, 4) DEFAULT 0.0825, tax_amount numeric(10, 2) GENERATED ALWAYS AS (subtotal * tax_rate) STORED, total numeric(10, 2) GENERATED ALWAYS AS (subtotal + tax_amount) STORED)"`; + +exports[`non-pretty: pretty/create_table-4.sql 1`] = `"CREATE TABLE sales (id serial, sale_date date NOT NULL, amount numeric(10, 2), region varchar(50)) PARTITION BY RANGE (sale_date)"`; + +exports[`non-pretty: pretty/create_table-5.sql 1`] = `"CREATE TEMPORARY TABLE temp_calculations (id int, value numeric(15, 5), result text)"`; + +exports[`non-pretty: pretty/create_table-6.sql 1`] = `"CREATE TABLE orders (id serial PRIMARY KEY, user_id int NOT NULL, total numeric(10, 2) CHECK (total > 0), status varchar(20) DEFAULT 'pending', created_at pg_catalog.timestamp DEFAULT now(), FOREIGN KEY (user_id) REFERENCES users (id))"`; + +exports[`pretty: pretty/create_table-1.sql 1`] = ` "CREATE TABLE users ( id serial PRIMARY KEY, name text NOT NULL, email text UNIQUE -);" +)" `; -exports[`Pretty CREATE TABLE formatting should format complex CREATE TABLE with pretty option enabled 1`] = ` -"CREATE TABLE orders ( +exports[`pretty: pretty/create_table-2.sql 1`] = ` +"CREATE TABLE products ( id serial PRIMARY KEY, - user_id int NOT NULL, - total numeric(10, 2) CHECK (total > 0), - status varchar(20) DEFAULT 'pending', + name varchar(255) NOT NULL, + price numeric(10, 2) CHECK (price > 0), + category_id int, + description text, created_at pg_catalog.timestamp DEFAULT now(), - FOREIGN KEY (user_id) REFERENCES users (id) -);" + updated_at pg_catalog.timestamp, + UNIQUE (name, category_id), + FOREIGN KEY(category_id) + REFERENCES categories (id) +)" `; -exports[`Pretty CREATE TABLE formatting should maintain single-line format for complex table when pretty disabled 1`] = `"CREATE TABLE orders (id serial PRIMARY KEY, user_id int NOT NULL, total numeric(10, 2) CHECK (total > 0), status varchar(20) DEFAULT 'pending', created_at pg_catalog.timestamp DEFAULT now(), FOREIGN KEY (user_id) REFERENCES users (id));"`; +exports[`pretty: pretty/create_table-3.sql 1`] = ` +"CREATE TABLE orders ( + id serial PRIMARY KEY, + subtotal numeric(10, 2) NOT NULL, + tax_rate numeric(5, 4) DEFAULT 0.0825, + tax_amount numeric(10, 2) GENERATED ALWAYS AS (subtotal * tax_rate) STORED, + total numeric(10, 2) GENERATED ALWAYS AS (subtotal + tax_amount) STORED +)" +`; -exports[`Pretty CREATE TABLE formatting should maintain single-line format when pretty option disabled 1`] = `"CREATE TABLE users (id serial PRIMARY KEY, name text NOT NULL, email text UNIQUE);"`; +exports[`pretty: pretty/create_table-4.sql 1`] = ` +"CREATE TABLE sales ( + id serial, + sale_date date NOT NULL, + amount numeric(10, 2), + region varchar(50) +) PARTITION BY RANGE (sale_date)" +`; -exports[`Pretty CREATE TABLE formatting should use custom newline and tab characters in pretty mode 1`] = ` -"CREATE TABLE users ( - id serial PRIMARY KEY, - name text NOT NULL, - email text UNIQUE -);" +exports[`pretty: pretty/create_table-5.sql 1`] = ` +"CREATE TEMPORARY TABLE temp_calculations ( + id int, + value numeric(15, 5), + result text +)" +`; + +exports[`pretty: pretty/create_table-6.sql 1`] = ` +"CREATE TABLE orders ( + id serial PRIMARY KEY, + user_id int NOT NULL, + total numeric(10, 2) CHECK (total > 0), + status varchar(20) DEFAULT 'pending', + created_at pg_catalog.timestamp DEFAULT now(), + FOREIGN KEY(user_id) + REFERENCES users (id) +)" `; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/cte-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/cte-pretty.test.ts.snap index 132770d7..15d2a15f 100644 --- a/packages/deparser/__tests__/pretty/__snapshots__/cte-pretty.test.ts.snap +++ b/packages/deparser/__tests__/pretty/__snapshots__/cte-pretty.test.ts.snap @@ -1,6 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Pretty CTE (Common Table Expressions) formatting should format basic CTE with pretty option enabled 1`] = ` +exports[`non-pretty: pretty/cte-1.sql 1`] = `"WITH regional_sales AS (SELECT region, sum(sales_amount) AS total_sales FROM sales GROUP BY region) SELECT * FROM regional_sales"`; + +exports[`non-pretty: pretty/cte-2.sql 1`] = `"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"`; + +exports[`non-pretty: pretty/cte-3.sql 1`] = `"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 AS e JOIN employee_hierarchy AS eh ON e.manager_id = eh.id) SELECT * FROM employee_hierarchy"`; + +exports[`non-pretty: pretty/cte-4.sql 1`] = `"WITH sales_summary AS (SELECT region, product_category, sum(amount) AS total FROM sales GROUP BY region, product_category), regional_totals AS (SELECT region, sum(total) AS region_total FROM sales_summary GROUP BY region) SELECT s.region, s.product_category, s.total, r.region_total FROM sales_summary AS s JOIN regional_totals AS r ON s.region = r.region"`; + +exports[`pretty: pretty/cte-1.sql 1`] = ` "WITH regional_sales AS (SELECT region, @@ -8,12 +16,11 @@ exports[`Pretty CTE (Common Table Expressions) formatting should format basic CT FROM sales GROUP BY region) -SELECT - * -FROM regional_sales;" +SELECT * +FROM regional_sales" `; -exports[`Pretty CTE (Common Table Expressions) formatting should format complex CTE with multiple CTEs with pretty option enabled 1`] = ` +exports[`pretty: pretty/cte-2.sql 1`] = ` "WITH regional_sales AS (SELECT region, @@ -21,42 +28,15 @@ exports[`Pretty CTE (Common Table Expressions) formatting should format complex FROM sales GROUP BY region), - top_regions AS (SELECT - region + top_regions AS (SELECT region FROM regional_sales WHERE total_sales > 1000000) -SELECT - * -FROM top_regions;" -`; - -exports[`Pretty CTE (Common Table Expressions) formatting should format nested CTE with complex joins with pretty option enabled 1`] = ` -"WITH - sales_summary AS (SELECT - region, - product_category, - sum(amount) AS total - FROM sales - GROUP BY - region, - product_category), - regional_totals AS (SELECT - region, - sum(total) AS region_total - FROM sales_summary - GROUP BY - region) -SELECT - s.region, - s.product_category, - s.total, - r.region_total -FROM sales_summary AS s -JOIN regional_totals AS r ON s.region = r.region;" +SELECT * +FROM top_regions" `; -exports[`Pretty CTE (Common Table Expressions) formatting should format recursive CTE with pretty option enabled 1`] = ` +exports[`pretty: pretty/cte-3.sql 1`] = ` "WITH RECURSIVE employee_hierarchy AS (SELECT id, @@ -75,22 +55,31 @@ exports[`Pretty CTE (Common Table Expressions) formatting should format recursiv eh.level + 1 FROM employees AS e JOIN employee_hierarchy AS eh ON e.manager_id = eh.id) -SELECT - * -FROM employee_hierarchy;" +SELECT * +FROM employee_hierarchy" `; -exports[`Pretty CTE (Common Table Expressions) formatting should maintain single-line format when pretty option disabled 1`] = `"WITH regional_sales AS (SELECT region, sum(sales_amount) AS total_sales FROM sales GROUP BY region) SELECT * FROM regional_sales;"`; - -exports[`Pretty CTE (Common Table Expressions) formatting should use custom newline and tab characters in pretty mode 1`] = ` +exports[`pretty: pretty/cte-4.sql 1`] = ` "WITH - regional_sales AS (SELECT - region, - sum(sales_amount) AS total_sales - FROM sales - GROUP BY - region) + sales_summary AS (SELECT + region, + product_category, + sum(amount) AS total + FROM sales + GROUP BY + region, + product_category), + regional_totals AS (SELECT + region, + sum(total) AS region_total + FROM sales_summary + GROUP BY + region) SELECT - * -FROM regional_sales;" + s.region, + s.product_category, + s.total, + r.region_total +FROM sales_summary AS s +JOIN regional_totals AS r ON s.region = r.region" `; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/misc-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/misc-pretty.test.ts.snap index 2e1359cd..8811ecb9 100644 --- a/packages/deparser/__tests__/pretty/__snapshots__/misc-pretty.test.ts.snap +++ b/packages/deparser/__tests__/pretty/__snapshots__/misc-pretty.test.ts.snap @@ -1,8 +1,38 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Pretty Misc SQL formatting should format misc-1: Complex CTE with joins and aggregation (non-pretty) 1`] = `"WITH recent_orders AS (SELECT o.id, o.user_id, o.created_at FROM orders AS o WHERE o.created_at > (now() - '30 days'::interval)), high_value_orders AS (SELECT r.user_id, count(*) AS order_count, sum(oi.price * oi.quantity) AS total_spent FROM recent_orders AS r JOIN order_items AS oi ON r.id = oi.order_id GROUP BY r.user_id) SELECT u.id, u.name, h.total_spent FROM users AS u JOIN high_value_orders AS h ON u.id = h.user_id WHERE h.total_spent > 1000 ORDER BY h.total_spent DESC"`; +exports[`non-pretty: pretty/misc-1.sql 1`] = `"WITH recent_orders AS (SELECT o.id, o.user_id, o.created_at FROM orders AS o WHERE o.created_at > (now() - '30 days'::interval)), high_value_orders AS (SELECT r.user_id, count(*) AS order_count, sum(oi.price * oi.quantity) AS total_spent FROM recent_orders AS r JOIN order_items AS oi ON r.id = oi.order_id GROUP BY r.user_id) SELECT u.id, u.name, h.total_spent FROM users AS u JOIN high_value_orders AS h ON u.id = h.user_id WHERE h.total_spent > 1000 ORDER BY h.total_spent DESC"`; -exports[`Pretty Misc SQL formatting should format misc-1: Complex CTE with joins and aggregation (pretty) 1`] = ` +exports[`non-pretty: pretty/misc-2.sql 1`] = `"SELECT department, employee_id, count(*) FILTER (WHERE status = 'active') OVER (PARTITION BY department) AS active_count, rank() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_rank FROM employee_status GROUP BY GROUPING SETS (department, (department, employee_id))"`; + +exports[`non-pretty: pretty/misc-3.sql 1`] = `"SELECT u.id, u.name, j.key, j.value FROM users AS u, LATERAL jsonb_each_text(u.preferences) AS j(key, value) WHERE j.key LIKE 'notif_%' AND CAST(j.value AS boolean) = true"`; + +exports[`non-pretty: pretty/misc-4.sql 1`] = `"SELECT p.id, p.title, CASE WHEN EXISTS (SELECT 1 FROM reviews AS r WHERE r.product_id = p.id AND r.rating >= 4) THEN 'Popular' ELSE 'Unrated' END AS status FROM products AS p WHERE p.archived = false"`; + +exports[`non-pretty: pretty/misc-5.sql 1`] = `"WITH logs AS (SELECT id, payload::json ->> 'event' AS event, CAST(payload::json ->> 'ts' AS pg_catalog.timestamp) AS ts FROM event_log WHERE ts > (now() - '7 days'::interval)) SELECT event, count(*) AS freq FROM ( SELECT DISTINCT event, ts::date AS event_day FROM logs ) AS d GROUP BY event ORDER BY freq DESC"`; + +exports[`non-pretty: pretty/misc-6.sql 1`] = `"SELECT o.id AS order_id, u.name AS user_name, p.name AS product_name, s.status, sh.shipped_at, r.refund_amount FROM orders AS o JOIN users AS u ON o.user_id = u.id JOIN order_items AS oi ON oi.order_id = o.id JOIN products AS p ON (p.id = oi.product_id AND p.available = true) OR (p.sku = oi.product_sku AND (p.discontinued = false OR p.replacement_id IS NOT NULL)) LEFT JOIN shipping AS sh ON sh.order_id = o.id AND ((sh.carrier = 'UPS' AND sh.tracking_number IS NOT NULL) OR (sh.carrier = 'FedEx' AND sh.shipped_at > (o.created_at + '1 day'::interval))) LEFT JOIN statuses AS s ON s.id = o.status_id AND (s.name <> 'cancelled' OR (s.name = 'cancelled' AND s.updated_at > (now() - '7 days'::interval))) LEFT JOIN refunds AS r ON r.order_id = o.id AND ((r.status = 'approved' AND r.processed_at IS NOT NULL) OR (r.status = 'pending' AND r.requested_at < (now() - '14 days'::interval))) WHERE o.created_at > (now() - '90 days'::interval) AND u.active = true AND (s.status = 'shipped' OR (s.status = 'processing' AND EXISTS (SELECT 1 FROM order_notes AS n WHERE (n.order_id = o.id AND n.note ILIKE '%expedite%')))) ORDER BY o.created_at DESC"`; + +exports[`non-pretty: pretty/misc-7.sql 1`] = `"SELECT CASE WHEN n = 2 THEN ARRAY['month'] WHEN n = 4 THEN ARRAY['year'] WHEN n = 6 THEN ARRAY['year', 'month'] WHEN n = 8 THEN ARRAY['day'] WHEN n = 1024 THEN ARRAY['hour'] WHEN n = 1032 THEN ARRAY['day', 'hour'] WHEN n = 2048 THEN ARRAY['minute'] WHEN n = 3072 THEN ARRAY['hour', 'minute'] WHEN n = 3080 THEN ARRAY['day', 'minute'] WHEN n = 4096 THEN ARRAY['second'] WHEN n = 6144 THEN ARRAY['minute', 'second'] WHEN n = 7168 THEN ARRAY['hour', 'second'] WHEN n = 7176 THEN ARRAY['day', 'second'] WHEN n = 32767 THEN CAST(ARRAY[] AS text[]) END"`; + +exports[`non-pretty: pretty/misc-8.sql 1`] = `"SELECT CASE WHEN n = 2 OR n = 3 THEN ARRAY['month', COALESCE(extra_label, 'unknown')] WHEN n IN (4, 5) THEN CASE WHEN is_leap_year THEN ARRAY['year', 'leap'] ELSE ARRAY['year'] END WHEN n = 6 THEN ARRAY['year', 'month', 'quarter'] WHEN n = 8 THEN ARRAY['day', 'week', compute_label(n)] WHEN n = 1024 THEN ARRAY['hour', format('%s-hour', extra_label)] WHEN n = 1032 AND flag = true THEN ARRAY['day', 'hour', 'flagged'] WHEN n BETWEEN 2048 AND 2049 THEN ARRAY['minute', 'tick'] WHEN n = 3072 THEN ARRAY['hour', 'minute', current_setting('timezone')] WHEN n = 3080 THEN ARRAY['day', 'minute', to_char(now(), 'HH24:MI')] WHEN n IN (4096, 4097, 4098) THEN ARRAY['second', 'millisecond'] WHEN n = 6144 THEN ARRAY['minute', 'second', CASE WHEN use_micro = true THEN 'microsecond' ELSE 'none' END] WHEN n = 7168 OR (n > 7170 AND n < 7180) THEN ARRAY['hour', 'second', 'buffered'] WHEN n = 7176 THEN ARRAY['day', 'second', extra_info::text] WHEN n = 32767 THEN CAST(ARRAY[] AS text[]) ELSE ARRAY['undefined', 'unknown', 'fallback'] END"`; + +exports[`non-pretty: pretty/misc-9.sql 1`] = `"SELECT user_id, CASE WHEN EXISTS (SELECT 1 FROM logins WHERE logins.user_id = users.user_id AND success = false) THEN 'risky' ELSE 'safe' END AS risk_status FROM users"`; + +exports[`non-pretty: pretty/misc-10.sql 1`] = `"SELECT * FROM orders WHERE status = (CASE WHEN shipped_at IS NOT NULL THEN 'shipped' WHEN canceled_at IS NOT NULL THEN 'canceled' ELSE 'processing' END)"`; + +exports[`non-pretty: pretty/misc-11.sql 1`] = `"SELECT * FROM users AS u, LATERAL ( SELECT CASE WHEN u.is_admin THEN 'admin_dashboard' ELSE 'user_dashboard' END AS dashboard_view ) AS derived"`; + +exports[`non-pretty: pretty/misc-12.sql 1`] = `"SELECT id, (SELECT CASE WHEN count(*) > 5 THEN 'frequent' ELSE 'occasional' END FROM purchases AS p WHERE p.user_id = u.id) AS purchase_freq FROM users AS u"`; + +exports[`non-pretty: pretty/misc-13.sql 1`] = `"SELECT id, CASE WHEN rank() OVER (ORDER BY score DESC) = 1 THEN 'top' ELSE 'normal' END AS tier FROM players"`; + +exports[`non-pretty: pretty/misc-14.sql 1`] = `"CREATE TRIGGER decrease_job_queue_count_on_delete AFTER DELETE ON dashboard_jobs.jobs FOR EACH ROW WHEN ( old.queue_name IS NOT NULL ) EXECUTE FUNCTION dashboard_jobs.tg_decrease_job_queue_count ()"`; + +exports[`non-pretty: pretty/misc-15.sql 1`] = `"ALTER DEFAULT PRIVILEGES IN SCHEMA dashboard_jobs GRANT EXECUTE ON FUNCTIONS TO administrator"`; + +exports[`non-pretty: pretty/misc-16.sql 1`] = `"GRANT EXECUTE ON FUNCTION dashboard_private.uuid_generate_seeded_uuid TO PUBLIC"`; + +exports[`pretty: pretty/misc-1.sql 1`] = ` "WITH recent_orders AS (SELECT o.id, @@ -31,9 +61,7 @@ ORDER BY h.total_spent DESC" `; -exports[`Pretty Misc SQL formatting should format misc-2: Window functions with FILTER and GROUPING SETS (non-pretty) 1`] = `"SELECT department, employee_id, count(*) FILTER (WHERE status = 'active') OVER (PARTITION BY department) AS active_count, rank() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_rank FROM employee_status GROUP BY GROUPING SETS (department, (department, employee_id))"`; - -exports[`Pretty Misc SQL formatting should format misc-2: Window functions with FILTER and GROUPING SETS (pretty) 1`] = ` +exports[`pretty: pretty/misc-2.sql 1`] = ` "SELECT department, employee_id, @@ -47,9 +75,7 @@ GROUP BY GROUPING SETS (department, (department, employee_id))" `; -exports[`Pretty Misc SQL formatting should format misc-3: LATERAL joins with JSON functions (non-pretty) 1`] = `"SELECT u.id, u.name, j.key, j.value FROM users AS u, LATERAL jsonb_each_text(u.preferences) AS j(key, value) WHERE j.key LIKE 'notif_%' AND CAST(j.value AS boolean) = true"`; - -exports[`Pretty Misc SQL formatting should format misc-3: LATERAL joins with JSON functions (pretty) 1`] = ` +exports[`pretty: pretty/misc-3.sql 1`] = ` "SELECT u.id, u.name, @@ -61,15 +87,12 @@ WHERE AND CAST(j.value AS boolean) = true" `; -exports[`Pretty Misc SQL formatting should format misc-4: EXISTS with nested subqueries and CASE (non-pretty) 1`] = `"SELECT p.id, p.title, CASE WHEN EXISTS (SELECT 1 FROM reviews AS r WHERE r.product_id = p.id AND r.rating >= 4) THEN 'Popular' ELSE 'Unrated' END AS status FROM products AS p WHERE p.archived = false"`; - -exports[`Pretty Misc SQL formatting should format misc-4: EXISTS with nested subqueries and CASE (pretty) 1`] = ` +exports[`pretty: pretty/misc-4.sql 1`] = ` "SELECT p.id, p.title, CASE - WHEN EXISTS (SELECT - 1 + WHEN EXISTS (SELECT 1 FROM reviews AS r WHERE r.product_id = p.id @@ -81,14 +104,12 @@ WHERE p.archived = false" `; -exports[`Pretty Misc SQL formatting should format misc-5: Nested CTEs with type casts and subqueries (non-pretty) 1`] = `"WITH logs AS (SELECT id, CAST(payload AS pg_catalog.json) ->> 'event' AS event, CAST(CAST(payload AS pg_catalog.json) ->> 'ts' AS pg_catalog.timestamp) AS ts FROM event_log WHERE ts > (now() - '7 days'::interval)) SELECT event, count(*) AS freq FROM ( SELECT DISTINCT event, ts::date AS event_day FROM logs ) AS d GROUP BY event ORDER BY freq DESC"`; - -exports[`Pretty Misc SQL formatting should format misc-5: Nested CTEs with type casts and subqueries (pretty) 1`] = ` +exports[`pretty: pretty/misc-5.sql 1`] = ` "WITH logs AS (SELECT id, - CAST(payload AS pg_catalog.json) ->> 'event' AS event, - CAST(CAST(payload AS pg_catalog.json) ->> 'ts' AS pg_catalog.timestamp) AS ts + payload::json ->> 'event' AS event, + CAST(payload::json ->> 'ts' AS pg_catalog.timestamp) AS ts FROM event_log WHERE ts > (now() - '7 days'::interval)) @@ -105,9 +126,7 @@ ORDER BY freq DESC" `; -exports[`Pretty Misc SQL formatting should format misc-6: Complex multi-table joins with nested conditions (non-pretty) 1`] = `"SELECT o.id AS order_id, u.name AS user_name, p.name AS product_name, s.status, sh.shipped_at, r.refund_amount FROM orders AS o JOIN users AS u ON o.user_id = u.id JOIN order_items AS oi ON oi.order_id = o.id JOIN products AS p ON (p.id = oi.product_id AND p.available = true) OR (p.sku = oi.product_sku AND (p.discontinued = false OR p.replacement_id IS NOT NULL)) LEFT JOIN shipping AS sh ON sh.order_id = o.id AND ((sh.carrier = 'UPS' AND sh.tracking_number IS NOT NULL) OR (sh.carrier = 'FedEx' AND sh.shipped_at > (o.created_at + '1 day'::interval))) LEFT JOIN statuses AS s ON s.id = o.status_id AND (s.name <> 'cancelled' OR (s.name = 'cancelled' AND s.updated_at > (now() - '7 days'::interval))) LEFT JOIN refunds AS r ON r.order_id = o.id AND ((r.status = 'approved' AND r.processed_at IS NOT NULL) OR (r.status = 'pending' AND r.requested_at < (now() - '14 days'::interval))) WHERE o.created_at > (now() - '90 days'::interval) AND u.active = true AND (s.status = 'shipped' OR (s.status = 'processing' AND EXISTS (SELECT 1 FROM order_notes AS n WHERE (n.order_id = o.id AND n.note ILIKE '%expedite%')))) ORDER BY o.created_at DESC"`; - -exports[`Pretty Misc SQL formatting should format misc-6: Complex multi-table joins with nested conditions (pretty) 1`] = ` +exports[`pretty: pretty/misc-6.sql 1`] = ` "SELECT o.id AS order_id, u.name AS user_name, @@ -143,8 +162,7 @@ WHERE AND u.active = true AND (s.status = 'shipped' OR (s.status = 'processing' - AND EXISTS (SELECT - 1 + AND EXISTS (SELECT 1 FROM order_notes AS n WHERE (n.order_id = o.id @@ -153,9 +171,7 @@ ORDER BY o.created_at DESC" `; -exports[`Pretty Misc SQL formatting should format misc-7: Large Case Stmt (non-pretty) 1`] = `"SELECT CASE WHEN n = 2 THEN ARRAY['month'] WHEN n = 4 THEN ARRAY['year'] WHEN n = 6 THEN ARRAY['year', 'month'] WHEN n = 8 THEN ARRAY['day'] WHEN n = 1024 THEN ARRAY['hour'] WHEN n = 1032 THEN ARRAY['day', 'hour'] WHEN n = 2048 THEN ARRAY['minute'] WHEN n = 3072 THEN ARRAY['hour', 'minute'] WHEN n = 3080 THEN ARRAY['day', 'minute'] WHEN n = 4096 THEN ARRAY['second'] WHEN n = 6144 THEN ARRAY['minute', 'second'] WHEN n = 7168 THEN ARRAY['hour', 'second'] WHEN n = 7176 THEN ARRAY['day', 'second'] WHEN n = 32767 THEN CAST(ARRAY[] AS text[]) END"`; - -exports[`Pretty Misc SQL formatting should format misc-7: Large Case Stmt (pretty) 1`] = ` +exports[`pretty: pretty/misc-7.sql 1`] = ` "SELECT CASE WHEN n = 2 THEN ARRAY['month'] @@ -175,9 +191,7 @@ CASE END" `; -exports[`Pretty Misc SQL formatting should format misc-8: Large Case Stmt (non-pretty) 1`] = `"SELECT CASE WHEN n = 2 OR n = 3 THEN ARRAY['month', COALESCE(extra_label, 'unknown')] WHEN n IN (4, 5) THEN CASE WHEN is_leap_year THEN ARRAY['year', 'leap'] ELSE ARRAY['year'] END WHEN n = 6 THEN ARRAY['year', 'month', 'quarter'] WHEN n = 8 THEN ARRAY['day', 'week', compute_label(n)] WHEN n = 1024 THEN ARRAY['hour', format('%s-hour', extra_label)] WHEN n = 1032 AND flag = true THEN ARRAY['day', 'hour', 'flagged'] WHEN n BETWEEN 2048 AND 2049 THEN ARRAY['minute', 'tick'] WHEN n = 3072 THEN ARRAY['hour', 'minute', current_setting('timezone')] WHEN n = 3080 THEN ARRAY['day', 'minute', to_char(now(), 'HH24:MI')] WHEN n IN (4096, 4097, 4098) THEN ARRAY['second', 'millisecond'] WHEN n = 6144 THEN ARRAY['minute', 'second', CASE WHEN use_micro = true THEN 'microsecond' ELSE 'none' END] WHEN n = 7168 OR (n > 7170 AND n < 7180) THEN ARRAY['hour', 'second', 'buffered'] WHEN n = 7176 THEN ARRAY['day', 'second', CAST(extra_info AS text)] WHEN n = 32767 THEN CAST(ARRAY[] AS text[]) ELSE ARRAY['undefined', 'unknown', 'fallback'] END"`; - -exports[`Pretty Misc SQL formatting should format misc-8: Large Case Stmt (pretty) 1`] = ` +exports[`pretty: pretty/misc-8.sql 1`] = ` "SELECT CASE WHEN n = 2 @@ -202,20 +216,17 @@ END] WHEN n = 7168 OR (n > 7170 AND n < 7180) THEN ARRAY['hour', 'second', 'buffered'] - WHEN n = 7176 THEN ARRAY['day', 'second', CAST(extra_info AS text)] + WHEN n = 7176 THEN ARRAY['day', 'second', extra_info::text] WHEN n = 32767 THEN CAST(ARRAY[] AS text[]) ELSE ARRAY['undefined', 'unknown', 'fallback'] END" `; -exports[`Pretty Misc SQL formatting should format misc-9: Large Case Stmt (non-pretty) 1`] = `"SELECT user_id, CASE WHEN EXISTS (SELECT 1 FROM logins WHERE logins.user_id = users.user_id AND success = false) THEN 'risky' ELSE 'safe' END AS risk_status FROM users"`; - -exports[`Pretty Misc SQL formatting should format misc-9: Large Case Stmt (pretty) 1`] = ` +exports[`pretty: pretty/misc-9.sql 1`] = ` "SELECT user_id, CASE - WHEN EXISTS (SELECT - 1 + WHEN EXISTS (SELECT 1 FROM logins WHERE logins.user_id = users.user_id @@ -225,11 +236,8 @@ END AS risk_status FROM users" `; -exports[`Pretty Misc SQL formatting should format misc-10: Where Clause (non-pretty) 1`] = `"SELECT * FROM orders WHERE status = (CASE WHEN shipped_at IS NOT NULL THEN 'shipped' WHEN canceled_at IS NOT NULL THEN 'canceled' ELSE 'processing' END)"`; - -exports[`Pretty Misc SQL formatting should format misc-10: Where Clause (pretty) 1`] = ` -"SELECT - * +exports[`pretty: pretty/misc-10.sql 1`] = ` +"SELECT * FROM orders WHERE status = (CASE @@ -239,11 +247,8 @@ WHERE END)" `; -exports[`Pretty Misc SQL formatting should format misc-11: Lateral Join (non-pretty) 1`] = `"SELECT * FROM users AS u, LATERAL ( SELECT CASE WHEN u.is_admin THEN 'admin_dashboard' ELSE 'user_dashboard' END AS dashboard_view ) AS derived"`; - -exports[`Pretty Misc SQL formatting should format misc-11: Lateral Join (pretty) 1`] = ` -"SELECT - * +exports[`pretty: pretty/misc-11.sql 1`] = ` +"SELECT * FROM users AS u, LATERAL ( SELECT CASE WHEN u.is_admin THEN 'admin_dashboard' @@ -251,9 +256,7 @@ CASE END AS dashboard_view ) AS derived" `; -exports[`Pretty Misc SQL formatting should format misc-12: Scalar Subquery (non-pretty) 1`] = `"SELECT id, (SELECT CASE WHEN count(*) > 5 THEN 'frequent' ELSE 'occasional' END FROM purchases AS p WHERE p.user_id = u.id) AS purchase_freq FROM users AS u"`; - -exports[`Pretty Misc SQL formatting should format misc-12: Scalar Subquery (pretty) 1`] = ` +exports[`pretty: pretty/misc-12.sql 1`] = ` "SELECT id, (SELECT @@ -267,9 +270,7 @@ WHERE FROM users AS u" `; -exports[`Pretty Misc SQL formatting should format misc-13: Window Function (non-pretty) 1`] = `"SELECT id, CASE WHEN rank() OVER (ORDER BY score DESC) = 1 THEN 'top' ELSE 'normal' END AS tier FROM players"`; - -exports[`Pretty Misc SQL formatting should format misc-13: Window Function (pretty) 1`] = ` +exports[`pretty: pretty/misc-13.sql 1`] = ` "SELECT id, CASE @@ -278,3 +279,19 @@ CASE END AS tier FROM players" `; + +exports[`pretty: pretty/misc-14.sql 1`] = ` +"CREATE TRIGGER decrease_job_queue_count_on_delete + AFTER DELETE + ON dashboard_jobs.jobs + FOR EACH ROW + WHEN (old.queue_name IS NOT NULL) + EXECUTE PROCEDURE dashboard_jobs.tg_decrease_job_queue_count()" +`; + +exports[`pretty: pretty/misc-15.sql 1`] = ` +"ALTER DEFAULT PRIVILEGES IN SCHEMA dashboard_jobs + GRANT EXECUTE ON FUNCTIONS TO administrator" +`; + +exports[`pretty: pretty/misc-16.sql 1`] = `"GRANT EXECUTE ON FUNCTION dashboard_private.uuid_generate_seeded_uuid TO PUBLIC"`; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/procedures-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/procedures-pretty.test.ts.snap new file mode 100644 index 00000000..9b8145bb --- /dev/null +++ b/packages/deparser/__tests__/pretty/__snapshots__/procedures-pretty.test.ts.snap @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`non-pretty: pretty/procedures-1.sql 1`] = `"SELECT handle_insert('TYPE_A')"`; + +exports[`non-pretty: pretty/procedures-2.sql 1`] = `"SELECT "HandleInsert"('TYPE_A', 'Region-1')"`; + +exports[`non-pretty: pretty/procedures-3.sql 1`] = `"SELECT compute_score(42, true)"`; + +exports[`non-pretty: pretty/procedures-4.sql 1`] = `"SELECT metrics.get_total('2025-01-01', '2025-01-31')"`; + +exports[`non-pretty: pretty/procedures-5.sql 1`] = `"SELECT * FROM users WHERE is_active(user_id)"`; + +exports[`non-pretty: pretty/procedures-6.sql 1`] = `"SELECT * FROM get_user_details(1001)"`; + +exports[`non-pretty: pretty/procedures-7.sql 1`] = `"SELECT * FROM get_recent_events('login') AS events"`; + +exports[`non-pretty: pretty/procedures-8.sql 1`] = `"SELECT "Analytics"."RunQuery"('Q-123', '2025-06')"`; + +exports[`non-pretty: pretty/procedures-9.sql 1`] = `"SELECT calculate_discount(price * quantity, customer_tier)"`; + +exports[`non-pretty: pretty/procedures-10.sql 1`] = `"SELECT perform_backup('daily', false)"`; + +exports[`pretty: pretty/procedures-1.sql 1`] = `"SELECT handle_insert('TYPE_A')"`; + +exports[`pretty: pretty/procedures-2.sql 1`] = `"SELECT "HandleInsert"('TYPE_A', 'Region-1')"`; + +exports[`pretty: pretty/procedures-3.sql 1`] = `"SELECT compute_score(42, true)"`; + +exports[`pretty: pretty/procedures-4.sql 1`] = `"SELECT metrics.get_total('2025-01-01', '2025-01-31')"`; + +exports[`pretty: pretty/procedures-5.sql 1`] = ` +"SELECT * +FROM users +WHERE + is_active(user_id)" +`; + +exports[`pretty: pretty/procedures-6.sql 1`] = ` +"SELECT * +FROM get_user_details(1001)" +`; + +exports[`pretty: pretty/procedures-7.sql 1`] = ` +"SELECT * +FROM get_recent_events('login') AS events" +`; + +exports[`pretty: pretty/procedures-8.sql 1`] = `"SELECT "Analytics"."RunQuery"('Q-123', '2025-06')"`; + +exports[`pretty: pretty/procedures-9.sql 1`] = `"SELECT calculate_discount(price * quantity, customer_tier)"`; + +exports[`pretty: pretty/procedures-10.sql 1`] = `"SELECT perform_backup('daily', false)"`; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/select-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/select-pretty.test.ts.snap index 7f3e6932..c51727b2 100644 --- a/packages/deparser/__tests__/pretty/__snapshots__/select-pretty.test.ts.snap +++ b/packages/deparser/__tests__/pretty/__snapshots__/select-pretty.test.ts.snap @@ -1,19 +1,100 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Pretty SELECT formatting should format SELECT with UNION with pretty option enabled 1`] = ` +exports[`non-pretty: pretty/selects-1.sql 1`] = `"SELECT 1"`; + +exports[`non-pretty: pretty/selects-2.sql 1`] = `"SELECT 'abc'::text"`; + +exports[`non-pretty: pretty/selects-3.sql 1`] = `"SELECT now() AT TIME ZONE 'UTC'"`; + +exports[`non-pretty: pretty/selects-4.sql 1`] = `"SELECT 1, 2"`; + +exports[`non-pretty: pretty/selects-5.sql 1`] = `"SELECT id, name, email FROM users"`; + +exports[`non-pretty: pretty/selects-6.sql 1`] = `"SELECT DISTINCT id FROM users"`; + +exports[`non-pretty: pretty/selects-7.sql 1`] = `"SELECT DISTINCT id, name FROM users"`; + +exports[`non-pretty: pretty/selects-8.sql 1`] = `"SELECT id, upper(name) AS name_upper, created_at + '1 day'::interval AS expires_at FROM accounts"`; + +exports[`non-pretty: pretty/selects-9.sql 1`] = `"SELECT (SELECT max(score) FROM results)"`; + +exports[`non-pretty: pretty/selects-10.sql 1`] = `"SELECT count(*) OVER (), u.id FROM users AS u"`; + +exports[`non-pretty: pretty/selects-11.sql 1`] = `"SELECT name FROM customers UNION ALL SELECT name FROM suppliers ORDER BY name"`; + +exports[`non-pretty: pretty/selects-12.sql 1`] = `"SELECT u.id, u.name, u.email, p.title FROM users AS u JOIN profiles AS p ON u.id = p.user_id LEFT JOIN orders AS o ON u.id = o.user_id RIGHT JOIN addresses AS a ON u.id = a.user_id WHERE u.active = true"`; + +exports[`non-pretty: pretty/selects-13.sql 1`] = `"SELECT id, name FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 100)"`; + +exports[`non-pretty: pretty/selects-14.sql 1`] = `"SELECT id, name, email FROM users WHERE active = true"`; + +exports[`non-pretty: pretty/selects-15.sql 1`] = `"SELECT u.id, u.name, u.email, p.title FROM users AS u JOIN profiles AS p ON u.id = p.user_id WHERE u.active = true AND u.created_at > '2023-01-01' GROUP BY u.id, u.name, u.email, p.title HAVING count(*) > 1 ORDER BY u.created_at DESC, u.name ASC LIMIT 10 OFFSET 5"`; + +exports[`pretty: pretty/selects-1.sql 1`] = `"SELECT 1"`; + +exports[`pretty: pretty/selects-2.sql 1`] = `"SELECT 'abc'::text"`; + +exports[`pretty: pretty/selects-3.sql 1`] = `"SELECT now() AT TIME ZONE 'UTC'"`; + +exports[`pretty: pretty/selects-4.sql 1`] = ` +"SELECT + 1, + 2" +`; + +exports[`pretty: pretty/selects-5.sql 1`] = ` "SELECT + id, + name, + email +FROM users" +`; + +exports[`pretty: pretty/selects-6.sql 1`] = ` +"SELECT DISTINCT id +FROM users" +`; + +exports[`pretty: pretty/selects-7.sql 1`] = ` +"SELECT DISTINCT + id, name +FROM users" +`; + +exports[`pretty: pretty/selects-8.sql 1`] = ` +"SELECT + id, + upper(name) AS name_upper, + created_at + '1 day'::interval AS expires_at +FROM accounts" +`; + +exports[`pretty: pretty/selects-9.sql 1`] = ` +"SELECT + (SELECT max(score) + FROM results)" +`; + +exports[`pretty: pretty/selects-10.sql 1`] = ` +"SELECT + count(*) OVER (), + u.id +FROM users AS u" +`; + +exports[`pretty: pretty/selects-11.sql 1`] = ` +"SELECT name FROM customers UNION ALL -SELECT - name +SELECT name FROM suppliers ORDER BY - name;" + name" `; -exports[`Pretty SELECT formatting should format SELECT with multiple JOINs with pretty option enabled 1`] = ` +exports[`pretty: pretty/selects-12.sql 1`] = ` "SELECT u.id, u.name, @@ -24,33 +105,32 @@ JOIN profiles AS p ON u.id = p.user_id LEFT JOIN orders AS o ON u.id = o.user_id RIGHT JOIN addresses AS a ON u.id = a.user_id WHERE - u.active = true;" + u.active = true" `; -exports[`Pretty SELECT formatting should format SELECT with subquery with pretty option enabled 1`] = ` +exports[`pretty: pretty/selects-13.sql 1`] = ` "SELECT id, name FROM users WHERE - id IN (SELECT - user_id + id IN (SELECT user_id FROM orders WHERE - total > 100);" + total > 100)" `; -exports[`Pretty SELECT formatting should format basic SELECT with pretty option enabled 1`] = ` +exports[`pretty: pretty/selects-14.sql 1`] = ` "SELECT id, name, email FROM users WHERE - active = true;" + active = true" `; -exports[`Pretty SELECT formatting should format complex SELECT with pretty option enabled 1`] = ` +exports[`pretty: pretty/selects-15.sql 1`] = ` "SELECT u.id, u.name, @@ -72,19 +152,5 @@ ORDER BY u.created_at DESC, u.name ASC LIMIT 10 -OFFSET 5;" -`; - -exports[`Pretty SELECT formatting should maintain single-line format for complex SELECT when pretty disabled 1`] = `"SELECT u.id, u.name, u.email, p.title FROM users AS u JOIN profiles AS p ON u.id = p.user_id WHERE u.active = true AND u.created_at > '2023-01-01' GROUP BY u.id, u.name, u.email, p.title HAVING count(*) > 1 ORDER BY u.created_at DESC, u.name ASC LIMIT 10 OFFSET 5;"`; - -exports[`Pretty SELECT formatting should maintain single-line format when pretty option disabled 1`] = `"SELECT id, name, email FROM users WHERE active = true;"`; - -exports[`Pretty SELECT formatting should use custom newline and tab characters in pretty mode 1`] = ` -"SELECT - id, - name, - email -FROM users -WHERE - active = true;" +OFFSET 5" `; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/tables-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/tables-pretty.test.ts.snap new file mode 100644 index 00000000..a2ade73d --- /dev/null +++ b/packages/deparser/__tests__/pretty/__snapshots__/tables-pretty.test.ts.snap @@ -0,0 +1,206 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`non-pretty: pretty/tables-1.sql 1`] = `"CREATE TABLE public.users (id serial PRIMARY KEY, name text NOT NULL)"`; + +exports[`non-pretty: pretty/tables-2.sql 1`] = `"CREATE TABLE "App"."User Data" ("User ID" uuid PRIMARY KEY, "Full Name" text NOT NULL)"`; + +exports[`non-pretty: pretty/tables-3.sql 1`] = `"CREATE TABLE system.settings (setting_key text PRIMARY KEY, setting_value text, CONSTRAINT "Default Setting Check" CHECK (setting_value IS NOT NULL))"`; + +exports[`non-pretty: pretty/tables-4.sql 1`] = `"CREATE TABLE "Inventory"."StockItems" ("ItemID" int PRIMARY KEY, "Tags" text[])"`; + +exports[`non-pretty: pretty/tables-5.sql 1`] = `"CREATE TABLE "Orders"."OrderLines" (id serial PRIMARY KEY, order_id int, CONSTRAINT "FK Order Reference" FOREIGN KEY (order_id) REFERENCES "Orders"."Order" ("OrderID"))"`; + +exports[`non-pretty: pretty/tables-6.sql 1`] = `"CREATE TABLE contact_info (id int PRIMARY KEY, location address)"`; + +exports[`non-pretty: pretty/tables-7.sql 1`] = `"CREATE TABLE "Archive"."OldUsers" (archived_at timestamptz DEFAULT now()) INHERITS ("Users"."User Data")"`; + +exports[`non-pretty: pretty/tables-8.sql 1`] = `"CREATE TABLE logging.audit_trail (log_id int GENERATED BY DEFAULT AS IDENTITY, message text, CONSTRAINT "PK_Audit" PRIMARY KEY (log_id))"`; + +exports[`non-pretty: pretty/tables-9.sql 1`] = `"CREATE TABLE finance.transactions (amount numeric, tax_rate numeric, total numeric GENERATED ALWAYS AS (amount * (1 + tax_rate)) STORED)"`; + +exports[`non-pretty: pretty/tables-10.sql 1`] = `"CREATE TABLE metrics.monthly_stats (stat_id serial, recorded_at date) PARTITION BY RANGE (recorded_at)"`; + +exports[`non-pretty: pretty/tables-11.sql 1`] = `"CREATE TABLE school.attendance ("Student ID" uuid, "Class ID" uuid, attended_on date DEFAULT CURRENT_DATE, PRIMARY KEY ("Student ID", "Class ID"))"`; + +exports[`non-pretty: pretty/tables-12.sql 1`] = `"CREATE TABLE secure.sessions (session_id uuid PRIMARY KEY, user_id uuid, CONSTRAINT "fk-user->session" FOREIGN KEY (user_id) REFERENCES users (id))"`; + +exports[`non-pretty: pretty/tables-13.sql 1`] = `"CREATE TABLE public."API Keys" ("KeyID" uuid PRIMARY KEY, "ClientName" text, "KeyValue" text UNIQUE, CONSTRAINT "Unique_ClientName" UNIQUE ("ClientName"))"`; + +exports[`non-pretty: pretty/tables-14.sql 1`] = `"CREATE TABLE alerts (alert_id serial PRIMARY KEY, level "AlertLevel" NOT NULL)"`; + +exports[`non-pretty: pretty/tables-15.sql 1`] = `"CREATE TABLE "Billing"."Invoices" (invoice_id uuid PRIMARY KEY, "Client ID" uuid, CONSTRAINT "FK_Client" FOREIGN KEY ("Client ID") REFERENCES "Clients"."ClientBase" ("Client ID"))"`; + +exports[`non-pretty: pretty/tables-16.sql 1`] = `"CREATE TABLE media.assets (id uuid PRIMARY KEY, url text, CONSTRAINT "Check-URL-NonEmpty" CHECK (url <> ''))"`; + +exports[`non-pretty: pretty/tables-17.sql 1`] = `"CREATE TABLE data.snapshots (id serial PRIMARY KEY, metadata jsonb, context address)"`; + +exports[`non-pretty: pretty/tables-18.sql 1`] = `"CREATE TABLE "x-Schema"."z-Table" ("Z-ID" int PRIMARY KEY, "Z-Name" text, CONSTRAINT "z-Name-Check" CHECK ("Z-Name" ~ '^[A-Z]'))"`; + +exports[`non-pretty: pretty/tables-19.sql 1`] = `"CREATE TABLE users.details (first_name text NOT NULL, last_name text, CONSTRAINT first_name_required CHECK (first_name <> ''))"`; + +exports[`non-pretty: pretty/tables-20.sql 1`] = `"CREATE TABLE "Calculated"."Metrics" (base int, adjustment int DEFAULT 0, "Total" int GENERATED ALWAYS AS (base + adjustment) STORED)"`; + +exports[`pretty: pretty/tables-1.sql 1`] = ` +"CREATE TABLE public.users ( + id serial PRIMARY KEY, + name text NOT NULL +)" +`; + +exports[`pretty: pretty/tables-2.sql 1`] = ` +"CREATE TABLE "App"."User Data" ( + "User ID" uuid PRIMARY KEY, + "Full Name" text NOT NULL +)" +`; + +exports[`pretty: pretty/tables-3.sql 1`] = ` +"CREATE TABLE system.settings ( + setting_key text PRIMARY KEY, + setting_value text, + CONSTRAINT "Default Setting Check" + CHECK (setting_value IS NOT NULL) +)" +`; + +exports[`pretty: pretty/tables-4.sql 1`] = ` +"CREATE TABLE "Inventory"."StockItems" ( + "ItemID" int PRIMARY KEY, + "Tags" text[] +)" +`; + +exports[`pretty: pretty/tables-5.sql 1`] = ` +"CREATE TABLE "Orders"."OrderLines" ( + id serial PRIMARY KEY, + order_id int, + CONSTRAINT "FK Order Reference" + FOREIGN KEY(order_id) + REFERENCES "Orders"."Order" ("OrderID") +)" +`; + +exports[`pretty: pretty/tables-6.sql 1`] = ` +"CREATE TABLE contact_info ( + id int PRIMARY KEY, + location address +)" +`; + +exports[`pretty: pretty/tables-7.sql 1`] = ` +"CREATE TABLE "Archive"."OldUsers" ( + archived_at timestamptz DEFAULT now() +) INHERITS ("Users"."User Data")" +`; + +exports[`pretty: pretty/tables-8.sql 1`] = ` +"CREATE TABLE logging.audit_trail ( + log_id int GENERATED BY DEFAULT AS IDENTITY, + message text, + CONSTRAINT "PK_Audit" PRIMARY KEY (log_id) +)" +`; + +exports[`pretty: pretty/tables-9.sql 1`] = ` +"CREATE TABLE finance.transactions ( + amount numeric, + tax_rate numeric, + total numeric GENERATED ALWAYS AS (amount * (1 + tax_rate)) STORED +)" +`; + +exports[`pretty: pretty/tables-10.sql 1`] = ` +"CREATE TABLE metrics.monthly_stats ( + stat_id serial, + recorded_at date +) PARTITION BY RANGE (recorded_at)" +`; + +exports[`pretty: pretty/tables-11.sql 1`] = ` +"CREATE TABLE school.attendance ( + "Student ID" uuid, + "Class ID" uuid, + attended_on date DEFAULT CURRENT_DATE, + PRIMARY KEY ("Student ID", "Class ID") +)" +`; + +exports[`pretty: pretty/tables-12.sql 1`] = ` +"CREATE TABLE secure.sessions ( + session_id uuid PRIMARY KEY, + user_id uuid, + CONSTRAINT "fk-user->session" + FOREIGN KEY(user_id) + REFERENCES users (id) +)" +`; + +exports[`pretty: pretty/tables-13.sql 1`] = ` +"CREATE TABLE public."API Keys" ( + "KeyID" uuid PRIMARY KEY, + "ClientName" text, + "KeyValue" text UNIQUE, + CONSTRAINT "Unique_ClientName" + UNIQUE ("ClientName") +)" +`; + +exports[`pretty: pretty/tables-14.sql 1`] = ` +"CREATE TABLE alerts ( + alert_id serial PRIMARY KEY, + level "AlertLevel" NOT NULL +)" +`; + +exports[`pretty: pretty/tables-15.sql 1`] = ` +"CREATE TABLE "Billing"."Invoices" ( + invoice_id uuid PRIMARY KEY, + "Client ID" uuid, + CONSTRAINT "FK_Client" + FOREIGN KEY("Client ID") + REFERENCES "Clients"."ClientBase" ("Client ID") +)" +`; + +exports[`pretty: pretty/tables-16.sql 1`] = ` +"CREATE TABLE media.assets ( + id uuid PRIMARY KEY, + url text, + CONSTRAINT "Check-URL-NonEmpty" + CHECK (url <> '') +)" +`; + +exports[`pretty: pretty/tables-17.sql 1`] = ` +"CREATE TABLE data.snapshots ( + id serial PRIMARY KEY, + metadata jsonb, + context address +)" +`; + +exports[`pretty: pretty/tables-18.sql 1`] = ` +"CREATE TABLE "x-Schema"."z-Table" ( + "Z-ID" int PRIMARY KEY, + "Z-Name" text, + CONSTRAINT "z-Name-Check" + CHECK ("Z-Name" ~ '^[A-Z]') +)" +`; + +exports[`pretty: pretty/tables-19.sql 1`] = ` +"CREATE TABLE users.details ( + first_name text NOT NULL, + last_name text, + CONSTRAINT first_name_required + CHECK (first_name <> '') +)" +`; + +exports[`pretty: pretty/tables-20.sql 1`] = ` +"CREATE TABLE "Calculated"."Metrics" ( + base int, + adjustment int DEFAULT 0, + "Total" int GENERATED ALWAYS AS (base + adjustment) STORED +)" +`; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/triggers-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/triggers-pretty.test.ts.snap new file mode 100644 index 00000000..19f69008 --- /dev/null +++ b/packages/deparser/__tests__/pretty/__snapshots__/triggers-pretty.test.ts.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`non-pretty: pretty/triggers-1.sql 1`] = `"CREATE TRIGGER audit_insert_trigger AFTER INSERT ON public.users FOR EACH ROW EXECUTE FUNCTION log_user_insert ()"`; + +exports[`non-pretty: pretty/triggers-2.sql 1`] = `"CREATE TRIGGER "AuditTrigger" AFTER DELETE ON "SensitiveData" FOR EACH ROW EXECUTE FUNCTION public.log_deletion ()"`; + +exports[`non-pretty: pretty/triggers-3.sql 1`] = `"CREATE TRIGGER archive_if_inactive BEFORE UPDATE ON accounts FOR EACH ROW WHEN ( old.active = false ) EXECUTE FUNCTION "ArchiveFunction" ()"`; + +exports[`non-pretty: pretty/triggers-4.sql 1`] = `"CREATE TRIGGER update_stats_on_change AFTER INSERT OR UPDATE ON metrics.stats FOR EACH ROW EXECUTE FUNCTION metrics.update_stats ( 'user', 'true' )"`; + +exports[`non-pretty: pretty/triggers-5.sql 1`] = `"CREATE TRIGGER "TrickyTrigger" BEFORE DELETE ON "weirdSchema"."ComplexTable" FOR EACH ROW WHEN ( old.status = 'pending' ) EXECUTE FUNCTION "weirdSchema"."ComplexFn" ( 'arg1', '42' )"`; + +exports[`non-pretty: pretty/triggers-6.sql 1`] = `"CREATE TRIGGER user_activity_log AFTER INSERT OR DELETE OR UPDATE ON users FOR EACH ROW EXECUTE FUNCTION audit.activity_log ()"`; + +exports[`non-pretty: pretty/triggers-7.sql 1`] = `"CREATE TRIGGER no_schema BEFORE INSERT ON log_table FOR EACH ROW EXECUTE FUNCTION update_log ()"`; + +exports[`non-pretty: pretty/triggers-8.sql 1`] = `"CREATE TRIGGER flag_special_updates AFTER UPDATE ON profiles FOR EACH ROW WHEN ( new."accessLevel" = 'admin' ) EXECUTE FUNCTION flag_admin_change ()"`; + +exports[`non-pretty: pretty/triggers-9.sql 1`] = `"CREATE TRIGGER "TriggerMixedCase" BEFORE INSERT ON datapoints FOR EACH ROW EXECUTE FUNCTION "HandleInsert" ( 'TYPE_A', 'Region-1' )"`; + +exports[`non-pretty: pretty/triggers-10.sql 1`] = `"CREATE TRIGGER cascade_on_partition AFTER DELETE ON events_log_partition FOR EACH ROW EXECUTE FUNCTION propagate_deletion ()"`; + +exports[`pretty: pretty/triggers-1.sql 1`] = ` +"CREATE TRIGGER audit_insert_trigger + AFTER INSERT + ON public.users + FOR EACH ROW + EXECUTE PROCEDURE log_user_insert()" +`; + +exports[`pretty: pretty/triggers-2.sql 1`] = ` +"CREATE TRIGGER "AuditTrigger" + AFTER DELETE + ON "SensitiveData" + FOR EACH ROW + EXECUTE PROCEDURE public.log_deletion()" +`; + +exports[`pretty: pretty/triggers-3.sql 1`] = ` +"CREATE TRIGGER archive_if_inactive + BEFORE UPDATE + ON accounts + FOR EACH ROW + WHEN (old.active = false) + EXECUTE PROCEDURE "ArchiveFunction"()" +`; + +exports[`pretty: pretty/triggers-4.sql 1`] = ` +"CREATE TRIGGER update_stats_on_change + AFTER INSERT OR UPDATE + ON metrics.stats + FOR EACH ROW + EXECUTE PROCEDURE metrics.update_stats('user', 'true')" +`; + +exports[`pretty: pretty/triggers-5.sql 1`] = ` +"CREATE TRIGGER "TrickyTrigger" + BEFORE DELETE + ON "weirdSchema"."ComplexTable" + FOR EACH ROW + WHEN (old.status = 'pending') + EXECUTE PROCEDURE "weirdSchema"."ComplexFn"('arg1', '42')" +`; + +exports[`pretty: pretty/triggers-6.sql 1`] = ` +"CREATE TRIGGER user_activity_log + AFTER INSERT OR DELETE OR UPDATE + ON users + FOR EACH ROW + EXECUTE PROCEDURE audit.activity_log()" +`; + +exports[`pretty: pretty/triggers-7.sql 1`] = ` +"CREATE TRIGGER no_schema + BEFORE INSERT + ON log_table + FOR EACH ROW + EXECUTE PROCEDURE update_log()" +`; + +exports[`pretty: pretty/triggers-8.sql 1`] = ` +"CREATE TRIGGER flag_special_updates + AFTER UPDATE + ON profiles + FOR EACH ROW + WHEN (new."accessLevel" = 'admin') + EXECUTE PROCEDURE flag_admin_change()" +`; + +exports[`pretty: pretty/triggers-9.sql 1`] = ` +"CREATE TRIGGER "TriggerMixedCase" + BEFORE INSERT + ON datapoints + FOR EACH ROW + EXECUTE PROCEDURE "HandleInsert"('TYPE_A', 'Region-1')" +`; + +exports[`pretty: pretty/triggers-10.sql 1`] = ` +"CREATE TRIGGER cascade_on_partition + AFTER DELETE + ON events_log_partition + FOR EACH ROW + EXECUTE PROCEDURE propagate_deletion()" +`; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/types-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/types-pretty.test.ts.snap new file mode 100644 index 00000000..cb8c4989 --- /dev/null +++ b/packages/deparser/__tests__/pretty/__snapshots__/types-pretty.test.ts.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`non-pretty: pretty/types-1.sql 1`] = `"CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')"`; + +exports[`non-pretty: pretty/types-2.sql 1`] = `"CREATE TYPE "AlertLevel" AS ENUM ('Low', 'MEDIUM', 'High', 'CRITICAL')"`; + +exports[`non-pretty: pretty/types-3.sql 1`] = `"CREATE TYPE address AS (street text, city text, zip_code int)"`; + +exports[`non-pretty: pretty/types-4.sql 1`] = `"CREATE TYPE "PostalInfo" AS ("Street" text, "City" text, "ZipCode" int)"`; + +exports[`non-pretty: pretty/types-5.sql 1`] = `"CREATE TYPE public.user_metadata AS (key text, value jsonb)"`; + +exports[`non-pretty: pretty/types-6.sql 1`] = `"CREATE TYPE tsrange_custom AS RANGE (subtype = timestamp with time zone, subtype_diff = timestamp_diff, canonical = normalize_tsrange)"`; + +exports[`non-pretty: pretty/types-7.sql 1`] = `"CREATE TYPE version_enum AS ENUM ('1.0', '1.1', '2.0')"`; + +exports[`non-pretty: pretty/types-8.sql 1`] = `"CREATE TYPE full_location AS (address address, region_code char(2))"`; + +exports[`non-pretty: pretty/types-9.sql 1`] = `"CREATE TYPE "Workflow-State" AS ENUM ('draft', 'in-review', 'needs-fix', 'finalized')"`; + +exports[`pretty: pretty/types-1.sql 1`] = `"CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')"`; + +exports[`pretty: pretty/types-2.sql 1`] = `"CREATE TYPE "AlertLevel" AS ENUM ('Low', 'MEDIUM', 'High', 'CRITICAL')"`; + +exports[`pretty: pretty/types-3.sql 1`] = `"CREATE TYPE address AS (street text, city text, zip_code int)"`; + +exports[`pretty: pretty/types-4.sql 1`] = `"CREATE TYPE "PostalInfo" AS ("Street" text, "City" text, "ZipCode" int)"`; + +exports[`pretty: pretty/types-5.sql 1`] = `"CREATE TYPE public.user_metadata AS (key text, value jsonb)"`; + +exports[`pretty: pretty/types-6.sql 1`] = `"CREATE TYPE tsrange_custom AS RANGE (subtype = timestamp with time zone, subtype_diff = timestamp_diff, canonical = normalize_tsrange)"`; + +exports[`pretty: pretty/types-7.sql 1`] = `"CREATE TYPE version_enum AS ENUM ('1.0', '1.1', '2.0')"`; + +exports[`pretty: pretty/types-8.sql 1`] = `"CREATE TYPE full_location AS (address address, region_code char(2))"`; + +exports[`pretty: pretty/types-9.sql 1`] = `"CREATE TYPE "Workflow-State" AS ENUM ('draft', 'in-review', 'needs-fix', 'finalized')"`; diff --git a/packages/deparser/__tests__/pretty/casing-pretty.test.ts b/packages/deparser/__tests__/pretty/casing-pretty.test.ts new file mode 100644 index 00000000..46d7153c --- /dev/null +++ b/packages/deparser/__tests__/pretty/casing-pretty.test.ts @@ -0,0 +1,37 @@ +import { PrettyTest } from '../../test-utils/PrettyTest'; +const prettyTest = new PrettyTest([ + 'pretty/casing-1.sql', + 'pretty/casing-2.sql', + 'pretty/casing-3.sql', + 'pretty/casing-4.sql', + 'pretty/casing-5.sql', + 'pretty/casing-6.sql', + 'pretty/casing-7.sql', + 'pretty/casing-8.sql', + 'pretty/casing-9.sql', + 'pretty/casing-10.sql', + 'pretty/casing-11.sql', + 'pretty/casing-12.sql', + 'pretty/casing-13.sql', + 'pretty/casing-14.sql', + 'pretty/casing-15.sql', + 'pretty/casing-16.sql', + 'pretty/casing-17.sql', + 'pretty/casing-18.sql', + 'pretty/casing-19.sql', + 'pretty/casing-20.sql', + 'pretty/casing-21.sql', + 'pretty/casing-22.sql', + 'pretty/casing-23.sql', + 'pretty/casing-24.sql', + 'pretty/casing-25.sql', + 'pretty/casing-26.sql', + 'pretty/casing-27.sql', + 'pretty/casing-28.sql', + 'pretty/casing-29.sql', + 'pretty/casing-30.sql', + 'pretty/casing-31.sql', + 'pretty/casing-32.sql', +]); + +prettyTest.generateTests(); \ No newline at end of file diff --git a/packages/deparser/__tests__/pretty/constraints-pretty.test.ts b/packages/deparser/__tests__/pretty/constraints-pretty.test.ts index 5ce28679..92ef7384 100644 --- a/packages/deparser/__tests__/pretty/constraints-pretty.test.ts +++ b/packages/deparser/__tests__/pretty/constraints-pretty.test.ts @@ -1,49 +1,22 @@ -import { expectParseDeparse } from '../../test-utils'; - -describe('Pretty constraint formatting', () => { - const foreignKeyConstraintSql = `ALTER TABLE products ADD CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES categories(id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED;`; - - const checkConstraintSql = `ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0);`; - - const complexTableSql = `CREATE TABLE orders ( - id SERIAL PRIMARY KEY, - user_id INTEGER NOT NULL, - total DECIMAL(10,2) CHECK (total > 0), - status VARCHAR(20) DEFAULT 'pending', - CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - );`; - - it('should format foreign key constraint with pretty option enabled', async () => { - const result = await expectParseDeparse(foreignKeyConstraintSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should maintain single-line format when pretty option disabled', async () => { - const result = await expectParseDeparse(foreignKeyConstraintSql, { pretty: false }); - expect(result).toMatchSnapshot(); - }); - - it('should format check constraint with pretty option enabled', async () => { - const result = await expectParseDeparse(checkConstraintSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should format complex table with constraints with pretty option enabled', async () => { - const result = await expectParseDeparse(complexTableSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should maintain single-line format for complex table when pretty disabled', async () => { - const result = await expectParseDeparse(complexTableSql, { pretty: false }); - expect(result).toMatchSnapshot(); - }); - - it('should use custom newline and tab characters in pretty mode', async () => { - const result = await expectParseDeparse(foreignKeyConstraintSql, { - pretty: true, - newline: '\r\n', - tab: ' ' - }); - expect(result).toMatchSnapshot(); - }); -}); +import { PrettyTest } from '../../test-utils/PrettyTest'; +const prettyTest = new PrettyTest([ + 'pretty/constraints-1.sql', + 'pretty/constraints-2.sql', + 'pretty/constraints-3.sql', + 'pretty/constraints-4.sql', + 'pretty/constraints-5.sql', + 'pretty/constraints-6.sql', + 'pretty/constraints-7.sql', + 'pretty/constraints-8.sql', + 'pretty/constraints-9.sql', + 'pretty/constraints-10.sql', + 'pretty/constraints-11.sql', + 'pretty/constraints-12.sql', + 'pretty/constraints-13.sql', + 'pretty/constraints-14.sql', + 'pretty/constraints-15.sql', + 'pretty/constraints-16.sql', + 'pretty/constraints-17.sql', +]); + +prettyTest.generateTests(); \ No newline at end of file diff --git a/packages/deparser/__tests__/pretty/create-policy-pretty.test.ts b/packages/deparser/__tests__/pretty/create-policy-pretty.test.ts index 4a8b8557..01411f17 100644 --- a/packages/deparser/__tests__/pretty/create-policy-pretty.test.ts +++ b/packages/deparser/__tests__/pretty/create-policy-pretty.test.ts @@ -1,50 +1,12 @@ -import { expectParseDeparse } from '../../test-utils'; - -describe('Pretty CREATE POLICY formatting', () => { - const basicPolicySql = `CREATE POLICY user_policy ON users FOR ALL TO authenticated_users USING (user_id = current_user_id());`; - - const complexPolicySql = `CREATE POLICY admin_policy ON sensitive_data AS RESTRICTIVE FOR SELECT TO admin_role USING (department = current_user_department()) WITH CHECK (approved = true);`; - - const veryComplexPolicySql = `CREATE POLICY complex_policy ON sensitive_data AS RESTRICTIVE FOR SELECT TO admin_role USING (department = current_user_department() AND EXISTS (SELECT 1 FROM user_permissions WHERE user_id = current_user_id() AND permission = 'read_sensitive')) WITH CHECK (approved = true AND created_by = current_user_id());`; - - const simplePolicySql = `CREATE POLICY simple_policy ON posts FOR SELECT TO public USING (published = true);`; - - it('should format basic CREATE POLICY with pretty option enabled', async () => { - const result = await expectParseDeparse(basicPolicySql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should maintain single-line format when pretty option disabled', async () => { - const result = await expectParseDeparse(basicPolicySql, { pretty: false }); - expect(result).toMatchSnapshot(); - }); - - it('should format complex CREATE POLICY with pretty option enabled', async () => { - const result = await expectParseDeparse(complexPolicySql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should maintain single-line format for complex policy when pretty disabled', async () => { - const result = await expectParseDeparse(complexPolicySql, { pretty: false }); - expect(result).toMatchSnapshot(); - }); - - it('should format simple CREATE POLICY with pretty option enabled', async () => { - const result = await expectParseDeparse(simplePolicySql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should format very complex CREATE POLICY with pretty option enabled', async () => { - const result = await expectParseDeparse(veryComplexPolicySql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should use custom newline and tab characters in pretty mode', async () => { - const result = await expectParseDeparse(basicPolicySql, { - pretty: true, - newline: '\r\n', - tab: ' ' - }); - expect(result).toMatchSnapshot(); - }); -}); +import { PrettyTest } from '../../test-utils/PrettyTest'; +const prettyTest = new PrettyTest([ + 'pretty/create_policy-1.sql', + 'pretty/create_policy-2.sql', + 'pretty/create_policy-3.sql', + 'pretty/create_policy-4.sql', + 'pretty/create_policy-5.sql', + 'pretty/create_policy-6.sql', + 'pretty/create_policy-7.sql', +]); + +prettyTest.generateTests(); \ No newline at end of file diff --git a/packages/deparser/__tests__/pretty/create-table-pretty.test.ts b/packages/deparser/__tests__/pretty/create-table-pretty.test.ts index 5d1a810d..1e758051 100644 --- a/packages/deparser/__tests__/pretty/create-table-pretty.test.ts +++ b/packages/deparser/__tests__/pretty/create-table-pretty.test.ts @@ -1,43 +1,13 @@ -import { expectParseDeparse } from '../../test-utils'; - -describe('Pretty CREATE TABLE formatting', () => { - const basicTableSql = `CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE);`; - - const complexTableSql = `CREATE TABLE orders ( - id SERIAL PRIMARY KEY, - user_id INTEGER NOT NULL, - total DECIMAL(10,2) CHECK (total > 0), - status VARCHAR(20) DEFAULT 'pending', - created_at TIMESTAMP DEFAULT now(), - FOREIGN KEY (user_id) REFERENCES users(id) - );`; - - it('should format basic CREATE TABLE with pretty option enabled', async () => { - const result = await expectParseDeparse(basicTableSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should maintain single-line format when pretty option disabled', async () => { - const result = await expectParseDeparse(basicTableSql, { pretty: false }); - expect(result).toMatchSnapshot(); - }); - - it('should format complex CREATE TABLE with pretty option enabled', async () => { - const result = await expectParseDeparse(complexTableSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should maintain single-line format for complex table when pretty disabled', async () => { - const result = await expectParseDeparse(complexTableSql, { pretty: false }); - expect(result).toMatchSnapshot(); - }); - - it('should use custom newline and tab characters in pretty mode', async () => { - const result = await expectParseDeparse(basicTableSql, { - pretty: true, - newline: '\r\n', - tab: ' ' - }); - expect(result).toMatchSnapshot(); - }); -}); +import { PrettyTest } from '../../test-utils/PrettyTest'; + +const testCases = [ + 'pretty/create_table-1.sql', + 'pretty/create_table-2.sql', + 'pretty/create_table-3.sql', + 'pretty/create_table-4.sql', + 'pretty/create_table-5.sql', + 'pretty/create_table-6.sql' +]; + +const prettyTest = new PrettyTest(testCases); +prettyTest.generateTests(); diff --git a/packages/deparser/__tests__/pretty/cte-pretty.test.ts b/packages/deparser/__tests__/pretty/cte-pretty.test.ts index aa411e15..62a90368 100644 --- a/packages/deparser/__tests__/pretty/cte-pretty.test.ts +++ b/packages/deparser/__tests__/pretty/cte-pretty.test.ts @@ -1,45 +1,11 @@ -import { expectParseDeparse } from '../../test-utils'; +import { PrettyTest } from '../../test-utils/PrettyTest'; -describe('Pretty CTE (Common Table Expressions) formatting', () => { - const basicCteSql = `WITH regional_sales AS (SELECT region, SUM(sales_amount) as total_sales FROM sales GROUP BY region) SELECT * FROM regional_sales;`; - - const complexCteSql = `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;`; +const testCases = [ + 'pretty/cte-1.sql', + 'pretty/cte-2.sql', + 'pretty/cte-3.sql', + 'pretty/cte-4.sql', +]; - const recursiveCteSql = `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;`; - - const nestedCteSql = `WITH sales_summary AS (SELECT region, product_category, SUM(amount) as total FROM sales GROUP BY region, product_category), regional_totals AS (SELECT region, SUM(total) as region_total FROM sales_summary GROUP BY region) SELECT s.region, s.product_category, s.total, r.region_total FROM sales_summary s JOIN regional_totals r ON s.region = r.region;`; - - it('should format basic CTE with pretty option enabled', async () => { - const result = await expectParseDeparse(basicCteSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should maintain single-line format when pretty option disabled', async () => { - const result = await expectParseDeparse(basicCteSql, { pretty: false }); - expect(result).toMatchSnapshot(); - }); - - it('should format complex CTE with multiple CTEs with pretty option enabled', async () => { - const result = await expectParseDeparse(complexCteSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should format recursive CTE with pretty option enabled', async () => { - const result = await expectParseDeparse(recursiveCteSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should format nested CTE with complex joins with pretty option enabled', async () => { - const result = await expectParseDeparse(nestedCteSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should use custom newline and tab characters in pretty mode', async () => { - const result = await expectParseDeparse(basicCteSql, { - pretty: true, - newline: '\r\n', - tab: ' ' - }); - expect(result).toMatchSnapshot(); - }); -}); +const prettyTest = new PrettyTest(testCases); +prettyTest.generateTests(); \ No newline at end of file diff --git a/packages/deparser/__tests__/pretty/misc-pretty.test.ts b/packages/deparser/__tests__/pretty/misc-pretty.test.ts index fa366902..92c96f57 100644 --- a/packages/deparser/__tests__/pretty/misc-pretty.test.ts +++ b/packages/deparser/__tests__/pretty/misc-pretty.test.ts @@ -1,87 +1,23 @@ -import { deparseSync } from '../../src'; -import { parse } from 'libpg-query'; -import { expectParseDeparse } from '../../test-utils'; +import { PrettyTest } from '../../test-utils/PrettyTest'; -const generateCoded = require('../../../../__fixtures__/generated/generated.json'); +const testCases = [ + 'pretty/misc-1.sql', + 'pretty/misc-2.sql', + 'pretty/misc-3.sql', + 'pretty/misc-4.sql', + 'pretty/misc-5.sql', + 'pretty/misc-6.sql', + 'pretty/misc-7.sql', + 'pretty/misc-8.sql', + 'pretty/misc-9.sql', + 'pretty/misc-10.sql', + 'pretty/misc-11.sql', + 'pretty/misc-12.sql', + 'pretty/misc-13.sql', + 'pretty/misc-14.sql', + 'pretty/misc-15.sql', + 'pretty/misc-16.sql' +]; -describe('Pretty Misc SQL formatting', () => { - const testCases = [ - { - key: 'pretty/misc-1.sql', - description: 'Complex CTE with joins and aggregation' - }, - { - key: 'pretty/misc-2.sql', - description: 'Window functions with FILTER and GROUPING SETS' - }, - { - key: 'pretty/misc-3.sql', - description: 'LATERAL joins with JSON functions' - }, - { - key: 'pretty/misc-4.sql', - description: 'EXISTS with nested subqueries and CASE' - }, - { - key: 'pretty/misc-5.sql', - description: 'Nested CTEs with type casts and subqueries' - }, - { - key: 'pretty/misc-6.sql', - description: 'Complex multi-table joins with nested conditions' - }, - { - key: 'pretty/misc-7.sql', - description: 'Large Case Stmt' - }, - { - key: 'pretty/misc-8.sql', - description: 'Large Case Stmt' - }, - { - key: 'pretty/misc-9.sql', - description: 'Large Case Stmt' - }, - { - key: 'pretty/misc-10.sql', - description: 'Where Clause' - }, - { - key: 'pretty/misc-11.sql', - description: 'Lateral Join' - }, - { - key: 'pretty/misc-12.sql', - description: 'Scalar Subquery' - }, - { - key: 'pretty/misc-13.sql', - description: 'Window Function' - } - ]; - - // Generate individual tests for each case and format type - testCases.forEach(({ key, description }, index) => { - const sql = generateCoded[key]; - const testName = `misc-${index + 1}`; - - it(`should format ${testName}: ${description} (pretty)`, async () => { - const result = await expectParseDeparse(sql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it(`should format ${testName}: ${description} (non-pretty)`, async () => { - const result = await expectParseDeparse(sql, { pretty: false }); - expect(result).toMatchSnapshot(); - }); - }); - - it('should validate AST equivalence for all misc cases', async () => { - const allSql = testCases.map(({ key }) => generateCoded[key]); - - for (const sql of allSql) { - await expectParseDeparse(sql, { pretty: false }); - await expectParseDeparse(sql, { pretty: true }); - } - }); -}); +const prettyTest = new PrettyTest(testCases); +prettyTest.generateTests(); diff --git a/packages/deparser/__tests__/pretty/procedures-pretty.test.ts b/packages/deparser/__tests__/pretty/procedures-pretty.test.ts new file mode 100644 index 00000000..d0e14c4b --- /dev/null +++ b/packages/deparser/__tests__/pretty/procedures-pretty.test.ts @@ -0,0 +1,15 @@ +import { PrettyTest } from '../../test-utils/PrettyTest'; +const prettyTest = new PrettyTest([ + 'pretty/procedures-1.sql', + 'pretty/procedures-2.sql', + 'pretty/procedures-3.sql', + 'pretty/procedures-4.sql', + 'pretty/procedures-5.sql', + 'pretty/procedures-6.sql', + 'pretty/procedures-7.sql', + 'pretty/procedures-8.sql', + 'pretty/procedures-9.sql', + 'pretty/procedures-10.sql', +]); + +prettyTest.generateTests(); \ No newline at end of file diff --git a/packages/deparser/__tests__/pretty/select-pretty.test.ts b/packages/deparser/__tests__/pretty/select-pretty.test.ts index 3508cc95..41f5cd5d 100644 --- a/packages/deparser/__tests__/pretty/select-pretty.test.ts +++ b/packages/deparser/__tests__/pretty/select-pretty.test.ts @@ -1,57 +1,21 @@ -import { expectParseDeparse } from '../../test-utils'; - -describe('Pretty SELECT formatting', () => { - const basicSelectSql = `SELECT id, name, email FROM users WHERE active = true;`; - - const complexSelectSql = `SELECT u.id, u.name, u.email, p.title FROM users u JOIN profiles p ON u.id = p.user_id WHERE u.active = true AND u.created_at > '2023-01-01' GROUP BY u.id, u.name, u.email, p.title HAVING COUNT(*) > 1 ORDER BY u.created_at DESC, u.name ASC LIMIT 10 OFFSET 5;`; - - const multipleJoinsSql = `SELECT u.id, u.name, u.email, p.title FROM users AS u JOIN profiles AS p ON u.id = p.user_id LEFT JOIN orders AS o ON u.id = o.user_id RIGHT JOIN addresses AS a ON u.id = a.user_id WHERE u.active = true;`; - - const selectWithSubquerySql = `SELECT id, name FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 100);`; - - const selectUnionSql = `SELECT name FROM customers UNION ALL SELECT name FROM suppliers ORDER BY name;`; - - it('should format basic SELECT with pretty option enabled', async () => { - const result = await expectParseDeparse(basicSelectSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should maintain single-line format when pretty option disabled', async () => { - const result = await expectParseDeparse(basicSelectSql, { pretty: false }); - expect(result).toMatchSnapshot(); - }); - - it('should format complex SELECT with pretty option enabled', async () => { - const result = await expectParseDeparse(complexSelectSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should maintain single-line format for complex SELECT when pretty disabled', async () => { - const result = await expectParseDeparse(complexSelectSql, { pretty: false }); - expect(result).toMatchSnapshot(); - }); - - it('should format SELECT with subquery with pretty option enabled', async () => { - const result = await expectParseDeparse(selectWithSubquerySql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should format SELECT with UNION with pretty option enabled', async () => { - const result = await expectParseDeparse(selectUnionSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should format SELECT with multiple JOINs with pretty option enabled', async () => { - const result = await expectParseDeparse(multipleJoinsSql, { pretty: true }); - expect(result).toMatchSnapshot(); - }); - - it('should use custom newline and tab characters in pretty mode', async () => { - const result = await expectParseDeparse(basicSelectSql, { - pretty: true, - newline: '\r\n', - tab: ' ' - }); - expect(result).toMatchSnapshot(); - }); -}); +import { PrettyTest } from '../../test-utils/PrettyTest'; + +const prettyTest = new PrettyTest([ + 'pretty/selects-1.sql', + 'pretty/selects-2.sql', + 'pretty/selects-3.sql', + 'pretty/selects-4.sql', + 'pretty/selects-5.sql', + 'pretty/selects-6.sql', + 'pretty/selects-7.sql', + 'pretty/selects-8.sql', + 'pretty/selects-9.sql', + 'pretty/selects-10.sql', + 'pretty/selects-11.sql', + 'pretty/selects-12.sql', + 'pretty/selects-13.sql', + 'pretty/selects-14.sql', + 'pretty/selects-15.sql', +]); + +prettyTest.generateTests(); diff --git a/packages/deparser/__tests__/pretty/tables-pretty.test.ts b/packages/deparser/__tests__/pretty/tables-pretty.test.ts new file mode 100644 index 00000000..2caec1ad --- /dev/null +++ b/packages/deparser/__tests__/pretty/tables-pretty.test.ts @@ -0,0 +1,27 @@ +import { PrettyTest } from '../../test-utils/PrettyTest'; + +const testCases = [ + 'pretty/tables-1.sql', + 'pretty/tables-2.sql', + 'pretty/tables-3.sql', + 'pretty/tables-4.sql', + 'pretty/tables-5.sql', + 'pretty/tables-6.sql', + 'pretty/tables-7.sql', + 'pretty/tables-8.sql', + 'pretty/tables-9.sql', + 'pretty/tables-10.sql', + 'pretty/tables-11.sql', + 'pretty/tables-12.sql', + 'pretty/tables-13.sql', + 'pretty/tables-14.sql', + 'pretty/tables-15.sql', + 'pretty/tables-16.sql', + 'pretty/tables-17.sql', + 'pretty/tables-18.sql', + 'pretty/tables-19.sql', + 'pretty/tables-20.sql' +]; + +const prettyTest = new PrettyTest(testCases); +prettyTest.generateTests(); diff --git a/packages/deparser/__tests__/pretty/triggers-pretty.test.ts b/packages/deparser/__tests__/pretty/triggers-pretty.test.ts new file mode 100644 index 00000000..a0d784e1 --- /dev/null +++ b/packages/deparser/__tests__/pretty/triggers-pretty.test.ts @@ -0,0 +1,15 @@ +import { PrettyTest } from '../../test-utils/PrettyTest'; +const prettyTest = new PrettyTest([ + 'pretty/triggers-1.sql', + 'pretty/triggers-2.sql', + 'pretty/triggers-3.sql', + 'pretty/triggers-4.sql', + 'pretty/triggers-5.sql', + 'pretty/triggers-6.sql', + 'pretty/triggers-7.sql', + 'pretty/triggers-8.sql', + 'pretty/triggers-9.sql', + 'pretty/triggers-10.sql', +]); + +prettyTest.generateTests(); \ No newline at end of file diff --git a/packages/deparser/__tests__/pretty/types-pretty.test.ts b/packages/deparser/__tests__/pretty/types-pretty.test.ts new file mode 100644 index 00000000..70fc0ce1 --- /dev/null +++ b/packages/deparser/__tests__/pretty/types-pretty.test.ts @@ -0,0 +1,14 @@ +import { PrettyTest } from '../../test-utils/PrettyTest'; +const prettyTest = new PrettyTest([ + 'pretty/types-1.sql', + 'pretty/types-2.sql', + 'pretty/types-3.sql', + 'pretty/types-4.sql', + 'pretty/types-5.sql', + 'pretty/types-6.sql', + 'pretty/types-7.sql', + 'pretty/types-8.sql', + 'pretty/types-9.sql', +]); + +prettyTest.generateTests(); \ No newline at end of file diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 169d74c1..8d30ed11 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -5,6 +5,73 @@ import { QuoteUtils } from './utils/quote-utils'; import { ListUtils } from './utils/list-utils'; import * as t from '@pgsql/types'; +/** + * List of real PostgreSQL built-in types as they appear in pg_catalog.pg_type.typname. + * These are stored in lowercase in PostgreSQL system catalogs. + * Use these for lookups, validations, or introspection logic. + */ + +const pgCatalogTypes = [ + // Integers + 'int2', // smallint + 'int4', // integer + 'int8', // bigint + + // Floating-point & numeric + 'float4', // real + 'float8', // double precision + 'numeric', // arbitrary precision (aka "decimal") + + // Text & string + 'varchar', // variable-length string + 'char', // internal one-byte type (used in special cases) + 'bpchar', // blank-padded char(n) + 'text', // unlimited string + 'bool', // boolean + + // Dates & times + 'date', // calendar date + 'time', // time without time zone + 'timetz', // time with time zone + 'timestamp', // timestamp without time zone + 'timestamptz', // timestamp with time zone + 'interval', // duration + + // Binary & structured + 'bytea', // binary data + 'uuid', // universally unique identifier + + // JSON & XML + 'json', // textual JSON + 'jsonb', // binary JSON + 'xml', // XML format + + // Money & bitstrings + 'money', // currency value + 'bit', // fixed-length bit string + 'varbit', // variable-length bit string + + // Network types + 'inet', // IPv4 or IPv6 address + 'cidr', // network address + 'macaddr', // MAC address (6 bytes) + 'macaddr8' // MAC address (8 bytes) +]; + + +/** + * Parser-level type aliases accepted by PostgreSQL SQL syntax, + * but not present in pg_catalog.pg_type. These are resolved to + * real types during parsing and never appear in introspection. + */ +const pgCatalogTypeAliases: [string, string[]][] = [ + ['numeric', ['decimal', 'dec']], + ['int4', ['int', 'integer']], + ['float8', ['float']], + ['bpchar', ['character']], + ['varchar', ['character varying']] +]; + export interface DeparserOptions { newline?: string; tab?: string; @@ -303,17 +370,34 @@ export class Deparser implements DeparserVisitor { if (node.targetList) { const targetList = ListUtils.unwrapList(node.targetList); if (this.formatter.isPretty()) { - const targetStrings = targetList - .map(e => { - const targetStr = this.visit(e as Node, { ...context, select: true }); - if (this.containsMultilineStringLiteral(targetStr)) { - return targetStr; + if (targetList.length === 1) { + const targetNode = targetList[0] as Node; + const target = this.visit(targetNode, { ...context, select: true }); + + // Check if single target is complex - if so, use multiline format + if (this.isComplexSelectTarget(targetNode)) { + output.push('SELECT' + distinctPart); + if (this.containsMultilineStringLiteral(target)) { + output.push(target); + } else { + output.push(this.formatter.indent(target)); } - return this.formatter.indent(targetStr); - }); - const formattedTargets = targetStrings.join(',' + this.formatter.newline()); - output.push('SELECT' + distinctPart); - output.push(formattedTargets); + } else { + output.push('SELECT' + distinctPart + ' ' + target); + } + } else { + const targetStrings = targetList + .map(e => { + const targetStr = this.visit(e as Node, { ...context, select: true }); + if (this.containsMultilineStringLiteral(targetStr)) { + return targetStr; + } + return this.formatter.indent(targetStr); + }); + const formattedTargets = targetStrings.join(',' + this.formatter.newline()); + output.push('SELECT' + distinctPart); + output.push(formattedTargets); + } } else { const targets = targetList .map(e => this.visit(e as Node, { ...context, select: true })) @@ -752,6 +836,71 @@ export class Deparser implements DeparserVisitor { ); } + private isComplexSelectTarget(node: any): boolean { + if (!node) return false; + + if (node.ResTarget?.val) { + return this.isComplexExpression(node.ResTarget.val); + } + + // Always complex: CASE expressions + if (node.CaseExpr) return true; + + // Always complex: Subqueries and subselects + if (node.SubLink) return true; + + // Always complex: Boolean tests and expressions + if (node.NullTest || node.BooleanTest || node.BoolExpr) return true; + + // COALESCE and similar functions - complex if multiple arguments + if (node.CoalesceExpr) { + const args = node.CoalesceExpr.args; + if (args && Array.isArray(args) && args.length > 1) return true; + } + + // Function calls - complex if multiple args or has clauses + if (node.FuncCall) { + const funcCall = node.FuncCall; + const args = funcCall.args ? (Array.isArray(funcCall.args) ? funcCall.args : [funcCall.args]) : []; + + // Complex if has window clause, filter, order by, etc. + if (funcCall.over || funcCall.agg_filter || funcCall.agg_order || funcCall.agg_distinct) { + return true; + } + + // Complex if multiple arguments + if (args.length > 1) return true; + + if (args.length === 1) { + return this.isComplexSelectTarget(args[0]); + } + } + + if (node.A_Expr) { + const expr = node.A_Expr; + // Check if operands are complex + if (expr.lexpr && this.isComplexSelectTarget(expr.lexpr)) return true; + if (expr.rexpr && this.isComplexSelectTarget(expr.rexpr)) return true; + return false; + } + + if (node.TypeCast) { + return this.isComplexSelectTarget(node.TypeCast.arg); + } + + if (node.A_ArrayExpr) return true; + + if (node.A_Indirection) { + return this.isComplexSelectTarget(node.A_Indirection.arg); + } + + if (node.A_Const || node.ColumnRef || node.ParamRef || node.A_Star) { + return false; + } + + return false; + } + visitBetweenRange(rexpr: any, context: DeparserContext): string { if (rexpr && 'List' in rexpr && rexpr.List?.items) { const items = rexpr.List.items.map((item: any) => this.visit(item, context)); @@ -1609,10 +1758,6 @@ export class Deparser implements DeparserVisitor { } if (catalog === 'pg_catalog') { - const builtinTypes = ['int2', 'int4', 'int8', 'float4', 'float8', 'numeric', 'decimal', - 'varchar', 'char', 'bpchar', 'text', 'bool', 'date', 'time', 'timestamp', - 'timestamptz', 'interval', 'bytea', 'uuid', 'json', 'jsonb']; - let typeName = `${catalog}.${type}`; if (type === 'bpchar' && args) { @@ -1922,6 +2067,22 @@ export class Deparser implements DeparserVisitor { } } + isPgCatalogType(typeName: string): boolean { + const cleanTypeName = typeName.replace(/^pg_catalog\./, ''); + + if (pgCatalogTypes.includes(cleanTypeName)) { + return true; + } + + for (const [realType, aliases] of pgCatalogTypeAliases) { + if (aliases.includes(cleanTypeName)) { + return true; + } + } + + return false; + } + A_ArrayExpr(node: t.A_ArrayExpr, context: DeparserContext): string { const elements = ListUtils.unwrapList(node.elements); const elementStrs = elements.map(el => this.visit(el, context)); @@ -2030,30 +2191,32 @@ export class Deparser implements DeparserVisitor { const arg = this.visit(node.arg, context); const typeName = this.TypeName(node.typeName, context); - // Check if this is a bpchar typecast that should use traditional char syntax - if (typeName === 'bpchar' && node.typeName && node.typeName.names) { - const names = ListUtils.unwrapList(node.typeName.names); - if (names.length === 2 && - names[0].String?.sval === 'pg_catalog' && - names[1].String?.sval === 'bpchar') { - return `char ${arg}`; + // Check if this is a bpchar typecast that should preserve original syntax for AST consistency + if (typeName === 'bpchar' || typeName === 'pg_catalog.bpchar') { + const names = node.typeName?.names; + const isQualifiedBpchar = names && names.length === 2 && + (names[0] as any)?.String?.sval === 'pg_catalog' && + (names[1] as any)?.String?.sval === 'bpchar'; + + if (isQualifiedBpchar) { + return `CAST(${arg} AS ${typeName})`; } } - // Check if the argument is a complex expression that should preserve CAST syntax - const argType = this.getNodeType(node.arg); - const isComplexExpression = argType === 'A_Expr' || argType === 'FuncCall' || argType === 'OpExpr'; - - if (!isComplexExpression && (typeName.startsWith('interval') || - typeName.startsWith('char') || - typeName === '"char"' || - typeName.startsWith('bpchar') || - typeName === 'bytea' || - typeName === 'orderedarray' || - typeName === 'date')) { - // Remove pg_catalog prefix for :: syntax - const cleanTypeName = typeName.replace('pg_catalog.', ''); - return `${arg}::${cleanTypeName}`; + if (this.isPgCatalogType(typeName)) { + const argType = this.getNodeType(node.arg); + + const isSimpleArgument = argType === 'A_Const' || argType === 'ColumnRef'; + const isFunctionCall = argType === 'FuncCall'; + + if (isSimpleArgument || isFunctionCall) { + // For simple arguments, avoid :: syntax if they have complex structure + if (isSimpleArgument && (arg.includes('(') || arg.startsWith('-'))) { + } else { + const cleanTypeName = typeName.replace('pg_catalog.', ''); + return `${arg}::${cleanTypeName}`; + } + } } return `CAST(${arg} AS ${typeName})`; @@ -2276,14 +2439,19 @@ export class Deparser implements DeparserVisitor { }); if (this.formatter.isPretty()) { - const formattedElements = elementStrs.map(el => - this.formatter.indent(el) - ).join(',' + this.formatter.newline()); + const formattedElements = elementStrs.map(el => { + const trimmedEl = el.trim(); + // Remove leading newlines from constraint elements to avoid extra blank lines + if (trimmedEl.startsWith('\n')) { + return this.formatter.indent(trimmedEl.substring(1)); + } + return this.formatter.indent(trimmedEl); + }).join(',' + this.formatter.newline()); output.push('(' + this.formatter.newline() + formattedElements + this.formatter.newline() + ')'); } else { output.push(this.formatter.parens(elementStrs.join(', '))); } - } else if (!node.partbound) { + }else if (!node.partbound) { output.push(this.formatter.parens('')); } @@ -2454,9 +2622,22 @@ export class Deparser implements DeparserVisitor { } break; case 'CONSTR_CHECK': - output.push('CHECK'); + if (this.formatter.isPretty() && !context.isColumnConstraint) { + output.push('\n' + this.formatter.indent('CHECK')); + } else { + output.push('CHECK'); + } if (node.raw_expr) { - output.push(this.formatter.parens(this.visit(node.raw_expr, context))); + if (this.formatter.isPretty()) { + const checkExpr = this.visit(node.raw_expr, context); + if (checkExpr.includes('\n')) { + output.push('(\n' + this.formatter.indent(checkExpr) + '\n)'); + } else { + output.push(`(${checkExpr})`); + } + } else { + output.push(this.formatter.parens(this.visit(node.raw_expr, context))); + } } // Handle NOT VALID for check constraints if (node.skip_validation) { @@ -2528,7 +2709,11 @@ export class Deparser implements DeparserVisitor { } break; case 'CONSTR_UNIQUE': - output.push('UNIQUE'); + if (this.formatter.isPretty() && !context.isColumnConstraint) { + output.push('\n' + this.formatter.indent('UNIQUE')); + } else { + output.push('UNIQUE'); + } if (node.nulls_not_distinct) { output.push('NULLS NOT DISTINCT'); } @@ -2546,33 +2731,70 @@ export class Deparser implements DeparserVisitor { case 'CONSTR_FOREIGN': // Only add "FOREIGN KEY" for table-level constraints, not column-level constraints if (!context.isColumnConstraint) { - output.push('FOREIGN KEY'); - if (node.fk_attrs && node.fk_attrs.length > 0) { - const fkAttrs = ListUtils.unwrapList(node.fk_attrs) - .map(attr => this.visit(attr, context)) - .join(', '); - output.push(`(${fkAttrs})`); + if (this.formatter.isPretty()) { + output.push('\n' + this.formatter.indent('FOREIGN KEY')); + if (node.fk_attrs && node.fk_attrs.length > 0) { + const fkAttrs = ListUtils.unwrapList(node.fk_attrs) + .map(attr => this.visit(attr, context)) + .join(', '); + output.push(`(${fkAttrs})`); + } + output.push('\n' + this.formatter.indent('REFERENCES')); + } else { + output.push('FOREIGN KEY'); + if (node.fk_attrs && node.fk_attrs.length > 0) { + const fkAttrs = ListUtils.unwrapList(node.fk_attrs) + .map(attr => this.visit(attr, context)) + .join(', '); + output.push(`(${fkAttrs})`); + } + output.push('REFERENCES'); } + } else { + output.push('REFERENCES'); } - output.push('REFERENCES'); if (node.pktable) { - output.push(this.RangeVar(node.pktable, context)); + if (this.formatter.isPretty() && !context.isColumnConstraint) { + const lastIndex = output.length - 1; + if (lastIndex >= 0 && output[lastIndex].includes('REFERENCES')) { + output[lastIndex] += ' ' + this.RangeVar(node.pktable, context); + } else { + output.push(this.RangeVar(node.pktable, context)); + } + } else { + output.push(this.RangeVar(node.pktable, context)); + } } if (node.pk_attrs && node.pk_attrs.length > 0) { const pkAttrs = ListUtils.unwrapList(node.pk_attrs) .map(attr => this.visit(attr, context)) .join(', '); - output.push(`(${pkAttrs})`); + if (this.formatter.isPretty() && !context.isColumnConstraint) { + const lastIndex = output.length - 1; + if (lastIndex >= 0) { + output[lastIndex] += ` (${pkAttrs})`; + } else { + output.push(`(${pkAttrs})`); + } + } else { + output.push(`(${pkAttrs})`); + } } if (node.fk_matchtype && node.fk_matchtype !== 's') { + let matchClause = ''; switch (node.fk_matchtype) { case 'f': - output.push('MATCH FULL'); + matchClause = 'MATCH FULL'; break; case 'p': - output.push('MATCH PARTIAL'); + matchClause = 'MATCH PARTIAL'; break; } + if (this.formatter.isPretty() && !context.isColumnConstraint) { + output.push('\n' + this.formatter.indent(matchClause)); + } else { + output.push(matchClause); + } } if (node.fk_upd_action && node.fk_upd_action !== 'a') { let updateClause = 'ON UPDATE '; @@ -2622,7 +2844,11 @@ export class Deparser implements DeparserVisitor { } // Handle NOT VALID for foreign key constraints - only for table constraints, not domain constraints if (node.skip_validation && !context.isDomainConstraint) { - output.push('NOT VALID'); + if (this.formatter.isPretty() && !context.isColumnConstraint) { + output.push('\n' + this.formatter.indent('NOT VALID')); + } else { + output.push('NOT VALID'); + } } break; case 'CONSTR_ATTR_DEFERRABLE': @@ -3718,8 +3944,8 @@ export class Deparser implements DeparserVisitor { } else if (nodeData.sval !== undefined) { // Handle nested sval structure: { sval: { sval: "value" } } const svalValue = typeof nodeData.sval === 'object' ? nodeData.sval.sval : nodeData.sval; - const stringValue = svalValue.replace(/'/g, '').toLowerCase(); - boolValue = stringValue === 'on' || stringValue === 'true'; + const stringValue = svalValue.replace(/'/g, ''); + boolValue = stringValue.toLowerCase() === 'on' || stringValue.toLowerCase() === 'true'; } } return boolValue ? 'READ ONLY' : 'READ WRITE'; @@ -3739,8 +3965,8 @@ export class Deparser implements DeparserVisitor { } else if (nodeData.sval !== undefined) { // Handle nested sval structure: { sval: { sval: "value" } } const svalValue = typeof nodeData.sval === 'object' ? nodeData.sval.sval : nodeData.sval; - const stringValue = svalValue.replace(/'/g, '').toLowerCase(); - boolValue = stringValue === 'on' || stringValue === 'true'; + const stringValue = svalValue.replace(/'/g, ''); + boolValue = stringValue.toLowerCase() === 'on' || stringValue.toLowerCase() === 'true'; } } return boolValue ? 'DEFERRABLE' : 'NOT DEFERRABLE'; @@ -3809,8 +4035,8 @@ export class Deparser implements DeparserVisitor { } else if (nodeData.sval !== undefined) { // Handle nested sval structure: { sval: { sval: "value" } } const svalValue = typeof nodeData.sval === 'object' ? nodeData.sval.sval : nodeData.sval; - const stringValue = svalValue.replace(/'/g, '').toLowerCase(); - boolValue = stringValue === 'on' || stringValue === 'true'; + const stringValue = svalValue.replace(/'/g, ''); + boolValue = stringValue.toLowerCase() === 'on' || stringValue.toLowerCase() === 'true'; } } transactionOptions.push(boolValue ? 'READ ONLY' : 'READ WRITE'); @@ -3826,8 +4052,8 @@ export class Deparser implements DeparserVisitor { } else if (nodeData.sval !== undefined) { // Handle nested sval structure: { sval: { sval: "value" } } const svalValue = typeof nodeData.sval === 'object' ? nodeData.sval.sval : nodeData.sval; - const stringValue = svalValue.replace(/'/g, '').toLowerCase(); - boolValue = stringValue === 'on' || stringValue === 'true'; + const stringValue = svalValue.replace(/'/g, ''); + boolValue = stringValue.toLowerCase() === 'on' || stringValue.toLowerCase() === 'true'; } } transactionOptions.push(boolValue ? 'DEFERRABLE' : 'NOT DEFERRABLE'); @@ -3900,7 +4126,7 @@ export class Deparser implements DeparserVisitor { switch (node.roletype) { case 'ROLESPEC_PUBLIC': - return 'public'; + return 'PUBLIC'; case 'ROLESPEC_CURRENT_USER': return 'CURRENT_USER'; case 'ROLESPEC_SESSION_USER': @@ -3908,7 +4134,7 @@ export class Deparser implements DeparserVisitor { case 'ROLESPEC_CURRENT_ROLE': return 'CURRENT_ROLE'; default: - return 'public'; + return 'PUBLIC'; } } @@ -4408,10 +4634,24 @@ export class Deparser implements DeparserVisitor { } if (node.cmds && node.cmds.length > 0) { - const commandsStr = ListUtils.unwrapList(node.cmds) - .map(cmd => this.visit(cmd, alterContext)) - .join(', '); - output.push(commandsStr); + const commands = ListUtils.unwrapList(node.cmds); + if (this.formatter.isPretty()) { + const commandsStr = commands + .map(cmd => { + const cmdStr = this.visit(cmd, alterContext); + if (cmdStr.startsWith('ADD CONSTRAINT') || cmdStr.startsWith('ADD ')) { + return this.formatter.newline() + this.formatter.indent(cmdStr); + } + return cmdStr; + }) + .join(','); + output.push(commandsStr); + } else { + const commandsStr = commands + .map(cmd => this.visit(cmd, alterContext)) + .join(', '); + output.push(commandsStr); + } } return output.join(' '); @@ -6516,7 +6756,7 @@ export class Deparser implements DeparserVisitor { const initialParts = ['CREATE', 'POLICY']; if (node.policy_name) { - initialParts.push(`"${node.policy_name}"`); + initialParts.push(QuoteUtils.quote(node.policy_name)); } output.push(initialParts.join(' ')); @@ -6595,7 +6835,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'POLICY']; if (node.policy_name) { - output.push(`"${node.policy_name}"`); + output.push(QuoteUtils.quote(node.policy_name)); } if (node.table) { @@ -8290,7 +8530,11 @@ export class Deparser implements DeparserVisitor { if (node.action) { const actionStr = this.GrantStmt(node.action, context); - output.push(actionStr); + if (this.formatter.isPretty()) { + return output.join(' ') + this.formatter.newline() + this.formatter.indent(actionStr); + } else { + output.push(actionStr); + } } return output.join(' '); @@ -8474,87 +8718,165 @@ export class Deparser implements DeparserVisitor { output.push(QuoteUtils.quote(node.trigname)); } - const timing: string[] = []; - if (node.timing & 2) timing.push('BEFORE'); - else if (node.timing & 64) timing.push('INSTEAD OF'); - else timing.push('AFTER'); // Default timing when no specific timing is set - output.push(timing.join(' ')); - - const events: string[] = []; - if (node.events & 4) events.push('INSERT'); - if (node.events & 8) events.push('DELETE'); - if (node.events & 16) events.push('UPDATE'); - if (node.events & 32) events.push('TRUNCATE'); - output.push(events.join(' OR ')); + if (this.formatter.isPretty()) { + const components: string[] = []; + + const timing: string[] = []; + if (node.timing & 2) timing.push('BEFORE'); + else if (node.timing & 64) timing.push('INSTEAD OF'); + else timing.push('AFTER'); + + const events: string[] = []; + if (node.events & 4) events.push('INSERT'); + if (node.events & 8) events.push('DELETE'); + if (node.events & 16) { + let updateStr = 'UPDATE'; + if (node.columns && node.columns.length > 0) { + const columnNames = ListUtils.unwrapList(node.columns) + .map(col => this.visit(col, context)) + .join(', '); + updateStr += ' OF ' + columnNames; + } + events.push(updateStr); + } + if (node.events & 32) events.push('TRUNCATE'); + + components.push(this.formatter.indent(timing.join(' ') + ' ' + events.join(' OR '))); + + if (node.relation) { + components.push(this.formatter.indent('ON ' + this.RangeVar(node.relation, context))); + } + + if (node.transitionRels && node.transitionRels.length > 0) { + const transitionClauses = ListUtils.unwrapList(node.transitionRels) + .map(rel => this.visit(rel, context)) + .join(' '); + components.push(this.formatter.indent('REFERENCING ' + transitionClauses)); + } + + if (node.deferrable) { + components.push(this.formatter.indent('DEFERRABLE')); + } + + if (node.initdeferred) { + components.push(this.formatter.indent('INITIALLY DEFERRED')); + } + + if (node.row) { + components.push(this.formatter.indent('FOR EACH ROW')); + } else { + components.push(this.formatter.indent('FOR EACH STATEMENT')); + } + + if (node.whenClause) { + const whenStr = 'WHEN (' + this.visit(node.whenClause, context) + ')'; + components.push(this.formatter.indent(whenStr)); + } + + let executeStr = 'EXECUTE'; + if (node.funcname && node.funcname.length > 0) { + const funcName = ListUtils.unwrapList(node.funcname) + .map(name => this.visit(name, context)) + .join('.'); + executeStr += ' PROCEDURE ' + funcName; + } + + if (node.args && node.args.length > 0) { + const argContext = { ...context, isStringLiteral: true }; + const args = ListUtils.unwrapList(node.args) + .map(arg => this.visit(arg, argContext)) + .join(', '); + executeStr += '(' + args + ')'; + } else { + executeStr += '()'; + } + + components.push(this.formatter.indent(executeStr)); + + return output.join(' ') + this.formatter.newline() + components.join(this.formatter.newline()); + } else { + const timing: string[] = []; + if (node.timing & 2) timing.push('BEFORE'); + else if (node.timing & 64) timing.push('INSTEAD OF'); + else timing.push('AFTER'); + output.push(timing.join(' ')); + + const events: string[] = []; + if (node.events & 4) events.push('INSERT'); + if (node.events & 8) events.push('DELETE'); + if (node.events & 16) events.push('UPDATE'); + if (node.events & 32) events.push('TRUNCATE'); + output.push(events.join(' OR ')); + + if (node.columns && node.columns.length > 0) { + output.push('OF'); + const columnNames = ListUtils.unwrapList(node.columns) + .map(col => this.visit(col, context)) + .join(', '); + output.push(columnNames); + } - if (node.columns && node.columns.length > 0) { - output.push('OF'); - const columnNames = ListUtils.unwrapList(node.columns) - .map(col => this.visit(col, context)) - .join(', '); - output.push(columnNames); - } + output.push('ON'); + if (node.relation) { + output.push(this.RangeVar(node.relation, context)); + } - output.push('ON'); - if (node.relation) { - output.push(this.RangeVar(node.relation, context)); - } + if (node.constrrel) { + output.push('FROM'); + output.push(this.RangeVar(node.constrrel, context)); + } - if (node.constrrel) { - output.push('FROM'); - output.push(this.RangeVar(node.constrrel, context)); - } + if (node.deferrable) { + output.push('DEFERRABLE'); + } - if (node.deferrable) { - output.push('DEFERRABLE'); - } + if (node.initdeferred) { + output.push('INITIALLY DEFERRED'); + } - if (node.initdeferred) { - output.push('INITIALLY DEFERRED'); - } + if (node.transitionRels && node.transitionRels.length > 0) { + output.push('REFERENCING'); + const transitionClauses = ListUtils.unwrapList(node.transitionRels) + .map(rel => this.visit(rel, context)) + .join(' '); + output.push(transitionClauses); + } - // Handle REFERENCING clauses - if (node.transitionRels && node.transitionRels.length > 0) { - output.push('REFERENCING'); - const transitionClauses = ListUtils.unwrapList(node.transitionRels) - .map(rel => this.visit(rel, context)) - .join(' '); - output.push(transitionClauses); - } + if (node.row) { + output.push('FOR EACH ROW'); + } else { + output.push('FOR EACH STATEMENT'); + } - if (node.row) { - output.push('FOR EACH ROW'); - } else { - output.push('FOR EACH STATEMENT'); - } + if (node.whenClause) { + output.push('WHEN'); + output.push('('); + output.push(this.visit(node.whenClause, context)); + output.push(')'); + } - if (node.whenClause) { - output.push('WHEN'); - output.push('('); - output.push(this.visit(node.whenClause, context)); - output.push(')'); - } + output.push('EXECUTE'); + if (node.funcname && node.funcname.length > 0) { + const funcName = ListUtils.unwrapList(node.funcname) + .map(name => this.visit(name, context)) + .join('.'); + output.push('FUNCTION', funcName); + } - output.push('EXECUTE'); - if (node.funcname && node.funcname.length > 0) { - const funcName = ListUtils.unwrapList(node.funcname) - .map(name => this.visit(name, context)) - .join('.'); - output.push('FUNCTION', funcName); - } + if (node.args && node.args.length > 0) { + output.push('('); + const argContext = { ...context, isStringLiteral: true }; + const args = ListUtils.unwrapList(node.args) + .map(arg => this.visit(arg, argContext)) + .join(', '); + output.push(args); + output.push(')'); + } else { + output.push('()'); + } - if (node.args && node.args.length > 0) { - output.push('('); - const args = ListUtils.unwrapList(node.args) - .map(arg => this.visit(arg, context)) - .join(', '); - output.push(args); - output.push(')'); - } else { - output.push('()'); + return output.join(' '); } - - return output.join(' '); } TriggerTransition(node: t.TriggerTransition, context: DeparserContext): string { @@ -9248,7 +9570,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = []; if (node.priv_name) { - output.push(node.priv_name); + output.push(node.priv_name.toUpperCase()); } else { output.push('ALL'); } diff --git a/packages/deparser/src/visitors/base.ts b/packages/deparser/src/visitors/base.ts index c5650824..55e8cf6e 100644 --- a/packages/deparser/src/visitors/base.ts +++ b/packages/deparser/src/visitors/base.ts @@ -3,6 +3,7 @@ import { Node } from '@pgsql/types'; export interface DeparserContext { isStringLiteral?: boolean; parentNodeTypes: string[]; + indentLevel?: number; [key: string]: any; } diff --git a/packages/deparser/test-utils/PrettyTest.ts b/packages/deparser/test-utils/PrettyTest.ts new file mode 100644 index 00000000..b041e00b --- /dev/null +++ b/packages/deparser/test-utils/PrettyTest.ts @@ -0,0 +1,31 @@ +import { expectParseDeparse } from './index'; + +const generateCoded = require('../../../__fixtures__/generated/generated.json'); + +export class PrettyTest { + private testCases: string[]; + + constructor(testCases: string[]) { + this.testCases = testCases; + } + + /** + * Generate individual tests for each test case with both pretty and non-pretty formatting + */ + generateTests(): void { + this.testCases.forEach((testName, index) => { + const sql = generateCoded[testName]; + + it(`pretty: ${testName}`, async () => { + const result = await expectParseDeparse(sql, { pretty: true }); + expect(result).toMatchSnapshot(); + }); + + it(`non-pretty: ${testName}`, async () => { + const result = await expectParseDeparse(sql, { pretty: false }); + expect(result).toMatchSnapshot(); + }); + }); + } + +} \ No newline at end of file diff --git a/packages/parser/package.json b/packages/parser/package.json index 21e2d6c0..42d1a23a 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -27,7 +27,8 @@ "build:dev": "npm run clean && tsc --declarationMap && tsc -p tsconfig.esm.json && npm run copy", "lint": "eslint . --fix", "test": "jest", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "test:ast": "ts-node scripts/test-ast.ts" }, "keywords": [ "sql", diff --git a/packages/parser/scripts/.gitignore b/packages/parser/scripts/.gitignore new file mode 100644 index 00000000..74cebca6 --- /dev/null +++ b/packages/parser/scripts/.gitignore @@ -0,0 +1,2 @@ +input.sql +output.json diff --git a/packages/parser/scripts/test-ast.ts b/packages/parser/scripts/test-ast.ts new file mode 100644 index 00000000..dd39e2ef --- /dev/null +++ b/packages/parser/scripts/test-ast.ts @@ -0,0 +1,26 @@ +import { parse } from '../src/'; +import { cleanTree } from '../src/utils'; +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +async function testAst() { + try { + // Read SQL from input.sql in the same directory + const inputPath = join(__dirname, 'input.sql'); + const sql = readFileSync(inputPath, 'utf8'); + + // Parse the SQL + const ast = await parse(sql); + const cleanedAst = cleanTree(ast); + + // Write JSON to output.json in the same directory + const outputPath = join(__dirname, 'output.json'); + writeFileSync(outputPath, JSON.stringify(cleanedAst, null, 2)); + + console.log(`Successfully parsed SQL from ${inputPath} and wrote AST to ${outputPath}`); + } catch (error) { + console.error('Error processing SQL:', error); + } +} + +testAst(); \ No newline at end of file