prettier-plugin-tsql 0.6.5 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,46 @@
1
1
  import { keyword, getDensity, hardSep, softSep, hardline, join, group, indent, line, softline, fill, appendTrailingLines, parenList, aliasDoc, } from '../_core/printer/utils.js';
2
2
  import { prop, propArr, propStr, propBool, schemaObjectName, assignmentOp } from './helpers.js';
3
3
  // ---------------------------------------------------------------------------
4
+ // Module-level lookup tables (created once, not per call)
5
+ // ---------------------------------------------------------------------------
6
+ const BINARY_OP_MAP = {
7
+ Add: '+',
8
+ Subtract: '-',
9
+ Multiply: '*',
10
+ Divide: '/',
11
+ Modulo: '%',
12
+ BitwiseAnd: '&',
13
+ BitwiseOr: '|',
14
+ BitwiseXor: '^',
15
+ Concatenate: '+',
16
+ Concat: '||',
17
+ };
18
+ const CMP_OP_MAP = {
19
+ Equals: '=',
20
+ NotEqualToBrackets: '<>',
21
+ NotEqualToExclamation: '!=',
22
+ GreaterThan: '>',
23
+ LessThan: '<',
24
+ GreaterThanOrEqualTo: '>=',
25
+ LessThanOrEqualTo: '<=',
26
+ LeftOuterJoin: '*=',
27
+ RightOuterJoin: '=*',
28
+ NotLessThan: '!<',
29
+ NotGreaterThan: '!>',
30
+ };
31
+ const JOIN_TYPE_MAP = {
32
+ Inner: 'INNER JOIN',
33
+ LeftOuter: 'LEFT JOIN',
34
+ RightOuter: 'RIGHT JOIN',
35
+ FullOuter: 'FULL JOIN',
36
+ };
37
+ const JOIN_TYPE_WORD = {
38
+ Inner: 'INNER',
39
+ LeftOuter: 'LEFT',
40
+ RightOuter: 'RIGHT',
41
+ FullOuter: 'FULL',
42
+ };
43
+ // ---------------------------------------------------------------------------
4
44
  // Scalar expressions
5
45
  // ---------------------------------------------------------------------------
6
46
  export function printExpression(node, opts, printFn) {
@@ -261,16 +301,25 @@ function printFunctionCall(node, opts, printFn) {
261
301
  }
262
302
  return argsDoc;
263
303
  }
264
- // Flatten a left-recursive + (Add/Concatenate) chain into its leaf terms.
304
+ // Flatten a left-recursive chain of the given operators into leaf terms.
265
305
  // Stops at any other operator so e.g. the `a * b` in `a * b + c` stays grouped.
