Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ Wylie Conlon <wylie@superblockshq.com>
Xin Hu <hoosin.git@gmail.com>
Zhongxian Liang <zhongxian.liang@shopee.com>
0xflotus <0xflotus@gmail.com>
Swayam Shah(dlh.io) <swayam@aicg.com | helloswayamshah@gmail.com>
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
- [**`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.
Expand Down
81 changes: 81 additions & 0 deletions docs/commaPosition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 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');
Comment thread
swayam-aicg marked this conversation as resolved.
Outdated

-- 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;
```
2 changes: 2 additions & 0 deletions src/FormatOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type DataTypeCase = KeywordCase;
export type FunctionCase = KeywordCase;

export type LogicalOperatorNewline = 'before' | 'after';
export type CommaPosition = 'leading' | 'trailing' | 'leadingWithSpace';

export interface FormatOptions {
tabWidth: number;
Expand All @@ -20,6 +21,7 @@ export interface FormatOptions {
functionCase: FunctionCase;
indentStyle: IndentStyle;
logicalOperatorNewline: LogicalOperatorNewline;
commaPosition: CommaPosition;
expressionWidth: number;
linesBetweenQueries: number;
denseOperators: boolean;
Expand Down
147 changes: 144 additions & 3 deletions src/formatter/ExpressionFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
swayam-aicg marked this conversation as resolved.
Outdated

private inline = false;
private nodes: AstNode[] = [];
Expand Down Expand Up @@ -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);
}
Comment thread
swayam-aicg marked this conversation as resolved.
Outdated
}

private formatParenthesis(node: ParenthesisNode) {
Expand Down Expand Up @@ -341,11 +371,100 @@ export default class ExpressionFormatter {

private formatComma(_node: CommaNode) {
if (!this.inline) {
this.layout.add(WS.NO_SPACE, ',', WS.NEWLINE, WS.INDENT);
if (this.cfg.commaPosition === 'leading' || this.cfg.commaPosition === 'leadingWithSpace') {
// Look ahead: check if next node is a line comment
this.formatLeadingComma();
Comment thread
swayam-aicg marked this conversation as resolved.
Outdated
} else {
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() {
Comment thread
swayam-aicg marked this conversation as resolved.
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);
Expand All @@ -367,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) {
Expand All @@ -376,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);
Expand Down
1 change: 1 addition & 0 deletions src/sqlFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const defaultOptions: FormatOptions = {
functionCase: 'preserve',
indentStyle: 'standard',
logicalOperatorNewline: 'before',
commaPosition: 'trailing',
expressionWidth: 50,
linesBetweenQueries: 1,
denseOperators: false,
Expand Down
1 change: 0 additions & 1 deletion src/validateConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export function validateConfig(cfg: FormatOptions): FormatOptions {
'newlineBeforeOpenParen',
'newlineBeforeCloseParen',
'aliasAs',
'commaPosition',
'tabulateAlias',
];
for (const optionName of removedOptions) {
Expand Down
2 changes: 2 additions & 0 deletions test/behavesLikeSqlFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 supportsCommaPosition from './options/commaPosition.js';
import supportsParamTypes from './options/paramTypes.js';
import supportsWindowFunctions from './features/windowFunctions.js';
import supportsFunctionCase from './options/functionCase.js';
Expand All @@ -36,6 +37,7 @@ export default function behavesLikeSqlFormatter(format: FormatFn) {
supportsExpressionWidth(format);
supportsNewlineBeforeSemicolon(format);
supportsLogicalOperatorNewline(format);
supportsCommaPosition(format);
supportsParamTypes(format);
supportsWindowFunctions(format);

Expand Down
3 changes: 1 addition & 2 deletions test/features/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ export default function supportsComments(format: FormatFn, opts: CommentsConfig
`)
).toBe(dedent`
SELECT
a --comment
,
a, --comment
Comment thread
swayam-aicg marked this conversation as resolved.
b
`);
});
Expand Down
Loading