From 653f62c13cc9e1556301e3d594d97a12c02f1e43 Mon Sep 17 00:00:00 2001 From: Swayam Date: Tue, 13 Jan 2026 15:37:51 -0800 Subject: [PATCH 1/8] feat: Ability to control newline placement around comma when seperating multiple columns/tables Add option that can control if the newline is present after the comma or before the comma when listing multiple columns/tables --- test/options/commaNewline.ts | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/options/commaNewline.ts diff --git a/test/options/commaNewline.ts b/test/options/commaNewline.ts new file mode 100644 index 000000000..8e5db579e --- /dev/null +++ b/test/options/commaNewline.ts @@ -0,0 +1,37 @@ +import dedent from 'dedent-js'; + +import { FormatFn } from '../../src/sqlFormatter.js'; + +export default function supportsCommaNewline(format: FormatFn) { + it('by default adds newline after comma', () => { + const result = format(`SELECT id, first_name, last_name, email FROM users;`); + expect(result).toBe( + dedent` + SELECT + id, + first_name, + last_name, + email + FROM + users; + ` + ); + }); + + it('supports newline before comma', () => { + const result = format(`SELECT id, first_name, last_name, email FROM users;`, { + commaNewline: 'before', + }); + expect(result).toBe( + dedent` + SELECT + id + , first_name + , last_name + , email + FROM + users; + ` + ); + }); +} From 31609bb13a2ef72bd2affcc2d0258a1911e8ca63 Mon Sep 17 00:00:00 2001 From: Swayam Date: Tue, 13 Jan 2026 15:42:20 -0800 Subject: [PATCH 2/8] feat: Ability to control newline placement around comma when seperating multiple columns/tables Add option that can control if the newline is present after the comma or before the comma when listing multiple columns/tables --- README.md | 1 + docs/commaNewline.md | 38 ++++++++++++++++++++++++++++ src/FormatOptions.ts | 2 ++ src/formatter/ExpressionFormatter.ts | 6 ++++- src/sqlFormatter.ts | 1 + test/behavesLikeSqlFormatter.ts | 2 ++ 6 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 docs/commaNewline.md diff --git a/README.md b/README.md index 27306c955..96f311d53 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ All fields are optional and all fields that are not specified will be filled wit - [**`identifierCase`**](docs/identifierCase.md) uppercases or lowercases identifiers. (**experimental!**) - [**`indentStyle`**](docs/indentStyle.md) defines overall indentation style. (**deprecated!**) - [**`logicalOperatorNewline`**](docs/logicalOperatorNewline.md) newline before or after boolean operator (AND, OR, XOR). +- [**`commaNewline`**](docs/commaNewline.md) newline before or after comma seperating multiple columns/tables. - [**`expressionWidth`**](docs/expressionWidth.md) maximum number of characters in parenthesized expressions to be kept on single line. - [**`linesBetweenQueries`**](docs/linesBetweenQueries.md) how many newlines to insert between queries. - [**`denseOperators`**](docs/denseOperators.md) packs operators densely without spaces. diff --git a/docs/commaNewline.md b/docs/commaNewline.md new file mode 100644 index 000000000..163045ab5 --- /dev/null +++ b/docs/commaNewline.md @@ -0,0 +1,38 @@ +# commaNewline + +Decides newline placement before or after commas seperating columns/tables. + +## Options + +- `"after"` (default) adds newline after the comma. +- `"before"` adds newline before the comma with a following space. + +### after + +```sql +SELECT + name, + age, + height +FROM + persons +WHERE + age > 10 + AND height < 150 + OR occupation IS NULL +``` + +### before + +```sql +SELECT + name + , age + , height +FROM + persons +WHERE + age > 10 AND + height < 150 OR + occupation IS NULL +``` diff --git a/src/FormatOptions.ts b/src/FormatOptions.ts index 4af41184b..16121ebb8 100644 --- a/src/FormatOptions.ts +++ b/src/FormatOptions.ts @@ -10,6 +10,7 @@ export type DataTypeCase = KeywordCase; export type FunctionCase = KeywordCase; export type LogicalOperatorNewline = 'before' | 'after'; +export type CommaNewline = LogicalOperatorNewline; export interface FormatOptions { tabWidth: number; @@ -20,6 +21,7 @@ export interface FormatOptions { functionCase: FunctionCase; indentStyle: IndentStyle; logicalOperatorNewline: LogicalOperatorNewline; + commaNewline: CommaNewline; expressionWidth: number; linesBetweenQueries: number; denseOperators: boolean; diff --git a/src/formatter/ExpressionFormatter.ts b/src/formatter/ExpressionFormatter.ts index a306e22fd..cb5805ff5 100644 --- a/src/formatter/ExpressionFormatter.ts +++ b/src/formatter/ExpressionFormatter.ts @@ -341,7 +341,11 @@ export default class ExpressionFormatter { private formatComma(_node: CommaNode) { if (!this.inline) { - this.layout.add(WS.NO_SPACE, ',', WS.NEWLINE, WS.INDENT); + if (this.cfg.commaNewline === 'before') { + this.layout.add(WS.NEWLINE, WS.INDENT, ',', WS.SPACE); + } else { + this.layout.add(WS.NO_SPACE, ',', WS.NEWLINE, WS.INDENT); + } } else { this.layout.add(WS.NO_SPACE, ',', WS.SPACE); } diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts index f89573c80..538626132 100644 --- a/src/sqlFormatter.ts +++ b/src/sqlFormatter.ts @@ -48,6 +48,7 @@ const defaultOptions: FormatOptions = { functionCase: 'preserve', indentStyle: 'standard', logicalOperatorNewline: 'before', + commaNewline: 'after', expressionWidth: 50, linesBetweenQueries: 1, denseOperators: false, diff --git a/test/behavesLikeSqlFormatter.ts b/test/behavesLikeSqlFormatter.ts index 1b9e636ba..7df8097c7 100644 --- a/test/behavesLikeSqlFormatter.ts +++ b/test/behavesLikeSqlFormatter.ts @@ -13,6 +13,7 @@ import supportsIndentStyle from './options/indentStyle.js'; import supportsLinesBetweenQueries from './options/linesBetweenQueries.js'; import supportsNewlineBeforeSemicolon from './options/newlineBeforeSemicolon.js'; import supportsLogicalOperatorNewline from './options/logicalOperatorNewline.js'; +import supportsCommaNewline from './options/commaNewline.js'; import supportsParamTypes from './options/paramTypes.js'; import supportsWindowFunctions from './features/windowFunctions.js'; import supportsFunctionCase from './options/functionCase.js'; @@ -36,6 +37,7 @@ export default function behavesLikeSqlFormatter(format: FormatFn) { supportsExpressionWidth(format); supportsNewlineBeforeSemicolon(format); supportsLogicalOperatorNewline(format); + supportsCommaNewline(format); supportsParamTypes(format); supportsWindowFunctions(format); From c2849f5907d01812224e130005ea08c6365a189b Mon Sep 17 00:00:00 2001 From: swayam-aicg Date: Tue, 13 Jan 2026 16:15:07 -0800 Subject: [PATCH 3/8] feat: Ability to control newline placement around comma when separating multiple columns/tablesAdd option that can control if the newline is present after the comma or before the comma when listing multiple columns/tables --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index e5375b560..d52f367bb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -72,3 +72,4 @@ Wylie Conlon Xin Hu Zhongxian Liang 0xflotus <0xflotus@gmail.com> +Swayam Shah(dlh.io) From f754470d7a791551741ab8988e86f041285381a5 Mon Sep 17 00:00:00 2001 From: Swayam Date: Tue, 13 Jan 2026 16:16:27 -0800 Subject: [PATCH 4/8] feat: Ability to control newline placement around comma when seperating multiple columns/tables Add option that can control if the newline is present after the comma or before the comma when listing multiple columns/tables --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index e5375b560..73608bde8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -72,3 +72,4 @@ Wylie Conlon Xin Hu Zhongxian Liang 0xflotus <0xflotus@gmail.com> +Swayam Shah(dlh.io) \ No newline at end of file From 3068e7c2476e14b78b48ffeed36ad11e3bc211c3 Mon Sep 17 00:00:00 2001 From: Swayam Date: Wed, 14 Jan 2026 19:09:17 -0800 Subject: [PATCH 5/8] fix: Add proper support for commaPosition with support for comments and all types of statements Added support for commaPosition with proper support for different types of comments and making sure logical format for different kinds of statements with options of 'leading' commas, 'trailing' commas, and 'leadingWithSpace' commas. --- AUTHORS | 1 + README.md | 2 +- docs/commaNewline.md | 38 ----- docs/commaPosition.md | 79 +++++++++ src/FormatOptions.ts | 4 +- src/formatter/ExpressionFormatter.ts | 66 +++++++- src/sqlFormatter.ts | 2 +- src/validateConfig.ts | 1 - test/behavesLikeSqlFormatter.ts | 4 +- test/options/commaNewline.ts | 37 ---- test/options/commaPosition.ts | 245 +++++++++++++++++++++++++++ test/sqlFormatter.test.ts | 6 - 12 files changed, 395 insertions(+), 90 deletions(-) delete mode 100644 docs/commaNewline.md create mode 100644 docs/commaPosition.md delete mode 100644 test/options/commaNewline.ts create mode 100644 test/options/commaPosition.ts diff --git a/AUTHORS b/AUTHORS index e5375b560..d9e8ddd07 100644 --- a/AUTHORS +++ b/AUTHORS @@ -72,3 +72,4 @@ Wylie Conlon Xin Hu Zhongxian Liang 0xflotus <0xflotus@gmail.com> +Swayam Shah(dlh.io) \ No newline at end of file diff --git a/README.md b/README.md index 96f311d53..ac131c42d 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ All fields are optional and all fields that are not specified will be filled wit - [**`identifierCase`**](docs/identifierCase.md) uppercases or lowercases identifiers. (**experimental!**) - [**`indentStyle`**](docs/indentStyle.md) defines overall indentation style. (**deprecated!**) - [**`logicalOperatorNewline`**](docs/logicalOperatorNewline.md) newline before or after boolean operator (AND, OR, XOR). -- [**`commaNewline`**](docs/commaNewline.md) newline before or after comma seperating multiple columns/tables. +- [**`commaPosition`**](docs/commaPosition.md) decides comma position of commas between multiple columns/tables. - [**`expressionWidth`**](docs/expressionWidth.md) maximum number of characters in parenthesized expressions to be kept on single line. - [**`linesBetweenQueries`**](docs/linesBetweenQueries.md) how many newlines to insert between queries. - [**`denseOperators`**](docs/denseOperators.md) packs operators densely without spaces. diff --git a/docs/commaNewline.md b/docs/commaNewline.md deleted file mode 100644 index 163045ab5..000000000 --- a/docs/commaNewline.md +++ /dev/null @@ -1,38 +0,0 @@ -# commaNewline - -Decides newline placement before or after commas seperating columns/tables. - -## Options - -- `"after"` (default) adds newline after the comma. -- `"before"` adds newline before the comma with a following space. - -### after - -```sql -SELECT - name, - age, - height -FROM - persons -WHERE - age > 10 - AND height < 150 - OR occupation IS NULL -``` - -### before - -```sql -SELECT - name - , age - , height -FROM - persons -WHERE - age > 10 AND - height < 150 OR - occupation IS NULL -``` diff --git a/docs/commaPosition.md b/docs/commaPosition.md new file mode 100644 index 000000000..c92ec0434 --- /dev/null +++ b/docs/commaPosition.md @@ -0,0 +1,79 @@ +# commaPosition + +Decides comma position of commas between multiple columns/tables. + +## Options + +- `"trailing"` (default) comma appears at the end of line. +- `"leading"` comma appears at the start of the line. +- `"leadingWithSpace"`: comma appears at the start of the line followed by a space. + +### trailing + +```sql +SELECT + name, + age, + height +FROM + persons +WHERE + age > 10 + AND height < 150 + OR occupation IS NULL; +``` + +### leading + +```sql +SELECT + name + ,age + ,height +FROM + persons +WHERE + age > 10 AND + height < 150 OR + occupation IS NULL; +``` + +### leadingWithSpace +```sql +SELECT + name + , age + , height +FROM + persons +WHERE + age > 10 AND + height < 150 OR + occupation IS NULL; +``` + +### Other examples +```sql +-- No effect on INSERT Statements +INSERT INTO + Customers (CustomerName, City, Country) +VALUES + ('Cardinal', 'Stavanger', 'Norway'); + +-- leading comma on UPDATE Statements +UPDATE Customers +SET + ContactName = 'Alfred Schmidt' + ,City = 'Frankfurt' +WHERE + CustomerID = 1; + +-- leading comma on Statements with comments +SELECT + a -- comment 1, comma part of comment + ,b -- comment 2 + /* block comment */ + ,c +FROM + tableA; +``` diff --git a/src/FormatOptions.ts b/src/FormatOptions.ts index 16121ebb8..d10882923 100644 --- a/src/FormatOptions.ts +++ b/src/FormatOptions.ts @@ -10,7 +10,7 @@ export type DataTypeCase = KeywordCase; export type FunctionCase = KeywordCase; export type LogicalOperatorNewline = 'before' | 'after'; -export type CommaNewline = LogicalOperatorNewline; +export type CommaPosition = 'leading' | 'trailing' | 'leadingWithSpace'; export interface FormatOptions { tabWidth: number; @@ -21,7 +21,7 @@ export interface FormatOptions { functionCase: FunctionCase; indentStyle: IndentStyle; logicalOperatorNewline: LogicalOperatorNewline; - commaNewline: CommaNewline; + commaPosition: CommaPosition; expressionWidth: number; linesBetweenQueries: number; denseOperators: boolean; diff --git a/src/formatter/ExpressionFormatter.ts b/src/formatter/ExpressionFormatter.ts index cb5805ff5..0dcbe4684 100644 --- a/src/formatter/ExpressionFormatter.ts +++ b/src/formatter/ExpressionFormatter.ts @@ -341,8 +341,9 @@ export default class ExpressionFormatter { private formatComma(_node: CommaNode) { if (!this.inline) { - if (this.cfg.commaNewline === 'before') { - this.layout.add(WS.NEWLINE, WS.INDENT, ',', WS.SPACE); + if (this.cfg.commaPosition === 'leading' || this.cfg.commaPosition === 'leadingWithSpace') { + // Look ahead: check if next node is a line comment + this.formatLeadingComma(); } else { this.layout.add(WS.NO_SPACE, ',', WS.NEWLINE, WS.INDENT); } @@ -351,6 +352,67 @@ export default class ExpressionFormatter { } } + private formatLeadingComma() { + const comments: AstNode[] = []; + let lookAheadIndex = this.index + 1; + + while (lookAheadIndex < this.nodes.length) { + const nextNode = this.nodes[lookAheadIndex]; + if (nextNode.type === NodeType.line_comment || nextNode.type === NodeType.block_comment) { + comments.push(nextNode); + lookAheadIndex++; + } else { + break; + } + } + + if (comments.length === 0) { + // No comments - simple case + if (this.cfg.commaPosition === 'leadingWithSpace') { + this.layout.add(WS.NEWLINE, WS.INDENT, ',', WS.SPACE); + } else { + this.layout.add(WS.NEWLINE, WS.INDENT, ','); + } + return; + } + + // First: output any line comment on the same line (belongs to previous item) + let lineCommentProcessed = false; + if (comments[0]?.type === NodeType.line_comment) { + this.layout.add((comments[0] as LineCommentNode).text); + lineCommentProcessed = true; + } + + // Second: output all block comments on their own lines BEFORE the comma + const startIndex = lineCommentProcessed ? 1 : 0; + for (let i = startIndex; i < comments.length; i++) { + const comment = comments[i]; + if (comment.type === NodeType.block_comment) { + const blockComment = comment as BlockCommentNode; + if (this.isMultilineBlockComment(blockComment)) { + this.splitBlockComment(blockComment.text).forEach(line => { + this.layout.add(WS.NEWLINE, WS.INDENT, line); + }); + } else { + this.layout.add(WS.NEWLINE, WS.INDENT, blockComment.text); + } + } else if (comment.type === NodeType.line_comment) { + // Additional line comments (rare case) - treat like block comments + this.layout.add(WS.NEWLINE, WS.INDENT, (comment as LineCommentNode).text); + } + } + + // Finally: add the comma + if (this.cfg.commaPosition === 'leadingWithSpace') { + this.layout.add(WS.NEWLINE, WS.INDENT, ',', WS.SPACE); + } else { + this.layout.add(WS.NEWLINE, WS.INDENT, ','); + } + + // Skip all processed comments + this.index = lookAheadIndex - 1; + } + private withComments(node: AstNode, fn: () => void) { this.formatComments(node.leadingComments); fn(); diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts index 538626132..0a92976d3 100644 --- a/src/sqlFormatter.ts +++ b/src/sqlFormatter.ts @@ -48,7 +48,7 @@ const defaultOptions: FormatOptions = { functionCase: 'preserve', indentStyle: 'standard', logicalOperatorNewline: 'before', - commaNewline: 'after', + commaPosition: 'trailing', expressionWidth: 50, linesBetweenQueries: 1, denseOperators: false, diff --git a/src/validateConfig.ts b/src/validateConfig.ts index 8789fe63d..15e879c24 100644 --- a/src/validateConfig.ts +++ b/src/validateConfig.ts @@ -10,7 +10,6 @@ export function validateConfig(cfg: FormatOptions): FormatOptions { 'newlineBeforeOpenParen', 'newlineBeforeCloseParen', 'aliasAs', - 'commaPosition', 'tabulateAlias', ]; for (const optionName of removedOptions) { diff --git a/test/behavesLikeSqlFormatter.ts b/test/behavesLikeSqlFormatter.ts index 7df8097c7..62600e53a 100644 --- a/test/behavesLikeSqlFormatter.ts +++ b/test/behavesLikeSqlFormatter.ts @@ -13,7 +13,7 @@ import supportsIndentStyle from './options/indentStyle.js'; import supportsLinesBetweenQueries from './options/linesBetweenQueries.js'; import supportsNewlineBeforeSemicolon from './options/newlineBeforeSemicolon.js'; import supportsLogicalOperatorNewline from './options/logicalOperatorNewline.js'; -import supportsCommaNewline from './options/commaNewline.js'; +import supportsCommaPosition from './options/commaPosition.js'; import supportsParamTypes from './options/paramTypes.js'; import supportsWindowFunctions from './features/windowFunctions.js'; import supportsFunctionCase from './options/functionCase.js'; @@ -37,7 +37,7 @@ export default function behavesLikeSqlFormatter(format: FormatFn) { supportsExpressionWidth(format); supportsNewlineBeforeSemicolon(format); supportsLogicalOperatorNewline(format); - supportsCommaNewline(format); + supportsCommaPosition(format); supportsParamTypes(format); supportsWindowFunctions(format); diff --git a/test/options/commaNewline.ts b/test/options/commaNewline.ts deleted file mode 100644 index 8e5db579e..000000000 --- a/test/options/commaNewline.ts +++ /dev/null @@ -1,37 +0,0 @@ -import dedent from 'dedent-js'; - -import { FormatFn } from '../../src/sqlFormatter.js'; - -export default function supportsCommaNewline(format: FormatFn) { - it('by default adds newline after comma', () => { - const result = format(`SELECT id, first_name, last_name, email FROM users;`); - expect(result).toBe( - dedent` - SELECT - id, - first_name, - last_name, - email - FROM - users; - ` - ); - }); - - it('supports newline before comma', () => { - const result = format(`SELECT id, first_name, last_name, email FROM users;`, { - commaNewline: 'before', - }); - expect(result).toBe( - dedent` - SELECT - id - , first_name - , last_name - , email - FROM - users; - ` - ); - }); -} diff --git a/test/options/commaPosition.ts b/test/options/commaPosition.ts new file mode 100644 index 000000000..52b1d31b6 --- /dev/null +++ b/test/options/commaPosition.ts @@ -0,0 +1,245 @@ +import dedent from 'dedent-js'; + +import { FormatFn } from '../../src/sqlFormatter.js'; + +export default function supportsCommaPosition(format: FormatFn) { + it('by default adds newline after comma', () => { + const result = format(`SELECT id, first_name, last_name, email FROM users;`); + expect(result).toBe( + dedent` + SELECT + id, + first_name, + last_name, + email + FROM + users; + ` + ); + }); + + it('supports newline before comma', () => { + const result = format(`SELECT id, first_name, last_name, email FROM users;`, { + commaPosition: 'leadingWithSpace', + }); + expect(result).toBe( + dedent` + SELECT + id + , first_name + , last_name + , email + FROM + users; + ` + ); + }); + + it('preserves row-wise format for INSERT VALUES and Statements', () => { + const result = format( + `INSERT INTO users (id, name, email, password) VALUES (1, 'John', 'john@example.com', 'hash');`, + { + commaPosition: 'leadingWithSpace', + } + ); + expect(result).toBe( + dedent` + INSERT INTO + users (id, name, email, password) + VALUES + (1, 'John', 'john@example.com', 'hash'); + ` + ); + }); + it('supports comma position for multiple rows in INSERT VALUES', () => { + const result = format( + `INSERT INTO users (id, name, email, password) VALUES (1, 'John', 'john@example.com', 'hash'), (2, 'Jane', 'jane@example.com', 'hash2');`, + { + commaPosition: 'leadingWithSpace', + } + ); + expect(result).toBe( + dedent` + INSERT INTO + users (id, name, email, password) + VALUES + (1, 'John', 'john@example.com', 'hash') + , (2, 'Jane', 'jane@example.com', 'hash2'); + ` + ); + }); + + it('supports trailing comma position for multiple rows in INSERT VALUES', () => { + const result = format( + `INSERT INTO users (id, name, email, password) VALUES (1, 'John', 'john@example.com', 'hash'), (2, 'Jane', 'jane@example.com', 'hash2');` + ); + expect(result).toBe( + dedent` + INSERT INTO + users (id, name, email, password) + VALUES + (1, 'John', 'john@example.com', 'hash'), + (2, 'Jane', 'jane@example.com', 'hash2'); + ` + ); + }); + + it('supports leadingWithSpace comma position in UPDATE statements', () => { + const result = format( + `UPDATE products SET price = price * 1.1, stock = stock - 1 WHERE category = 'Electronics'; + `, + { + commaPosition: 'leadingWithSpace', + } + ); + const acceptableFormats = [ + dedent` + UPDATE products + SET + price = price * 1.1 + , stock = stock - 1 + WHERE + category = 'Electronics'; + `, + // In case user has a different language setting like spark + dedent` + UPDATE products SET price = price * 1.1 + , stock = stock - 1 + WHERE + category = 'Electronics'; + `, + ]; + expect(acceptableFormats).toContain(result); + }); + + it('supports leadingWithSpace comma position with comments', () => { + const result = format( + `SELECT + foo, -- comment 1 + bar, -- comment 2 + baz + FROM + my_table; + `, + { + commaPosition: 'leadingWithSpace', + } + ); + expect(result).toBe( + dedent` + SELECT + foo -- comment 1 + , bar -- comment 2 + , baz + FROM + my_table; + ` + ); + }); + + it('supports leadingWithSpace comma position with comments(with commas)', () => { + const result = format( + `SELECT + foo, -- comment, with, commas + bar, -- another comment, with, commas + baz + FROM + my_table; + `, + { + commaPosition: 'leadingWithSpace', + } + ); + expect(result).toBe( + dedent` + SELECT + foo -- comment, with, commas + , bar -- another comment, with, commas + , baz + FROM + my_table; + ` + ); + }); + + it('supports leadingWithSpace comma position in complex queries with complex comments(with commas and all types of comments)', () => { + const result = format( + `SELECT + foo, -- comment, with, commas + /* block comment, with, commas */ + bar, -- another comment, with, commas + baz, /* inline block comment, with, commas */ + qux -- last comment, with, commas + FROM + my_table -- table comment, with, commas + WHERE + foo = 'value, with, commas' AND -- condition comment, with, commas + bar > 100; + `, + { + commaPosition: 'leadingWithSpace', + } + ); + expect(result).toBe( + dedent` + SELECT + foo -- comment, with, commas + /* block comment, with, commas */ + , bar -- another comment, with, commas + , baz + /* inline block comment, with, commas */ + , qux -- last comment, with, commas + FROM + my_table -- table comment, with, commas + WHERE + foo = 'value, with, commas' + AND -- condition comment, with, commas + bar > 100; + ` + ); + }); + + it('supports leadingWithSpace comma position in queries with function argument list', () => { + const result = format( + `SELECT CONCAT(first_name, ' ', last_name) AS full_name, SUM(salary, bonus) AS total_compensation FROM employees;`, + { + commaPosition: 'leadingWithSpace', + } + ); + const acceptableFormats = [ + dedent` + SELECT + CONCAT(first_name, ' ', last_name) AS full_name + , SUM(salary, bonus) AS total_compensation + FROM + employees; + `, + // In case user has a different language setting with spaces after CONCAT function signature + dedent` + SELECT + CONCAT (first_name, ' ', last_name) AS full_name + , SUM(salary, bonus) AS total_compensation + FROM + employees; + `, + ]; + expect(acceptableFormats).toContain(result); + }); + + it('supports leading comma', () => { + const result = format(`SELECT id, first_name, last_name, email FROM users;`, { + commaPosition: 'leading', + }); + expect(result).toBe( + dedent` + SELECT + id + ,first_name + ,last_name + ,email + FROM + users; + ` + ); + }); +} diff --git a/test/sqlFormatter.test.ts b/test/sqlFormatter.test.ts index 87e9cca08..8d8947235 100644 --- a/test/sqlFormatter.test.ts +++ b/test/sqlFormatter.test.ts @@ -77,12 +77,6 @@ describe('sqlFormatter', () => { }).toThrow('tabulateAlias config is no more supported.'); }); - it('throws error when commaPosition config option used', () => { - expect(() => { - format('SELECT *', { commaPosition: 'before' } as any); - }).toThrow('commaPosition config is no more supported.'); - }); - describe('formatDialect()', () => { it('allows passing Dialect config object as a dialect parameter', () => { expect(formatDialect('SELECT [foo], `bar`;', { dialect: sqlite })).toBe(dedent` From 8b7458db34b6c5709bea0f60d5289cb4320bd3bf Mon Sep 17 00:00:00 2001 From: Swayam Date: Wed, 14 Jan 2026 22:11:54 -0800 Subject: [PATCH 6/8] fix(comments): Added a major fix to how comments are handled to make it consistent accross different situations Make comments of all types to work when converting leading commas to trailing comma structure, and vice versa. --- docs/commaPosition.md | 2 + src/formatter/ExpressionFormatter.ts | 83 ++++++++++++++++++++++++++-- test/features/comments.ts | 3 +- test/options/commaPosition.ts | 27 +++++++++ 4 files changed, 109 insertions(+), 6 deletions(-) diff --git a/docs/commaPosition.md b/docs/commaPosition.md index c92ec0434..2ae6de9bc 100644 --- a/docs/commaPosition.md +++ b/docs/commaPosition.md @@ -39,6 +39,7 @@ WHERE ``` ### leadingWithSpace + ```sql SELECT name @@ -53,6 +54,7 @@ WHERE ``` ### Other examples + ```sql -- No effect on INSERT Statements INSERT INTO diff --git a/src/formatter/ExpressionFormatter.ts b/src/formatter/ExpressionFormatter.ts index 0dcbe4684..9f117c3f1 100644 --- a/src/formatter/ExpressionFormatter.ts +++ b/src/formatter/ExpressionFormatter.ts @@ -68,6 +68,7 @@ export default class ExpressionFormatter { private dialectCfg: ProcessedDialectFormatOptions; private params: Params; private layout: Layout; + private pendingLineComment: (LineCommentNode | BlockCommentNode | DisableCommentNode)[] = []; // Keeps track if any pending Line/Comment is remaining private inline = false; private nodes: AstNode[] = []; @@ -184,9 +185,38 @@ export default class ExpressionFormatter { } private formatPropertyAccess(node: PropertyAccessNode) { - this.formatNode(node.object); + this.formatComments(node.object.leadingComments); + this.formatNodeWithoutComments(node.object); + + // Handle trailing comments of object BEFORE the dot (inline) + if (node.object.trailingComments && node.object.trailingComments.length > 0) { + for (const comment of node.object.trailingComments) { + if (comment.type === NodeType.block_comment) { + this.layout.add(WS.NO_SPACE, WS.SPACE, (comment as BlockCommentNode).text); + } else if (comment.type === NodeType.line_comment) { + this.layout.add(WS.NO_SPACE, WS.SPACE, (comment as LineCommentNode).text); + } + } + } + + // Add the dot operator this.layout.add(WS.NO_SPACE, node.operator); - this.formatNode(node.property); + + // Handle leading comments of property AFTER the dot (inline) + if (node.property.leadingComments && node.property.leadingComments.length > 0) { + for (const comment of node.property.leadingComments) { + if (comment.type === NodeType.block_comment) { + this.layout.add((comment as BlockCommentNode).text, WS.SPACE); + } else if (comment.type === NodeType.line_comment) { + this.layout.add((comment as LineCommentNode).text, WS.SPACE); + } + } + // Format property without leading comments (already handled above) + this.formatNodeWithoutComments(node.property); + this.formatComments(node.property.trailingComments); + } else { + this.formatNode(node.property); + } } private formatParenthesis(node: ParenthesisNode) { @@ -345,13 +375,36 @@ export default class ExpressionFormatter { // Look ahead: check if next node is a line comment this.formatLeadingComma(); } else { - this.layout.add(WS.NO_SPACE, ',', WS.NEWLINE, WS.INDENT); + this.formatTrailingComma(); } } else { this.layout.add(WS.NO_SPACE, ',', WS.SPACE); } } - + private formatTrailingComma() { + if (this.pendingLineComment && this.pendingLineComment.length > 0) { + // We have a line comment that should come after the comma + while (this.pendingLineComment.length > 0) { + const comment = this.pendingLineComment.shift(); + if (comment) { + if (comment.type === NodeType.line_comment) { + this.layout.add( + WS.NO_SPACE, + ',', + WS.SPACE, + (comment as LineCommentNode).text, + WS.MANDATORY_NEWLINE, + WS.INDENT + ); + } else { + this.layout.add((comment as BlockCommentNode).text, WS.MANDATORY_NEWLINE, WS.INDENT); + } + } + } + } else { + this.layout.add(WS.NO_SPACE, ',', WS.NEWLINE, WS.INDENT); + } + } private formatLeadingComma() { const comments: AstNode[] = []; let lookAheadIndex = this.index + 1; @@ -433,6 +486,11 @@ export default class ExpressionFormatter { } private formatLineComment(node: LineCommentNode) { + if (!this.inline && this.cfg.commaPosition === 'trailing' && this.isNextNonCommentNodeComma()) { + // Store the comment to be output after the comma + this.pendingLineComment.push(node); + return; + } if (isMultiline(node.precedingWhitespace || '')) { this.layout.add(WS.NEWLINE, WS.INDENT, node.text, WS.MANDATORY_NEWLINE, WS.INDENT); } else if (this.layout.getLayoutItems().length > 0) { @@ -442,8 +500,25 @@ export default class ExpressionFormatter { this.layout.add(node.text, WS.MANDATORY_NEWLINE, WS.INDENT); } } + private isNextNonCommentNodeComma(): boolean { + let lookAheadIndex = this.index + 1; + while (lookAheadIndex < this.nodes.length) { + const nextNode = this.nodes[lookAheadIndex]; + if (nextNode.type === NodeType.line_comment || nextNode.type === NodeType.block_comment) { + lookAheadIndex++; + } else { + return nextNode.type === NodeType.comma; + } + } + return false; + } private formatBlockComment(node: BlockCommentNode | DisableCommentNode) { + if (!this.inline && this.cfg.commaPosition === 'trailing' && this.isNextNonCommentNodeComma()) { + // Store the comment to be output after the comma + this.pendingLineComment.push(node); + return; + } if (node.type === NodeType.block_comment && this.isMultilineBlockComment(node)) { this.splitBlockComment(node.text).forEach(line => { this.layout.add(WS.NEWLINE, WS.INDENT, line); diff --git a/test/features/comments.ts b/test/features/comments.ts index 3cb57070e..c2b9f6d16 100644 --- a/test/features/comments.ts +++ b/test/features/comments.ts @@ -93,8 +93,7 @@ export default function supportsComments(format: FormatFn, opts: CommentsConfig `) ).toBe(dedent` SELECT - a --comment - , + a, --comment b `); }); diff --git a/test/options/commaPosition.ts b/test/options/commaPosition.ts index 52b1d31b6..269b9290c 100644 --- a/test/options/commaPosition.ts +++ b/test/options/commaPosition.ts @@ -242,4 +242,31 @@ export default function supportsCommaPosition(format: FormatFn) { ` ); }); + + it('supports trailing comma with comments from a leading comma formatted query', () => { + const result = format( + `SELECT + id -- comment 1 + ,first_name -- comment 2 + /* block comment */ + ,last_name + ,email + FROM + users; + `, + { commaPosition: 'trailing' } + ); + expect(result).toBe( + dedent` + SELECT + id, -- comment 1 + first_name, -- comment 2 + /* block comment */ + last_name, + email + FROM + users; + ` + ); + }); } From 4c253c49a1fd1e1969790503cbde60cf74023493 Mon Sep 17 00:00:00 2001 From: Swayam Date: Thu, 15 Jan 2026 17:20:20 -0800 Subject: [PATCH 7/8] fix(commaPosition): Don't change order of nodes for commaPosition trailing when having jump over block comments due bugs in implementation The commaPosition of trailing will not jump over block comments and be present at the end of the column if no block comment between column and comma and at the end of block comment in trailing manner in case a block comment is present. Docs has examples Affects: DLHX:4845 --- README.md | 626 +- docs/commaPosition.md | 219 +- package.json | 266 +- src/formatter/ExpressionFormatter.ts | 76 +- src/validateConfig.ts | 6 + test/options/commaPosition.ts | 153 +- test/sqlFormatter.test.ts | 8 + webpack.prod.js | 22 +- yarn.lock | 13948 ++++++++++++------------- 9 files changed, 7697 insertions(+), 7627 deletions(-) diff --git a/README.md b/README.md index ac131c42d..d8b3af83b 100644 --- a/README.md +++ b/README.md @@ -1,313 +1,313 @@ - - -# SQL Formatter [![NPM version](https://img.shields.io/npm/v/sql-formatter.svg)](https://npmjs.com/package/sql-formatter) ![Build status](https://img.shields.io/github/actions/workflow/status/sql-formatter-org/sql-formatter/coveralls.yaml) [![Coverage Status](https://coveralls.io/repos/github/sql-formatter-org/sql-formatter/badge.svg?branch=master)](https://coveralls.io/github/sql-formatter-org/sql-formatter?branch=master) - -**SQL Formatter** is a JavaScript library for pretty-printing SQL queries. - -It started as a port of a [PHP Library][], but has since considerably diverged. - -It supports various SQL dialects: -GCP BigQuery, IBM DB2, DuckDB, Apache Hive, MariaDB, MySQL, TiDB, Couchbase N1QL, Oracle PL/SQL, PostgreSQL, Amazon Redshift, SingleStoreDB, Snowflake, Spark, SQL Server Transact-SQL, Trino (and Presto). -See [language option docs](docs/language.md) for more details. - -It does not support: - -- Stored procedures. -- Changing of the delimiter type to something else than `;`. - -→ [Try the demo.](https://sql-formatter-org.github.io/sql-formatter) - -## Install - -Get the latest version from NPM: - -```sh -npm install sql-formatter -``` - -Also available with yarn: - -```sh -yarn add sql-formatter -``` - -## Usage - -### Usage as library - -```js -import { format } from 'sql-formatter'; - -console.log(format('SELECT * FROM tbl', { language: 'mysql' })); -``` - -This will output: - -```sql -SELECT - * -FROM - tbl -``` - -You can also pass in configuration options: - -```js -format('SELECT * FROM tbl', { - language: 'spark', - tabWidth: 2, - keywordCase: 'upper', - linesBetweenQueries: 2, -}); -``` - -### Disabling the formatter - -You can disable the formatter for a section of SQL by surrounding it with disable/enable comments: - -```sql -/* sql-formatter-disable */ -SELECT * FROM tbl1; -/* sql-formatter-enable */ -SELECT * FROM tbl2; -``` - -which produces: - -```sql -/* sql-formatter-disable */ -SELECT * FROM tbl1; -/* sql-formatter-enable */ -SELECT - * -FROM - tbl2; -``` - -The formatter doesn't even parse the code between these comments. -So in case there's some SQL that happens to crash SQL Formatter, -you can comment the culprit out (at least until the issue gets -fixed in SQL Formatter). - -### Placeholders replacement - -In addition to formatting, this library can also perform placeholder replacement in prepared SQL statements: - -```js -format('SELECT * FROM tbl WHERE foo = ?', { - params: ["'bar'"], -}); -``` - -Results in: - -```sql -SELECT - * -FROM - tbl -WHERE - foo = 'bar' -``` - -For more details see [docs of params option.](docs/params.md) - -### Usage from command line - -The CLI tool will be installed under `sql-formatter` -and may be invoked via `npx sql-formatter`: - -```sh -sql-formatter -h -``` - -``` -usage: sql-formatter [-h] [-o OUTPUT] \ -[-l {bigquery,db2,db2i,hive,mariadb,mysql,n1ql,plsql,postgresql,redshift,singlestoredb,snowflake,spark,sql,sqlite,tidb,transactsql,trino,tsql}] [-c CONFIG] [--version] [FILE] - -SQL Formatter - -positional arguments: - FILE Input SQL file (defaults to stdin) - -optional arguments: - -h, --help show this help message and exit - -o, --output OUTPUT - File to write SQL output (defaults to stdout) - --fix Update the file in-place - -l, --language {bigquery,db2,db2i,hive,mariadb,mysql,n1ql,plsql,postgresql,redshift,singlestoredb,snowflake,spark,sql,sqlite,tidb,trino,tsql} - SQL dialect (defaults to basic sql) - -c, --config CONFIG - Path to config JSON file or json string (will find a file named '.sql-formatter.json' or use default configs if unspecified) - --version show program's version number and exit -``` - -By default, the tool takes queries from stdin and processes them to stdout but -one can also name an input file name or use the `--output` option. - -```sh -echo 'select * from tbl where id = 3' | sql-formatter -``` - -```sql -select - * -from - tbl -where - id = 3 -``` - -The tool also accepts a JSON config file named .sql-formatter.json in the current or any parent directory, or with the `--config` option that takes this form: - -```json -{ - "language": "spark", - "tabWidth": 2, - "keywordCase": "upper", - "linesBetweenQueries": 2 -} -``` - -All fields are optional and all fields that are not specified will be filled with their default values. - -### Configuration options - -- [**`language`**](docs/language.md) the SQL dialect to use (when using `format()`). -- [**`dialect`**](docs/dialect.md) the SQL dialect to use (when using `formatDialect()` since version 12). -- [**`tabWidth`**](docs/tabWidth.md) amount of indentation to use. -- [**`useTabs`**](docs/useTabs.md) to use tabs for indentation. -- [**`keywordCase`**](docs/keywordCase.md) uppercases or lowercases keywords. -- [**`dataTypeCase`**](docs/dataTypeCase.md) uppercases or lowercases data types. -- [**`functionCase`**](docs/functionCase.md) uppercases or lowercases function names. -- [**`identifierCase`**](docs/identifierCase.md) uppercases or lowercases identifiers. (**experimental!**) -- [**`indentStyle`**](docs/indentStyle.md) defines overall indentation style. (**deprecated!**) -- [**`logicalOperatorNewline`**](docs/logicalOperatorNewline.md) newline before or after boolean operator (AND, OR, XOR). -- [**`commaPosition`**](docs/commaPosition.md) decides comma position of commas between multiple columns/tables. -- [**`expressionWidth`**](docs/expressionWidth.md) maximum number of characters in parenthesized expressions to be kept on single line. -- [**`linesBetweenQueries`**](docs/linesBetweenQueries.md) how many newlines to insert between queries. -- [**`denseOperators`**](docs/denseOperators.md) packs operators densely without spaces. -- [**`newlineBeforeSemicolon`**](docs/newlineBeforeSemicolon.md) places semicolon on separate line. -- [**`params`**](docs/params.md) collection of values for placeholder replacement. -- [**`paramTypes`**](docs/paramTypes.md) specifies parameter placeholders types to support. - -### Usage without NPM - -If you don't use a module bundler, clone the repository, run `npm install` and grab a file from `/dist` directory to use inside a `