prettier-plugin-tsql 0.6.6 → 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,17 +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
- // Flat: "a + b + c". Wrapping: "a + b\n + c + d".
285
- // Using fill (not group) so outer flat-mode contexts don't suppress breaks.
286
- // The indent is on the separator so continuation lines are indented one level.
287
- const parts = [termDocs[0]];
288
- for (let i = 1; i < termDocs.length; i++) {
289
- parts.push(indent([line, '+ ']));
290
- parts.push(termDocs[i]);
291
- }
292
- 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)), '|| ');
293
338
  }
294
339
  const left = prop(node, 'left');
295
340
  const right = prop(node, 'right');
@@ -303,18 +348,7 @@ function printBinaryExpr(node, opts, printFn) {
303
348
  ]);
304
349
  }
305
350
  function mapBinaryOp(op) {
306
- const map = {
307
- Add: '+',
308
- Subtract: '-',
309
- Multiply: '*',
310
- Divide: '/',
311
- Modulo: '%',
312
- BitwiseAnd: '&',
313
- BitwiseOr: '|',
314
- BitwiseXor: '^',
315
- Concatenate: '+',
316
- };
317
- return map[op] ?? op;
351
+ return BINARY_OP_MAP[op] ?? op;
318
352
  }
319
353
  function printUnaryExpr(node, opts, printFn) {
320
354
  const expr = prop(node, 'expr');
@@ -514,6 +548,8 @@ export function printQueryExpression(node, opts, printFn) {
514
548
  }
515
549
  function printQuerySpec(node, opts, printFn) {
516
550
  const density = getDensity(opts);
551
+ const compact = density === 'compact';
552
+ const sep = compact ? line : hardline;
517
553
  const uniqueRowFilter = propStr(node, 'uniqueRowFilter');
518
554
  const top = prop(node, 'top');
519
555
  const selectElements = propArr(node, 'selectElements');
@@ -531,17 +567,16 @@ function printQuerySpec(node, opts, printFn) {
531
567
  const selectKw = uniqueRowFilter === 'Distinct' ? keyword('SELECT DISTINCT', opts) : keyword('SELECT', opts);
532
568
  const topDoc = top ? printTop(top, opts, printFn) : null;
533
569
  const colDocs = selectElements.map((se) => printExpression(se, opts, printFn));
534
- if (density === 'compact') {
570
+ const parts = [selectKw, ...(topDoc ? [' ', topDoc] : [])];
571
+ if (compact) {
535
572
  // Compact: fill-pack each list clause — as many items per line as fit, wrapping only when needed.
536
573
  // indent() ensures wrapped lines are indented one level under the keyword.
537
574
  const colList = indent(fill(colDocs.flatMap((d, i) => (i === 0 ? [d] : [[',', line], d]))));
538
- const parts = [selectKw, ...(topDoc ? [' ', topDoc] : []), ' ', colList];
539
- if (intoTarget) {
575
+ parts.push(' ', colList);
576
+ if (intoTarget)
540
577
  parts.push(line, keyword('INTO', opts), ' ', schemaObjectName(intoTarget));
541
- }
542
578
  if (from) {
543
- const tableRefs = propArr(from, 'tableReferences');
544
- const fromDocs = tableRefs.map((tr) => printTableRef(tr, opts, printFn));
579
+ const fromDocs = propArr(from, 'tableReferences').map((tr) => printTableRef(tr, opts, printFn));
545
580
  // Try to keep FROM on one line; if too long, each join on its own line
546
581
  parts.push(line, keyword('FROM', opts), ' ', group(indent(join(softSep(opts), fromDocs))));
547
582
  }
@@ -550,108 +585,89 @@ function printQuerySpec(node, opts, printFn) {
550
585
  parts.push(line, keyword('WHERE', opts), group([indent([line, boolWithTrailing(where, fillBoolChain(where, opts, printFn))])]));
551
586
  }
552
587
  if (groupBy) {
553
- const elems = propArr(groupBy, 'elements');
554
- const elemDocs = elems.map((e) => printExpression(e, opts, printFn));
588
+ const elemDocs = propArr(groupBy, 'elements').map((e) => printExpression(e, opts, printFn));
555
589
  parts.push(line, keyword('GROUP BY', opts), ' ', indent(fill(elemDocs.flatMap((d, i) => (i === 0 ? [d] : [[',', line], d])))));
556
590
  }
557
591
  if (having) {
558
592
  parts.push(line, keyword('HAVING', opts), group([indent([line, boolWithTrailing(having, fillBoolChain(having, opts, printFn))])]));
559
593
  }
560
- if (orderBy) {
561
- parts.push(line, printOrderByClause(orderBy, opts, printFn));
562
- }
563
- if (offsetNode) {
564
- parts.push(line, keyword('OFFSET', opts), ' ', printExpression(offsetNode, opts, printFn), ' ', keyword('ROWS', opts));
565
- if (fetchNode) {
566
- parts.push(line, keyword('FETCH NEXT', opts), ' ', printExpression(fetchNode, opts, printFn), ' ', keyword('ROWS ONLY', opts));
567
- }
568
- }
569
- if (windowDefs.length > 0) {
570
- parts.push(line, printWindowClause(windowDefs, opts, printFn));
571
- }
572
- if (forClause) {
573
- parts.push(line, printForClause(forClause, opts));
574
- }
575
- return group(parts);
576
- }
577
- // Standard / Spacious: single column stays inline; multiple each on own line.
578
- // A CASE expression always expands to multiple lines, so force it onto its own indented line.
579
- const singleExprType = (prop(selectElements[0], 'expression') ?? selectElements[0])?.type;
580
- const colList = density === 'standard' && colDocs.length === 1 && singleExprType !== 'CaseExpression'
581
- ? [' ', colDocs[0]]
582
- : indent([hardline, join(hardSep(opts), colDocs)]);
583
- const parts = [selectKw, ...(topDoc ? [' ', topDoc] : []), colList];
584
- // SELECT INTO target appears after the column list and before the FROM clause
585
- if (intoTarget) {
586
- parts.push(hardline, keyword('INTO', opts), ' ', schemaObjectName(intoTarget));
587
- }
588
- if (from) {
589
- const tableRefs = propArr(from, 'tableReferences');
590
- const fromDocs = tableRefs.map((tr) => printTableRef(tr, opts, printFn));
591
- // standard: single table (no joins) stays inline; multiple/joins each on own line
592
- // InlineDerivedTable uses a softline so it stays inline when it fits but breaks
593
- // FROM onto its own line (with the values block indented) when it doesn't
594
- const singleTable = density === 'standard' &&
595
- tableRefs.length === 1 &&
596
- tableRefs[0].type !== 'QualifiedJoin' &&
597
- tableRefs[0].type !== 'UnqualifiedJoin' &&
598
- tableRefs[0].type !== 'InlineDerivedTable';
599
- if (tableRefs.length === 1 && tableRefs[0].type === 'InlineDerivedTable') {
600
- parts.push(hardline, group([keyword('FROM', opts), indent([line, fromDocs[0]])]));
601
- }
602
- else if (singleTable) {
603
- parts.push(hardline, keyword('FROM', opts), ' ', fromDocs[0]);
604
- }
605
- else {
606
- parts.push(hardline, keyword('FROM', opts), indent([hardline, join(hardSep(opts), fromDocs)]));
607
- }
608
- }
609
- if (where) {
610
- // standard: single predicate inline; multiple each on own line
611
- // spacious: always indented
612
- const inline = density === 'standard' && where.type !== 'BooleanBinary';
613
- if (inline) {
614
- parts.push(hardline, keyword('WHERE', opts), ' ', boolWithTrailing(where, printBoolExpr(where, opts, printFn)));
615
- }
616
- else {
617
- parts.push(hardline, keyword('WHERE', opts), indent([hardline, boolWithTrailing(where, printBoolExpr(where, opts, printFn))]));
618
- }
619
594
  }
620
- if (groupBy) {
621
- const elems = propArr(groupBy, 'elements');
622
- const elemDocs = elems.map((e) => printExpression(e, opts, printFn));
623
- const inline = density === 'standard' && elems.length === 1;
624
- if (inline) {
625
- 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
+ }
626
626
  }
627
- else {
628
- 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
+ }
629
637
  }
630
- }
631
- if (having) {
632
- const inline = density === 'standard' && having.type !== 'BooleanBinary';
633
- if (inline) {
634
- 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
+ }
635
647
  }
636
- else {
637
- 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
+ }
638
656
  }
639
657
  }
