pgsql-deparser 17.13.0 → 17.15.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 +11 -8
- package/esm/deparser.js +11 -8
- package/esm/utils/quote-utils.js +135 -7
- package/package.json +2 -2
- package/utils/quote-utils.d.ts +48 -2
- package/utils/quote-utils.js +135 -7
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.
|
|
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.
|
|
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
|
|
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);
|
|
@@ -1850,8 +1851,9 @@ class Deparser {
|
|
|
1850
1851
|
return output.join(' ');
|
|
1851
1852
|
}
|
|
1852
1853
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1854
|
+
// Use type-name quoting for non-pg_catalog types
|
|
1855
|
+
// This allows keywords like 'json', 'int', 'boolean' to remain unquoted in type positions
|
|
1856
|
+
let result = mods(quote_utils_1.QuoteUtils.quoteTypeDottedName(names), args);
|
|
1855
1857
|
if (node.arrayBounds && node.arrayBounds.length > 0) {
|
|
1856
1858
|
result += formatArrayBounds(node.arrayBounds);
|
|
1857
1859
|
}
|
|
@@ -1891,9 +1893,9 @@ class Deparser {
|
|
|
1891
1893
|
if (node.catalogname) {
|
|
1892
1894
|
tableName = quote_utils_1.QuoteUtils.quoteIdentifier(node.catalogname);
|
|
1893
1895
|
if (node.schemaname) {
|
|
1894
|
-
tableName += '.' + quote_utils_1.QuoteUtils.
|
|
1896
|
+
tableName += '.' + quote_utils_1.QuoteUtils.quoteIdentifierAfterDot(node.schemaname);
|
|
1895
1897
|
}
|
|
1896
|
-
tableName += '.' + quote_utils_1.QuoteUtils.
|
|
1898
|
+
tableName += '.' + quote_utils_1.QuoteUtils.quoteIdentifierAfterDot(node.relname);
|
|
1897
1899
|
}
|
|
1898
1900
|
else if (node.schemaname) {
|
|
1899
1901
|
tableName = quote_utils_1.QuoteUtils.quoteQualifiedIdentifier(node.schemaname, node.relname);
|
|
@@ -5158,7 +5160,8 @@ class Deparser {
|
|
|
5158
5160
|
output.push('FUNCTION');
|
|
5159
5161
|
}
|
|
5160
5162
|
if (node.funcname && node.funcname.length > 0) {
|
|
5161
|
-
const
|
|
5163
|
+
const funcnameParts = node.funcname.map((name) => name.String?.sval || name.String?.str || '').filter((s) => s);
|
|
5164
|
+
const funcName = quote_utils_1.QuoteUtils.quoteDottedName(funcnameParts);
|
|
5162
5165
|
if (node.parameters && node.parameters.length > 0) {
|
|
5163
5166
|
const params = node.parameters
|
|
5164
5167
|
.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.
|
|
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.
|
|
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
|
|
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);
|
|
@@ -1847,8 +1848,9 @@ export class Deparser {
|
|
|
1847
1848
|
return output.join(' ');
|
|
1848
1849
|
}
|
|
1849
1850
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1851
|
+
// Use type-name quoting for non-pg_catalog types
|
|
1852
|
+
// This allows keywords like 'json', 'int', 'boolean' to remain unquoted in type positions
|
|
1853
|
+
let result = mods(QuoteUtils.quoteTypeDottedName(names), args);
|
|
1852
1854
|
if (node.arrayBounds && node.arrayBounds.length > 0) {
|
|
1853
1855
|
result += formatArrayBounds(node.arrayBounds);
|
|
1854
1856
|
}
|
|
@@ -1888,9 +1890,9 @@ export class Deparser {
|
|
|
1888
1890
|
if (node.catalogname) {
|
|
1889
1891
|
tableName = QuoteUtils.quoteIdentifier(node.catalogname);
|
|
1890
1892
|
if (node.schemaname) {
|
|
1891
|
-
tableName += '.' + QuoteUtils.
|
|
1893
|
+
tableName += '.' + QuoteUtils.quoteIdentifierAfterDot(node.schemaname);
|
|
1892
1894
|
}
|
|
1893
|
-
tableName += '.' + QuoteUtils.
|
|
1895
|
+
tableName += '.' + QuoteUtils.quoteIdentifierAfterDot(node.relname);
|
|
1894
1896
|
}
|
|
1895
1897
|
else if (node.schemaname) {
|
|
1896
1898
|
tableName = QuoteUtils.quoteQualifiedIdentifier(node.schemaname, node.relname);
|
|
@@ -5155,7 +5157,8 @@ export class Deparser {
|
|
|
5155
5157
|
output.push('FUNCTION');
|
|
5156
5158
|
}
|
|
5157
5159
|
if (node.funcname && node.funcname.length > 0) {
|
|
5158
|
-
const
|
|
5160
|
+
const funcnameParts = node.funcname.map((name) => name.String?.sval || name.String?.str || '').filter((s) => s);
|
|
5161
|
+
const funcName = QuoteUtils.quoteDottedName(funcnameParts);
|
|
5159
5162
|
if (node.parameters && node.parameters.length > 0) {
|
|
5160
5163
|
const params = node.parameters
|
|
5161
5164
|
.filter((param) => {
|
package/esm/utils/quote-utils.js
CHANGED
|
@@ -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,19 +90,151 @@ 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
|
|
101
|
-
*
|
|
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.
|
|
167
|
+
return `${QuoteUtils.quoteIdentifier(qualifier)}.${QuoteUtils.quoteIdentifierAfterDot(ident)}`;
|
|
109
168
|
}
|
|
110
169
|
return QuoteUtils.quoteIdentifier(ident);
|
|
111
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Quote an identifier that appears as a type name.
|
|
173
|
+
*
|
|
174
|
+
* Type names in PostgreSQL have a less strict quoting policy than standalone identifiers.
|
|
175
|
+
* In type positions, COL_NAME_KEYWORD and TYPE_FUNC_NAME_KEYWORD are allowed unquoted
|
|
176
|
+
* (e.g., 'json', 'int', 'boolean', 'interval'). Only RESERVED_KEYWORD must be quoted.
|
|
177
|
+
*
|
|
178
|
+
* This is different from:
|
|
179
|
+
* - quoteIdentifier(): quotes all keywords except UNRESERVED_KEYWORD
|
|
180
|
+
* - quoteIdentifierAfterDot(): only quotes for lexical reasons (no keyword checking)
|
|
181
|
+
*
|
|
182
|
+
* Type names still need quoting for lexical reasons (uppercase, special chars, etc.).
|
|
183
|
+
*/
|
|
184
|
+
static quoteIdentifierTypeName(ident) {
|
|
185
|
+
if (!ident)
|
|
186
|
+
return ident;
|
|
187
|
+
let safe = true;
|
|
188
|
+
// Check first character: must be lowercase letter or underscore
|
|
189
|
+
const firstChar = ident[0];
|
|
190
|
+
if (!((firstChar >= 'a' && firstChar <= 'z') || firstChar === '_')) {
|
|
191
|
+
safe = false;
|
|
192
|
+
}
|
|
193
|
+
// Check all characters
|
|
194
|
+
for (let i = 0; i < ident.length; i++) {
|
|
195
|
+
const ch = ident[i];
|
|
196
|
+
if ((ch >= 'a' && ch <= 'z') ||
|
|
197
|
+
(ch >= '0' && ch <= '9') ||
|
|
198
|
+
(ch === '_')) {
|
|
199
|
+
// okay
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
safe = false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (safe) {
|
|
206
|
+
// For type names, only quote RESERVED_KEYWORD
|
|
207
|
+
// COL_NAME_KEYWORD and TYPE_FUNC_NAME_KEYWORD are allowed unquoted in type positions
|
|
208
|
+
const kwKind = keywordKindOf(ident);
|
|
209
|
+
if (kwKind === 'RESERVED_KEYWORD') {
|
|
210
|
+
safe = false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (safe) {
|
|
214
|
+
return ident; // no change needed
|
|
215
|
+
}
|
|
216
|
+
// Build quoted identifier with escaped embedded quotes
|
|
217
|
+
let result = '"';
|
|
218
|
+
for (let i = 0; i < ident.length; i++) {
|
|
219
|
+
const ch = ident[i];
|
|
220
|
+
if (ch === '"') {
|
|
221
|
+
result += '"'; // escape " as ""
|
|
222
|
+
}
|
|
223
|
+
result += ch;
|
|
224
|
+
}
|
|
225
|
+
result += '"';
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Quote a dotted type name (e.g., schema.typename).
|
|
230
|
+
*
|
|
231
|
+
* For type names, we use type-name quoting for all parts since the entire
|
|
232
|
+
* qualified name is in a type context. This allows keywords like 'json',
|
|
233
|
+
* 'int', 'boolean' to remain unquoted in user-defined schema-qualified types.
|
|
234
|
+
*/
|
|
235
|
+
static quoteTypeDottedName(parts) {
|
|
236
|
+
if (!parts || parts.length === 0)
|
|
237
|
+
return '';
|
|
238
|
+
return parts.map(part => QuoteUtils.quoteIdentifierTypeName(part)).join('.');
|
|
239
|
+
}
|
|
112
240
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgsql-deparser",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.15.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": "
|
|
63
|
+
"gitHead": "df8eb8b89aee325cd9005599bc2ac67a746aade7"
|
|
64
64
|
}
|
package/utils/quote-utils.d.ts
CHANGED
|
@@ -28,14 +28,60 @@ 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
|
|
35
|
-
*
|
|
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.
|
|
39
63
|
*/
|
|
40
64
|
static quoteQualifiedIdentifier(qualifier: string | null | undefined, ident: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Quote an identifier that appears as a type name.
|
|
67
|
+
*
|
|
68
|
+
* Type names in PostgreSQL have a less strict quoting policy than standalone identifiers.
|
|
69
|
+
* In type positions, COL_NAME_KEYWORD and TYPE_FUNC_NAME_KEYWORD are allowed unquoted
|
|
70
|
+
* (e.g., 'json', 'int', 'boolean', 'interval'). Only RESERVED_KEYWORD must be quoted.
|
|
71
|
+
*
|
|
72
|
+
* This is different from:
|
|
73
|
+
* - quoteIdentifier(): quotes all keywords except UNRESERVED_KEYWORD
|
|
74
|
+
* - quoteIdentifierAfterDot(): only quotes for lexical reasons (no keyword checking)
|
|
75
|
+
*
|
|
76
|
+
* Type names still need quoting for lexical reasons (uppercase, special chars, etc.).
|
|
77
|
+
*/
|
|
78
|
+
static quoteIdentifierTypeName(ident: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Quote a dotted type name (e.g., schema.typename).
|
|
81
|
+
*
|
|
82
|
+
* For type names, we use type-name quoting for all parts since the entire
|
|
83
|
+
* qualified name is in a type context. This allows keywords like 'json',
|
|
84
|
+
* 'int', 'boolean' to remain unquoted in user-defined schema-qualified types.
|
|
85
|
+
*/
|
|
86
|
+
static quoteTypeDottedName(parts: string[]): string;
|
|
41
87
|
}
|
package/utils/quote-utils.js
CHANGED
|
@@ -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,20 +93,152 @@ 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
|
|
104
|
-
*
|
|
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.
|
|
170
|
+
return `${QuoteUtils.quoteIdentifier(qualifier)}.${QuoteUtils.quoteIdentifierAfterDot(ident)}`;
|
|
112
171
|
}
|
|
113
172
|
return QuoteUtils.quoteIdentifier(ident);
|
|
114
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Quote an identifier that appears as a type name.
|
|
176
|
+
*
|
|
177
|
+
* Type names in PostgreSQL have a less strict quoting policy than standalone identifiers.
|
|
178
|
+
* In type positions, COL_NAME_KEYWORD and TYPE_FUNC_NAME_KEYWORD are allowed unquoted
|
|
179
|
+
* (e.g., 'json', 'int', 'boolean', 'interval'). Only RESERVED_KEYWORD must be quoted.
|
|
180
|
+
*
|
|
181
|
+
* This is different from:
|
|
182
|
+
* - quoteIdentifier(): quotes all keywords except UNRESERVED_KEYWORD
|
|
183
|
+
* - quoteIdentifierAfterDot(): only quotes for lexical reasons (no keyword checking)
|
|
184
|
+
*
|
|
185
|
+
* Type names still need quoting for lexical reasons (uppercase, special chars, etc.).
|
|
186
|
+
*/
|
|
187
|
+
static quoteIdentifierTypeName(ident) {
|
|
188
|
+
if (!ident)
|
|
189
|
+
return ident;
|
|
190
|
+
let safe = true;
|
|
191
|
+
// Check first character: must be lowercase letter or underscore
|
|
192
|
+
const firstChar = ident[0];
|
|
193
|
+
if (!((firstChar >= 'a' && firstChar <= 'z') || firstChar === '_')) {
|
|
194
|
+
safe = false;
|
|
195
|
+
}
|
|
196
|
+
// Check all characters
|
|
197
|
+
for (let i = 0; i < ident.length; i++) {
|
|
198
|
+
const ch = ident[i];
|
|
199
|
+
if ((ch >= 'a' && ch <= 'z') ||
|
|
200
|
+
(ch >= '0' && ch <= '9') ||
|
|
201
|
+
(ch === '_')) {
|
|
202
|
+
// okay
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
safe = false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (safe) {
|
|
209
|
+
// For type names, only quote RESERVED_KEYWORD
|
|
210
|
+
// COL_NAME_KEYWORD and TYPE_FUNC_NAME_KEYWORD are allowed unquoted in type positions
|
|
211
|
+
const kwKind = (0, kwlist_1.keywordKindOf)(ident);
|
|
212
|
+
if (kwKind === 'RESERVED_KEYWORD') {
|
|
213
|
+
safe = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (safe) {
|
|
217
|
+
return ident; // no change needed
|
|
218
|
+
}
|
|
219
|
+
// Build quoted identifier with escaped embedded quotes
|
|
220
|
+
let result = '"';
|
|
221
|
+
for (let i = 0; i < ident.length; i++) {
|
|
222
|
+
const ch = ident[i];
|
|
223
|
+
if (ch === '"') {
|
|
224
|
+
result += '"'; // escape " as ""
|
|
225
|
+
}
|
|
226
|
+
result += ch;
|
|
227
|
+
}
|
|
228
|
+
result += '"';
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Quote a dotted type name (e.g., schema.typename).
|
|
233
|
+
*
|
|
234
|
+
* For type names, we use type-name quoting for all parts since the entire
|
|
235
|
+
* qualified name is in a type context. This allows keywords like 'json',
|
|
236
|
+
* 'int', 'boolean' to remain unquoted in user-defined schema-qualified types.
|
|
237
|
+
*/
|
|
238
|
+
static quoteTypeDottedName(parts) {
|
|
239
|
+
if (!parts || parts.length === 0)
|
|
240
|
+
return '';
|
|
241
|
+
return parts.map(part => QuoteUtils.quoteIdentifierTypeName(part)).join('.');
|
|
242
|
+
}
|
|
115
243
|
}
|
|
116
244
|
exports.QuoteUtils = QuoteUtils;
|