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
17 changes: 16 additions & 1 deletion docs/guide/formatting-recipes.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
---
title: Formatting Recipes
outline: deep
---
Expand Down Expand Up @@ -36,6 +36,7 @@ const { formattedSql, params } = formatter.format(query);
| `withClauseStyle` | `'standard'`, `'cte-oneline'`, `'full-oneline'` | `'standard'` | Expands or collapses common table expressions. |
| `parenthesesOneLine`, `betweenOneLine`, `valuesOneLine`, `joinOneLine`, `caseOneLine`, `subqueryOneLine` | `true` / `false` | `false` for each | Opt-in switches that keep the corresponding construct on a single line even if other break settings would expand it. |
| `exportComment` | `true` / `false` | `false` | Emits comments collected by the parser. Turn it on when you want annotations preserved. |
| `castStyle` | 'standard', 'postgres' | From preset or 'standard' | Chooses how CAST expressions are printed. 'standard' emits ANSI `CAST(expr AS type)` while 'postgres' emits `expr::type`. See "Controlling CAST style" below for usage notes and examples. |

Combine these settings to mirror house formatting conventions or align with existing lint rules. The following sections call out the options that trip up newcomers most often.

Expand Down Expand Up @@ -87,6 +88,20 @@ Default behaviour (`'block'`) leaves comments exactly as they were parsed. Switc

Use `valuesCommaBreak` when you need to keep the main query in trailing-comma style but prefer inline tuples inside a `VALUES` block (or vice versa). With `exportComment: true`, comments that appear before or after each tuple are preserved and printed alongside the formatted output, so inline annotations survive automated formatting.


### Controlling CAST style

`castStyle` lets you toggle between ANSI-compatible casts and PostgreSQL's shorthand.

```typescript
new SqlFormatter().format(expr); // cast("price" as NUMERIC(10, 2))
new SqlFormatter({ castStyle: 'postgres' }).format(expr); // "price"::NUMERIC(10, 2)
```

- Default (`'standard'`) keeps ANSI `CAST(... AS ...)`, which works across engines such as MySQL, SQL Server, DuckDB, and more.
- Set `castStyle: 'postgres'` when you explicitly target PostgreSQL-style `::` casts. Presets like `'postgres'`, `'redshift'`, and `'cockroachdb'` already switch this on.

If you are migrating away from PostgreSQL-only syntax, enforce `castStyle: 'standard'` and phase out `::` usage gradually.
## Sample

