diff --git a/src/syntax/comment.ts b/src/syntax/comment.ts index 0aebeb8..e29b36f 100644 --- a/src/syntax/comment.ts +++ b/src/syntax/comment.ts @@ -1,12 +1,15 @@ import { AllCommentNodes } from "sql-parser-cst"; -import { group, indent, line } from "../print_utils"; +import { group, line } from "../print_utils"; import { CstToDocMap } from "../CstToDocMap"; export const commentMap: CstToDocMap = { comment_stmt: (print) => group([ - print.spaced(["commentKw", "onKw", "target", "isKw"]), - indent([line, print("message")]), + print.spaced(["commentKw", "onKw"]), + line, + print("target"), + line, + print.spaced(["isKw", "message"]), ]), comment_target_aggregate: (print) => diff --git a/src/syntax/expr.ts b/src/syntax/expr.ts index 04b919e..3ff048f 100644 --- a/src/syntax/expr.ts +++ b/src/syntax/expr.ts @@ -83,6 +83,13 @@ export const exprMap: CstToDocMap = { ) { return print("expr"); } + if ( + isEmptyParenContent(node.expr) && + !hasComments(node) && + !hasComments(node.expr) + ) { + return ["(", print("expr"), ")"]; + } const lineStyle = isCreateTableStmt(parent) && print.dynamicLine() === hardline ? hardline @@ -101,7 +108,10 @@ export const exprMap: CstToDocMap = { // Some operators are better formatted without spaces around them return print(["left", "operator", "right"]); } - return print.spaced(["left", "operator", "right"]); + return group([ + print("left"), + group([" ", print.spaced("operator"), indent([line, print("right")])]), + ]); }, prefix_op_expr: (print, node) => (isString(node.operator) ? print : print.spaced)(["operator", "expr"]), @@ -117,11 +127,19 @@ export const exprMap: CstToDocMap = { case_when: (print, node) => { if (isProgram(node.result)) { return [ - print.spaced(["whenKw", "condition", "thenKw"]), + group([ + group([print("whenKw"), indent([line, print("condition")])]), + line, + print("thenKw"), + ]), indent([hardline, stripTrailingHardline(print("result"))]), ]; } - return print.spaced(["whenKw", "condition", "thenKw", "result"]); + return group([ + group([print("whenKw"), indent([line, print("condition")])]), + line, + group([print("thenKw"), indent([line, print("result")])]), + ]); }, case_else: (print, node) => { if (isProgram(node.result)) { @@ -317,3 +335,23 @@ const isFunctionContext = ( const isBooleanOp = ({ name }: Keyword) => name === "AND" || name === "OR"; const isCompactOp = (op: string) => op === "->" || op === "->>"; + +const isEmptyParenContent = (expr: Node): boolean => { + if (isFuncArgs(expr)) { + return ( + expr.args.items.length === 0 && + !expr.distinctKw && + !expr.nullHandlingKw && + !expr.orderBy && + !expr.limit && + !expr.having + ); + } + if (isListExpr(expr)) { + return expr.items.length === 0; + } + return false; +}; + +const hasComments = (node: Node): boolean => + Boolean((node as Node & { comments?: unknown[] }).comments?.length); diff --git a/src/syntax/index.ts b/src/syntax/index.ts index e77eb7d..4af7264 100644 --- a/src/syntax/index.ts +++ b/src/syntax/index.ts @@ -1,24 +1,30 @@ import { AllIndexNodes } from "sql-parser-cst"; -import { group, join } from "../print_utils"; +import { group, join, line } from "../print_utils"; import { CstToDocMap } from "../CstToDocMap"; export const indexMap: CstToDocMap = { - create_index_stmt: (print) => + create_index_stmt: (print, node) => group( join(print.dynamicLine(), [ - print.spaced([ - "createKw", - "orReplaceKw", - "indexTypeKw", - "indexKw", - "concurrentlyKw", - "ifNotExistsKw", - "name", - "onKw", - "table", - "using", - "columns", - ]), + group( + join(line, [ + print.spaced([ + "createKw", + "orReplaceKw", + "indexTypeKw", + "indexKw", + "concurrentlyKw", + "ifNotExistsKw", + "name", + ]), + print.spaced([ + "onKw", + "table", + ...(node.using ? [] : (["columns"] as const)), + ]), + ...(node.using ? [print.spaced(["using", "columns"])] : []), + ]), + ), ...print("clauses"), ]), ), diff --git a/test/ddl/create_table.test.ts b/test/ddl/create_table.test.ts index 9c2559c..a33b562 100644 --- a/test/ddl/create_table.test.ts +++ b/test/ddl/create_table.test.ts @@ -395,7 +395,8 @@ describe("create table", () => { OPTIONS ( expiration_timestamp = TIMESTAMP "2025-01-01 00:00:00 UTC", partition_expiration_days = 1, - description = "a table that expires in 2025, with each partition living for 24 hours", + description = + "a table that expires in 2025, with each partition living for 24 hours", labels = [("org_unit", "development")] ) `); diff --git a/test/ddl/index.test.ts b/test/ddl/index.test.ts index 9291103..2710a5b 100644 --- a/test/ddl/index.test.ts +++ b/test/ddl/index.test.ts @@ -42,19 +42,49 @@ describe("index", () => { `); }); + it(`breaks long CREATE INDEX across CREATE, ON, and USING`, async () => { + await testPostgresql( + dedent` + CREATE INDEX my_index + ON my_table + USING btree (col) + `, + { printWidth: 50 }, + ); + }); + + it(`breaks long CREATE UNIQUE INDEX across CREATE, ON, and USING`, async () => { + await testPostgresql( + dedent` + CREATE UNIQUE INDEX my_index + ON my_table + USING btree ( + col_one, + col_two + ) + `, + { printWidth: 30 }, + ); + }); + it(`formats long columns list on multiple lines`, async () => { - await test(dedent` - CREATE UNIQUE INDEX IF NOT EXISTS my_index ON my_table ( - column_name_one, - column_name_two, - column_name_three - ) - `); + await test( + dedent` + CREATE UNIQUE INDEX IF NOT EXISTS my_index + ON my_table ( + col_one, + col_two, + col_three + ) + `, + { printWidth: 40 }, + ); }); it(`formats column list with various index parameters`, async () => { await testPostgresql(dedent` - CREATE INDEX my_index ON my_table ( + CREATE INDEX my_index + ON my_table ( column_name_one COLLATE "C" ASC NULLS FIRST, column_name_two DESC NULLS LAST, (col3 + col4) my_opclass (foo = 'bar', baz = 'qux') ASC diff --git a/test/expr/expr.test.ts b/test/expr/expr.test.ts index 68552c9..a4083cc 100644 --- a/test/expr/expr.test.ts +++ b/test/expr/expr.test.ts @@ -20,6 +20,27 @@ describe("expr", () => { `); }); + it(`keeps short binary expressions on one line`, async () => { + await test(dedent` + SELECT * + FROM foo + WHERE bar = short_func() + `); + }); + + it(`breaks long binary expressions into multiple lines with indentation`, async () => { + await test( + dedent` + SELECT * + FROM foo + WHERE + bar = + my_func() + `, + { printWidth: 15 }, + ); + }); + it(`formats IN expressions`, async () => { await test(`SELECT col1 IN (1, 2, 3), col2 NOT IN (4, 5, 6)`); }); @@ -171,6 +192,65 @@ describe("expr", () => { END `); }); + + it(`breaks long WHEN/THEN into separate lines`, async () => { + await test( + dedent` + SELECT + CASE + WHEN column_name = 1 + THEN result_name + END + `, + { printWidth: 40 }, + ); + }); + + it(`breaks multiple long WHEN/THEN clauses without blank lines between them`, async () => { + await test( + dedent` + SELECT + CASE + WHEN column_name = 1 + THEN result_name + WHEN column_name = 2 + THEN other_result + ELSE foo + END + `, + { printWidth: 40 }, + ); + }); + + it(`indents multi-condition WHEN clauses and keeps ORs parenthesized`, async () => { + await test( + dedent` + SELECT + CASE + WHEN + column_name = 1 + AND (other_name = 2 OR other_name = 3) + THEN result_name + END + `, + { printWidth: 50 }, + ); + }); + + it(`indents multi-expression THEN clauses and keeps ORs parenthesized`, async () => { + await test( + dedent` + SELECT + CASE + WHEN column_name = 1 + THEN + result_name = 1 + AND (other_name = 2 OR other_name = 3) + END + `, + { printWidth: 45 }, + ); + }); }); it(`formats quantifier expressions`, async () => { diff --git a/test/expr/func.test.ts b/test/expr/func.test.ts index 5b0af3e..19d666e 100644 --- a/test/expr/func.test.ts +++ b/test/expr/func.test.ts @@ -23,6 +23,66 @@ describe("functions", () => { `); }); + it(`keeps empty function args on one line`, async () => { + expect(await pretty(`SELECT my_func()`, { printWidth: 10 })).toBe(dedent` + SELECT + my_func() + `); + }); + + it(`does not treat count(DISTINCT) as empty function args`, async () => { + expect(await pretty(`SELECT count(DISTINCT id)`, { printWidth: 25 })) + .toBe(dedent` + SELECT + count(DISTINCT id) + `); + }); + + it(`keeps empty CREATE FUNCTION params on one line`, async () => { + expect( + await pretty( + `CREATE FUNCTION my_func() AS (SELECT 1)`, + { printWidth: 10, dialect: "bigquery" }, + ), + ).toBe(dedent` + CREATE FUNCTION my_func() AS + ( + SELECT + 1 + ) + `); + }); + + it(`preserves block comments inside empty function args`, async () => { + expect( + await pretty(`SELECT my_func(/* comment */)`, { printWidth: 25 }), + ).toBe(dedent` + SELECT + my_func( + /* comment */ + ) + `); + }); + + it(`preserves line comments inside empty function args`, async () => { + expect( + await pretty( + dedent` + SELECT my_func( + -- comment + ) + `, + { printWidth: 25 }, + ), + ).toBe(dedent` + SELECT + my_func( + -- comment + + ) + `); + }); + it(`formats named function arguments`, async () => { await testBigquery( `SELECT concat_lower_or_upper(a => 'Hello', b => 'World', uppercase => TRUE)`, diff --git a/test/postgresql/comment.test.ts b/test/postgresql/comment.test.ts index f038035..23a0076 100644 --- a/test/postgresql/comment.test.ts +++ b/test/postgresql/comment.test.ts @@ -10,30 +10,44 @@ describe("comment", () => { it(`formats long COMMENT ON`, async () => { await testPostgresql(dedent` - COMMENT ON CONSTRAINT constraint_name ON DOMAIN domain_name IS - 'This is a really nice comment here.' + COMMENT ON + CONSTRAINT constraint_name ON DOMAIN domain_name + IS 'This is a really nice comment here.' `); }); it(`formats multi-line comment`, async () => { await testPostgresql(dedent` - COMMENT ON TABLE foo IS - 'This is a multi-line comment, - that spans several lines. - In here.' + COMMENT ON + TABLE foo + IS 'This is a multi-line comment, + that spans several lines. + In here.' `); }); it(`formats long comment target`, async () => { await testPostgresql(dedent` - COMMENT ON FUNCTION my_absolutely_fantastic_function( + COMMENT ON + FUNCTION my_absolutely_fantastic_function( IN whoopsie CHARACTER VARYING, OUT doopsie TEXT - ) IS - 'This is a really nice comment here.' + ) + IS 'This is a really nice comment here.' `); }); + it(`formats long column name COMMENT ON`, async () => { + await testPostgresql( + dedent` + COMMENT ON + COLUMN column_name + IS E'foo' + `, + { printWidth: 35 }, + ); + }); + [ "ACCESS METHOD foo", "AGGREGATE foo(bar INT)", diff --git a/test/proc/set.test.ts b/test/proc/set.test.ts index b2e4770..9b1283d 100644 --- a/test/proc/set.test.ts +++ b/test/proc/set.test.ts @@ -16,10 +16,8 @@ describe("set", () => { it(`formats long SET expressions`, async () => { await testBigquery(dedent` - SET (first_variable, second_variable) = ( - FORMAT('%d', word_count), - FORMAT('%d', line_count) - ) + SET (first_variable, second_variable) = + (FORMAT('%d', word_count), FORMAT('%d', line_count)) `); }); diff --git a/test/select/select.test.ts b/test/select/select.test.ts index 7c1314b..f00858e 100644 --- a/test/select/select.test.ts +++ b/test/select/select.test.ts @@ -58,12 +58,14 @@ describe("select", () => { FROM my_super_long_table_name WHERE - my_table_name.x > my_table_name.y + my_table_name.x > + my_table_name.y GROUP BY long_col, even_longer_col HAVING - foo > some_long_col_name + foo > + some_long_col_name ORDER BY foo ASC, bar DESC NULLS FIRST