@@ -346,7 +346,7 @@ function deriveRiverWidth(node: AST.Node): number {
346346 return width ;
347347 }
348348 case 'create_index' : {
349- let width = 'ON ' . length ;
349+ let width = 'CREATE ' . length ;
350350 if ( node . using ) width = Math . max ( width , 'USING' . length ) ;
351351 if ( node . include && node . include . length > 0 ) width = Math . max ( width , 'INCLUDE' . length ) ;
352352 if ( node . where ) width = Math . max ( width , 'WHERE' . length ) ;
@@ -904,7 +904,20 @@ function formatSelect(node: AST.SelectStatement, ctx: FormatContext): string {
904904 : '' ;
905905 const topStr = node . top ? ` ${ node . top } ` : '' ;
906906 const colStartCol = contentCol ( ctx ) + stringDisplayWidth ( distinctStr + topStr ) ;
907- const colStr = formatColumnList ( node . columns , colStartCol , ctx ) ;
907+ const isTsql = isTsqlDialect ( ctx . runtime . dialect ) ;
908+ const forceTsqlSubqueryIdentifierColumns = isTsql
909+ && ctx . isSubquery
910+ && node . columns . length >= 3
911+ && node . columns . every ( col => col . alias === undefined
912+ && ( ! col . leadingComments || col . leadingComments . length === 0 )
913+ && col . trailingComment === undefined
914+ && col . expr . type === 'identifier' ) ;
915+ const forceMultilineColumns =
916+ ( node . lockingClause && / ^ ( J S O N | X M L ) \b / i. test ( node . lockingClause . trim ( ) ) )
917+ || ( node . top && node . columns . length > 1 )
918+ || forceTsqlSubqueryIdentifierColumns
919+ || selectHasPivotOrJsonWith ( node ) ;
920+ const colStr = formatColumnList ( node . columns , colStartCol , ctx , forceMultilineColumns ) ;
908921 const firstColumnHasLeadingComments = ! ! ( node . columns [ 0 ] ?. leadingComments && node . columns [ 0 ] . leadingComments . length > 0 ) ;
909922 if ( firstColumnHasLeadingComments ) {
910923 lines . push ( selectKw + distinctStr + topStr ) ;
@@ -1045,17 +1058,26 @@ interface FormattedColumnPart {
10451058 comment ?: AST . CommentNode ;
10461059}
10471060
1048- function formatColumnList ( columns : readonly AST . ColumnExpr [ ] , firstColStartCol : number , ctx : FormatContext ) : string {
1061+ function formatColumnList (
1062+ columns : readonly AST . ColumnExpr [ ] ,
1063+ firstColStartCol : number ,
1064+ ctx : FormatContext ,
1065+ forceOnePerLine : boolean = false ,
1066+ ) : string {
10491067 if ( columns . length === 0 ) return '' ;
10501068
10511069 const parts = buildFormattedColumnParts ( columns , ctx ) ;
1070+ const cCol = contentCol ( ctx ) ;
1071+ if ( forceOnePerLine ) {
1072+ const indent = ' ' . repeat ( firstColStartCol ) ;
1073+ return formatColumnsOnePerLine ( parts , indent ) ;
1074+ }
1075+ const indent = ' ' . repeat ( cCol ) ;
10521076 const inlineResult = tryFormatInlineColumnList ( parts , columns , firstColStartCol , ctx ) ;
10531077 if ( inlineResult ) return inlineResult ;
10541078
10551079 const hasMultiLine = parts . some ( p => p . text . includes ( '\n' ) ) ;
10561080 const hasLeadingComments = parts . some ( p => ! ! ( p . leadingComments && p . leadingComments . length > 0 ) ) ;
1057- const cCol = contentCol ( ctx ) ;
1058- const indent = ' ' . repeat ( cCol ) ;
10591081
10601082 // If any multi-line expression, one-per-line
10611083 if ( hasMultiLine || hasLeadingComments ) {
@@ -1069,6 +1091,14 @@ function formatColumnList(columns: readonly AST.ColumnExpr[], firstColStartCol:
10691091 return formatColumnListWithGroups ( parts , indent , cCol , ctx ) ;
10701092}
10711093
1094+ function selectHasPivotOrJsonWith ( node : AST . SelectStatement ) : boolean {
1095+ const from = node . from ;
1096+ if ( from ?. pivotClause || from ?. jsonWithClause ) return true ;
1097+ if ( node . additionalFromItems ?. some ( item => ! ! ( item . pivotClause || item . jsonWithClause ) ) ) return true ;
1098+ if ( node . joins . some ( j => ! ! ( j . pivotClause || j . jsonWithClause ) ) ) return true ;
1099+ return false ;
1100+ }
1101+
10721102function buildFormattedColumnParts ( columns : readonly AST . ColumnExpr [ ] , ctx : FormatContext ) : FormattedColumnPart [ ] {
10731103 return columns . map ( col => {
10741104 let text = formatExprInSelect ( col . expr , contentCol ( ctx ) , ctx , ctx . outerColumnOffset || 0 , ctx . depth ) ;
@@ -1658,7 +1688,14 @@ function formatFromClause(from: AST.FromClause, ctx: FormatContext): string {
16581688 if ( from . ordinality ) {
16591689 result += ' WITH ORDINALITY' ;
16601690 }
1661- if ( from . alias ) {
1691+ if ( from . jsonWithClause ) {
1692+ const withLine = normalizeOpenJsonWithClause ( from . jsonWithClause ) ;
1693+ const alias = from . alias
1694+ ? ' AS ' + formatAlias ( from . alias )
1695+ + ( from . aliasColumns && from . aliasColumns . length > 0 ? '(' + from . aliasColumns . join ( ', ' ) + ')' : '' )
1696+ : '' ;
1697+ result += '\n' + ' ' . repeat ( baseCol ) + withLine + alias ;
1698+ } else if ( from . alias ) {
16621699 result += ' AS ' + formatAlias ( from . alias ) ;
16631700 if ( from . aliasColumns && from . aliasColumns . length > 0 ) {
16641701 result += '(' + from . aliasColumns . join ( ', ' ) + ')' ;
@@ -1728,6 +1765,15 @@ function normalizePivotClauseText(text: string): string {
17281765 . replace ( / \b I N \s * \( / gi, 'IN (' ) ;
17291766}
17301767
1768+ function normalizeOpenJsonWithClause ( text : string ) : string {
1769+ const normalized = text . replace ( / \b W I T H \s * \( / gi, 'WITH (' ) ;
1770+ // OPENJSON schema clauses read well with lowercased type names.
1771+ return normalized . replace (
1772+ / \b ( B I G I N T | I N T | S M A L L I N T | T I N Y I N T | B I T | M O N E Y | S M A L L M O N E Y | D E C I M A L | N U M E R I C | F L O A T | R E A L | D A T E | D A T E T I M E 2 | D A T E T I M E O F F S E T | N V A R C H A R | V A R C H A R | N C H A R | C H A R | V A R B I N A R Y | B I N A R Y ) \b / g,
1773+ m => m . toLowerCase ( ) ,
1774+ ) ;
1775+ }
1776+
17311777// ─── JOIN ────────────────────────────────────────────────────────────
17321778
17331779function formatJoin ( join : AST . JoinClause , ctx : FormatContext , needsBlank : boolean ) : string {
@@ -1825,7 +1871,14 @@ function formatJoinTable(join: AST.JoinClause, tableStartCol: number, runtime: F
18251871 if ( join . only ) result = 'ONLY ' + result ;
18261872 if ( join . lateral ) result = 'LATERAL ' + result ;
18271873 if ( join . ordinality ) result += ' WITH ORDINALITY' ;
1828- if ( join . alias ) {
1874+ if ( join . jsonWithClause ) {
1875+ const withLine = normalizeOpenJsonWithClause ( join . jsonWithClause ) ;
1876+ const alias = join . alias
1877+ ? ' AS ' + formatAlias ( join . alias )
1878+ + ( join . aliasColumns && join . aliasColumns . length > 0 ? '(' + join . aliasColumns . join ( ', ' ) + ')' : '' )
1879+ : '' ;
1880+ result += '\n' + ' ' . repeat ( tableStartCol ) + withLine + alias ;
1881+ } else if ( join . alias ) {
18291882 result += ' AS ' + formatAlias ( join . alias ) ;
18301883 if ( join . aliasColumns && join . aliasColumns . length > 0 ) {
18311884 result += '(' + join . aliasColumns . join ( ', ' ) + ')' ;
@@ -2615,6 +2668,10 @@ function formatInsert(node: AST.InsertStatement, ctx: FormatContext): string {
26152668 lines . push ( header ) ;
26162669 }
26172670
2671+ if ( node . outputClause ) {
2672+ lines . push ( rightAlign ( 'OUTPUT' , dmlCtx ) + ' ' + node . outputClause . trim ( ) ) ;
2673+ }
2674+
26182675 if ( node . valueClauseLeadingComments && node . valueClauseLeadingComments . length > 0 ) {
26192676 emitComments ( node . valueClauseLeadingComments , lines ) ;
26202677 }
@@ -2772,7 +2829,11 @@ function formatUpdate(node: AST.UpdateStatement, ctx: FormatContext): string {
27722829 emitComments ( node . leadingComments , lines ) ;
27732830
27742831 const updateTargets : string [ ] = [ ] ;
2775- updateTargets . push ( lowerIdent ( node . table ) + ( node . alias ? ' AS ' + formatAlias ( node . alias ) : '' ) ) ;
2832+ updateTargets . push (
2833+ lowerIdent ( node . table )
2834+ + ( node . alias ? ' AS ' + formatAlias ( node . alias ) : '' )
2835+ + ( node . targetHint ? ' ' + node . targetHint : '' )
2836+ ) ;
27762837 if ( node . additionalTables && node . additionalTables . length > 0 ) {
27772838 for ( const tableRef of node . additionalTables ) {
27782839 updateTargets . push (
@@ -2885,6 +2946,10 @@ function formatDelete(node: AST.DeleteStatement, ctx: FormatContext): string {
28852946 } else {
28862947 lines . push ( deleteKw ) ;
28872948 }
2949+
2950+ if ( node . outputClause ) {
2951+ lines . push ( rightAlign ( 'OUTPUT' , dmlCtx ) + ' ' + node . outputClause . trim ( ) ) ;
2952+ }
28882953 lines . push ( rightAlign ( 'FROM' , dmlCtx ) + ' ' + lowerIdent ( node . from ) + ( node . alias ? ' AS ' + formatAlias ( node . alias ) : '' ) ) ;
28892954
28902955 if ( node . fromJoins && node . fromJoins . length > 0 ) {
@@ -2998,7 +3063,7 @@ function formatCreateIndex(node: AST.CreateIndexStatement, ctx: FormatContext):
29983063 const lines : string [ ] = [ ] ;
29993064 emitComments ( node . leadingComments , lines ) ;
30003065
3001- let header = 'CREATE' ;
3066+ let header = rightAlign ( 'CREATE' , idxCtx ) ;
30023067 if ( node . unique ) header += ' UNIQUE' ;
30033068 if ( node . clustered ) header += ' ' + node . clustered ;
30043069 header += ' INDEX' ;
@@ -3294,7 +3359,7 @@ function formatMerge(node: AST.MergeStatement, ctx: FormatContext): string {
32943359 const target = node . target . table + ( node . target . alias ? ' AS ' + node . target . alias : '' ) ;
32953360 const sourceTable = typeof node . source . table === 'string'
32963361 ? node . source . table
3297- : formatExpr ( node . source . table , 0 , mergeCtx . runtime ) ;
3362+ : formatExprAtColumn ( node . source . table , contentCol ( mergeCtx ) , mergeCtx . runtime ) ;
32983363 const source = sourceTable + ( node . source . alias ? ' AS ' + node . source . alias : '' ) ;
32993364
33003365 lines . push ( rightAlign ( 'MERGE' , mergeCtx ) + ' INTO ' + target ) ;
@@ -3306,7 +3371,7 @@ function formatMerge(node: AST.MergeStatement, ctx: FormatContext): string {
33063371 const actionPad = contentPad ( mergeCtx ) ;
33073372
33083373 for ( const wc of node . whenClauses ) {
3309- const branch = wc . matched ? 'MATCHED' : 'NOT MATCHED' ;
3374+ const branch = wc . matched ? 'MATCHED' : ( wc . matchKind ?? 'NOT MATCHED' ) ;
33103375 const cond = wc . condition ? ' AND ' + formatExpr ( wc . condition , 0 , mergeCtx . runtime ) : '' ;
33113376 lines . push ( rightAlign ( 'WHEN' , mergeCtx ) + ' ' + branch + cond + ' THEN' ) ;
33123377
@@ -3342,6 +3407,11 @@ function formatMerge(node: AST.MergeStatement, ctx: FormatContext): string {
33423407 }
33433408 }
33443409
3410+ if ( node . outputClause ) {
3411+ lines . push ( 'OUTPUT ' + node . outputClause . trim ( ) + ';' ) ;
3412+ return lines . join ( '\n' ) ;
3413+ }
3414+
33453415 lines [ lines . length - 1 ] += ';' ;
33463416 return lines . join ( '\n' ) ;
33473417}
@@ -3780,6 +3850,10 @@ function isMySqlDialect(dialect?: SQLDialect): boolean {
37803850 return dialect === 'mysql' || ( typeof dialect === 'object' && dialect . name === 'mysql' ) ;
37813851}
37823852
3853+ function isTsqlDialect ( dialect ?: SQLDialect ) : boolean {
3854+ return dialect === 'tsql' || ( typeof dialect === 'object' && dialect . name === 'tsql' ) ;
3855+ }
3856+
37833857function createTableElementIndent (
37843858 elem : AST . TableElement ,
37853859 tableConstraintIndent : string ,
0 commit comments