pgsql-deparser 18.4.3 → 18.4.5

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.
package/deparser.d.ts CHANGED
@@ -306,5 +306,4 @@ export declare class Deparser implements DeparserVisitor {
306
306
  AlterObjectSchemaStmt(node: t.AlterObjectSchemaStmt, context: DeparserContext): string;
307
307
  AlterRoleSetStmt(node: t.AlterRoleSetStmt, context: DeparserContext): string;
308
308
  CreateForeignTableStmt(node: t.CreateForeignTableStmt, context: DeparserContext): string;
309
- private containsMultilineStringLiteral;
310
309
  }
package/deparser.js CHANGED
@@ -500,12 +500,7 @@ class Deparser {
500
500
  // Check if single target is complex - if so, use multiline format
501
501
  if (this.isComplexSelectTarget(targetNode)) {
502
502
  output.push('SELECT' + distinctPart);
503
- if (this.containsMultilineStringLiteral(target)) {
504
- output.push(target);
505
- }
506
- else {
507
- output.push(context.indent(target));
508
- }
503
+ output.push(context.indent(target));
509
504
  }
510
505
  else {
511
506
  output.push('SELECT' + distinctPart + ' ' + target);
@@ -515,9 +510,6 @@ class Deparser {
515
510
  const targetStrings = targetList
516
511
  .map(e => {
517
512
  const targetStr = this.visit(e, context.spawn('SelectStmt', { select: true }));
518
- if (this.containsMultilineStringLiteral(targetStr)) {
519
- return targetStr;
520
- }
521
513
  return context.indent(targetStr);
522
514
  });
523
515
  const formattedTargets = targetStrings.join(',' + context.newline());
@@ -569,9 +561,6 @@ class Deparser {
569
561
  return context.parens(values.join(', '));
570
562
  });
571
563
  const indentedTuples = lists.map(tuple => {
572
- if (this.containsMultilineStringLiteral(tuple)) {
573
- return tuple;
574
- }
575
564
  return context.indent(tuple);
576
565
  });
577
566
  output.push(indentedTuples.join(',\n'));
@@ -591,9 +580,6 @@ class Deparser {
591
580
  const groupItems = groupList
592
581
  .map(e => {
593
582
  const groupStr = this.visit(e, context.spawn('SelectStmt', { group: true, indentLevel: context.indentLevel + 1 }));
594
- if (this.containsMultilineStringLiteral(groupStr)) {
595
- return groupStr;
596
- }
597
583
  return context.indent(groupStr);
598
584
  })
599
585
  .join(',' + context.newline());
@@ -612,12 +598,7 @@ class Deparser {
612
598
  if (context.isPretty()) {
613
599
  output.push('HAVING');
614
600
  const havingStr = this.visit(node.havingClause, context);
615
- if (this.containsMultilineStringLiteral(havingStr)) {
616
- output.push(havingStr);
617
- }
618
- else {
619
- output.push(context.indent(havingStr));
620
- }
601
+ output.push(context.indent(havingStr));
621
602
  }
622
603
  else {
623
604
  output.push('HAVING');
@@ -638,9 +619,6 @@ class Deparser {
638
619
  const sortItems = sortList
639
620
  .map(e => {
640
621
  const sortStr = this.visit(e, context.spawn('SelectStmt', { sort: true, indentLevel: context.indentLevel + 1 }));
641
- if (this.containsMultilineStringLiteral(sortStr)) {
642
- return sortStr;
643
- }
644
622
  return context.indent(sortStr);
645
623
  })
646
624
  .join(',' + context.newline());
@@ -1093,7 +1071,7 @@ class Deparser {
1093
1071
  if (context.isPretty()) {
1094
1072
  // Put each assignment on its own line for pretty printing
1095
1073
  const indentedTargets = targets.map(target => context.indent(target));
1096
- output.push('\n' + indentedTargets.join(',\n'));
1074
+ output[output.length - 1] += '\n' + indentedTargets.join(',\n');
1097
1075
  }
1098
1076
  else {
1099
1077
  output.push(targets.join(', '));
@@ -1245,9 +1223,6 @@ class Deparser {
1245
1223
  const cteStrings = ctes.map((cte, index) => {
1246
1224
  const cteStr = this.visit(cte, context);
1247
1225
  const prefix = index === 0 ? context.newline() : ',' + context.newline();
1248
- if (this.containsMultilineStringLiteral(cteStr)) {
1249
- return prefix + cteStr;
1250
- }
1251
1226
  return prefix + context.indent(cteStr);
1252
1227
  });
1253
1228
  output.push(cteStrings.join(''));
@@ -1422,7 +1397,9 @@ class Deparser {
1422
1397
  const source = this.visit(args[1], context);
1423
1398
  // EXTRACT requires an unquoted uppercase keyword (EPOCH, YEAR, etc.)
1424
1399
  // but A_Const sval deparsing wraps it in single quotes — strip and uppercase
1425
- if (field.startsWith("'") && field.endsWith("'")) {
1400
+ // only when it is spelled the way the bare-keyword form parses back
1401
+ if (/^'(?:year|month|day|hour|minute|second)'$/.test(field)
1402
+ || (/^'[a-z_][a-z0-9_]*'$/.test(field) && (0, quotes_1.keywordKindOf)(field.slice(1, -1)) === 'NO_KEYWORD')) {
1426
1403
  field = field.slice(1, -1).toUpperCase();
1427
1404
  }
1428
1405
  return `EXTRACT(${field} FROM ${source})`;
@@ -2220,7 +2197,9 @@ class Deparser {
2220
2197
  A_Indirection(node, context) {
2221
2198
  let argStr = this.visit(node.arg, context);
2222
2199
  const argType = this.getNodeType(node.arg);
2223
- if (argType === 'TypeCast' || argType === 'SubLink' || argType === 'A_Expr' || argType === 'FuncCall' || argType === 'A_Indirection' || argType === 'ColumnRef' || argType === 'RowExpr') {
2200
+ // Grammar: only a parameter takes indirection bare; every other target is
2201
+ // `'(' a_expr ')' opt_indirection` (a plain column subscript is the same node).
2202
+ if (argType !== 'ParamRef') {
2224
2203
  argStr = `(${argStr})`;
2225
2204
  }
2226
2205
  const output = [argStr];
@@ -2248,21 +2227,11 @@ class Deparser {
2248
2227
  if (context.isPretty() && args.length > 0) {
2249
2228
  for (const arg of args) {
2250
2229
  const whenClause = this.visit(arg, context);
2251
- if (this.containsMultilineStringLiteral(whenClause)) {
2252
- output.push(context.newline() + whenClause);
2253
- }
2254
- else {
2255
- output.push(context.newline() + context.indent(whenClause));
2256
- }
2230
+ output.push(context.newline() + context.indent(whenClause));
2257
2231
  }
2258
2232
  if (node.defresult) {
2259
2233
  const elseResult = this.visit(node.defresult, context);
2260
- if (this.containsMultilineStringLiteral(elseResult)) {
2261
- output.push(context.newline() + 'ELSE ' + elseResult);
2262
- }
2263
- else {
2264
- output.push(context.newline() + context.indent('ELSE ' + elseResult));
2265
- }
2234
+ output.push(context.newline() + context.indent('ELSE ' + elseResult));
2266
2235
  }
2267
2236
  output.push(context.newline() + 'END');
2268
2237
  return output.join(' ');
@@ -3864,12 +3833,7 @@ class Deparser {
3864
3833
  if (context.isPretty()) {
3865
3834
  // For complex JOIN conditions, format with proper indentation
3866
3835
  if (qualsStr.includes('AND') || qualsStr.includes('OR') || qualsStr.length > 50) {
3867
- if (this.containsMultilineStringLiteral(qualsStr)) {
3868
- output.push(` ON ${qualsStr}`);
3869
- }
3870
- else {
3871
- output.push(` ON${context.newline()}${context.indent(qualsStr)}`);
3872
- }
3836
+ output.push(` ON${context.newline()}${context.indent(qualsStr)}`);
3873
3837
  }
3874
3838
  else {
3875
3839
  output.push(` ON ${qualsStr}`);
@@ -5891,7 +5855,7 @@ class Deparser {
5891
5855
  if (node.defname === 'new_version') {
5892
5856
  // argValue is unquoted due to DefElem context, so we need to quote it
5893
5857
  const quotedValue = this.quoteDefElemArg(node.arg, argValue);
5894
- return `UPDATE TO ${quotedValue}`;
5858
+ return `TO ${quotedValue}`;
5895
5859
  }
5896
5860
  if (node.defname === 'schema') {
5897
5861
  return `SET SCHEMA ${argValue}`;
@@ -5911,6 +5875,12 @@ class Deparser {
5911
5875
  if (node.defname === 'cascade') {
5912
5876
  return argValue === 'true' ? 'CASCADE' : '';
5913
5877
  }
5878
+ if (node.defname === 'new_version') {
5879
+ return `VERSION ${this.quoteDefElemArg(node.arg, argValue)}`;
5880
+ }
5881
+ if (node.defname === 'schema' && node.arg && this.getNodeType(node.arg) === 'String') {
5882
+ return `SCHEMA ${quotes_1.QuoteUtils.quoteIdentifier(this.getNodeData(node.arg).sval)}`;
5883
+ }
5914
5884
  return `${node.defname.toUpperCase()} ${argValue}`;
5915
5885
  }
5916
5886
  // Handle IndexStmt WITH clause options - no quotes, compact formatting
@@ -6168,6 +6138,7 @@ class Deparser {
6168
6138
  if (node.extname) {
6169
6139
  output.push(this.quoteIfNeeded(node.extname));
6170
6140
  }
6141
+ output.push('UPDATE');
6171
6142
  if (node.options && node.options.length > 0) {
6172
6143
  const extContext = context.spawn('AlterExtensionStmt');
6173
6144
  const options = list_utils_1.ListUtils.unwrapList(node.options)
@@ -8476,6 +8447,9 @@ class Deparser {
8476
8447
  .join(' ');
8477
8448
  components.push(context.indent('REFERENCING ' + transitionClauses));
8478
8449
  }
8450
+ if (node.constrrel) {
8451
+ components.push(context.indent('FROM ' + this.RangeVar(node.constrrel, context)));
8452
+ }
8479
8453
  if (node.deferrable) {
8480
8454
  components.push(context.indent('DEFERRABLE'));
8481
8455
  }
@@ -8497,7 +8471,7 @@ class Deparser {
8497
8471
  const funcName = list_utils_1.ListUtils.unwrapList(node.funcname)
8498
8472
  .map(name => this.visit(name, context))
8499
8473
  .join('.');
8500
- executeStr += ' PROCEDURE ' + funcName;
8474
+ executeStr += ' FUNCTION ' + funcName;
8501
8475
  }
8502
8476
  if (node.args && node.args.length > 0) {
8503
8477
  const argContext = context.spawn('CreateTrigStmt', { isStringLiteral: true });
@@ -10934,9 +10908,5 @@ class Deparser {
10934
10908
  }
10935
10909
  return output.join(' ');
10936
10910
  }
10937
- containsMultilineStringLiteral(content) {
10938
- const stringLiteralRegex = /'[^']*\n[^']*'/g;
10939
- return stringLiteralRegex.test(content);
10940
- }
10941
10911
  }
10942
10912
  exports.Deparser = Deparser;
package/esm/deparser.js CHANGED
@@ -497,12 +497,7 @@ export class Deparser {
497
497
  // Check if single target is complex - if so, use multiline format
498
498
  if (this.isComplexSelectTarget(targetNode)) {
499
499
  output.push('SELECT' + distinctPart);
500
- if (this.containsMultilineStringLiteral(target)) {
501
- output.push(target);
502
- }
503
- else {
504
- output.push(context.indent(target));
505
- }
500
+ output.push(context.indent(target));
506
501
  }
507
502
  else {
508
503
  output.push('SELECT' + distinctPart + ' ' + target);
@@ -512,9 +507,6 @@ export class Deparser {
512
507
  const targetStrings = targetList
513
508
  .map(e => {
514
509
  const targetStr = this.visit(e, context.spawn('SelectStmt', { select: true }));
515
- if (this.containsMultilineStringLiteral(targetStr)) {
516
- return targetStr;
517
- }
518
510
  return context.indent(targetStr);
519
511
  });
520
512
  const formattedTargets = targetStrings.join(',' + context.newline());
@@ -566,9 +558,6 @@ export class Deparser {
566
558
  return context.parens(values.join(', '));
567
559
  });
568
560
  const indentedTuples = lists.map(tuple => {
569
- if (this.containsMultilineStringLiteral(tuple)) {
570
- return tuple;
571
- }
572
561
  return context.indent(tuple);
573
562
  });
574
563
  output.push(indentedTuples.join(',\n'));
@@ -588,9 +577,6 @@ export class Deparser {
588
577
  const groupItems = groupList
589
578
  .map(e => {
590
579
  const groupStr = this.visit(e, context.spawn('SelectStmt', { group: true, indentLevel: context.indentLevel + 1 }));
591
- if (this.containsMultilineStringLiteral(groupStr)) {
592
- return groupStr;
593
- }
594
580
  return context.indent(groupStr);
595
581
  })
596
582
  .join(',' + context.newline());
@@ -609,12 +595,7 @@ export class Deparser {
609
595
  if (context.isPretty()) {
610
596
  output.push('HAVING');
611
597
  const havingStr = this.visit(node.havingClause, context);
612
- if (this.containsMultilineStringLiteral(havingStr)) {
613
- output.push(havingStr);
614
- }
615
- else {
616
- output.push(context.indent(havingStr));
617
- }
598
+ output.push(context.indent(havingStr));
618
599
  }
619
600
  else {
620
601
  output.push('HAVING');
@@ -635,9 +616,6 @@ export class Deparser {
635
616
  const sortItems = sortList
636
617
  .map(e => {
637
618
  const sortStr = this.visit(e, context.spawn('SelectStmt', { sort: true, indentLevel: context.indentLevel + 1 }));
638
- if (this.containsMultilineStringLiteral(sortStr)) {
639
- return sortStr;
640
- }
641
619
  return context.indent(sortStr);
642
620
  })
643
621
  .join(',' + context.newline());
@@ -1090,7 +1068,7 @@ export class Deparser {
1090
1068
  if (context.isPretty()) {
1091
1069
  // Put each assignment on its own line for pretty printing
1092
1070
  const indentedTargets = targets.map(target => context.indent(target));
1093
- output.push('\n' + indentedTargets.join(',\n'));
1071
+ output[output.length - 1] += '\n' + indentedTargets.join(',\n');
1094
1072
  }
1095
1073
  else {
1096
1074
  output.push(targets.join(', '));
@@ -1242,9 +1220,6 @@ export class Deparser {
1242
1220
  const cteStrings = ctes.map((cte, index) => {
1243
1221
  const cteStr = this.visit(cte, context);
1244
1222
  const prefix = index === 0 ? context.newline() : ',' + context.newline();
1245
- if (this.containsMultilineStringLiteral(cteStr)) {
1246
- return prefix + cteStr;
1247
- }
1248
1223
  return prefix + context.indent(cteStr);
1249
1224
  });
1250
1225
  output.push(cteStrings.join(''));
@@ -1419,7 +1394,9 @@ export class Deparser {
1419
1394
  const source = this.visit(args[1], context);
1420
1395
  // EXTRACT requires an unquoted uppercase keyword (EPOCH, YEAR, etc.)
1421
1396
  // but A_Const sval deparsing wraps it in single quotes — strip and uppercase
1422
- if (field.startsWith("'") && field.endsWith("'")) {
1397
+ // only when it is spelled the way the bare-keyword form parses back
1398
+ if (/^'(?:year|month|day|hour|minute|second)'$/.test(field)
1399
+ || (/^'[a-z_][a-z0-9_]*'$/.test(field) && keywordKindOf(field.slice(1, -1)) === 'NO_KEYWORD')) {
1423
1400
  field = field.slice(1, -1).toUpperCase();
1424
1401
  }
1425
1402
  return `EXTRACT(${field} FROM ${source})`;
@@ -2217,7 +2194,9 @@ export class Deparser {
2217
2194
  A_Indirection(node, context) {
2218
2195
  let argStr = this.visit(node.arg, context);
2219
2196
  const argType = this.getNodeType(node.arg);
2220
- if (argType === 'TypeCast' || argType === 'SubLink' || argType === 'A_Expr' || argType === 'FuncCall' || argType === 'A_Indirection' || argType === 'ColumnRef' || argType === 'RowExpr') {
2197
+ // Grammar: only a parameter takes indirection bare; every other target is
2198
+ // `'(' a_expr ')' opt_indirection` (a plain column subscript is the same node).
2199
+ if (argType !== 'ParamRef') {
2221
2200
  argStr = `(${argStr})`;
2222
2201
  }
2223
2202
  const output = [argStr];
@@ -2245,21 +2224,11 @@ export class Deparser {
2245
2224
  if (context.isPretty() && args.length > 0) {
2246
2225
  for (const arg of args) {
2247
2226
  const whenClause = this.visit(arg, context);
2248
- if (this.containsMultilineStringLiteral(whenClause)) {
2249
- output.push(context.newline() + whenClause);
2250
- }
2251
- else {
2252
- output.push(context.newline() + context.indent(whenClause));
2253
- }
2227
+ output.push(context.newline() + context.indent(whenClause));
2254
2228
  }
2255
2229
  if (node.defresult) {
2256
2230
  const elseResult = this.visit(node.defresult, context);
2257
- if (this.containsMultilineStringLiteral(elseResult)) {
2258
- output.push(context.newline() + 'ELSE ' + elseResult);
2259
- }
2260
- else {
2261
- output.push(context.newline() + context.indent('ELSE ' + elseResult));
2262
- }
2231
+ output.push(context.newline() + context.indent('ELSE ' + elseResult));
2263
2232
  }
2264
2233
  output.push(context.newline() + 'END');
2265
2234
  return output.join(' ');
@@ -3861,12 +3830,7 @@ export class Deparser {
3861
3830
  if (context.isPretty()) {
3862
3831
  // For complex JOIN conditions, format with proper indentation
3863
3832
  if (qualsStr.includes('AND') || qualsStr.includes('OR') || qualsStr.length > 50) {
3864
- if (this.containsMultilineStringLiteral(qualsStr)) {
3865
- output.push(` ON ${qualsStr}`);
3866
- }
3867
- else {
3868
- output.push(` ON${context.newline()}${context.indent(qualsStr)}`);
3869
- }
3833
+ output.push(` ON${context.newline()}${context.indent(qualsStr)}`);
3870
3834
  }
3871
3835
  else {
3872
3836
  output.push(` ON ${qualsStr}`);
@@ -5888,7 +5852,7 @@ export class Deparser {
5888
5852
  if (node.defname === 'new_version') {
5889
5853
  // argValue is unquoted due to DefElem context, so we need to quote it
5890
5854
  const quotedValue = this.quoteDefElemArg(node.arg, argValue);
5891
- return `UPDATE TO ${quotedValue}`;
5855
+ return `TO ${quotedValue}`;
5892
5856
  }
5893
5857
  if (node.defname === 'schema') {
5894
5858
  return `SET SCHEMA ${argValue}`;
@@ -5908,6 +5872,12 @@ export class Deparser {
5908
5872
  if (node.defname === 'cascade') {
5909
5873
  return argValue === 'true' ? 'CASCADE' : '';
5910
5874
  }
5875
+ if (node.defname === 'new_version') {
5876
+ return `VERSION ${this.quoteDefElemArg(node.arg, argValue)}`;
5877
+ }
5878
+ if (node.defname === 'schema' && node.arg && this.getNodeType(node.arg) === 'String') {
5879
+ return `SCHEMA ${QuoteUtils.quoteIdentifier(this.getNodeData(node.arg).sval)}`;
5880
+ }
5911
5881
  return `${node.defname.toUpperCase()} ${argValue}`;
5912
5882
  }
5913
5883
  // Handle IndexStmt WITH clause options - no quotes, compact formatting
@@ -6165,6 +6135,7 @@ export class Deparser {
6165
6135
  if (node.extname) {
6166
6136
  output.push(this.quoteIfNeeded(node.extname));
6167
6137
  }
6138
+ output.push('UPDATE');
6168
6139
  if (node.options && node.options.length > 0) {
6169
6140
  const extContext = context.spawn('AlterExtensionStmt');
6170
6141
  const options = ListUtils.unwrapList(node.options)
@@ -8473,6 +8444,9 @@ export class Deparser {
8473
8444
  .join(' ');
8474
8445
  components.push(context.indent('REFERENCING ' + transitionClauses));
8475
8446
  }
8447
+ if (node.constrrel) {
8448
+ components.push(context.indent('FROM ' + this.RangeVar(node.constrrel, context)));
8449
+ }
8476
8450
  if (node.deferrable) {
8477
8451
  components.push(context.indent('DEFERRABLE'));
8478
8452
  }
@@ -8494,7 +8468,7 @@ export class Deparser {
8494
8468
  const funcName = ListUtils.unwrapList(node.funcname)
8495
8469
  .map(name => this.visit(name, context))
8496
8470
  .join('.');
8497
- executeStr += ' PROCEDURE ' + funcName;
8471
+ executeStr += ' FUNCTION ' + funcName;
8498
8472
  }
8499
8473
  if (node.args && node.args.length > 0) {
8500
8474
  const argContext = context.spawn('CreateTrigStmt', { isStringLiteral: true });
@@ -10931,8 +10905,4 @@ export class Deparser {
10931
10905
  }
10932
10906
  return output.join(' ');
10933
10907
  }
10934
- containsMultilineStringLiteral(content) {
10935
- const stringLiteralRegex = /'[^']*\n[^']*'/g;
10936
- return stringLiteralRegex.test(content);
10937
- }
10938
10908
  }
package/esm/index.js CHANGED
@@ -7,4 +7,5 @@ export const deparse = async (...args) => {
7
7
  return deparseMethod(...args);
8
8
  };
9
9
  export { Deparser };
10
+ export { splitSqlLines } from './utils/sql-formatter';
10
11
  export { QuoteUtils } from '@pgsql/quotes';
@@ -1,3 +1,105 @@
1
+ const isIdentChar = (ch) => ch !== undefined && (/[A-Za-z0-9_$]/.test(ch) || ch.charCodeAt(0) >= 0x80);
2
+ const DOLLAR_TAG = /\$(?:[A-Za-z_\u0080-\uffff][A-Za-z0-9_\u0080-\uffff]*)?\$/y;
3
+ /**
4
+ * Splits SQL text into lines, breaking only at newlines that sit between
5
+ * tokens. A newline inside a string literal ('...', E'...', $tag$...$tag$),
6
+ * a quoted identifier or a block comment is part of that token's value and
7
+ * stays inside its line, so callers can re-indent the lines without changing
8
+ * what the SQL means.
9
+ */
10
+ export function splitSqlLines(text, newlineChar = '\n') {
11
+ if (!newlineChar || !text.includes(newlineChar)) {
12
+ return [text];
13
+ }
14
+ const lines = [];
15
+ const length = text.length;
16
+ let lineStart = 0;
17
+ let i = 0;
18
+ while (i < length) {
19
+ const ch = text[i];
20
+ if (ch === "'") {
21
+ const prev = text[i - 1];
22
+ const backslashEscapes = (prev === 'E' || prev === 'e') && !isIdentChar(text[i - 2]);
23
+ i++;
24
+ while (i < length) {
25
+ if (backslashEscapes && text[i] === '\\') {
26
+ i += 2;
27
+ }
28
+ else if (text[i] === "'") {
29
+ if (text[i + 1] === "'") {
30
+ i += 2;
31
+ }
32
+ else {
33
+ break;
34
+ }
35
+ }
36
+ else {
37
+ i++;
38
+ }
39
+ }
40
+ i++;
41
+ }
42
+ else if (ch === '"') {
43
+ i++;
44
+ while (i < length) {
45
+ if (text[i] === '"') {
46
+ if (text[i + 1] === '"') {
47
+ i += 2;
48
+ }
49
+ else {
50
+ break;
51
+ }
52
+ }
53
+ else {
54
+ i++;
55
+ }
56
+ }
57
+ i++;
58
+ }
59
+ else if (ch === '$' && !isIdentChar(text[i - 1])) {
60
+ DOLLAR_TAG.lastIndex = i;
61
+ const tag = DOLLAR_TAG.exec(text);
62
+ if (tag) {
63
+ const close = text.indexOf(tag[0], i + tag[0].length);
64
+ i = close === -1 ? length : close + tag[0].length;
65
+ }
66
+ else {
67
+ i++;
68
+ }
69
+ }
70
+ else if (ch === '-' && text[i + 1] === '-') {
71
+ const end = text.indexOf('\n', i);
72
+ i = end === -1 ? length : end;
73
+ }
74
+ else if (ch === '/' && text[i + 1] === '*') {
75
+ let depth = 1;
76
+ i += 2;
77
+ while (i < length && depth > 0) {
78
+ if (text[i] === '/' && text[i + 1] === '*') {
79
+ depth++;
80
+ i += 2;
81
+ }
82
+ else if (text[i] === '*' && text[i + 1] === '/') {
83
+ depth--;
84
+ i += 2;
85
+ }
86
+ else {
87
+ i++;
88
+ }
89
+ }
90
+ }
91
+ else if (text.startsWith(newlineChar, i)) {
92
+ lines.push(text.slice(lineStart, i));
93
+ i += newlineChar.length;
94
+ lineStart = i;
95
+ }
96
+ else {
97
+ i++;
98
+ }
99
+ }
100
+ lines.push(text.slice(lineStart));
101
+ return lines;
102
+ }
1
103
  export class SqlFormatter {
2
104
  newlineChar;
3
105
  tabChar;
@@ -15,7 +117,7 @@ export class SqlFormatter {
15
117
  return text;
16
118
  }
17
119
  const indentation = this.tabChar.repeat(count);
18
- return text.split(this.newlineChar).map(line => line.trim() ? indentation + line : line).join(this.newlineChar);
120
+ return splitSqlLines(text, this.newlineChar).map(line => line.trim() ? indentation + line : line).join(this.newlineChar);
19
121
  }
20
122
  parens(content) {
21
123
  return `(${content})`;
package/index.d.ts CHANGED
@@ -3,4 +3,5 @@ declare const deparseMethod: typeof Deparser.deparse;
3
3
  export declare const deparseSync: typeof Deparser.deparse;
4
4
  export declare const deparse: (...args: Parameters<typeof deparseMethod>) => Promise<ReturnType<typeof deparseMethod>>;
5
5
  export { Deparser, DeparserOptions };
6
+ export { splitSqlLines } from './utils/sql-formatter';
6
7
  export { QuoteUtils } from '@pgsql/quotes';
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.QuoteUtils = exports.Deparser = exports.deparse = exports.deparseSync = void 0;
3
+ exports.QuoteUtils = exports.splitSqlLines = exports.Deparser = exports.deparse = exports.deparseSync = void 0;
4
4
  const deparser_1 = require("./deparser");
5
5
  Object.defineProperty(exports, "Deparser", { enumerable: true, get: function () { return deparser_1.Deparser; } });
6
6
  const deparseMethod = deparser_1.Deparser.deparse;
@@ -11,5 +11,7 @@ const deparse = async (...args) => {
11
11
  return deparseMethod(...args);
12
12
  };
13
13
  exports.deparse = deparse;
14
+ var sql_formatter_1 = require("./utils/sql-formatter");
15
+ Object.defineProperty(exports, "splitSqlLines", { enumerable: true, get: function () { return sql_formatter_1.splitSqlLines; } });
14
16
  var quotes_1 = require("@pgsql/quotes");
15
17
  Object.defineProperty(exports, "QuoteUtils", { enumerable: true, get: function () { return quotes_1.QuoteUtils; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgsql-deparser",
3
- "version": "18.4.3",
3
+ "version": "18.4.5",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "PostgreSQL AST Deparser",
6
6
  "main": "index.js",
@@ -53,12 +53,12 @@
53
53
  "database"
54
54
  ],
55
55
  "devDependencies": {
56
- "libpg-query": "18.2.1",
56
+ "libpg-query": "18.2.4",
57
57
  "makage": "^0.5.1"
58
58
  },
59
59
  "dependencies": {
60
60
  "@pgsql/quotes": "18.2.4",
61
61
  "@pgsql/types": "^18.0.0"
62
62
  },
63
- "gitHead": "ce03e09604aea4b0fd7d62cf23e72d0de8ad33c1"
63
+ "gitHead": "2ea833400ad4f4b440b318b84fe9f0364db686fb"
64
64
  }
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Splits SQL text into lines, breaking only at newlines that sit between
3
+ * tokens. A newline inside a string literal ('...', E'...', $tag$...$tag$),
4
+ * a quoted identifier or a block comment is part of that token's value and
5
+ * stays inside its line, so callers can re-indent the lines without changing
6
+ * what the SQL means.
7
+ */
8
+ export declare function splitSqlLines(text: string, newlineChar?: string): string[];
1
9
  export declare class SqlFormatter {
2
10
  private newlineChar;
3
11
  private tabChar;
@@ -1,6 +1,109 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SqlFormatter = void 0;
4
+ exports.splitSqlLines = splitSqlLines;
5
+ const isIdentChar = (ch) => ch !== undefined && (/[A-Za-z0-9_$]/.test(ch) || ch.charCodeAt(0) >= 0x80);
6
+ const DOLLAR_TAG = /\$(?:[A-Za-z_\u0080-\uffff][A-Za-z0-9_\u0080-\uffff]*)?\$/y;
7
+ /**
8
+ * Splits SQL text into lines, breaking only at newlines that sit between
9
+ * tokens. A newline inside a string literal ('...', E'...', $tag$...$tag$),
10
+ * a quoted identifier or a block comment is part of that token's value and
11
+ * stays inside its line, so callers can re-indent the lines without changing
12
+ * what the SQL means.
13
+ */
14
+ function splitSqlLines(text, newlineChar = '\n') {
15
+ if (!newlineChar || !text.includes(newlineChar)) {
16
+ return [text];
17
+ }
18
+ const lines = [];
19
+ const length = text.length;
20
+ let lineStart = 0;
21
+ let i = 0;
22
+ while (i < length) {
23
+ const ch = text[i];
24
+ if (ch === "'") {
25
+ const prev = text[i - 1];
26
+ const backslashEscapes = (prev === 'E' || prev === 'e') && !isIdentChar(text[i - 2]);
27
+ i++;
28
+ while (i < length) {
29
+ if (backslashEscapes && text[i] === '\\') {
30
+ i += 2;
31
+ }
32
+ else if (text[i] === "'") {
33
+ if (text[i + 1] === "'") {
34
+ i += 2;
35
+ }
36
+ else {
37
+ break;
38
+ }
39
+ }
40
+ else {
41
+ i++;
42
+ }
43
+ }
44
+ i++;
45
+ }
46
+ else if (ch === '"') {
47
+ i++;
48
+ while (i < length) {
49
+ if (text[i] === '"') {
50
+ if (text[i + 1] === '"') {
51
+ i += 2;
52
+ }
53
+ else {
54
+ break;
55
+ }
56
+ }
57
+ else {
58
+ i++;
59
+ }
60
+ }
61
+ i++;
62
+ }
63
+ else if (ch === '$' && !isIdentChar(text[i - 1])) {
64
+ DOLLAR_TAG.lastIndex = i;
65
+ const tag = DOLLAR_TAG.exec(text);
66
+ if (tag) {
67
+ const close = text.indexOf(tag[0], i + tag[0].length);
68
+ i = close === -1 ? length : close + tag[0].length;
69
+ }
70
+ else {
71
+ i++;
72
+ }
73
+ }
74
+ else if (ch === '-' && text[i + 1] === '-') {
75
+ const end = text.indexOf('\n', i);
76
+ i = end === -1 ? length : end;
77
+ }
78
+ else if (ch === '/' && text[i + 1] === '*') {
79
+ let depth = 1;
80
+ i += 2;
81
+ while (i < length && depth > 0) {
82
+ if (text[i] === '/' && text[i + 1] === '*') {
83
+ depth++;
84
+ i += 2;
85
+ }
86
+ else if (text[i] === '*' && text[i + 1] === '/') {
87
+ depth--;
88
+ i += 2;
89
+ }
90
+ else {
91
+ i++;
92
+ }
93
+ }
94
+ }
95
+ else if (text.startsWith(newlineChar, i)) {
96
+ lines.push(text.slice(lineStart, i));
97
+ i += newlineChar.length;
98
+ lineStart = i;
99
+ }
100
+ else {
101
+ i++;
102
+ }
103
+ }
104
+ lines.push(text.slice(lineStart));
105
+ return lines;
106
+ }
4
107
  class SqlFormatter {
5
108
  newlineChar;
6
109
  tabChar;
@@ -18,7 +121,7 @@ class SqlFormatter {
18
121
  return text;
19
122
  }
20
123
  const indentation = this.tabChar.repeat(count);
21
- return text.split(this.newlineChar).map(line => line.trim() ? indentation + line : line).join(this.newlineChar);
124
+ return splitSqlLines(text, this.newlineChar).map(line => line.trim() ? indentation + line : line).join(this.newlineChar);
22
125
  }
23
126
  parens(content) {
24
127
  return `(${content})`;