diff --git a/PUBLISH.md b/PUBLISH.md index fd41b4a0..fe9261d5 100644 --- a/PUBLISH.md +++ b/PUBLISH.md @@ -136,7 +136,7 @@ This script (`scripts/prepare-versions.ts`): #### Build and Publish Individual Versions ```bash # Navigate to a specific version directory -cd packages/parser/versions/17 +cd packages/parser/versions/13 # Build the package npm run build @@ -146,7 +146,7 @@ npm run build cd dist/ # Publish with the correct tag -npm publish --tag pg17 +npm publish --tag pg13 ``` #### Publish All Parser Versions diff --git a/packages/deparser/package.json b/packages/deparser/package.json index 8d0f3883..86170292 100644 --- a/packages/deparser/package.json +++ b/packages/deparser/package.json @@ -40,7 +40,8 @@ "organize-transformers": "ts-node scripts/organize-transformers-by-version.ts", "generate-version-deparsers": "ts-node scripts/generate-version-deparsers.ts", "generate-packages": "ts-node scripts/generate-version-packages.ts", - "prepare-versions": "npm run strip-transformer-types && npm run strip-direct-transformer-types && npm run strip-deparser-types && npm run organize-transformers && npm run generate-version-deparsers && npm run generate-packages" + "prepare-versions": "npm run strip-transformer-types && npm run strip-direct-transformer-types && npm run strip-deparser-types && npm run organize-transformers && npm run generate-version-deparsers && npm run generate-packages", + "keywords": "ts-node scripts/keywords.ts" }, "keywords": [ "sql", diff --git a/packages/deparser/scripts/keywords.ts b/packages/deparser/scripts/keywords.ts new file mode 100644 index 00000000..3bd2ad68 --- /dev/null +++ b/packages/deparser/scripts/keywords.ts @@ -0,0 +1,114 @@ +import fs from "node:fs"; +import path from "node:path"; +import readline from "node:readline"; + +function ask(question: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question(`${question}: `, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +function requireNonEmpty(value: string | undefined, label: string): string { + if (!value) { + console.error(`❌ Missing ${label}.`); + process.exit(1); + } + return value; +} + +function expandTilde(p: string): string { + if (p.startsWith("~/")) { + return path.join(process.env.HOME || "", p.slice(2)); + } + return p; +} + +async function main() { + const [, , kwlistArg, outArg] = process.argv; + + // kwlist.h path is required (CLI arg or prompt), output defaults to src/kwlist.ts + let kwlistPathInput = kwlistArg; + if (!kwlistPathInput) { + console.log("e.g. ~/code/postgres/postgres/src/include/parser/kwlist.h"); + kwlistPathInput = requireNonEmpty(await ask("Path to PostgreSQL kwlist.h"), "kwlist.h path"); + } + + const outPathInput = outArg ?? path.resolve(__dirname, "../src/kwlist.ts"); + + const kwlistPath = path.resolve(expandTilde(kwlistPathInput)); + const outPath = path.resolve(outPathInput); + + if (!fs.existsSync(kwlistPath)) { + console.error(`❌ kwlist.h not found: ${kwlistPath}`); + process.exit(1); + } + + const src = fs.readFileSync(kwlistPath, "utf8"); + + // PG_KEYWORD("word", TOKEN, KIND_KEYWORD, ...) + const re = /^PG_KEYWORD\("([^"]+)",\s*[^,]+,\s*([A-Z_]+)_KEYWORD\b/gm; + + const kinds = new Map>(); + let m: RegExpExecArray | null; + while ((m = re.exec(src))) { + const word = m[1].toLowerCase(); + const kind = `${m[2]}_KEYWORD`; + + if (!kinds.has(kind)) kinds.set(kind, new Set()); + kinds.get(kind)!.add(word); + } + + // Stable, sorted output + const keywordsByKind: Record = {}; + for (const [kind, set] of kinds.entries()) { + keywordsByKind[kind] = [...set].sort(); + } + + const ts = `/* eslint-disable */ +/** + * Generated from PostgreSQL kwlist.h + * DO NOT EDIT BY HAND. + */ + +export type KeywordKind = + | "NO_KEYWORD" + | "UNRESERVED_KEYWORD" + | "COL_NAME_KEYWORD" + | "TYPE_FUNC_NAME_KEYWORD" + | "RESERVED_KEYWORD"; + +export const kwlist = ${JSON.stringify(keywordsByKind, null, 2) + .replace(/"([A-Z_]+)"/g, "$1")} as const; + +export const RESERVED_KEYWORDS: Set = new Set(kwlist.RESERVED_KEYWORD ?? []); +export const UNRESERVED_KEYWORDS: Set = new Set(kwlist.UNRESERVED_KEYWORD ?? []); +export const COL_NAME_KEYWORDS: Set = new Set(kwlist.COL_NAME_KEYWORD ?? []); +export const TYPE_FUNC_NAME_KEYWORDS: Set = new Set(kwlist.TYPE_FUNC_NAME_KEYWORD ?? []); + +export function keywordKindOf(word: string): KeywordKind { + const w = word.toLowerCase(); + if (RESERVED_KEYWORDS.has(w)) return "RESERVED_KEYWORD"; + if (TYPE_FUNC_NAME_KEYWORDS.has(w)) return "TYPE_FUNC_NAME_KEYWORD"; + if (COL_NAME_KEYWORDS.has(w)) return "COL_NAME_KEYWORD"; + if (UNRESERVED_KEYWORDS.has(w)) return "UNRESERVED_KEYWORD"; + return "NO_KEYWORD"; +} +`; + + fs.writeFileSync(outPath, ts, "utf8"); + console.log(`✅ Wrote ${outPath}`); + console.log(` Source: ${kwlistPath}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 52c52db0..19ab64bb 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -1311,13 +1311,13 @@ export class Deparser implements DeparserVisitor { const output: string[] = []; if (context.update && node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); // Handle indirection (array indexing, field access, etc.) if (node.indirection && node.indirection.length > 0) { const indirectionStrs = ListUtils.unwrapList(node.indirection).map(item => { if (item.String) { - return `.${QuoteUtils.quote(item.String.sval || item.String.str)}`; + return `.${QuoteUtils.quoteIdentifier(item.String.sval || item.String.str)}`; } return this.visit(item, context); }); @@ -1329,13 +1329,13 @@ export class Deparser implements DeparserVisitor { output.push(this.deparse(node.val, context)); } } else if (context.insertColumns && node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); // Handle indirection for INSERT column lists (e.g., q.c1.r) if (node.indirection && node.indirection.length > 0) { const indirectionStrs = ListUtils.unwrapList(node.indirection).map(item => { if (item.String) { - return `.${QuoteUtils.quote(item.String.sval || item.String.str)}`; + return `.${QuoteUtils.quoteIdentifier(item.String.sval || item.String.str)}`; } return this.visit(item, context); }); @@ -1348,7 +1348,7 @@ export class Deparser implements DeparserVisitor { if (node.name) { output.push('AS'); - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } } @@ -1364,7 +1364,7 @@ export class Deparser implements DeparserVisitor { if (this.getNodeType(item) === 'ResTarget') { const resTarget = this.getNodeData(item) as any; const val = resTarget.val ? this.visit(resTarget.val, context) : ''; - const alias = resTarget.name ? ` AS ${QuoteUtils.quote(resTarget.name)}` : ''; + const alias = resTarget.name ? ` AS ${QuoteUtils.quoteIdentifier(resTarget.name)}` : ''; return val + alias; } else { const val = this.visit(item, context); @@ -1798,7 +1798,7 @@ export class Deparser implements DeparserVisitor { const fields = ListUtils.unwrapList(node.fields); return fields.map(field => { if (field.String) { - return QuoteUtils.quote(field.String.sval || field.String.str); + return QuoteUtils.quoteIdentifier(field.String.sval || field.String.str); } else if (field.A_Star) { return '*'; } @@ -1883,8 +1883,7 @@ export class Deparser implements DeparserVisitor { return output.join(' '); } - const quotedTypeName = QuoteUtils.quote(typeName); - let result = mods(quotedTypeName, args); + let result = mods(typeName, args); if (node.arrayBounds && node.arrayBounds.length > 0) { result += formatArrayBounds(node.arrayBounds); @@ -1972,7 +1971,7 @@ export class Deparser implements DeparserVisitor { } } - const quotedNames = names.map((name: string) => QuoteUtils.quote(name)); + const quotedNames = names.map((name: string) => QuoteUtils.quoteIdentifier(name)); let result = mods(quotedNames.join('.'), args); if (node.arrayBounds && node.arrayBounds.length > 0) { @@ -2017,15 +2016,15 @@ export class Deparser implements DeparserVisitor { let tableName = ''; if (node.catalogname) { - tableName = QuoteUtils.quote(node.catalogname); + tableName = QuoteUtils.quoteIdentifier(node.catalogname); if (node.schemaname) { - tableName += '.' + QuoteUtils.quote(node.schemaname); + tableName += '.' + QuoteUtils.quoteIdentifier(node.schemaname); } - tableName += '.' + QuoteUtils.quote(node.relname); + tableName += '.' + QuoteUtils.quoteIdentifier(node.relname); } else if (node.schemaname) { - tableName = QuoteUtils.quote(node.schemaname) + '.' + QuoteUtils.quote(node.relname); + tableName = QuoteUtils.quoteQualifiedIdentifier(node.schemaname, node.relname); } else { - tableName = QuoteUtils.quote(node.relname); + tableName = QuoteUtils.quoteIdentifier(node.relname); } output.push(tableName); @@ -2281,7 +2280,7 @@ export class Deparser implements DeparserVisitor { for (const subnode of indirection) { if (subnode.String || subnode.A_Star) { - const value = subnode.A_Star ? '*' : QuoteUtils.quote(subnode.String.sval || subnode.String.str); + const value = subnode.A_Star ? '*' : QuoteUtils.quoteIdentifier(subnode.String.sval || subnode.String.str); output.push(`.${value}`); } else { output.push(this.visit(subnode, context)); @@ -2457,36 +2456,8 @@ export class Deparser implements DeparserVisitor { return output.join(' '); } - private static readonly RESERVED_WORDS = new Set([ - 'all', 'analyse', 'analyze', 'and', 'any', 'array', 'as', 'asc', 'asymmetric', 'both', - 'case', 'cast', 'check', 'collate', 'column', 'constraint', 'create', 'current_catalog', - 'current_date', 'current_role', 'current_time', 'current_timestamp', 'current_user', - 'default', 'deferrable', 'desc', 'distinct', 'do', 'else', 'end', 'except', 'false', - 'fetch', 'for', 'foreign', 'from', 'grant', 'group', 'having', 'in', 'initially', - 'intersect', 'into', 'lateral', 'leading', 'limit', 'localtime', 'localtimestamp', - 'not', 'null', 'offset', 'on', 'only', 'or', 'order', 'placing', 'primary', - 'references', 'returning', 'select', 'session_user', 'some', 'symmetric', 'table', - 'then', 'to', 'trailing', 'true', 'union', 'unique', 'user', 'using', 'variadic', - 'when', 'where', 'window', 'with' - ]); - - private static needsQuotes(value: string): boolean { - if (!value) return false; - - const needsQuotesRegex = /[a-z]+[\W\w]*[A-Z]+|[A-Z]+[\W\w]*[a-z]+|\W/; - - const isAllUppercase = /^[A-Z]+$/.test(value); - - return needsQuotesRegex.test(value) || - Deparser.RESERVED_WORDS.has(value.toLowerCase()) || - isAllUppercase; - } - quoteIfNeeded(value: string): string { - if (Deparser.needsQuotes(value)) { - return `"${value}"`; - } - return value; + return QuoteUtils.quoteIdentifier(value); } preserveOperatorDefElemCase(defName: string): string { @@ -2528,7 +2499,7 @@ export class Deparser implements DeparserVisitor { } } - return Deparser.needsQuotes(value) ? `"${value}"` : value; + return QuoteUtils.quoteIdentifier(value); } Integer(node: t.Integer, context: DeparserContext): string { @@ -2718,7 +2689,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = []; if (node.colname) { - output.push(QuoteUtils.quote(node.colname)); + output.push(QuoteUtils.quoteIdentifier(node.colname)); } if (node.typeName) { @@ -2769,7 +2740,7 @@ export class Deparser implements DeparserVisitor { // Handle constraint name if present if (node.conname && (node.contype === 'CONSTR_CHECK' || node.contype === 'CONSTR_UNIQUE' || node.contype === 'CONSTR_PRIMARY' || node.contype === 'CONSTR_FOREIGN')) { output.push('CONSTRAINT'); - output.push(QuoteUtils.quote(node.conname)); + output.push(QuoteUtils.quoteIdentifier(node.conname)); } switch (node.contype) { @@ -3701,7 +3672,7 @@ export class Deparser implements DeparserVisitor { } if (node.idxname) { - output.push(QuoteUtils.quote(node.idxname)); + output.push(QuoteUtils.quoteIdentifier(node.idxname)); } output.push('ON'); @@ -3744,7 +3715,7 @@ export class Deparser implements DeparserVisitor { if (node.tableSpace) { output.push('TABLESPACE'); - output.push(QuoteUtils.quote(node.tableSpace)); + output.push(QuoteUtils.quoteIdentifier(node.tableSpace)); } return output.join(' '); @@ -3754,7 +3725,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = []; if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } else if (node.expr) { output.push(context.parens(this.visit(node.expr, context))); } @@ -3813,7 +3784,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = []; if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } else if (node.expr) { output.push(context.parens(this.visit(node.expr, context))); } @@ -4081,19 +4052,19 @@ export class Deparser implements DeparserVisitor { case 'TRANS_STMT_SAVEPOINT': output.push('SAVEPOINT'); if (node.savepoint_name) { - output.push(QuoteUtils.quote(node.savepoint_name)); + output.push(QuoteUtils.quoteIdentifier(node.savepoint_name)); } break; case 'TRANS_STMT_RELEASE': output.push('RELEASE SAVEPOINT'); if (node.savepoint_name) { - output.push(QuoteUtils.quote(node.savepoint_name)); + output.push(QuoteUtils.quoteIdentifier(node.savepoint_name)); } break; case 'TRANS_STMT_ROLLBACK_TO': output.push('ROLLBACK TO'); if (node.savepoint_name) { - output.push(QuoteUtils.quote(node.savepoint_name)); + output.push(QuoteUtils.quoteIdentifier(node.savepoint_name)); } break; case 'TRANS_STMT_PREPARE': @@ -4190,18 +4161,18 @@ export class Deparser implements DeparserVisitor { return this.visit(arg, context); }).join(', ') : ''; - // Handle args - always include TO clause if args exist (even if empty string) - const paramName = node.name && (node.name.includes('.') || node.name.includes('-') || /[A-Z]/.test(node.name)) ? `"${node.name}"` : node.name; - if (!node.args || node.args.length === 0) { - return `SET ${localPrefix}${paramName}`; - } - return `SET ${localPrefix}${paramName} TO ${args}`; - case 'VAR_SET_DEFAULT': - const defaultParamName = node.name && (node.name.includes('.') || node.name.includes('-') || /[A-Z]/.test(node.name)) ? `"${node.name}"` : node.name; - return `SET ${defaultParamName} TO DEFAULT`; - case 'VAR_SET_CURRENT': - const currentParamName = node.name && (node.name.includes('.') || node.name.includes('-') || /[A-Z]/.test(node.name)) ? `"${node.name}"` : node.name; - return `SET ${currentParamName} FROM CURRENT`; + // Handle args - always include TO clause if args exist (even if empty string) + const paramName = QuoteUtils.quoteIdentifier(node.name); + if (!node.args || node.args.length === 0) { + return `SET ${localPrefix}${paramName}`; + } + return `SET ${localPrefix}${paramName} TO ${args}`; + case 'VAR_SET_DEFAULT': + const defaultParamName = QuoteUtils.quoteIdentifier(node.name); + return `SET ${defaultParamName} TO DEFAULT`; + case 'VAR_SET_CURRENT': + const currentParamName = QuoteUtils.quoteIdentifier(node.name); + return `SET ${currentParamName} FROM CURRENT`; case 'VAR_SET_MULTI': if (node.name === 'TRANSACTION' || node.name === 'SESSION CHARACTERISTICS') { // Handle SET TRANSACTION statements specially @@ -4267,9 +4238,9 @@ export class Deparser implements DeparserVisitor { }).join(', ') : ''; return `SET ${assignments}`; } - case 'VAR_RESET': - const resetParamName = node.name && (node.name.includes('.') || node.name.includes('-') || /[A-Z]/.test(node.name)) ? `"${node.name}"` : node.name; - return `RESET ${resetParamName}`; + case 'VAR_RESET': + const resetParamName = QuoteUtils.quoteIdentifier(node.name); + return `RESET ${resetParamName}`; case 'VAR_RESET_ALL': return 'RESET ALL'; default: @@ -4552,7 +4523,7 @@ export class Deparser implements DeparserVisitor { if (objList && objList.List && objList.List.items) { const items = objList.List.items.map((item: any) => { if (item.String && item.String.sval) { - return QuoteUtils.quote(item.String.sval); + return QuoteUtils.quoteIdentifier(item.String.sval); } return this.visit(item, context); }).filter((name: string) => name && name.trim()); @@ -4587,12 +4558,12 @@ export class Deparser implements DeparserVisitor { if (items.length === 2) { const accessMethod = items[0]; const objectName = items[1]; - return `${QuoteUtils.quote(objectName)} USING ${accessMethod}`; + return `${QuoteUtils.quoteIdentifier(objectName)} USING ${accessMethod}`; } else if (items.length === 3) { const accessMethod = items[0]; const schemaName = items[1]; const objectName = items[2]; - return `${QuoteUtils.quote(schemaName)}.${QuoteUtils.quote(objectName)} USING ${accessMethod}`; + return `${QuoteUtils.quoteQualifiedIdentifier(schemaName, objectName)} USING ${accessMethod}`; } return items.join('.'); } @@ -4637,7 +4608,7 @@ export class Deparser implements DeparserVisitor { if (objList && objList.List && objList.List.items) { const items = objList.List.items.map((item: any) => { if (item.String && item.String.sval) { - return QuoteUtils.quote(item.String.sval); + return QuoteUtils.quoteIdentifier(item.String.sval); } return this.visit(item, context); }).filter((name: string) => name && name.trim()); @@ -4698,7 +4669,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = []; if (node.name) { - let nameWithIndirection = QuoteUtils.quote(node.name); + let nameWithIndirection = QuoteUtils.quoteIdentifier(node.name); if (node.indirection && node.indirection.length > 0) { const indirectionStr = node.indirection @@ -4865,7 +4836,7 @@ export class Deparser implements DeparserVisitor { const indentedParts: string[] = []; if (colDefData.colname) { - parts.push(QuoteUtils.quote(colDefData.colname)); + parts.push(QuoteUtils.quoteIdentifier(colDefData.colname)); } if (colDefData.typeName) { @@ -4929,7 +4900,7 @@ export class Deparser implements DeparserVisitor { const parts: string[] = []; if (colDefData.colname) { - parts.push(QuoteUtils.quote(colDefData.colname)); + parts.push(QuoteUtils.quoteIdentifier(colDefData.colname)); } if (colDefData.typeName) { @@ -4987,7 +4958,7 @@ export class Deparser implements DeparserVisitor { } } if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } if (node.behavior === 'DROP_CASCADE') { output.push('CASCADE'); @@ -5002,7 +4973,7 @@ export class Deparser implements DeparserVisitor { output.push('ALTER COLUMN'); } if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('TYPE'); if (node.def) { @@ -5028,7 +4999,7 @@ export class Deparser implements DeparserVisitor { case 'AT_SetTableSpace': output.push('SET TABLESPACE'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'AT_AddConstraint': @@ -5045,7 +5016,7 @@ export class Deparser implements DeparserVisitor { output.push('DROP CONSTRAINT'); } if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } if (node.behavior === 'DROP_CASCADE') { output.push('CASCADE'); @@ -5080,7 +5051,7 @@ export class Deparser implements DeparserVisitor { case 'AT_ColumnDefault': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } if (node.def) { output.push('SET DEFAULT'); @@ -5092,7 +5063,7 @@ export class Deparser implements DeparserVisitor { case 'AT_SetStorage': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('SET STORAGE'); if (node.def) { @@ -5103,7 +5074,7 @@ export class Deparser implements DeparserVisitor { case 'AT_ClusterOn': output.push('CLUSTER ON'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'AT_DropCluster': @@ -5130,21 +5101,21 @@ export class Deparser implements DeparserVisitor { case 'AT_SetNotNull': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('SET NOT NULL'); break; case 'AT_DropNotNull': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('DROP NOT NULL'); break; case 'AT_SetStatistics': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } else if (node.num !== undefined && node.num !== null) { output.push(node.num.toString()); } @@ -5156,7 +5127,7 @@ export class Deparser implements DeparserVisitor { case 'AT_SetOptions': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('SET'); if (node.def) { @@ -5172,7 +5143,7 @@ export class Deparser implements DeparserVisitor { case 'AT_ResetOptions': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('RESET'); if (node.def) { @@ -5188,7 +5159,7 @@ export class Deparser implements DeparserVisitor { case 'AT_SetCompression': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('SET COMPRESSION'); if (node.def) { @@ -5198,31 +5169,31 @@ export class Deparser implements DeparserVisitor { case 'AT_ValidateConstraint': output.push('VALIDATE CONSTRAINT'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'AT_EnableTrig': output.push('ENABLE TRIGGER'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'AT_EnableAlwaysTrig': output.push('ENABLE ALWAYS TRIGGER'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'AT_EnableReplicaTrig': output.push('ENABLE REPLICA TRIGGER'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'AT_DisableTrig': output.push('DISABLE TRIGGER'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'AT_EnableTrigAll': @@ -5240,31 +5211,31 @@ export class Deparser implements DeparserVisitor { case 'AT_EnableRule': output.push('ENABLE RULE'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'AT_EnableAlwaysRule': output.push('ENABLE ALWAYS RULE'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'AT_EnableReplicaRule': output.push('ENABLE REPLICA RULE'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'AT_DisableRule': output.push('DISABLE RULE'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'AT_SetAccessMethod': output.push('SET ACCESS METHOD'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } else { // Handle DEFAULT access method case output.push('DEFAULT'); @@ -5317,7 +5288,7 @@ export class Deparser implements DeparserVisitor { case 'AT_CookedColumnDefault': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } if (node.def) { output.push('SET DEFAULT'); @@ -5329,7 +5300,7 @@ export class Deparser implements DeparserVisitor { case 'AT_SetExpression': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('SET EXPRESSION'); if (node.def) { @@ -5339,14 +5310,14 @@ export class Deparser implements DeparserVisitor { case 'AT_DropExpression': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('DROP EXPRESSION'); break; case 'AT_CheckNotNull': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('SET NOT NULL'); break; @@ -5379,7 +5350,7 @@ export class Deparser implements DeparserVisitor { if (node.def && this.getNodeType(node.def) === 'Constraint') { const constraintData = this.getNodeData(node.def) as any; if (constraintData.conname) { - output.push(QuoteUtils.quote(constraintData.conname)); + output.push(QuoteUtils.quoteIdentifier(constraintData.conname)); if (constraintData.deferrable !== undefined) { output.push(constraintData.deferrable ? 'DEFERRABLE' : 'NOT DEFERRABLE'); } @@ -5388,7 +5359,7 @@ export class Deparser implements DeparserVisitor { } } } else if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); if (node.def) { output.push(this.visit(node.def, context)); } @@ -5409,7 +5380,7 @@ export class Deparser implements DeparserVisitor { case 'AT_AlterColumnGenericOptions': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('OPTIONS'); if (node.def) { @@ -5462,7 +5433,7 @@ export class Deparser implements DeparserVisitor { case 'AT_AddIdentity': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('ADD'); if (node.def) { @@ -5472,7 +5443,7 @@ export class Deparser implements DeparserVisitor { case 'AT_SetIdentity': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('SET'); if (node.def) { @@ -5482,7 +5453,7 @@ export class Deparser implements DeparserVisitor { case 'AT_DropIdentity': output.push('ALTER COLUMN'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } output.push('DROP IDENTITY'); if (node.behavior === 'DROP_CASCADE') { @@ -5630,7 +5601,7 @@ export class Deparser implements DeparserVisitor { } if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } if (node.argType) { @@ -5715,7 +5686,7 @@ export class Deparser implements DeparserVisitor { } if (node.role) { - const roleName = Deparser.needsQuotes(node.role) ? `"${node.role}"` : node.role; + const roleName = QuoteUtils.quoteIdentifier(node.role); output.push(roleName); } @@ -5759,14 +5730,14 @@ export class Deparser implements DeparserVisitor { return `${node.defname}=${this.visit(node.arg, context.spawn('DefElem'))}`; } - // Handle CREATE OPERATOR boolean flags - MUST be first to preserve case - if (context.parentNodeTypes.includes('DefineStmt') && - ['hashes', 'merges'].includes(node.defname.toLowerCase()) && !node.arg) { - if (node.defname !== node.defname.toLowerCase() && node.defname !== node.defname.toUpperCase()) { - return `"${node.defname}"`; - } - return node.defname.charAt(0).toUpperCase() + node.defname.slice(1).toLowerCase(); - } + // Handle CREATE OPERATOR boolean flags - MUST be first to preserve case + if (context.parentNodeTypes.includes('DefineStmt') && + ['hashes', 'merges'].includes(node.defname.toLowerCase()) && !node.arg) { + if (node.defname !== node.defname.toLowerCase() && node.defname !== node.defname.toUpperCase()) { + return QuoteUtils.quoteIdentifier(node.defname); + } + return node.defname.charAt(0).toUpperCase() + node.defname.slice(1).toLowerCase(); + } // Handle FDW-related statements and ALTER OPTIONS that use space format for options if (context.parentNodeTypes.includes('AlterFdwStmt') || context.parentNodeTypes.includes('CreateFdwStmt') || context.parentNodeTypes.includes('CreateForeignServerStmt') || context.parentNodeTypes.includes('AlterForeignServerStmt') || context.parentNodeTypes.includes('CreateUserMappingStmt') || context.parentNodeTypes.includes('AlterUserMappingStmt') || context.parentNodeTypes.includes('ColumnDef') || context.parentNodeTypes.includes('CreateForeignTableStmt') || context.parentNodeTypes.includes('ImportForeignSchemaStmt') || context.alterColumnOptions || context.alterTableOptions) { @@ -5788,9 +5759,7 @@ export class Deparser implements DeparserVisitor { ? `'${argValue}'` : argValue; - const quotedDefname = node.defname.includes(' ') || node.defname.includes('-') || Deparser.needsQuotes(node.defname) - ? `"${node.defname}"` - : node.defname; + const quotedDefname = QuoteUtils.quoteIdentifier(node.defname); if (node.defaction === 'DEFELEM_ADD') { return `ADD ${quotedDefname} ${finalValue}`; @@ -5815,10 +5784,8 @@ export class Deparser implements DeparserVisitor { return `SET ${node.defname} ${quotedValue}`; } - const quotedDefname = node.defname.includes(' ') || node.defname.includes('-') - ? `"${node.defname}"` - : node.defname; - return `${quotedDefname} ${quotedValue}`; + const quotedDefname = QuoteUtils.quoteIdentifier(node.defname); + return `${quotedDefname} ${quotedValue}`; } else if (node.defaction === 'DEFELEM_DROP') { // Handle DROP without argument return `DROP ${node.defname}`; @@ -5883,10 +5850,8 @@ export class Deparser implements DeparserVisitor { const quotedValue = typeof argValue === 'string' ? QuoteUtils.escape(argValue) : argValue; - const quotedDefname = node.defname.includes(' ') || node.defname.includes('-') - ? `"${node.defname}"` - : node.defname; - return `${quotedDefname} ${quotedValue}`; + const quotedDefname = QuoteUtils.quoteIdentifier(node.defname); + return `${quotedDefname} ${quotedValue}`; } @@ -5965,14 +5930,11 @@ export class Deparser implements DeparserVisitor { const listItems = ListUtils.unwrapList(listData.items); const parts = listItems.map(item => { const itemData = this.getNodeData(item); - if (this.getNodeType(item) === 'String') { - // Check if this identifier needs quotes to preserve case - const value = itemData.sval; - if (Deparser.needsQuotes(value)) { - return `"${value}"`; - } - return value; - } + if (this.getNodeType(item) === 'String') { + // Check if this identifier needs quotes to preserve case + const value = itemData.sval; + return QuoteUtils.quoteIdentifier(value); + } return this.visit(item, context); }); return `OWNED BY ${parts.join('.')}`; @@ -6236,18 +6198,18 @@ export class Deparser implements DeparserVisitor { return preservedName; } - // Handle boolean flags (no arguments) - preserve quoted case - if (['hashes', 'merges'].includes(node.defname.toLowerCase())) { - if (node.defname !== node.defname.toLowerCase() && node.defname !== node.defname.toUpperCase()) { - return `"${node.defname}"`; - } - return preservedName.toUpperCase(); - } + // Handle boolean flags (no arguments) - preserve quoted case + if (['hashes', 'merges'].includes(node.defname.toLowerCase())) { + if (node.defname !== node.defname.toLowerCase() && node.defname !== node.defname.toUpperCase()) { + return QuoteUtils.quoteIdentifier(node.defname); + } + return preservedName.toUpperCase(); + } - // Handle CREATE AGGREGATE quoted identifiers - preserve quotes when needed - if (Deparser.needsQuotes(node.defname)) { - const quotedDefname = `"${node.defname}"`; - if (node.arg) { + // Handle CREATE AGGREGATE quoted identifiers - preserve quotes when needed + const quotedDefname = QuoteUtils.quoteIdentifier(node.defname); + if (quotedDefname !== node.defname) { + if (node.arg) { if (this.getNodeType(node.arg) === 'String') { const stringData = this.getNodeData(node.arg); // Handle boolean string values without quotes @@ -6306,13 +6268,13 @@ export class Deparser implements DeparserVisitor { return `${node.defname} = ${quotedValue}`; } - // Handle CREATE TYPE boolean flags - preserve quoted case for attributes like "Passedbyvalue" - if (context.parentNodeTypes.includes('DefineStmt') && !node.arg) { - // Check if the original defname appears to be quoted (mixed case that's not all upper/lower) - if (node.defname !== node.defname.toLowerCase() && node.defname !== node.defname.toUpperCase()) { - return `"${node.defname}"`; - } - } + // Handle CREATE TYPE boolean flags - preserve quoted case for attributes like "Passedbyvalue" + if (context.parentNodeTypes.includes('DefineStmt') && !node.arg) { + // Check if the original defname appears to be quoted (mixed case that's not all upper/lower) + if (node.defname !== node.defname.toLowerCase() && node.defname !== node.defname.toUpperCase()) { + return QuoteUtils.quoteIdentifier(node.defname); + } + } return node.defname.toUpperCase(); } @@ -6534,7 +6496,7 @@ export class Deparser implements DeparserVisitor { case 'REPLICA_IDENTITY_INDEX': output.push('USING', 'INDEX'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; default: @@ -6601,7 +6563,7 @@ export class Deparser implements DeparserVisitor { output.push('IF', 'EXISTS'); } if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } if (node.behavior === 'DROP_CASCADE') { output.push('CASCADE'); @@ -6610,7 +6572,7 @@ export class Deparser implements DeparserVisitor { case 'AT_ValidateConstraint': output.push('VALIDATE', 'CONSTRAINT'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'C': @@ -6627,7 +6589,7 @@ export class Deparser implements DeparserVisitor { output.push('IF', 'EXISTS'); } if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } if (node.behavior === 'DROP_CASCADE') { output.push('CASCADE'); @@ -6636,7 +6598,7 @@ export class Deparser implements DeparserVisitor { case 'V': output.push('VALIDATE', 'CONSTRAINT'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } break; case 'O': @@ -7048,7 +7010,7 @@ export class Deparser implements DeparserVisitor { const initialParts = ['CREATE', 'POLICY']; if (node.policy_name) { - initialParts.push(QuoteUtils.quote(node.policy_name)); + initialParts.push(QuoteUtils.quoteIdentifier(node.policy_name)); } output.push(initialParts.join(' ')); @@ -7127,7 +7089,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'POLICY']; if (node.policy_name) { - output.push(QuoteUtils.quote(node.policy_name)); + output.push(QuoteUtils.quoteIdentifier(node.policy_name)); } if (node.table) { @@ -7174,7 +7136,7 @@ export class Deparser implements DeparserVisitor { output.push('SERVER'); if (node.servername) { - output.push(`"${node.servername}"`); + output.push(QuoteUtils.quoteIdentifier(node.servername)); } if (node.options && node.options.length > 0) { @@ -7235,7 +7197,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['CREATE', 'PUBLICATION']; if (node.pubname) { - output.push(`"${node.pubname}"`); + output.push(QuoteUtils.quoteIdentifier(node.pubname)); } if (node.pubobjects && node.pubobjects.length > 0) { @@ -7259,7 +7221,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['CREATE', 'SUBSCRIPTION']; if (node.subname) { - output.push(`"${node.subname}"`); + output.push(QuoteUtils.quoteIdentifier(node.subname)); } output.push('CONNECTION'); @@ -7288,7 +7250,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'PUBLICATION']; if (node.pubname) { - output.push(`"${node.pubname}"`); + output.push(QuoteUtils.quoteIdentifier(node.pubname)); } if (node.action) { @@ -7328,7 +7290,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'SUBSCRIPTION']; if (node.subname) { - output.push(`"${node.subname}"`); + output.push(QuoteUtils.quoteIdentifier(node.subname)); } if (node.kind) { @@ -7380,7 +7342,7 @@ export class Deparser implements DeparserVisitor { } if (node.subname) { - output.push(`"${node.subname}"`); + output.push(QuoteUtils.quoteIdentifier(node.subname)); } if (node.behavior) { @@ -7669,7 +7631,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['CLOSE']; if (node.portalname) { - output.push(QuoteUtils.quote(node.portalname)); + output.push(QuoteUtils.quoteIdentifier(node.portalname)); } else { output.push('ALL'); } @@ -7725,7 +7687,7 @@ export class Deparser implements DeparserVisitor { } if (node.portalname) { - output.push(QuoteUtils.quote(node.portalname)); + output.push(QuoteUtils.quoteIdentifier(node.portalname)); } return output.join(' '); @@ -7807,7 +7769,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'FOREIGN', 'DATA', 'WRAPPER']; if (node.fdwname) { - output.push(QuoteUtils.quote(node.fdwname)); + output.push(QuoteUtils.quoteIdentifier(node.fdwname)); } if (node.func_options && node.func_options.length > 0) { @@ -7834,7 +7796,7 @@ export class Deparser implements DeparserVisitor { } if (node.servername) { - output.push(QuoteUtils.quote(node.servername)); + output.push(QuoteUtils.quoteIdentifier(node.servername)); } if (node.servertype) { @@ -7846,7 +7808,7 @@ export class Deparser implements DeparserVisitor { } if (node.fdwname) { - output.push('FOREIGN', 'DATA', 'WRAPPER', QuoteUtils.quote(node.fdwname)); + output.push('FOREIGN', 'DATA', 'WRAPPER', QuoteUtils.quoteIdentifier(node.fdwname)); } if (node.options && node.options.length > 0) { @@ -7865,7 +7827,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'SERVER']; if (node.servername) { - output.push(QuoteUtils.quote(node.servername)); + output.push(QuoteUtils.quoteIdentifier(node.servername)); } if (node.version) { @@ -7896,7 +7858,7 @@ export class Deparser implements DeparserVisitor { output.push('SERVER'); if (node.servername) { - output.push(QuoteUtils.quote(node.servername)); + output.push(QuoteUtils.quoteIdentifier(node.servername)); } if (node.options && node.options.length > 0) { @@ -7927,7 +7889,7 @@ export class Deparser implements DeparserVisitor { output.push('SERVER'); if (node.servername) { - output.push(QuoteUtils.quote(node.servername)); + output.push(QuoteUtils.quoteIdentifier(node.servername)); } return output.join(' '); @@ -7937,7 +7899,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['IMPORT', 'FOREIGN', 'SCHEMA']; if (node.remote_schema) { - output.push(QuoteUtils.quote(node.remote_schema)); + output.push(QuoteUtils.quoteIdentifier(node.remote_schema)); } if (node.list_type) { @@ -7966,13 +7928,13 @@ export class Deparser implements DeparserVisitor { output.push('FROM', 'SERVER'); if (node.server_name) { - output.push(QuoteUtils.quote(node.server_name)); + output.push(QuoteUtils.quoteIdentifier(node.server_name)); } output.push('INTO'); if (node.local_schema) { - output.push(QuoteUtils.quote(node.local_schema)); + output.push(QuoteUtils.quoteIdentifier(node.local_schema)); } if (node.options && node.options.length > 0) { @@ -7991,7 +7953,7 @@ export class Deparser implements DeparserVisitor { output.push(this.RangeVar(node.relation, context)); if (node.indexname) { - output.push('USING', `"${node.indexname}"`); + output.push('USING', QuoteUtils.quoteIdentifier(node.indexname)); } } @@ -8070,7 +8032,7 @@ export class Deparser implements DeparserVisitor { } if (node.name) { - output.push(`"${node.name}"`); + output.push(QuoteUtils.quoteIdentifier(node.name)); } return output.join(' '); @@ -8117,7 +8079,7 @@ export class Deparser implements DeparserVisitor { throw new Error('CreatedbStmt requires dbname'); } - output.push(`"${node.dbname}"`); + output.push(QuoteUtils.quoteIdentifier(node.dbname)); if (node.options && node.options.length > 0) { const options = ListUtils.unwrapList(node.options) @@ -8140,7 +8102,7 @@ export class Deparser implements DeparserVisitor { throw new Error('DropdbStmt requires dbname'); } - output.push(`"${node.dbname}"`); + output.push(QuoteUtils.quoteIdentifier(node.dbname)); if (node.options && node.options.length > 0) { const options = ListUtils.unwrapList(node.options) @@ -8241,7 +8203,7 @@ export class Deparser implements DeparserVisitor { case 'OBJECT_POLICY': output.push('POLICY'); if (node.subname) { - output.push(QuoteUtils.quote(node.subname)); + output.push(QuoteUtils.quoteIdentifier(node.subname)); } break; case 'OBJECT_PUBLICATION': @@ -8300,7 +8262,7 @@ export class Deparser implements DeparserVisitor { // Handle OBJECT_RULE special case: rule_name ON table_name format if (node.renameType === 'OBJECT_RULE' && node.subname && node.relation) { - output.push(QuoteUtils.quote(node.subname)); + output.push(QuoteUtils.quoteIdentifier(node.subname)); output.push('ON'); output.push(this.RangeVar(node.relation, context)); } else if (node.relation) { @@ -8319,7 +8281,7 @@ export class Deparser implements DeparserVisitor { if (items.length === 2) { const accessMethod = items[0].String?.sval || ''; const objectName = items[1].String?.sval || ''; - output.push(`${QuoteUtils.quote(objectName)} USING ${accessMethod}`); + output.push(`${QuoteUtils.quoteIdentifier(objectName)} USING ${accessMethod}`); } else { output.push(this.visit(node.object, context)); } @@ -8336,16 +8298,16 @@ export class Deparser implements DeparserVisitor { } } - if (node.renameType === 'OBJECT_COLUMN' && node.subname) { - output.push('RENAME COLUMN', `"${node.subname}"`, 'TO'); - } else if (node.renameType === 'OBJECT_DOMCONSTRAINT' && node.subname) { - output.push('RENAME CONSTRAINT', `"${node.subname}"`, 'TO'); - } else if (node.renameType === 'OBJECT_TABCONSTRAINT' && node.subname) { - output.push('RENAME CONSTRAINT', `"${node.subname}"`, 'TO'); - } else if (node.renameType === 'OBJECT_ATTRIBUTE' && node.subname) { - output.push('RENAME ATTRIBUTE', `"${node.subname}"`, 'TO'); - } else if (node.renameType === 'OBJECT_ROLE' && node.subname) { - output.push(`"${node.subname}"`, 'RENAME TO'); + if (node.renameType === 'OBJECT_COLUMN' && node.subname) { + output.push('RENAME COLUMN', QuoteUtils.quoteIdentifier(node.subname), 'TO'); + } else if (node.renameType === 'OBJECT_DOMCONSTRAINT' && node.subname) { + output.push('RENAME CONSTRAINT', QuoteUtils.quoteIdentifier(node.subname), 'TO'); + } else if (node.renameType === 'OBJECT_TABCONSTRAINT' && node.subname) { + output.push('RENAME CONSTRAINT', QuoteUtils.quoteIdentifier(node.subname), 'TO'); + } else if (node.renameType === 'OBJECT_ATTRIBUTE' && node.subname) { + output.push('RENAME ATTRIBUTE', QuoteUtils.quoteIdentifier(node.subname), 'TO'); + } else if (node.renameType === 'OBJECT_ROLE' && node.subname) { + output.push(QuoteUtils.quoteIdentifier(node.subname), 'RENAME TO'); } else if (node.renameType === 'OBJECT_SCHEMA' && node.subname) { output.push(this.quoteIfNeeded(node.subname), 'RENAME TO'); } else if (node.renameType === 'OBJECT_RULE') { @@ -8358,7 +8320,7 @@ export class Deparser implements DeparserVisitor { throw new Error('RenameStmt requires newname'); } - output.push(QuoteUtils.quote(node.newname)); + output.push(QuoteUtils.quoteIdentifier(node.newname)); // Handle CASCADE/RESTRICT behavior for RENAME operations if (node.behavior === 'DROP_CASCADE') { @@ -8386,7 +8348,7 @@ export class Deparser implements DeparserVisitor { if (items.length === 2) { const accessMethod = items[0].String?.sval || ''; const objectName = items[1].String?.sval || ''; - output.push(`${QuoteUtils.quote(objectName)} USING ${accessMethod}`); + output.push(`${QuoteUtils.quoteIdentifier(objectName)} USING ${accessMethod}`); } else { output.push(this.visit(node.object, context)); } @@ -8677,7 +8639,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['SECURITY LABEL']; if (node.provider) { - output.push('FOR', `"${node.provider}"`); + output.push('FOR', QuoteUtils.quoteIdentifier(node.provider)); } output.push('ON'); @@ -8852,7 +8814,7 @@ export class Deparser implements DeparserVisitor { output.push('LANGUAGE'); if (node.plname) { - output.push(QuoteUtils.quote(node.plname)); + output.push(QuoteUtils.quoteIdentifier(node.plname)); } if (node.plhandler && node.plhandler.length > 0) { @@ -8898,7 +8860,7 @@ export class Deparser implements DeparserVisitor { output.push('LANGUAGE'); if (node.lang) { - output.push(QuoteUtils.quote(node.lang)); + output.push(QuoteUtils.quoteIdentifier(node.lang)); } output.push('('); @@ -8936,7 +8898,7 @@ export class Deparser implements DeparserVisitor { output.push('TRIGGER'); if (node.trigname) { - output.push(QuoteUtils.quote(node.trigname)); + output.push(QuoteUtils.quoteIdentifier(node.trigname)); } if (context.isPretty()) { @@ -9110,7 +9072,7 @@ export class Deparser implements DeparserVisitor { } if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } return output.join(' '); @@ -9120,7 +9082,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['CREATE EVENT TRIGGER']; if (node.trigname) { - output.push(QuoteUtils.quote(node.trigname)); + output.push(QuoteUtils.quoteIdentifier(node.trigname)); } output.push('ON'); @@ -9154,7 +9116,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER EVENT TRIGGER']; if (node.trigname) { - output.push(QuoteUtils.quote(node.trigname)); + output.push(QuoteUtils.quoteIdentifier(node.trigname)); } if (node.tgenabled) { @@ -9323,13 +9285,13 @@ export class Deparser implements DeparserVisitor { output.push('ALL', 'IN', 'TABLESPACE'); if (node.orig_tablespacename) { - output.push(QuoteUtils.quote(node.orig_tablespacename)); + output.push(QuoteUtils.quoteIdentifier(node.orig_tablespacename)); } output.push('SET', 'TABLESPACE'); if (node.new_tablespacename) { - output.push(QuoteUtils.quote(node.new_tablespacename)); + output.push(QuoteUtils.quoteIdentifier(node.new_tablespacename)); } if (node.nowait) { @@ -9360,10 +9322,10 @@ export class Deparser implements DeparserVisitor { const sequenceName: string[] = []; const seq = node.sequence as any; if (seq.schemaname) { - sequenceName.push(QuoteUtils.quote(seq.schemaname)); + sequenceName.push(QuoteUtils.quoteIdentifier(seq.schemaname)); } if (seq.relname) { - sequenceName.push(QuoteUtils.quote(seq.relname)); + sequenceName.push(QuoteUtils.quoteIdentifier(seq.relname)); } output.push(sequenceName.join('.')); } @@ -9401,10 +9363,10 @@ export class Deparser implements DeparserVisitor { const sequenceName: string[] = []; const seq = node.sequence as any; if (seq.schemaname) { - sequenceName.push(QuoteUtils.quote(seq.schemaname)); + sequenceName.push(QuoteUtils.quoteIdentifier(seq.schemaname)); } if (seq.relname) { - sequenceName.push(QuoteUtils.quote(seq.relname)); + sequenceName.push(QuoteUtils.quoteIdentifier(seq.relname)); } output.push(sequenceName.join('.')); } @@ -9809,7 +9771,7 @@ export class Deparser implements DeparserVisitor { aliasname(node: any, context: DeparserContext): string { if (typeof node === 'string') { - return QuoteUtils.quote(node); + return QuoteUtils.quoteIdentifier(node); } return this.visit(node, context); } @@ -9846,32 +9808,28 @@ export class Deparser implements DeparserVisitor { const defName = defElem.defname; const defValue = defElem.arg; - if (defName && defValue) { - let preservedDefName; - if (Deparser.needsQuotes(defName)) { - preservedDefName = `"${defName}"`; - } else { - preservedDefName = this.preserveOperatorDefElemCase(defName); - } - - if ((defName.toLowerCase() === 'commutator' || defName.toLowerCase() === 'negator') && defValue.List) { - const listItems = ListUtils.unwrapList(defValue.List.items); - if (listItems.length === 1 && listItems[0].String) { - return `${preservedDefName} = ${listItems[0].String.sval}`; - } - } - // For commutator/negator, we already handled them above - if ((defName.toLowerCase() === 'commutator' || defName.toLowerCase() === 'negator')) { - return `${preservedDefName} = ${this.visit(defValue, context)}`; - } - return `${preservedDefName} = ${this.visit(defValue, context)}`; - } else if (defName && !defValue) { - // Handle boolean flags like HASHES, MERGES - preserve original case - if (defName === 'Hashes' || defName === 'Merges') { - return `"${defName}"`; - } - return this.preserveOperatorDefElemCase(defName).toUpperCase(); - } + if (defName && defValue) { + const quotedDefName = QuoteUtils.quoteIdentifier(defName); + const preservedDefName = quotedDefName !== defName ? quotedDefName : this.preserveOperatorDefElemCase(defName); + + if ((defName.toLowerCase() === 'commutator' || defName.toLowerCase() === 'negator') && defValue.List) { + const listItems = ListUtils.unwrapList(defValue.List.items); + if (listItems.length === 1 && listItems[0].String) { + return `${preservedDefName} = ${listItems[0].String.sval}`; + } + } + // For commutator/negator, we already handled them above + if ((defName.toLowerCase() === 'commutator' || defName.toLowerCase() === 'negator')) { + return `${preservedDefName} = ${this.visit(defValue, context)}`; + } + return `${preservedDefName} = ${this.visit(defValue, context)}`; + } else if (defName && !defValue) { + // Handle boolean flags like HASHES, MERGES - preserve original case + if (defName === 'Hashes' || defName === 'Merges') { + return QuoteUtils.quoteIdentifier(defName); + } + return this.preserveOperatorDefElemCase(defName).toUpperCase(); + } } return this.visit(def, context); }); @@ -10010,20 +9968,16 @@ export class Deparser implements DeparserVisitor { const defName = defElem.defname; const defValue = defElem.arg; - if (defName && defValue) { - let preservedDefName; - if (Deparser.needsQuotes(defName)) { - preservedDefName = `"${defName}"`; - } else { - preservedDefName = defName; - } + if (defName && defValue) { + const quotedDefName = QuoteUtils.quoteIdentifier(defName); + const preservedDefName = quotedDefName !== defName ? quotedDefName : defName; - // Handle String arguments with single quotes for string literals - if (defValue.String) { - return `${preservedDefName} = '${defValue.String.sval}'`; - } - return `${preservedDefName} = ${this.visit(defValue, context)}`; - } + // Handle String arguments with single quotes for string literals + if (defValue.String) { + return `${preservedDefName} = '${defValue.String.sval}'`; + } + return `${preservedDefName} = ${this.visit(defValue, context)}`; + } } return this.visit(def, context); }); @@ -10196,7 +10150,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'DATABASE']; if (node.dbname) { - output.push(QuoteUtils.quote(node.dbname)); + output.push(QuoteUtils.quoteIdentifier(node.dbname)); } if (node.options && node.options.length > 0) { @@ -10211,7 +10165,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'DATABASE']; if (node.dbname) { - output.push(QuoteUtils.quote(node.dbname)); + output.push(QuoteUtils.quoteIdentifier(node.dbname)); } output.push('REFRESH', 'COLLATION', 'VERSION'); @@ -10223,7 +10177,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'DATABASE']; if (node.dbname) { - output.push(QuoteUtils.quote(node.dbname)); + output.push(QuoteUtils.quoteIdentifier(node.dbname)); } if (node.setstmt) { @@ -10238,7 +10192,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['DECLARE']; if (node.portalname) { - output.push(QuoteUtils.quote(node.portalname)); + output.push(QuoteUtils.quoteIdentifier(node.portalname)); } // Handle cursor options before CURSOR keyword @@ -10287,7 +10241,7 @@ export class Deparser implements DeparserVisitor { } else if (node.pubobjtype === 'PUBLICATIONOBJ_TABLES_IN_SCHEMA') { output.push('TABLES IN SCHEMA'); if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } } else if (node.pubobjtype === 'PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA') { output.push('TABLES IN SCHEMA CURRENT_SCHEMA'); @@ -10320,7 +10274,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['CREATE', 'ACCESS', 'METHOD']; if (node.amname) { - output.push(QuoteUtils.quote(node.amname)); + output.push(QuoteUtils.quoteIdentifier(node.amname)); } output.push('TYPE'); @@ -10391,7 +10345,7 @@ export class Deparser implements DeparserVisitor { } if (node.tableSpaceName) { - output.push('TABLESPACE', QuoteUtils.quote(node.tableSpaceName)); + output.push('TABLESPACE', QuoteUtils.quoteIdentifier(node.tableSpaceName)); } return output.join(' '); @@ -10578,7 +10532,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = []; if (node.colname) { - output.push(QuoteUtils.quote(node.colname)); + output.push(QuoteUtils.quoteIdentifier(node.colname)); } if (node.for_ordinality) { @@ -10735,9 +10689,9 @@ export class Deparser implements DeparserVisitor { if (node.op === 'IS_XMLPI') { if (node.name && node.args && node.args.length > 0) { const argStrs = ListUtils.unwrapList(node.args).map(arg => this.visit(arg, context)); - return `xmlpi(name ${QuoteUtils.quote(node.name)}, ${argStrs.join(', ')})`; + return `xmlpi(name ${QuoteUtils.quoteIdentifier(node.name)}, ${argStrs.join(', ')})`; } else if (node.name) { - return `xmlpi(name ${QuoteUtils.quote(node.name)})`; + return `xmlpi(name ${QuoteUtils.quoteIdentifier(node.name)})`; } else { return 'XMLPI()'; } @@ -10753,7 +10707,7 @@ export class Deparser implements DeparserVisitor { output.push('XMLELEMENT'); const elementParts: string[] = []; if (node.name) { - elementParts.push(`NAME ${QuoteUtils.quote(node.name)}`); + elementParts.push(`NAME ${QuoteUtils.quoteIdentifier(node.name)}`); } if (node.named_args && node.named_args.length > 0) { const namedArgStrs = ListUtils.unwrapList(node.named_args).map(arg => this.visit(arg, context)); @@ -10850,7 +10804,7 @@ export class Deparser implements DeparserVisitor { // Handle name and args for operations that don't have special handling if (node.op !== 'IS_XMLELEMENT' && node.op !== 'IS_XMLPARSE' && node.op !== 'IS_XMLROOT' && node.op !== 'IS_DOCUMENT') { if (node.name) { - const quotedName = QuoteUtils.quote(node.name); + const quotedName = QuoteUtils.quoteIdentifier(node.name); output.push(`NAME ${quotedName}`); } @@ -10874,26 +10828,26 @@ export class Deparser implements DeparserVisitor { schemaname(node: any, context: DeparserContext): string { if (typeof node === 'string') { - return QuoteUtils.quote(node); + return QuoteUtils.quoteIdentifier(node); } if (node && node.String && node.String.sval) { - return QuoteUtils.quote(node.String.sval); + return QuoteUtils.quoteIdentifier(node.String.sval); } // Handle other node types without recursion if (node && typeof node === 'object') { if (node.sval !== undefined) { - return QuoteUtils.quote(node.sval); + return QuoteUtils.quoteIdentifier(node.sval); } // Handle List nodes that might contain schema names if (node.List && Array.isArray(node.List.items)) { const items = node.List.items; if (items.length > 0 && items[0].String && items[0].String.sval) { - return QuoteUtils.quote(items[0].String.sval); + return QuoteUtils.quoteIdentifier(items[0].String.sval); } } // For other complex nodes, try to extract string value without recursion if (node.val !== undefined) { - return QuoteUtils.quote(node.val); + return QuoteUtils.quoteIdentifier(node.val); } return ''; } @@ -10977,7 +10931,7 @@ export class Deparser implements DeparserVisitor { output.push('RULE'); if (node.rulename) { - output.push(QuoteUtils.quote(node.rulename)); + output.push(QuoteUtils.quoteIdentifier(node.rulename)); } output.push('AS ON'); @@ -11057,44 +11011,44 @@ export class Deparser implements DeparserVisitor { relname(node: any, context: DeparserContext): string { if (typeof node === 'string') { - return QuoteUtils.quote(node); + return QuoteUtils.quoteIdentifier(node); } if (node && node.String && node.String.sval) { - return QuoteUtils.quote(node.String.sval); + return QuoteUtils.quoteIdentifier(node.String.sval); } if (node && typeof node === 'object' && node.relname) { - return QuoteUtils.quote(node.relname); + return QuoteUtils.quoteIdentifier(node.relname); } return this.visit(node, context); } rel(node: any, context: DeparserContext): string { if (typeof node === 'string') { - return QuoteUtils.quote(node); + return QuoteUtils.quoteIdentifier(node); } if (node && node.String && node.String.sval) { - return QuoteUtils.quote(node.String.sval); + return QuoteUtils.quoteIdentifier(node.String.sval); } if (node && node.RangeVar) { return this.RangeVar(node.RangeVar, context); } if (node && typeof node === 'object' && node.relname) { - return QuoteUtils.quote(node.relname); + return QuoteUtils.quoteIdentifier(node.relname); } return this.visit(node, context); } objname(node: any, context: DeparserContext): string { if (typeof node === 'string') { - return QuoteUtils.quote(node); + return QuoteUtils.quoteIdentifier(node); } if (node && node.String && node.String.sval) { - return QuoteUtils.quote(node.String.sval); + return QuoteUtils.quoteIdentifier(node.String.sval); } if (Array.isArray(node)) { const parts = node.map(part => { if (part && part.String && part.String.sval) { - return QuoteUtils.quote(part.String.sval); + return QuoteUtils.quoteIdentifier(part.String.sval); } return this.visit(part, context); }); @@ -11175,7 +11129,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['CURRENT OF']; if (node.cursor_name) { - output.push(QuoteUtils.quote(node.cursor_name)); + output.push(QuoteUtils.quoteIdentifier(node.cursor_name)); } if (node.cursor_param > 0) { @@ -11263,7 +11217,7 @@ export class Deparser implements DeparserVisitor { if (items.length === 2) { const schemaName = items[0].String?.sval || ''; const domainName = items[1].String?.sval || ''; - output.push(`${QuoteUtils.quote(schemaName)}.${QuoteUtils.quote(domainName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, domainName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11273,7 +11227,7 @@ export class Deparser implements DeparserVisitor { if (items.length === 2) { const schemaName = items[0].String?.sval || ''; const typeName = items[1].String?.sval || ''; - output.push(`${QuoteUtils.quote(schemaName)}.${QuoteUtils.quote(typeName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, typeName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11283,7 +11237,7 @@ export class Deparser implements DeparserVisitor { if (items.length === 2) { const schemaName = items[0].String?.sval || ''; const conversionName = items[1].String?.sval || ''; - output.push(`${QuoteUtils.quote(schemaName)}.${QuoteUtils.quote(conversionName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, conversionName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11293,7 +11247,7 @@ export class Deparser implements DeparserVisitor { if (items.length === 2) { const schemaName = items[0].String?.sval || ''; const parserName = items[1].String?.sval || ''; - output.push(`${QuoteUtils.quote(schemaName)}.${QuoteUtils.quote(parserName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, parserName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11303,7 +11257,7 @@ export class Deparser implements DeparserVisitor { if (items.length === 2) { const schemaName = items[0].String?.sval || ''; const configName = items[1].String?.sval || ''; - output.push(`${QuoteUtils.quote(schemaName)}.${QuoteUtils.quote(configName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, configName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11313,7 +11267,7 @@ export class Deparser implements DeparserVisitor { if (items.length === 2) { const schemaName = items[0].String?.sval || ''; const templateName = items[1].String?.sval || ''; - output.push(`${QuoteUtils.quote(schemaName)}.${QuoteUtils.quote(templateName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, templateName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11323,7 +11277,7 @@ export class Deparser implements DeparserVisitor { if (items.length === 2) { const schemaName = items[0].String?.sval || ''; const dictionaryName = items[1].String?.sval || ''; - output.push(`${QuoteUtils.quote(schemaName)}.${QuoteUtils.quote(dictionaryName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, dictionaryName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11333,12 +11287,12 @@ export class Deparser implements DeparserVisitor { if (items.length === 2) { const accessMethod = items[0].String?.sval || ''; const opClassName = items[1].String?.sval || ''; - output.push(`${QuoteUtils.quote(opClassName)} USING ${accessMethod}`); + output.push(`${QuoteUtils.quoteIdentifier(opClassName)} USING ${accessMethod}`); } else if (items.length === 3) { const accessMethod = items[0].String?.sval || ''; const schemaName = items[1].String?.sval || ''; const opClassName = items[2].String?.sval || ''; - output.push(`${QuoteUtils.quote(schemaName)}.${QuoteUtils.quote(opClassName)} USING ${accessMethod}`); + output.push(`${QuoteUtils.quoteQualifiedIdentifier(schemaName, opClassName)} USING ${accessMethod}`); } else { output.push(this.visit(node.object as any, context)); } @@ -11348,12 +11302,12 @@ export class Deparser implements DeparserVisitor { if (items.length === 2) { const accessMethod = items[0].String?.sval || ''; const opFamilyName = items[1].String?.sval || ''; - output.push(`${QuoteUtils.quote(opFamilyName)} USING ${accessMethod}`); + output.push(`${QuoteUtils.quoteIdentifier(opFamilyName)} USING ${accessMethod}`); } else if (items.length === 3) { const accessMethod = items[0].String?.sval || ''; const schemaName = items[1].String?.sval || ''; const opFamilyName = items[2].String?.sval || ''; - output.push(`${QuoteUtils.quote(schemaName)}.${QuoteUtils.quote(opFamilyName)} USING ${accessMethod}`); + output.push(`${QuoteUtils.quoteQualifiedIdentifier(schemaName, opFamilyName)} USING ${accessMethod}`); } else { output.push(this.visit(node.object as any, context)); } @@ -11365,7 +11319,7 @@ export class Deparser implements DeparserVisitor { output.push('SET SCHEMA'); if (node.newschema) { - output.push(QuoteUtils.quote(node.newschema)); + output.push(QuoteUtils.quoteIdentifier(node.newschema)); } return output.join(' '); @@ -11442,7 +11396,7 @@ export class Deparser implements DeparserVisitor { if (node.servername) { output.push('SERVER'); - output.push(QuoteUtils.quote(node.servername)); + output.push(QuoteUtils.quoteIdentifier(node.servername)); } if (node.options && node.options.length > 0) { diff --git a/packages/deparser/src/kwlist.ts b/packages/deparser/src/kwlist.ts new file mode 100644 index 00000000..64c55207 --- /dev/null +++ b/packages/deparser/src/kwlist.ts @@ -0,0 +1,537 @@ +/* eslint-disable */ +/** + * Generated from PostgreSQL kwlist.h + * DO NOT EDIT BY HAND. + */ + +export type KeywordKind = + | "NO_KEYWORD" + | "UNRESERVED_KEYWORD" + | "COL_NAME_KEYWORD" + | "TYPE_FUNC_NAME_KEYWORD" + | "RESERVED_KEYWORD"; + +export const kwlist = { + UNRESERVED_KEYWORD: [ + "abort", + "absent", + "absolute", + "access", + "action", + "add", + "admin", + "after", + "aggregate", + "also", + "alter", + "always", + "asensitive", + "assertion", + "assignment", + "at", + "atomic", + "attach", + "attribute", + "backward", + "before", + "begin", + "breadth", + "by", + "cache", + "call", + "called", + "cascade", + "cascaded", + "catalog", + "chain", + "characteristics", + "checkpoint", + "class", + "close", + "cluster", + "columns", + "comment", + "comments", + "commit", + "committed", + "compression", + "conditional", + "configuration", + "conflict", + "connection", + "constraints", + "content", + "continue", + "conversion", + "copy", + "cost", + "csv", + "cube", + "current", + "cursor", + "cycle", + "data", + "database", + "day", + "deallocate", + "declare", + "defaults", + "deferred", + "definer", + "delete", + "delimiter", + "delimiters", + "depends", + "depth", + "detach", + "dictionary", + "disable", + "discard", + "document", + "domain", + "double", + "drop", + "each", + "empty", + "enable", + "encoding", + "encrypted", + "enforced", + "enum", + "error", + "escape", + "event", + "exclude", + "excluding", + "exclusive", + "execute", + "explain", + "expression", + "extension", + "external", + "family", + "filter", + "finalize", + "first", + "following", + "force", + "format", + "forward", + "function", + "functions", + "generated", + "global", + "granted", + "groups", + "handler", + "header", + "hold", + "hour", + "identity", + "if", + "ignore", + "immediate", + "immutable", + "implicit", + "import", + "include", + "including", + "increment", + "indent", + "index", + "indexes", + "inherit", + "inherits", + "inline", + "input", + "insensitive", + "insert", + "instead", + "invoker", + "isolation", + "keep", + "key", + "keys", + "label", + "language", + "large", + "last", + "leakproof", + "level", + "listen", + "load", + "local", + "location", + "lock", + "locked", + "logged", + "lsn", + "mapping", + "match", + "matched", + "materialized", + "maxvalue", + "merge", + "method", + "minute", + "minvalue", + "mode", + "month", + "move", + "name", + "names", + "nested", + "new", + "next", + "nfc", + "nfd", + "nfkc", + "nfkd", + "no", + "normalized", + "nothing", + "notify", + "nowait", + "nulls", + "object", + "objects", + "of", + "off", + "oids", + "old", + "omit", + "operator", + "option", + "options", + "ordinality", + "others", + "over", + "overriding", + "owned", + "owner", + "parallel", + "parameter", + "parser", + "partial", + "partition", + "partitions", + "passing", + "password", + "path", + "period", + "plan", + "plans", + "policy", + "preceding", + "prepare", + "prepared", + "preserve", + "prior", + "privileges", + "procedural", + "procedure", + "procedures", + "program", + "publication", + "quote", + "quotes", + "range", + "read", + "reassign", + "recursive", + "ref", + "referencing", + "refresh", + "reindex", + "relative", + "release", + "rename", + "repeatable", + "replace", + "replica", + "reset", + "respect", + "restart", + "restrict", + "return", + "returns", + "revoke", + "role", + "rollback", + "rollup", + "routine", + "routines", + "rows", + "rule", + "savepoint", + "scalar", + "schema", + "schemas", + "scroll", + "search", + "second", + "security", + "sequence", + "sequences", + "serializable", + "server", + "session", + "set", + "sets", + "share", + "show", + "simple", + "skip", + "snapshot", + "source", + "split", + "sql", + "stable", + "standalone", + "start", + "statement", + "statistics", + "stdin", + "stdout", + "storage", + "stored", + "strict", + "string", + "strip", + "subscription", + "support", + "sysid", + "system", + "tables", + "tablespace", + "target", + "temp", + "template", + "temporary", + "text", + "ties", + "transaction", + "transform", + "trigger", + "truncate", + "trusted", + "type", + "types", + "uescape", + "unbounded", + "uncommitted", + "unconditional", + "unencrypted", + "unknown", + "unlisten", + "unlogged", + "until", + "update", + "vacuum", + "valid", + "validate", + "validator", + "value", + "varying", + "version", + "view", + "views", + "virtual", + "volatile", + "wait", + "whitespace", + "within", + "without", + "work", + "wrapper", + "write", + "xml", + "year", + "yes", + "zone" + ], + RESERVED_KEYWORD: [ + "all", + "analyse", + "analyze", + "and", + "any", + "array", + "as", + "asc", + "asymmetric", + "both", + "case", + "cast", + "check", + "collate", + "column", + "constraint", + "create", + "current_catalog", + "current_date", + "current_role", + "current_time", + "current_timestamp", + "current_user", + "default", + "deferrable", + "desc", + "distinct", + "do", + "else", + "end", + "except", + "false", + "fetch", + "for", + "foreign", + "from", + "grant", + "group", + "having", + "in", + "initially", + "intersect", + "into", + "lateral", + "leading", + "limit", + "localtime", + "localtimestamp", + "not", + "null", + "offset", + "on", + "only", + "or", + "order", + "placing", + "primary", + "references", + "returning", + "select", + "session_user", + "some", + "symmetric", + "system_user", + "table", + "then", + "to", + "trailing", + "true", + "union", + "unique", + "user", + "using", + "variadic", + "when", + "where", + "window", + "with" + ], + TYPE_FUNC_NAME_KEYWORD: [ + "authorization", + "binary", + "collation", + "concurrently", + "cross", + "current_schema", + "freeze", + "full", + "ilike", + "inner", + "is", + "isnull", + "join", + "left", + "like", + "natural", + "notnull", + "outer", + "overlaps", + "right", + "similar", + "tablesample", + "verbose" + ], + COL_NAME_KEYWORD: [ + "between", + "bigint", + "bit", + "boolean", + "char", + "character", + "coalesce", + "dec", + "decimal", + "exists", + "extract", + "float", + "greatest", + "grouping", + "inout", + "int", + "integer", + "interval", + "json", + "json_array", + "json_arrayagg", + "json_exists", + "json_object", + "json_objectagg", + "json_query", + "json_scalar", + "json_serialize", + "json_table", + "json_value", + "least", + "merge_action", + "national", + "nchar", + "none", + "normalize", + "nullif", + "numeric", + "out", + "overlay", + "position", + "precision", + "real", + "row", + "setof", + "smallint", + "substring", + "time", + "timestamp", + "treat", + "trim", + "values", + "varchar", + "xmlattributes", + "xmlconcat", + "xmlelement", + "xmlexists", + "xmlforest", + "xmlnamespaces", + "xmlparse", + "xmlpi", + "xmlroot", + "xmlserialize", + "xmltable" + ] +} as const; + +export const RESERVED_KEYWORDS: Set = new Set(kwlist.RESERVED_KEYWORD ?? []); +export const UNRESERVED_KEYWORDS: Set = new Set(kwlist.UNRESERVED_KEYWORD ?? []); +export const COL_NAME_KEYWORDS: Set = new Set(kwlist.COL_NAME_KEYWORD ?? []); +export const TYPE_FUNC_NAME_KEYWORDS: Set = new Set(kwlist.TYPE_FUNC_NAME_KEYWORD ?? []); + +export function keywordKindOf(word: string): KeywordKind { + const w = word.toLowerCase(); + if (RESERVED_KEYWORDS.has(w)) return "RESERVED_KEYWORD"; + if (TYPE_FUNC_NAME_KEYWORDS.has(w)) return "TYPE_FUNC_NAME_KEYWORD"; + if (COL_NAME_KEYWORDS.has(w)) return "COL_NAME_KEYWORD"; + if (UNRESERVED_KEYWORDS.has(w)) return "UNRESERVED_KEYWORD"; + return "NO_KEYWORD"; +} diff --git a/packages/deparser/src/utils/quote-utils.ts b/packages/deparser/src/utils/quote-utils.ts index babb5384..a2668690 100644 --- a/packages/deparser/src/utils/quote-utils.ts +++ b/packages/deparser/src/utils/quote-utils.ts @@ -1,62 +1,6 @@ -const RESERVED_WORDS = new Set([ - 'all', 'analyse', 'analyze', 'and', 'any', 'array', 'as', 'asc', 'asymmetric', - 'authorization', 'binary', 'both', 'case', 'cast', 'check', 'collate', 'collation', - 'column', 'concurrently', 'constraint', 'create', 'cross', 'current_catalog', - 'current_date', 'current_role', 'current_schema', 'current_time', 'current_timestamp', - 'current_user', 'default', 'deferrable', 'desc', 'distinct', 'do', 'else', 'end', - 'except', 'false', 'fetch', 'for', 'foreign', 'freeze', 'from', 'full', 'grant', - 'group', 'having', 'ilike', 'in', 'initially', 'inner', 'intersect', 'into', 'is', - 'isnull', 'join', 'lateral', 'leading', 'left', 'like', 'limit', 'localtime', - 'localtimestamp', 'natural', 'not', 'notnull', 'null', 'offset', 'on', 'only', - 'or', 'order', 'outer', 'overlaps', 'placing', 'primary', 'references', 'returning', - 'right', 'select', 'session_user', 'similar', 'some', 'symmetric', 'table', 'tablesample', - 'then', 'to', 'trailing', 'true', 'union', 'unique', 'user', 'using', 'variadic', - 'verbose', 'when', 'where', 'window', 'with' -]); +import { keywordKindOf } from '../kwlist'; export class QuoteUtils { - static needsQuotes(value: string): boolean { - if (!value || typeof value !== 'string') { - return false; - } - - const lowerValue = value.toLowerCase(); - - if (RESERVED_WORDS.has(lowerValue)) { - return true; - } - - if (!/^[a-z_][a-z0-9_$]*$/i.test(value)) { - return true; - } - - if (value !== value.toLowerCase()) { - return true; - } - - return false; - } - - static quote(value: any): any { - if (value == null) { - return null; - } - - if (Array.isArray(value)) { - return value.map(v => this.quote(v)); - } - - if (typeof value !== 'string') { - return value; - } - - if (this.needsQuotes(value)) { - return `"${value}"`; - } - - return value; - } - static escape(literal: string): string { return `'${literal.replace(/'/g, "''")}'`; } @@ -92,5 +36,88 @@ export class QuoteUtils { // unless it's a raw \x... bytea-style literal. return !/^\\x[0-9a-fA-F]+$/i.test(value) && value.includes('\\'); } + + /** + * Quote an identifier only if needed + * + * This is a TypeScript port of PostgreSQL's quote_identifier() function from ruleutils.c + * https://github.com/postgres/postgres/blob/fab5cd3dd1323f9e66efeb676c4bb212ff340204/src/backend/utils/adt/ruleutils.c#L13055-L13137 + * + * Can avoid quoting if ident starts with a lowercase letter or underscore + * and contains only lowercase letters, digits, and underscores, *and* is + * not any SQL keyword. Otherwise, supply quotes. + * + * When quotes are needed, embedded double quotes are properly escaped as "". + */ + static quoteIdentifier(ident: string): string { + if (!ident) return ident; + + let nquotes = 0; + let safe = true; + + // Check first character: must be lowercase letter or underscore + const firstChar = ident[0]; + if (!((firstChar >= 'a' && firstChar <= 'z') || firstChar === '_')) { + safe = false; + } + + // Check all characters + for (let i = 0; i < ident.length; i++) { + const ch = ident[i]; + if ((ch >= 'a' && ch <= 'z') || + (ch >= '0' && ch <= '9') || + (ch === '_')) { + // okay + } else { + safe = false; + if (ch === '"') { + nquotes++; + } + } + } + + if (safe) { + // Check for keyword. We quote keywords except for unreserved ones. + // (In some cases we could avoid quoting a col_name or type_func_name + // keyword, but it seems much harder than it's worth to tell that.) + const kwKind = keywordKindOf(ident); + if (kwKind !== 'NO_KEYWORD' && kwKind !== 'UNRESERVED_KEYWORD') { + safe = false; + } + } + + if (safe) { + return ident; // no change needed + } + + // Build quoted identifier with escaped embedded quotes + let result = '"'; + for (let i = 0; i < ident.length; i++) { + const ch = ident[i]; + if (ch === '"') { + result += '"'; // escape " as "" + } + result += ch; + } + result += '"'; + + return result; + } + + /** + * Quote a possibly-qualified identifier + * + * This is a TypeScript port of PostgreSQL's quote_qualified_identifier() function from ruleutils.c + * https://github.com/postgres/postgres/blob/fab5cd3dd1323f9e66efeb676c4bb212ff340204/src/backend/utils/adt/ruleutils.c#L13139-L13156 + * + * Return a name of the form qualifier.ident, or just ident if qualifier + * is null/undefined, quoting each component if necessary. + */ + static quoteQualifiedIdentifier(qualifier: string | null | undefined, ident: string): string { + if (qualifier) { + return `${QuoteUtils.quoteIdentifier(qualifier)}.${QuoteUtils.quoteIdentifier(ident)}`; + } + return QuoteUtils.quoteIdentifier(ident); + } -} \ No newline at end of file +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c5095b6a..eb9bf185 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - 'packages/*' - 'packages/deparser/versions/*' + - 'packages/parser/versions/*'