640
- if (orderBy) {
641
- parts.push(hardline, printOrderByClause(orderBy, opts, printFn));
642
- }
658
+ // Tail clauses — same layout intent for all densities, sep varies
659
+ if (orderBy)
660
+ parts.push(sep, printOrderByClause(orderBy, opts, printFn));
643
661
  if (offsetNode) {
644
- 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));
645
663
  if (fetchNode) {
646
- 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));
647
665
  }
648
666
  }
649
- if (windowDefs.length > 0) {
650
- parts.push(hardline, printWindowClause(windowDefs, opts, printFn));
651
- }
652
- if (forClause) {
653
- parts.push(hardline, printForClause(forClause, opts));
654
- }
667
+ if (windowDefs.length > 0)
668
+ parts.push(sep, printWindowClause(windowDefs, opts, printFn));
669
+ if (forClause)
670
+ parts.push(sep, printForClause(forClause, opts));
655
671
  return group(parts);
656
672
  }
657
673
  function printTop(node, opts, printFn) {
@@ -836,25 +852,14 @@ export function printBoolExpr(node, opts, printFn) {
836
852
  return printDistinctPredicate(node, opts, printFn);
837
853
  case 'SubqueryComparisonPredicate':
838
854
  return printSubqueryComparison(node, opts, printFn);
855
+ case 'RegexpLikePredicate':
856
+ return printRegexpLikePredicate(node, opts, printFn);
839
857
  default:
840
858
  return node.text ?? `/* ${node.type} */`;
841
859
  }
842
860
  }