```json
Expand Down
30 changes: 28 additions & 2 deletions packages/core/src/parsers/SqlPrintTokenParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export enum ParameterStyle {
Named = 'named'
}

export type CastStyle = 'postgres' | 'standard';

export interface FormatterConfig {
identifierEscape?: {
start: string;
Expand All @@ -58,6 +60,8 @@ export interface FormatterConfig {
* Parameter style: anonymous (?), indexed ($1), or named (:name)
*/
parameterStyle?: ParameterStyle;
/** Controls how CAST expressions are rendered */
castStyle?: CastStyle;
}

export const PRESETS: Record<string, FormatterConfig> = {
Expand All @@ -70,11 +74,13 @@ export const PRESETS: Record<string, FormatterConfig> = {
identifierEscape: { start: '"', end: '"' },
parameterSymbol: '$',
parameterStyle: ParameterStyle.Indexed,
castStyle: 'postgres',
},
postgresWithNamedParams: {
identifierEscape: { start: '"', end: '"' },
parameterSymbol: ':',
parameterStyle: ParameterStyle.Named,
castStyle: 'postgres',
},
sqlserver: {
identifierEscape: { start: '[', end: ']' },
Expand Down Expand Up @@ -125,6 +131,7 @@ export const PRESETS: Record<string, FormatterConfig> = {
identifierEscape: { start: '"', end: '"' },
parameterSymbol: '$',
parameterStyle: ParameterStyle.Indexed,
castStyle: 'postgres',
},
athena: {
identifierEscape: { start: '"', end: '"' },
Expand All @@ -150,6 +157,7 @@ export const PRESETS: Record<string, FormatterConfig> = {
identifierEscape: { start: '"', end: '"' },
parameterSymbol: '$',
parameterStyle: ParameterStyle.Indexed,
castStyle: 'postgres',
},
flinksql: {
identifierEscape: { start: '`', end: '`' },
Expand Down Expand Up @@ -201,12 +209,14 @@ export class SqlPrintTokenParser implements SqlComponentVisitor<SqlPrintToken> {
parameterDecorator: ParameterDecorator;
identifierDecorator: IdentifierDecorator;
index: number = 1;
private castStyle: CastStyle;

constructor(options?: {
preset?: FormatterConfig,
identifierEscape?: { start: string; end: string },
parameterSymbol?: string | { start: string; end: string },
parameterStyle?: 'anonymous' | 'indexed' | 'named'
parameterStyle?: 'anonymous' | 'indexed' | 'named',
castStyle?: CastStyle
}) {
if (options?.preset) {
const preset = options.preset
Expand All @@ -224,6 +234,8 @@ export class SqlPrintTokenParser implements SqlComponentVisitor<SqlPrintToken> {
end: options?.identifierEscape?.end ?? '"'
});

this.castStyle = options?.castStyle ?? 'standard';

this.handlers.set(ValueList.kind, (expr) => this.visitValueList(expr as ValueList));
this.handlers.set(ColumnReference.kind, (expr) => this.visitColumnReference(expr as ColumnReference));
this.handlers.set(QualifiedName.kind, (expr) => this.visitQualifiedName(expr as QualifiedName));
Expand Down Expand Up @@ -1310,9 +1322,23 @@ export class SqlPrintTokenParser implements SqlComponentVisitor<SqlPrintToken> {
private visitCastExpression(arg: CastExpression): SqlPrintToken {
const token = new SqlPrintToken(SqlPrintTokenType.container, '', SqlPrintTokenContainerType.CastExpression);

// Use PostgreSQL-specific :: casts only when the preset explicitly opts in.
if (this.castStyle === 'postgres') {
token.innerTokens.push(this.visit(arg.input));
token.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.operator, '::'));
token.innerTokens.push(this.visit(arg.castType));
return token;
}

// Default to ANSI-compliant CAST(expression AS type) syntax for broader compatibility.
token.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.keyword, 'cast'));
token.innerTokens.push(SqlPrintTokenParser.PAREN_OPEN_TOKEN);
token.innerTokens.push(this.visit(arg.input));
token.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.operator, '::'));
token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN);
token.innerTokens.push(new SqlPrintToken(SqlPrintTokenType.keyword, 'as'));
token.innerTokens.push(SqlPrintTokenParser.SPACE_TOKEN);
token.innerTokens.push(this.visit(arg.castType));
token.innerTokens.push(SqlPrintTokenParser.PAREN_CLOSE_TOKEN);

return token;
}
Expand Down
17 changes: 12 additions & 5 deletions packages/core/src/transformers/QueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { SetClause, SetClauseItem, FromClause, WhereClause, SelectClause, SelectItem, SourceAliasExpression, SourceExpression, SubQuerySource, WithClause, TableSource, UpdateClause, InsertClause, OrderByClause, DeleteClause, UsingClause } from '../models/Clause';
import { SetClause, SetClauseItem, FromClause, WhereClause, SelectClause, SelectItem, SourceAliasExpression, SourceExpression, SubQuerySource, WithClause, TableSource, UpdateClause, InsertClause, OrderByClause, DeleteClause } from '../models/Clause';
import { UpdateQuery } from '../models/UpdateQuery';
import { DeleteQuery } from '../models/DeleteQuery';
import { MergeQuery, MergeWhenClause, MergeUpdateAction, MergeDeleteAction, MergeInsertAction, MergeDoNothingAction } from '../models/MergeQuery';
import { BinaryExpression, ColumnReference, ValueList } from '../models/ValueComponent';
import { BinaryExpression, ColumnReference, InlineQuery, LiteralValue, UnaryExpression, ValueList } from '../models/ValueComponent';
import { SelectValueCollector } from './SelectValueCollector';
import { BinarySelectQuery, SelectQuery, SimpleSelectQuery, ValuesQuery } from "../models/SelectQuery";
import { CTECollector } from "./CTECollector";
Expand Down Expand Up @@ -309,12 +309,19 @@ export class QueryBuilder {

const withClause = QueryBuilder.extractWithClause(selectQuery);

const usingClause = new UsingClause([selectQuery.toSource(normalized.sourceAlias)]);
const whereClause = new WhereClause(QueryBuilder.buildEqualityPredicate(targetAlias, normalized.sourceAlias, predicateColumns));
// Build correlated EXISTS predicate instead of Postgres-specific USING clause.
const predicate = QueryBuilder.buildEqualityPredicate(targetAlias, normalized.sourceAlias, predicateColumns);
const sourceExpression = selectQuery.toSource(normalized.sourceAlias);
const existsSelectClause = new SelectClause([new SelectItem(new LiteralValue(1))]);
const existsSubquery = new SimpleSelectQuery({
selectClause: existsSelectClause,
fromClause: new FromClause(sourceExpression, null),
whereClause: new WhereClause(predicate)
});
const whereClause = new WhereClause(new UnaryExpression('exists', new InlineQuery(existsSubquery)));

return new DeleteQuery({
deleteClause,
usingClause,
whereClause,
withClause: withClause ?? undefined
});
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/transformers/SqlFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SqlPrintTokenParser, FormatterConfig, PRESETS } from '../parsers/SqlPrintTokenParser';
import { SqlPrintTokenParser, FormatterConfig, PRESETS, CastStyle } from '../parsers/SqlPrintTokenParser';
import { SqlPrinter, CommaBreakStyle, AndBreakStyle, OrBreakStyle } from './SqlPrinter';
import { IndentCharOption, NewlineOption } from './LinePrinter'; // Import types for compatibility
import { IdentifierEscapeOption, resolveIdentifierEscapeOption } from './FormatOptionResolver';
Expand Down Expand Up @@ -96,6 +96,8 @@ export interface SqlFormatterOptions extends BaseFormattingOptions {
parameterSymbol?: string | { start: string; end: string };
/** Style for parameter formatting */
parameterStyle?: 'anonymous' | 'indexed' | 'named';
/** Preferred CAST rendering style */
castStyle?: CastStyle;
}

/**
Expand Down Expand Up @@ -129,6 +131,7 @@ export class SqlFormatter {
identifierEscape: resolvedIdentifierEscape ?? presetConfig?.identifierEscape,
parameterSymbol: options.parameterSymbol ?? presetConfig?.parameterSymbol,
parameterStyle: options.parameterStyle ?? presetConfig?.parameterStyle,
castStyle: options.castStyle ?? presetConfig?.castStyle,
};

this.parser = new SqlPrintTokenParser({
Expand Down
2 changes: 1 addition & 1 deletion packages/core/tests/parsers/SqlPrintTokenParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ describe('SqlPrintTokenParser + SqlPrinter', () => {
const sql = printer.print(token);

// Assert
expect(sql).toBe('1::"pg_catalog".int4');
expect(sql).toBe('cast(1 as "pg_catalog".int4)');
});

it('should print string specifier', () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/tests/parsers/ValueParser.array-slice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('ValueParser - Array Slice Syntax (Implemented)', () => {

// Complex expressions with array slicing
["Function result with slice", "get_array()[1:2]", 'get_array()[1:2]'],
["Cast expression with slice", "(column::int[])[1:2]", '("column"::int[])[1:2]'],
["Cast expression with slice", "(column::int[])[1:2]", '(cast("column" as int[]))[1:2]'],
["Parenthesized expression with slice", "(a + b)[1:2]", '("a" + "b")[1:2]'],
])('%s: %s', (description, input, expected) => {
const value = ValueParser.parse(input);
Expand Down Expand Up @@ -84,7 +84,7 @@ describe('ValueParser - Array Slice Syntax (Implemented)', () => {
["Array function call", "ARRAY(SELECT 1)", "array(select 1)"],
["Parenthesized array", "(ARRAY[1,2,3])", "(array[1, 2, 3])"],
["Simple column reference", "column_name", '"column_name"'],
["PostgreSQL array literal", "'{1,2,3}'::int[]", "'{1,2,3}'::int[]"],
["PostgreSQL array literal", "'{1,2,3}'::int[]", "cast('{1,2,3}' as int[])"],
])('%s: %s', (description, input, expected) => {
const value = ValueParser.parse(input);
const formatted = formatter.format(value);
Expand All @@ -108,4 +108,4 @@ describe('ValueParser - Array Slice Syntax (Implemented)', () => {
expect(slice.endIndex).toBeTruthy();
});
});
});
});
Loading
Loading