266
- function collectConcatChain(node) {
306
+ // Keeping ops separate (Add/Concatenate vs Concat) prevents mixing + and || chains.
307
+ function collectBinaryChain(node, ...ops) {
267
308
  const op = propStr(node, 'operator');
268
- if (node.type !== 'BinaryExpression' || (op !== 'Add' && op !== 'Concatenate')) {
309
+ if (node.type !== 'BinaryExpression' || !ops.includes(op ?? '')) {
269
310
  return [node];
270
311
  }
271
312
  const left = prop(node, 'left');
272
313
  const right = prop(node, 'right');
273
- return [...(left ? collectConcatChain(left) : []), ...(right ? [right] : [])];
314
+ return [...(left ? collectBinaryChain(left, ...ops) : []), ...(right ? [right] : [])];
315
+ }
316
+ function buildFillChain(termDocs, sep) {
317
+ const parts = [termDocs[0]];
318
+ for (let i = 1; i < termDocs.length; i++) {
319
+ parts.push(indent([line, sep]));
320
+ parts.push(termDocs[i]);
321
+ }
322
+ return fill(parts);
274
323
  }
275
324
  function printBinaryExpr(node, opts, printFn) {
276
325
  const op = propStr(node, 'operator') ?? '+';
@@ -279,14 +328,13 @@ function printBinaryExpr(node, opts, printFn) {
279
328
  // term would overflow. Flat: "a + b + c". Filling: "a + b\n+ c + d".
280
329
  // This prevents Prettier from descending into function args to break there.
281
330
  if (op === 'Add' || op === 'Concatenate') {
282
- const terms = collectConcatChain(node);
283
- const termDocs = terms.map((t) => printExpression(t, opts, printFn));
284
- const parts = [termDocs[0]];
285
- for (let i = 1; i < termDocs.length; i++) {
286
- parts.push([line, '+ ']);
287
- parts.push(termDocs[i]);
288
- }
289
- return fill(parts);
331
+ const terms = collectBinaryChain(node, 'Add', 'Concatenate');
332
+ return buildFillChain(terms.map((t) => printExpression(t, opts, printFn)), '+ ');
333
+ }
334
+ // || chains work the same way but keep their own operator symbol.
335
+ if (op === 'Concat') {
336
+ const terms = collectBinaryChain(node, 'Concat');
337
+ return buildFillChain(terms.map((t) => printExpression(t, opts, printFn)), '|| ');
290
338
  }
291
339
  const left = prop(node, 'left');
292
340
  const right = prop(node, 'right');
@@ -300,18 +348,7 @@ function printBinaryExpr(node, opts, printFn) {
300
348
  ]);
301
349
  }
302
350
  function mapBinaryOp(op) {
303
- const map = {
304
- Add: '+',
305
- Subtract: '-',
306
- Multiply: '*',
307
- Divide: '/',
308
- Modulo: '%',
309
- BitwiseAnd: '&',
310
- BitwiseOr: '|',
311
- BitwiseXor: '^',
312
- Concatenate: '+',
313
- };
314
- return map[op] ?? op;
351
+ return BINARY_OP_MAP[op] ?? op;
315
352
  }
316
353
  function printUnaryExpr(node, opts, printFn) {
317
354
  const expr = prop(node, 'expr');
@@ -511,6 +548,8 @@ export function printQueryExpression(node, opts, printFn) {
511
548
  }
512
549
  function printQuerySpec(node, opts, printFn) {
513
550
  const density = getDensity(opts);
551
+ const compact = density === 'compact';
552
+ const sep = compact ? line : hardline;
514
553
  const uniqueRowFilter = propStr(node, 'uniqueRowFilter');
515
554
  const top = prop(node, 'top');
516
555
  const selectElements = propArr(node, 'selectElements');
@@ -528,17 +567,16 @@ function printQuerySpec(node, opts, printFn) {
528
567
  const selectKw = uniqueRowFilter === 'Distinct' ? keyword('SELECT DISTINCT', opts) : keyword('SELECT', opts);
529
568
  const topDoc = top ? printTop(top, opts, printFn) : null;
530
569
  const colDocs = selectElements.map((se) => printExpression(se, opts, printFn));
531
- if (density === 'compact') {
570
+ const parts = [selectKw, ...(topDoc ? [' ', topDoc] : [])];
571
+ if (compact) {
532
572
  // Compact: fill-pack each list clause — as many items per line as fit, wrapping only when needed.
533
573
  // indent() ensures wrapped lines are indented one level under the keyword.
534
574
  const colList = indent(fill(colDocs.flatMap((d, i) => (i === 0 ? [d] : [[',', line], d]))));
535
- const parts = [selectKw, ...(topDoc ? [' ', topDoc] : []), ' ', colList];
536
- if (intoTarget) {
575
+ parts.push(' ', colList);
576
+ if (intoTarget)
537
577
  parts.push(line, keyword('INTO', opts), ' ', schemaObjectName(intoTarget));
538
- }
539
578
  if (from) {
540
- const tableRefs = propArr(from, 'tableReferences');
541
- const fromDocs = tableRefs.map((tr) => printTableRef(tr, opts, printFn));
579
+ const fromDocs = propArr(from, 'tableReferences').map((tr) => printTableRef(tr, opts, printFn));
542
580
  // Try to keep FROM on one line; if too long, each join on its own line
543
581
  parts.push(line, keyword('FROM', opts), ' ', group(indent(join(softSep(opts), fromDocs))));
544
582
  }
@@ -547,108 +585,89 @@ function printQuerySpec(node, opts, printFn) {
547
585
  parts.push(line, keyword('WHERE', opts), group([indent([line, boolWithTrailing(where, fillBoolChain(where, opts, printFn))])]));
548
586
  }
549
587
  if (groupBy) {
550
- const elems = propArr(groupBy, 'elements');
551
- const elemDocs = elems.map((e) => printExpression(e, opts, printFn));
588
+ const elemDocs = propArr(groupBy, 'elements').map((e) => printExpression(e, opts, printFn));
552
589
  parts.push(line, keyword('GROUP BY', opts), ' ', indent(fill(elemDocs.flatMap((d, i) => (i === 0 ? [d] : [[',', line], d])))));
553
590
  }
554
591
  if (having) {
555
592
  parts.push(line, keyword('HAVING', opts), group([indent([line, boolWithTrailing(having, fillBoolChain(having, opts, printFn))])]));
556
593
  }
557
- if (orderBy) {
558
- parts.push(line, printOrderByClause(orderBy, opts, printFn));
559
- }
560
- if (offsetNode) {
561
- parts.push(line, keyword('OFFSET', opts), ' ', printExpression(offsetNode, opts, printFn), ' ', keyword('ROWS', opts));
562
- if (fetchNode) {
563
- parts.push(line, keyword('FETCH NEXT', opts), ' ', printExpression(fetchNode, opts, printFn), ' ', keyword('ROWS ONLY', opts));
564
- }
565
- }
566
- if (windowDefs.length > 0) {
567
- parts.push(line, printWindowClause(windowDefs, opts, printFn));
568
- }
569
- if (forClause) {
570
- parts.push(line, printForClause(forClause, opts));
571
- }
572
- return group(parts);
573
- }
574
- // Standard / Spacious: single column stays inline; multiple each on own line.
575
- // A CASE expression always expands to multiple lines, so force it onto its own indented line.
576
- const singleExprType = (prop(selectElements[0], 'expression') ?? selectElements[0])?.type;
577
- const colList = density === 'standard' && colDocs.length === 1 && singleExprType !== 'CaseExpression'
578
- ? [' ', colDocs[0]]
579
- : indent([hardline, join(hardSep(opts), colDocs)]);
580
- const parts = [selectKw, ...(topDoc ? [' ', topDoc] : []), colList];
581
- // SELECT INTO target appears after the column list and before the FROM clause
582
- if (intoTarget) {
583
- parts.push(hardline, keyword('INTO', opts), ' ', schemaObjectName(intoTarget));
584
- }
585
- if (from) {
586
- const tableRefs = propArr(from, 'tableReferences');
587
- const fromDocs = tableRefs.map((tr) => printTableRef(tr, opts, printFn));
588
- // standard: single table (no joins) stays inline; multiple/joins each on own line
589
- // InlineDerivedTable uses a softline so it stays inline when it fits but breaks
590
- // FROM onto its own line (with the values block indented) when it doesn't
591
- const singleTable = density === 'standard' &&
592
- tableRefs.length === 1 &&
593
- tableRefs[0].type !== 'QualifiedJoin' &&
594
- tableRefs[0].type !== 'UnqualifiedJoin' &&
595
- tableRefs[0].type !== 'InlineDerivedTable';
596
- if (tableRefs.length === 1 && tableRefs[0].type === 'InlineDerivedTable') {
597
- parts.push(hardline, group([keyword('FROM', opts), indent([line, fromDocs[0]])]));
598
- }
599
- else if (singleTable) {
600
- parts.push(hardline, keyword('FROM', opts), ' ', fromDocs[0]);
601
- }
602
- else {
603
- parts.push(hardline, keyword('FROM', opts), indent([hardline, join(hardSep(opts), fromDocs)]));
604
- }
605
- }
606
- if (where) {
607
- // standard: single predicate inline; multiple each on own line
608
- // spacious: always indented
609
- const inline = density === 'standard' && where.type !== 'BooleanBinary';
610
- if (inline) {
611
- parts.push(hardline, keyword('WHERE', opts), ' ', boolWithTrailing(where, printBoolExpr(where, opts, printFn)));
612
- }
613
- else {
614
- parts.push(hardline, keyword('WHERE', opts), indent([hardline, boolWithTrailing(where, printBoolExpr(where, opts, printFn))]));
615
- }
616
594
  }
617
- if (groupBy) {
618
- const elems = propArr(groupBy, 'elements');
619
- const elemDocs = elems.map((e) => printExpression(e, opts, printFn));
620
- const inline = density === 'standard' && elems.length === 1;
621
- if (inline) {
622
- parts.push(hardline, keyword('GROUP BY', opts), ' ', elemDocs[0]);
595
+ else {
596
+ // Standard / Spacious: single column stays inline; multiple each on own line.
597
+ // A CASE expression always expands to multiple lines, so force it onto its own indented line.
598
+ const singleExprType = (prop(selectElements[0], 'expression') ?? selectElements[0])?.type;
599
+ const colList = density === 'standard' && colDocs.length === 1 && singleExprType !== 'CaseExpression'
600
+ ? [' ', colDocs[0]]
601
+ : indent([hardline, join(hardSep(opts), colDocs)]);
602
+ parts.push(colList);
603
+ // SELECT INTO target appears after the column list and before the FROM clause
604
+ if (intoTarget)
605
+ parts.push(hardline, keyword('INTO', opts), ' ', schemaObjectName(intoTarget));
606
+ if (from) {
607
+ const tableRefs = propArr(from, 'tableReferences');
608
+ const fromDocs = tableRefs.map((tr) => printTableRef(tr, opts, printFn));
609
+ // standard: single table (no joins) stays inline; multiple/joins each on own line
610
+ // InlineDerivedTable uses a softline so it stays inline when it fits but breaks
611
+ // FROM onto its own line (with the values block indented) when it doesn't
612
+ const singleTable = density === 'standard' &&
613
+ tableRefs.length === 1 &&
614
+ tableRefs[0].type !== 'QualifiedJoin' &&
615
+ tableRefs[0].type !== 'UnqualifiedJoin' &&
616
+ tableRefs[0].type !== 'InlineDerivedTable';
617
+ if (tableRefs.length === 1 && tableRefs[0].type === 'InlineDerivedTable') {
618
+ parts.push(hardline, group([keyword('FROM', opts), indent([line, fromDocs[0]])]));
619
+ }
620
+ else if (singleTable) {
621
+ parts.push(hardline, keyword('FROM', opts), ' ', fromDocs[0]);
622
+ }
623
+ else {
624
+ parts.push(hardline, keyword('FROM', opts), indent([hardline, join(hardSep(opts), fromDocs)]));
625
+ }
623
626
  }
624
- else {
625
- parts.push(hardline, keyword('GROUP BY', opts), indent([hardline, join(hardSep(opts), elemDocs)]));
627
+ if (where) {
628
+ // standard: single predicate inline; multiple each on own line
629
+ // spacious: always indented
630
+ const inline = density === 'standard' && where.type !== 'BooleanBinary';
631
+ if (inline) {
632
+ parts.push(hardline, keyword('WHERE', opts), ' ', boolWithTrailing(where, printBoolExpr(where, opts, printFn)));
633
+ }
634
+ else {
635
+ parts.push(hardline, keyword('WHERE', opts), indent([hardline, boolWithTrailing(where, printBoolExpr(where, opts, printFn))]));
636
+ }
626
637
  }
627
- }
628
- if (having) {
629
- const inline = density === 'standard' && having.type !== 'BooleanBinary';
630
- if (inline) {
631
- parts.push(hardline, keyword('HAVING', opts), ' ', boolWithTrailing(having, printBoolExpr(having, opts, printFn)));
638
+ if (groupBy) {
639
+ const elems = propArr(groupBy, 'elements');
640
+ const elemDocs = elems.map((e) => printExpression(e, opts, printFn));
641
+ if (density === 'standard' && elems.length === 1) {
642
+ parts.push(hardline, keyword('GROUP BY', opts), ' ', elemDocs[0]);
643
+ }
644
+ else {
645
+ parts.push(hardline, keyword('GROUP BY', opts), indent([hardline, join(hardSep(opts), elemDocs)]));
646
+ }
632
647
  }
633
- else {
634
- parts.push(hardline, keyword('HAVING', opts), indent([hardline, boolWithTrailing(having, printBoolExpr(having, opts, printFn))]));
648
+ if (having) {
649
+ const inline = density === 'standard' && having.type !== 'BooleanBinary';
650
+ if (inline) {
651
+ parts.push(hardline, keyword('HAVING', opts), ' ', boolWithTrailing(having, printBoolExpr(having, opts, printFn)));
652
+ }
653
+ else {
654
+ parts.push(hardline, keyword('HAVING', opts), indent([hardline, boolWithTrailing(having, printBoolExpr(having, opts, printFn))]));
655
+ }
635
656
  }
636
657
  }
637
- if (orderBy) {
638
- parts.push(hardline, printOrderByClause(orderBy, opts, printFn));
639
- }
658
+ // Tail clauses — same layout intent for all densities, sep varies
659
+ if (orderBy)
660
+ parts.push(sep, printOrderByClause(orderBy, opts, printFn));
640
661
  if (offsetNode) {
641
- parts.push(hardline, keyword('OFFSET', opts), ' ', printExpression(offsetNode, opts, printFn), ' ', keyword('ROWS', opts));
662
+ parts.push(sep, keyword('OFFSET', opts), ' ', printExpression(offsetNode, opts, printFn), ' ', keyword('ROWS', opts));
642
663
  if (fetchNode) {
643
- parts.push(hardline, keyword('FETCH NEXT', opts), ' ', printExpression(fetchNode, opts, printFn), ' ', keyword('ROWS ONLY', opts));
664
+ parts.push(sep, keyword('FETCH NEXT', opts), ' ', printExpression(fetchNode, opts, printFn), ' ', keyword('ROWS ONLY', opts));
644
665
  }
645
666
  }
646
- if (windowDefs.length > 0) {
647
- parts.push(hardline, printWindowClause(windowDefs, opts, printFn));
648
- }
649
- if (forClause) {
650
- parts.push(hardline, printForClause(forClause, opts));
651
- }
667
+ if (windowDefs.length > 0)
668
+ parts.push(sep, printWindowClause(windowDefs, opts, printFn));
669
+ if (forClause)
670
+ parts.push(sep, printForClause(forClause, opts));
652
671
  return group(parts);
653
672
  }
654
673
  function printTop(node, opts, printFn) {
@@ -833,25 +852,14 @@ export function printBoolExpr(node, opts, printFn) {
833
852
  return printDistinctPredicate(node, opts, printFn);
834
853
  case 'SubqueryComparisonPredicate':
835
854
  return printSubqueryComparison(node, opts, printFn);
855
+ case 'RegexpLikePredicate':
856
+ return printRegexpLikePredicate(node, opts, printFn);
836
857
  default:
837
858
  return node.text ?? `/* ${node.type} */`;
838
859
  }
839
860
  }
840
861
  function cmpOp(op) {
841
- const map = {
842
- Equals: '=',
843
- NotEqualToBrackets: '<>',
844
- NotEqualToExclamation: '!=',
845
- GreaterThan: '>',
846
- LessThan: '<',
847
- GreaterThanOrEqualTo: '>=',
848
- LessThanOrEqualTo: '<=',
849
- LeftOuterJoin: '*=',
850
- RightOuterJoin: '=*',
851
- NotLessThan: '!<',
852
- NotGreaterThan: '!>',
853
- };
854
- return map[op] ?? op;
862
+ return CMP_OP_MAP[op] ?? op;
855
863
  }
856
864
  function printBoolComparison(node, opts, printFn) {
857
865
  const left = prop(node, 'left');
@@ -1006,6 +1014,16 @@ function printInPredicate(node, opts, printFn) {
1006
1014
  const valueDocs = values.map((v) => printExpression(v, opts, printFn));
1007
1015
  return [...lhs, group([' (', indent([softline, join([',', line], valueDocs)]), softline, ')'])];
1008
1016
  }
1017
+ function printRegexpLikePredicate(node, opts, printFn) {
1018
+ const args = [
1019
+ prop(node, 'value') ? printExpression(prop(node, 'value'), opts, printFn) : '',
1020
+ prop(node, 'pattern') ? printExpression(prop(node, 'pattern'), opts, printFn) : '',
1021
+ ];
1022
+ const flags = prop(node, 'flags');
1023
+ if (flags)
1024
+ args.push(printExpression(flags, opts, printFn));
1025
+ return [keyword('regexp_like', opts), parenList(args)];
1026
+ }
1009
1027
  function printLikePredicate(node, opts, printFn) {
1010
1028
  const expr = prop(node, 'expr');
1011
1029
  const pattern = prop(node, 'pattern');
@@ -1028,12 +1046,26 @@ function printExistsPredicate(node, opts, printFn) {
1028
1046
  const subquery = prop(node, 'subquery');
1029
1047
  if (!subquery)
1030
1048
  return keyword('EXISTS', opts) + '()';
1031
- const sep = getDensity(opts) === 'compact' ? softline : hardline;
1049
+ const density = getDensity(opts);
1050
+ if (density === 'spacious') {
1051
+ // Spacious: always expand
1052
+ return group([
1053
+ keyword('EXISTS', opts),
1054
+ ' (',
1055
+ indent([hardline, printQueryExpression(subquery, opts, printFn)]),
1056
+ hardline,
1057
+ ')',
1058
+ ]);
1059
+ }
1060
+ // compact + standard: render the inner query in compact mode so the group's
1061
+ // softline can keep simple subqueries inline. Complex ones still wrap because
1062
+ // their content exceeds printWidth and the group breaks.
1063
+ const compactOpts = { ...opts, sqlDensity: 'compact' };
1032
1064
  return group([
1033
1065
  keyword('EXISTS', opts),
1034
1066
  ' (',
1035
- indent([sep, printQueryExpression(subquery, opts, printFn)]),
1036
- sep,
1067
+ indent([softline, printQueryExpression(subquery, compactOpts, printFn)]),
1068
+ softline,
1037
1069
  ')',
1038
1070
  ]);
1039
1071
  }
@@ -1130,22 +1162,11 @@ function printNamedTableRef(node, opts, printFn) {
1130
1162
  return [nameDoc, temporalDoc, aliasPart, sampleDoc, hintsDoc];
1131
1163
  }
1132
1164
  function joinTypeKeyword(jt, opts, hint) {
1133
- const typeMap = {
1134
- Inner: 'INNER',
1135
- LeftOuter: 'LEFT',
1136
- RightOuter: 'RIGHT',
1137
- FullOuter: 'FULL',
1138
- };
1139
- const typeWord = typeMap[jt] ?? jt.toUpperCase();
1140
- if (hint)
1165
+ if (hint) {
1166
+ const typeWord = JOIN_TYPE_WORD[jt] ?? jt.toUpperCase();
1141
1167
  return keyword(`${typeWord} ${hint} JOIN`, opts);
1142
- const noHintMap = {
1143
- Inner: 'INNER JOIN',
1144
- LeftOuter: 'LEFT JOIN',
1145
- RightOuter: 'RIGHT JOIN',
1146
- FullOuter: 'FULL JOIN',
1147
- };
1148
- return keyword(noHintMap[jt] ?? `${typeWord} JOIN`, opts);
1168
+ }
1169
+ return keyword(JOIN_TYPE_MAP[jt] ?? `${(JOIN_TYPE_WORD[jt] ?? jt.toUpperCase())} JOIN`, opts);
1149
1170
  }
1150
1171
  /**
1151
1172
  * Walk the rightmost path of an AST subtree (props in reverse insertion order)