Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 10 additions & 1 deletion src/formatter/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,18 @@ export function formatTsql(text: string, { options, config, profile }: EngineCon
const clauseMatcher = new RegExp(`^(${indentCentralClauses.join('|')})\\b`, 'i');
out = out.split('\n').map((ln: string) => (clauseMatcher.test(ln) ? alignLine(ln) : ln)).join('\n');
// Optionally align SELECT items if requested
// Note: When central alignment targets 'SELECT', we first ensure items are on
// separate lines (splitting by commas if they are on a single line), then apply
// padding to each item so they visually align to the configured column. This
// makes alignment predictable and avoids ambiguity when items start on one line.
if (indentCentralClauses.map(c => c.toUpperCase()).includes('SELECT')) {
out = out.replace(/SELECT\s+([\s\S]*?)\s+FROM/gi, (m, list) => {
const lines = list.split(/\n+/);
let lines = list.split(/\n+/).map((l: string) => l.trim());
// If items are not already split by newline, split by commas and preserve trailing commas
if (lines.length === 1) {
const items = list.split(',').map((s: string) => s.trim());
lines = items.map((i: string, idx: number) => (idx < items.length - 1 ? `${i},` : i));
}
const aligned = lines.map((l: string) => alignLine(l));
return `SELECT\n${aligned.join('\n')} FROM`;
});
Expand Down
115 changes: 115 additions & 0 deletions src/test/formatter.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,119 @@ describe('Formatter Engine (Vitest)', () => {
const out = formatTsql(t, { options: {} as any, config: cfg({ newlineBeforeSemicolon: true, linesBetweenQueries: 2 }), profile: {} });
expect(out.includes('\n;\n')).toBe(true);
});

it('dense operators', () => {
const t = 'WHERE a = 10 AND b > 5';
const out = formatTsql(t, { options: {} as any, config: cfg({ denseOperators: true }), profile: {} });
expect(out).toBe('WHERE a=10AND\nb>5');
});

it('parenthesis spacing outside', () => {
const t = 'COUNT(*)';
const out = formatTsql(t, { options: {} as any, config: cfg({ parenthesisSpacing: 'outside' }), profile: {} });
expect(out).toBe('COUNT( * ) ');
});

it('join alignment left', () => {
const t = 'SELECT a FROM t INNER JOIN x ON t.id=x.id';
const out = formatTsql(t, { options: {} as any, config: cfg({ newlineAfterFrom: true, joinAlignment: 'left' }), profile: {} });
expect(/\nINNER JOIN /.test(out)).toBe(true);
});

it('logical operator newline before', () => {
const t = 'WHERE a = 1 AND b = 2';
const out = formatTsql(t, { options: {} as any, config: cfg({ logicalOperatorNewline: 'before' }), profile: {} });
expect(/\nAND /.test(out)).toBe(true);
});

it('alias keyword enable for table aliases', () => {
const t = 'SELECT a FROM dbo.Table t';
const out = formatTsql(t, { options: {} as any, config: cfg({ aliasKeyword: 'enable' }), profile: {} });
expect(out).toBe('SELECT\n a\nFROM dbo.TABLE AS t');
});

it('columns comma placement ignore', () => {
const t = 'SELECT a, b, c FROM t';
const out = formatTsql(t, { options: {} as any, config: cfg({ newlineAfterSelect: true, columnsCommaPlacement: 'ignore' }), profile: {} });
expect(/SELECT\n\s+a\n\s+b\n\s+c\nFROM/.test(out)).toBe(true);
});

it('expression width wrapping', () => {
const t = 'SELECT verylongcolumnname, anotherverylongcolumnname FROM t';
const out = formatTsql(t, { options: {} as any, config: cfg({ newlineAfterSelect: true, expressionWidth: 20, safeWrapDelimiters: ['comma'] }), profile: {} });
expect(out.includes('\n')).toBe(true);
});

it('cte style inline', () => {
const t = 'WITH c AS (SELECT * FROM t) SELECT * FROM c';
const out = formatTsql(t, { options: {} as any, config: cfg({ cteStyle: 'inline' }), profile: {} });
expect(out).toBe('WITH c AS(SELECT\n *\nFROM t) SELECT\n *\nFROM c');
});

it('window function compact style', () => {
const t = 'SELECT ROW_NUMBER() OVER(PARTITION BY a ORDER BY b) FROM t';
const out = formatTsql(t, { options: {} as any, config: cfg({ windowFunctionStyle: 'compact' }), profile: {} });
expect(/OVER\(PARTITION BY a ORDER BY b\)/.test(out)).toBe(true);
});

it('align column definitions false', () => {
const t = 'CREATE TABLE t (id INT, name VARCHAR(50))';
const out = formatTsql(t, { options: {} as any, config: cfg({ alignColumnDefinitions: false }), profile: {} });
expect(out).toBe('CREATE TABLE t(id INT, name VARCHAR(50))');
});

it('bracket identifiers ignore', () => {
const t = 'SELECT a FROM [dbo].[Table]';
const out = formatTsql(t, { options: {} as any, config: cfg({ bracketIdentifiers: 'ignore' }), profile: {} });
expect(out).toBe('SELECT\n a\nFROM [dbo].[TABLE]');
});

it('tabulate alias', () => {
const t = 'SELECT a AS alias, verylongcolumn AS longer FROM t';
const out = formatTsql(t, { options: {} as any, config: cfg({ newlineAfterSelect: true, tabulateAlias: true }), profile: {} });
expect(out).toBe('SELECT\n a AS alias, verylongcolumn AS longer\nFROM t');
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected output appears to be incorrect. With newlineAfterSelect: true, the formatter puts each column on a separate line (joined with newlines on line 142 of engine.ts). The expected output should have each column on its own line, something like:

expect(/SELECT\n\s+a.*AS alias,?\n\s+verylongcolumn.*AS longer\nFROM/.test(out)).toBe(true);

Additionally, with tabulateAlias: true, the code adds padding to align the AS keywords, which is not reflected in the current expected value.

Suggested change
expect(out).toBe('SELECT\n a AS alias, verylongcolumn AS longer\nFROM t');
expect(/SELECT\n\s+a\s+AS alias,?\n\s+verylongcolumn\s+AS longer\nFROM t/.test(out)).toBe(true);

Copilot uses AI. Check for mistakes.
});

it('expression width wrapping without delimiters', () => {
const t = 'SELECT verylongcolumnnamethatexceeds FROM t';
const out = formatTsql(t, { options: {} as any, config: cfg({ newlineAfterSelect: true, expressionWidth: 20 }), profile: {} });
expect(out.includes('\n')).toBe(true);
});

it('alias keyword remove for table aliases', () => {
const t = 'SELECT a FROM dbo.Table AS t';
const out = formatTsql(t, { options: {} as any, config: cfg({ aliasKeyword: 'remove' }), profile: {} });
expect(out).toBe('SELECT\n a\nFROM dbo.TABLE t');
});

it('central indent alignment', () => {
const t = 'SELECT a FROM t\nWHERE b = 1';
const out = formatTsql(t, { options: {} as any, config: cfg({ indentStyle: 'central', indentStyleMode: 'enable', indentAlignColumn: 20, indentCentralClauses: ['WHERE'] }), profile: {} });
expect(out.includes(' WHERE')).toBe(true);
});

it('central indent alignment select', () => {
const t = 'SELECT a, b FROM t';
const out = formatTsql(t, { options: {} as any, config: cfg({ newlineAfterSelect: true, indentStyle: 'central', indentStyleMode: 'enable', indentAlignColumn: 10, indentCentralClauses: ['SELECT'] }), profile: {} });
// Verify SELECT items are split and aligned consistently.
expect(/SELECT\s*\n\s+a,\n\s+b\s+FROM/.test(out)).toBe(true);
});

it('window function multiline style', () => {
const t = 'SELECT ROW_NUMBER() OVER(PARTITION BY a ORDER BY b) FROM t';
const out = formatTsql(t, { options: {} as any, config: cfg({ windowFunctionStyle: 'multiline' }), profile: {} });
expect(/OVER\s*\(\s*\n\s*PARTITION BY/.test(out)).toBe(true);
});

it('align column definitions true', () => {
const t = 'CREATE TABLE t (id INT, name VARCHAR(50))';
const out = formatTsql(t, { options: {} as any, config: cfg({ alignColumnDefinitions: true }), profile: {} });
expect(out).toBe('CREATE TABLE t (\n id INT,\n name VARCHAR(50)\n))');
});

it('bracket identifiers remove', () => {
const t = 'SELECT [a] FROM [t]';
const out = formatTsql(t, { options: {} as any, config: cfg({ bracketIdentifiers: 'remove' }), profile: {} });
expect(out).toBe('SELECT\n a\nFROM t');
});
});
Loading