843
861
  function cmpOp(op) {
844
- const map = {
845
- Equals: '=',
846
- NotEqualToBrackets: '<>',
847
- NotEqualToExclamation: '!=',
848
- GreaterThan: '>',
849
- LessThan: '<',
850
- GreaterThanOrEqualTo: '>=',
851
- LessThanOrEqualTo: '<=',
852
- LeftOuterJoin: '*=',
853
- RightOuterJoin: '=*',
854
- NotLessThan: '!<',
855
- NotGreaterThan: '!>',
856
- };
857
- return map[op] ?? op;
862
+ return CMP_OP_MAP[op] ?? op;
858
863
  }
859
864
  function printBoolComparison(node, opts, printFn) {
860
865
  const left = prop(node, 'left');
@@ -1009,6 +1014,16 @@ function printInPredicate(node, opts, printFn) {
1009
1014
  const valueDocs = values.map((v) => printExpression(v, opts, printFn));
1010
1015
  return [...lhs, group([' (', indent([softline, join([',', line], valueDocs)]), softline, ')'])];
1011
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
+ }
1012
1027
  function printLikePredicate(node, opts, printFn) {
1013
1028
  const expr = prop(node, 'expr');
1014
1029
  const pattern = prop(node, 'pattern');
@@ -1147,22 +1162,11 @@ function printNamedTableRef(node, opts, printFn) {
1147
1162
  return [nameDoc, temporalDoc, aliasPart, sampleDoc, hintsDoc];
1148
1163
  }
1149
1164
  function joinTypeKeyword(jt, opts, hint) {
1150
- const typeMap = {
1151
- Inner: 'INNER',
1152
- LeftOuter: 'LEFT',
1153
- RightOuter: 'RIGHT',
1154
- FullOuter: 'FULL',
1155
- };
1156
- const typeWord = typeMap[jt] ?? jt.toUpperCase();
1157
- if (hint)
1165
+ if (hint) {
1166
+ const typeWord = JOIN_TYPE_WORD[jt] ?? jt.toUpperCase();
1158
1167
  return keyword(`${typeWord} ${hint} JOIN`, opts);
1159
- const noHintMap = {
1160
- Inner: 'INNER JOIN',
1161
- LeftOuter: 'LEFT JOIN',
1162
- RightOuter: 'RIGHT JOIN',
1163
- FullOuter: 'FULL JOIN',
1164
- };
1165
- return keyword(noHintMap[jt] ?? `${typeWord} JOIN`, opts);
1168
+ }
1169
+ return keyword(JOIN_TYPE_MAP[jt] ?? `${(JOIN_TYPE_WORD[jt] ?? jt.toUpperCase())} JOIN`, opts);
1166
1170
  }
1167
1171
  /**
1168
1172
  * Walk the rightmost path of an AST subtree (props in reverse insertion order)