Skip to content

Commit 30cbd40

Browse files
feat: add quoteIdentifier and quoteQualifiedIdentifier from PostgreSQL ruleutils.c
Port PostgreSQL's quote_identifier() and quote_qualified_identifier() functions to TypeScript. These functions properly: - Quote identifiers only when needed (lowercase letters, digits, underscores) - Escape embedded double quotes as "" - Check against keyword categories (quote all except UNRESERVED_KEYWORD) References: - https://github.com/postgres/postgres/blob/fab5cd3dd1323f9e66efeb676c4bb212ff340204/src/backend/utils/adt/ruleutils.c#L13055-L13137 - https://github.com/postgres/postgres/blob/fab5cd3dd1323f9e66efeb676c4bb212ff340204/src/backend/utils/adt/ruleutils.c#L13139-L13156 Co-Authored-By: Dan Lynch <[email protected]>
1 parent 7caeee9 commit 30cbd40

File tree

1 file changed

+84
-1
lines changed

1 file changed

+84
-1
lines changed

packages/deparser/src/utils/quote-utils.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RESERVED_KEYWORDS, TYPE_FUNC_NAME_KEYWORDS } from '../kwlist';
1+
import { RESERVED_KEYWORDS, TYPE_FUNC_NAME_KEYWORDS, UNRESERVED_KEYWORDS, keywordKindOf } from '../kwlist';
22

33
export class QuoteUtils {
44
/**
@@ -105,5 +105,88 @@ export class QuoteUtils {
105105
// unless it's a raw \x... bytea-style literal.
106106
return !/^\\x[0-9a-fA-F]+$/i.test(value) && value.includes('\\');
107107
}
108+
109+
/**
110+
* Quote an identifier only if needed
111+
*
112+
* This is a TypeScript port of PostgreSQL's quote_identifier() function from ruleutils.c
113+
* https://github.com/postgres/postgres/blob/fab5cd3dd1323f9e66efeb676c4bb212ff340204/src/backend/utils/adt/ruleutils.c#L13055-L13137
114+
*
115+
* Can avoid quoting if ident starts with a lowercase letter or underscore
116+
* and contains only lowercase letters, digits, and underscores, *and* is
117+
* not any SQL keyword. Otherwise, supply quotes.
118+
*
119+
* When quotes are needed, embedded double quotes are properly escaped as "".
120+
*/
121+
static quoteIdentifier(ident: string): string {
122+
if (!ident) return ident;
123+
124+
let nquotes = 0;
125+
let safe = true;
126+
127+
// Check first character: must be lowercase letter or underscore
128+
const firstChar = ident[0];
129+
if (!((firstChar >= 'a' && firstChar <= 'z') || firstChar === '_')) {
130+
safe = false;
131+
}
132+
133+
// Check all characters
134+
for (let i = 0; i < ident.length; i++) {
135+
const ch = ident[i];
136+
if ((ch >= 'a' && ch <= 'z') ||
137+
(ch >= '0' && ch <= '9') ||
138+
(ch === '_')) {
139+
// okay
140+
} else {
141+
safe = false;
142+
if (ch === '"') {
143+
nquotes++;
144+
}
145+
}
146+
}
147+
148+
if (safe) {
149+
// Check for keyword. We quote keywords except for unreserved ones.
150+
// (In some cases we could avoid quoting a col_name or type_func_name
151+
// keyword, but it seems much harder than it's worth to tell that.)
152+
const kwKind = keywordKindOf(ident);
153+
if (kwKind !== 'NO_KEYWORD' && kwKind !== 'UNRESERVED_KEYWORD') {
154+
safe = false;
155+
}
156+
}
157+
158+
if (safe) {
159+
return ident; // no change needed
160+
}
161+
162+
// Build quoted identifier with escaped embedded quotes
163+
let result = '"';
164+
for (let i = 0; i < ident.length; i++) {
165+
const ch = ident[i];
166+
if (ch === '"') {
167+
result += '"'; // escape " as ""
168+
}
169+
result += ch;
170+
}
171+
result += '"';
172+
173+
return result;
174+
}
175+
176+
/**
177+
* Quote a possibly-qualified identifier
178+
*
179+
* This is a TypeScript port of PostgreSQL's quote_qualified_identifier() function from ruleutils.c
180+
* https://github.com/postgres/postgres/blob/fab5cd3dd1323f9e66efeb676c4bb212ff340204/src/backend/utils/adt/ruleutils.c#L13139-L13156
181+
*
182+
* Return a name of the form qualifier.ident, or just ident if qualifier
183+
* is null/undefined, quoting each component if necessary.
184+
*/
185+
static quoteQualifiedIdentifier(qualifier: string | null | undefined, ident: string): string {
186+
if (qualifier) {
187+
return `${QuoteUtils.quoteIdentifier(qualifier)}.${QuoteUtils.quoteIdentifier(ident)}`;
188+
}
189+
return QuoteUtils.quoteIdentifier(ident);
190+
}
108191

109192
}

0 commit comments

Comments
 (0)