pgsql-deparser 17.13.0 → 17.14.0

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.js CHANGED
@@ -1196,7 +1196,7 @@ class Deparser {
1196
1196
  if (node.indirection && node.indirection.length > 0) {
1197
1197
  const indirectionStrs = list_utils_1.ListUtils.unwrapList(node.indirection).map(item => {
1198
1198
  if (item.String) {
1199
- return `.${quote_utils_1.QuoteUtils.quoteIdentifier(item.String.sval || item.String.str)}`;
1199
+ return `.${quote_utils_1.QuoteUtils.quoteIdentifierAfterDot(item.String.sval || item.String.str)}`;
1200
1200
  }
1201
1201
  return this.visit(item, context);
1202
1202
  });
@@ -1213,7 +1213,7 @@ class Deparser {
1213
1213
  if (node.indirection && node.indirection.length > 0) {
1214
1214
  const indirectionStrs = list_utils_1.ListUtils.unwrapList(node.indirection).map(item => {
1215
1215
  if (item.String) {
1216
- return `.${quote_utils_1.QuoteUtils.quoteIdentifier(item.String.sval || item.String.str)}`;
1216
+ return `.${quote_utils_1.QuoteUtils.quoteIdentifierAfterDot(item.String.sval || item.String.str)}`;
1217
1217
  }
1218
1218
  return this.visit(item, context);
1219
1219
  });
@@ -1295,7 +1295,8 @@ class Deparser {
1295
1295
  FuncCall(node, context) {
1296
1296
  const funcname = list_utils_1.ListUtils.unwrapList(node.funcname);
1297
1297
  const args = list_utils_1.ListUtils.unwrapList(node.args);
1298
- const name = funcname.map(n => this.visit(n, context)).join('.');
1298
+ const funcnameParts = funcname.map((n) => n.String?.sval || n.String?.str || '').filter((s) => s);
1299
+ const name = quote_utils_1.QuoteUtils.quoteDottedName(funcnameParts);
1299
1300
  // Handle special SQL syntax functions like XMLEXISTS and EXTRACT
1300
1301
  if (node.funcformat === 'COERCE_SQL_SYNTAX' && name === 'pg_catalog.xmlexists' && args.length >= 2) {
1301
1302
  const xpath = this.visit(args[0], context);
@@ -1891,9 +1892,9 @@ class Deparser {
1891
1892
  if (node.catalogname) {
1892
1893
  tableName = quote_utils_1.QuoteUtils.quoteIdentifier(node.catalogname);
1893
1894
  if (node.schemaname) {
1894
- tableName += '.' + quote_utils_1.QuoteUtils.quoteIdentifier(node.schemaname);
1895
+ tableName += '.' + quote_utils_1.QuoteUtils.quoteIdentifierAfterDot(node.schemaname);
1895
1896
  }
1896
- tableName += '.' + quote_utils_1.QuoteUtils.quoteIdentifier(node.relname);
1897
+ tableName += '.' + quote_utils_1.QuoteUtils.quoteIdentifierAfterDot(node.relname);
1897
1898
  }
1898
1899
  else if (node.schemaname) {
1899
1900
  tableName = quote_utils_1.QuoteUtils.quoteQualifiedIdentifier(node.schemaname, node.relname);
@@ -5158,7 +5159,8 @@ class Deparser {
5158
5159
  output.push('FUNCTION');
5159
5160
  }
5160
5161
  if (node.funcname && node.funcname.length > 0) {
5161
- const funcName = node.funcname.map((name) => this.visit(name, context)).join('.');
5162
+ const funcnameParts = node.funcname.map((name) => name.String?.sval || name.String?.str || '').filter((s) => s);
5163
+ const funcName = quote_utils_1.QuoteUtils.quoteDottedName(funcnameParts);
5162
5164
  if (node.parameters && node.parameters.length > 0) {
5163
5165
  const params = node.parameters
5164
5166
  .filter((param) => {
package/esm/deparser.js CHANGED
@@ -1193,7 +1193,7 @@ export class Deparser {
1193
1193
  if (node.indirection && node.indirection.length > 0) {
1194
1194
  const indirectionStrs = ListUtils.unwrapList(node.indirection).map(item => {
1195
1195
  if (item.String) {
1196
- return `.${QuoteUtils.quoteIdentifier(item.String.sval || item.String.str)}`;
1196
+ return `.${QuoteUtils.quoteIdentifierAfterDot(item.String.sval || item.String.str)}`;
1197
1197
  }
1198
1198
  return this.visit(item, context);
1199
1199
  });
@@ -1210,7 +1210,7 @@ export class Deparser {
1210
1210
  if (node.indirection && node.indirection.length > 0) {
1211
1211
  const indirectionStrs = ListUtils.unwrapList(node.indirection).map(item => {
1212
1212
  if (item.String) {
1213
- return `.${QuoteUtils.quoteIdentifier(item.String.sval || item.String.str)}`;
1213
+ return `.${QuoteUtils.quoteIdentifierAfterDot(item.String.sval || item.String.str)}`;
1214
1214
  }
1215
1215
  return this.visit(item, context);
1216
1216
  });
@@ -1292,7 +1292,8 @@ export class Deparser {
1292
1292
  FuncCall(node, context) {
1293
1293
  const funcname = ListUtils.unwrapList(node.funcname);
1294
1294
  const args = ListUtils.unwrapList(node.args);
1295
- const name = funcname.map(n => this.visit(n, context)).join('.');
1295
+ const funcnameParts = funcname.map((n) => n.String?.sval || n.String?.str || '').filter((s) => s);
1296
+ const name = QuoteUtils.quoteDottedName(funcnameParts);
1296
1297
  // Handle special SQL syntax functions like XMLEXISTS and EXTRACT
1297
1298
  if (node.funcformat === 'COERCE_SQL_SYNTAX' && name === 'pg_catalog.xmlexists' && args.length >= 2) {
1298
1299
  const xpath = this.visit(args[0], context);
@@ -1888,9 +1889,9 @@ export class Deparser {
1888
1889
  if (node.catalogname) {
1889
1890
  tableName = QuoteUtils.quoteIdentifier(node.catalogname);
1890
1891
  if (node.schemaname) {
1891
- tableName += '.' + QuoteUtils.quoteIdentifier(node.schemaname);
1892
+ tableName += '.' + QuoteUtils.quoteIdentifierAfterDot(node.schemaname);
1892
1893
  }
1893
- tableName += '.' + QuoteUtils.quoteIdentifier(node.relname);
1894
+ tableName += '.' + QuoteUtils.quoteIdentifierAfterDot(node.relname);
1894
1895
  }
1895
1896
  else if (node.schemaname) {
1896
1897
  tableName = QuoteUtils.quoteQualifiedIdentifier(node.schemaname, node.relname);
@@ -5155,7 +5156,8 @@ export class Deparser {
5155
5156
  output.push('FUNCTION');
5156
5157
  }
5157
5158
  if (node.funcname && node.funcname.length > 0) {
5158
- const funcName = node.funcname.map((name) => this.visit(name, context)).join('.');
5159
+ const funcnameParts = node.funcname.map((name) => name.String?.sval || name.String?.str || '').filter((s) => s);
5160
+ const funcName = QuoteUtils.quoteDottedName(funcnameParts);
5159
5161
  if (node.parameters && node.parameters.length > 0) {
5160
5162
  const params = node.parameters
5161
5163
  .filter((param) => {
@@ -48,7 +48,6 @@ export class QuoteUtils {
48
48
  static quoteIdentifier(ident) {
49
49
  if (!ident)
50
50
  return ident;
51
- let nquotes = 0;
52
51
  let safe = true;
53
52
  // Check first character: must be lowercase letter or underscore
54
53
  const firstChar = ident[0];
@@ -65,9 +64,6 @@ export class QuoteUtils {
65
64
  }
66
65
  else {
67
66
  safe = false;
68
- if (ch === '"') {
69
- nquotes++;
70
- }
71
67
  }
72
68
  }
73
69
  if (safe) {
@@ -94,18 +90,81 @@ export class QuoteUtils {
94
90
  result += '"';
95
91
  return result;
96
92
  }
93
+ /**
94
+ * Quote an identifier that appears after a dot in a qualified name.
95
+ *
96
+ * In PostgreSQL's grammar, identifiers that appear after a dot (e.g., schema.name,
97
+ * table.column) are in a more permissive position that accepts all keyword categories
98
+ * including RESERVED_KEYWORD. This means we only need to quote for lexical reasons
99
+ * (uppercase, special characters, leading digits) not for keyword reasons.
100
+ *
101
+ * Empirically verified: `myschema.select`, `myschema.float`, `t.from` all parse
102
+ * successfully in PostgreSQL without quotes.
103
+ */
104
+ static quoteIdentifierAfterDot(ident) {
105
+ if (!ident)
106
+ return ident;
107
+ let safe = true;
108
+ const firstChar = ident[0];
109
+ if (!((firstChar >= 'a' && firstChar <= 'z') || firstChar === '_')) {
110
+ safe = false;
111
+ }
112
+ for (let i = 0; i < ident.length; i++) {
113
+ const ch = ident[i];
114
+ if ((ch >= 'a' && ch <= 'z') ||
115
+ (ch >= '0' && ch <= '9') ||
116
+ (ch === '_')) {
117
+ // okay
118
+ }
119
+ else {
120
+ safe = false;
121
+ }
122
+ }
123
+ if (safe) {
124
+ return ident;
125
+ }
126
+ let result = '"';
127
+ for (let i = 0; i < ident.length; i++) {
128
+ const ch = ident[i];
129
+ if (ch === '"') {
130
+ result += '"';
131
+ }
132
+ result += ch;
133
+ }
134
+ result += '"';
135
+ return result;
136
+ }
137
+ /**
138
+ * Quote a dotted name (e.g., schema.table, catalog.schema.table).
139
+ *
140
+ * The first part uses strict quoting (keywords are quoted), while subsequent
141
+ * parts use relaxed quoting (keywords allowed, only quote for lexical reasons).
142
+ *
143
+ * This reflects PostgreSQL's grammar where the first identifier in a statement
144
+ * may conflict with keywords, but identifiers after a dot are in a more
145
+ * permissive position.
146
+ */
147
+ static quoteDottedName(parts) {
148
+ if (!parts || parts.length === 0)
149
+ return '';
150
+ if (parts.length === 1) {
151
+ return QuoteUtils.quoteIdentifier(parts[0]);
152
+ }
153
+ return parts.map((part, index) => index === 0 ? QuoteUtils.quoteIdentifier(part) : QuoteUtils.quoteIdentifierAfterDot(part)).join('.');
154
+ }
97
155
  /**
98
156
  * Quote a possibly-qualified identifier
99
157
  *
100
- * This is a TypeScript port of PostgreSQL's quote_qualified_identifier() function from ruleutils.c
101
- * https://github.com/postgres/postgres/blob/fab5cd3dd1323f9e66efeb676c4bb212ff340204/src/backend/utils/adt/ruleutils.c#L13139-L13156
158
+ * This is inspired by PostgreSQL's quote_qualified_identifier() function from ruleutils.c
159
+ * but uses relaxed quoting for the tail component since PostgreSQL's grammar accepts
160
+ * all keywords in qualified name positions.
102
161
  *
103
162
  * Return a name of the form qualifier.ident, or just ident if qualifier
104
163
  * is null/undefined, quoting each component if necessary.
105
164
  */
106
165
  static quoteQualifiedIdentifier(qualifier, ident) {
107
166
  if (qualifier) {
108
- return `${QuoteUtils.quoteIdentifier(qualifier)}.${QuoteUtils.quoteIdentifier(ident)}`;
167
+ return `${QuoteUtils.quoteIdentifier(qualifier)}.${QuoteUtils.quoteIdentifierAfterDot(ident)}`;
109
168
  }
110
169
  return QuoteUtils.quoteIdentifier(ident);
111
170
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgsql-deparser",
3
- "version": "17.13.0",
3
+ "version": "17.14.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "PostgreSQL AST Deparser",
6
6
  "main": "index.js",
@@ -60,5 +60,5 @@
60
60
  "dependencies": {
61
61
  "@pgsql/types": "^17.6.2"
62
62
  },
63
- "gitHead": "7cbb9621ab078db873c1627713963ffa98b8447c"
63
+ "gitHead": "48ea4210dc676c26c3ca4de8650cdd64c4eb3bd3"
64
64
  }
@@ -28,11 +28,35 @@ export declare class QuoteUtils {
28
28
  * When quotes are needed, embedded double quotes are properly escaped as "".
29
29
  */
30
30
  static quoteIdentifier(ident: string): string;
31
+ /**
32
+ * Quote an identifier that appears after a dot in a qualified name.
33
+ *
34
+ * In PostgreSQL's grammar, identifiers that appear after a dot (e.g., schema.name,
35
+ * table.column) are in a more permissive position that accepts all keyword categories
36
+ * including RESERVED_KEYWORD. This means we only need to quote for lexical reasons
37
+ * (uppercase, special characters, leading digits) not for keyword reasons.
38
+ *
39
+ * Empirically verified: `myschema.select`, `myschema.float`, `t.from` all parse
40
+ * successfully in PostgreSQL without quotes.
41
+ */
42
+ static quoteIdentifierAfterDot(ident: string): string;
43
+ /**
44
+ * Quote a dotted name (e.g., schema.table, catalog.schema.table).
45
+ *
46
+ * The first part uses strict quoting (keywords are quoted), while subsequent
47
+ * parts use relaxed quoting (keywords allowed, only quote for lexical reasons).
48
+ *
49
+ * This reflects PostgreSQL's grammar where the first identifier in a statement
50
+ * may conflict with keywords, but identifiers after a dot are in a more
51
+ * permissive position.
52
+ */
53
+ static quoteDottedName(parts: string[]): string;
31
54
  /**
32
55
  * Quote a possibly-qualified identifier
33
56
  *
34
- * This is a TypeScript port of PostgreSQL's quote_qualified_identifier() function from ruleutils.c
35
- * https://github.com/postgres/postgres/blob/fab5cd3dd1323f9e66efeb676c4bb212ff340204/src/backend/utils/adt/ruleutils.c#L13139-L13156
57
+ * This is inspired by PostgreSQL's quote_qualified_identifier() function from ruleutils.c
58
+ * but uses relaxed quoting for the tail component since PostgreSQL's grammar accepts
59
+ * all keywords in qualified name positions.
36
60
  *
37
61
  * Return a name of the form qualifier.ident, or just ident if qualifier
38
62
  * is null/undefined, quoting each component if necessary.
@@ -51,7 +51,6 @@ class QuoteUtils {
51
51
  static quoteIdentifier(ident) {
52
52
  if (!ident)
53
53
  return ident;
54
- let nquotes = 0;
55
54
  let safe = true;
56
55
  // Check first character: must be lowercase letter or underscore
57
56
  const firstChar = ident[0];
@@ -68,9 +67,6 @@ class QuoteUtils {
68
67
  }
69
68
  else {
70
69
  safe = false;
71
- if (ch === '"') {
72
- nquotes++;
73
- }
74
70
  }
75
71
  }
76
72
  if (safe) {
@@ -97,18 +93,81 @@ class QuoteUtils {
97
93
  result += '"';
98
94
  return result;
99
95
  }
96
+ /**
97
+ * Quote an identifier that appears after a dot in a qualified name.
98
+ *
99
+ * In PostgreSQL's grammar, identifiers that appear after a dot (e.g., schema.name,
100
+ * table.column) are in a more permissive position that accepts all keyword categories
101
+ * including RESERVED_KEYWORD. This means we only need to quote for lexical reasons
102
+ * (uppercase, special characters, leading digits) not for keyword reasons.
103
+ *
104
+ * Empirically verified: `myschema.select`, `myschema.float`, `t.from` all parse
105
+ * successfully in PostgreSQL without quotes.
106
+ */
107
+ static quoteIdentifierAfterDot(ident) {
108
+ if (!ident)
109
+ return ident;
110
+ let safe = true;
111
+ const firstChar = ident[0];
112
+ if (!((firstChar >= 'a' && firstChar <= 'z') || firstChar === '_')) {
113
+ safe = false;
114
+ }
115
+ for (let i = 0; i < ident.length; i++) {
116
+ const ch = ident[i];
117
+ if ((ch >= 'a' && ch <= 'z') ||
118
+ (ch >= '0' && ch <= '9') ||
119
+ (ch === '_')) {
120
+ // okay
121
+ }
122
+ else {
123
+ safe = false;
124
+ }
125
+ }
126
+ if (safe) {
127
+ return ident;
128
+ }
129
+ let result = '"';
130
+ for (let i = 0; i < ident.length; i++) {
131
+ const ch = ident[i];
132
+ if (ch === '"') {
133
+ result += '"';
134
+ }
135
+ result += ch;
136
+ }
137
+ result += '"';
138
+ return result;
139
+ }
140
+ /**
141
+ * Quote a dotted name (e.g., schema.table, catalog.schema.table).
142
+ *
143
+ * The first part uses strict quoting (keywords are quoted), while subsequent
144
+ * parts use relaxed quoting (keywords allowed, only quote for lexical reasons).
145
+ *
146
+ * This reflects PostgreSQL's grammar where the first identifier in a statement
147
+ * may conflict with keywords, but identifiers after a dot are in a more
148
+ * permissive position.
149
+ */
150
+ static quoteDottedName(parts) {
151
+ if (!parts || parts.length === 0)
152
+ return '';
153
+ if (parts.length === 1) {
154
+ return QuoteUtils.quoteIdentifier(parts[0]);
155
+ }
156
+ return parts.map((part, index) => index === 0 ? QuoteUtils.quoteIdentifier(part) : QuoteUtils.quoteIdentifierAfterDot(part)).join('.');
157
+ }
100
158
  /**
101
159
  * Quote a possibly-qualified identifier
102
160
  *
103
- * This is a TypeScript port of PostgreSQL's quote_qualified_identifier() function from ruleutils.c
104
- * https://github.com/postgres/postgres/blob/fab5cd3dd1323f9e66efeb676c4bb212ff340204/src/backend/utils/adt/ruleutils.c#L13139-L13156
161
+ * This is inspired by PostgreSQL's quote_qualified_identifier() function from ruleutils.c
162
+ * but uses relaxed quoting for the tail component since PostgreSQL's grammar accepts
163
+ * all keywords in qualified name positions.
105
164
  *
106
165
  * Return a name of the form qualifier.ident, or just ident if qualifier
107
166
  * is null/undefined, quoting each component if necessary.
108
167
  */
109
168
  static quoteQualifiedIdentifier(qualifier, ident) {
110
169
  if (qualifier) {
111
- return `${QuoteUtils.quoteIdentifier(qualifier)}.${QuoteUtils.quoteIdentifier(ident)}`;
170
+ return `${QuoteUtils.quoteIdentifier(qualifier)}.${QuoteUtils.quoteIdentifierAfterDot(ident)}`;
112
171
  }
113
172
  return QuoteUtils.quoteIdentifier(ident);
114
173
  }