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..46e5557b --- /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 = new Set(kwlist.RESERVED_KEYWORD ?? []); +export const UNRESERVED_KEYWORDS = new Set(kwlist.UNRESERVED_KEYWORD ?? []); +export const COL_NAME_KEYWORDS = new Set(kwlist.COL_NAME_KEYWORD ?? []); +export const TYPE_FUNC_NAME_KEYWORDS = 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..cce3302d 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -2457,36 +2457,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.quoteString(value); } preserveOperatorDefElemCase(defName: string): string { @@ -2528,7 +2500,7 @@ export class Deparser implements DeparserVisitor { } } - return Deparser.needsQuotes(value) ? `"${value}"` : value; + return QuoteUtils.quoteString(value); } Integer(node: t.Integer, context: DeparserContext): string { @@ -4190,18 +4162,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.quoteString(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.quoteString(node.name); + return `SET ${defaultParamName} TO DEFAULT`; + case 'VAR_SET_CURRENT': + const currentParamName = QuoteUtils.quoteString(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 +4239,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.quoteString(node.name); + return `RESET ${resetParamName}`; case 'VAR_RESET_ALL': return 'RESET ALL'; default: @@ -5715,7 +5687,7 @@ export class Deparser implements DeparserVisitor { } if (node.role) { - const roleName = Deparser.needsQuotes(node.role) ? `"${node.role}"` : node.role; + const roleName = QuoteUtils.quoteString(node.role); output.push(roleName); } @@ -5759,14 +5731,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.quoteString(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 +5760,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.quoteString(node.defname); if (node.defaction === 'DEFELEM_ADD') { return `ADD ${quotedDefname} ${finalValue}`; @@ -5815,10 +5785,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.quoteString(node.defname); + return `${quotedDefname} ${quotedValue}`; } else if (node.defaction === 'DEFELEM_DROP') { // Handle DROP without argument return `DROP ${node.defname}`; @@ -5883,10 +5851,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.quoteString(node.defname); + return `${quotedDefname} ${quotedValue}`; } @@ -5965,14 +5931,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.quoteString(value); + } return this.visit(item, context); }); return `OWNED BY ${parts.join('.')}`; @@ -6236,18 +6199,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.quoteString(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 + if (QuoteUtils.needsQuotesForString(node.defname)) { + const quotedDefname = QuoteUtils.quoteString(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 +6269,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.quoteString(node.defname); + } + } return node.defname.toUpperCase(); } @@ -7174,7 +7137,7 @@ export class Deparser implements DeparserVisitor { output.push('SERVER'); if (node.servername) { - output.push(`"${node.servername}"`); + output.push(QuoteUtils.quoteString(node.servername)); } if (node.options && node.options.length > 0) { @@ -7235,7 +7198,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['CREATE', 'PUBLICATION']; if (node.pubname) { - output.push(`"${node.pubname}"`); + output.push(QuoteUtils.quoteString(node.pubname)); } if (node.pubobjects && node.pubobjects.length > 0) { @@ -7259,7 +7222,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['CREATE', 'SUBSCRIPTION']; if (node.subname) { - output.push(`"${node.subname}"`); + output.push(QuoteUtils.quoteString(node.subname)); } output.push('CONNECTION'); @@ -7288,7 +7251,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'PUBLICATION']; if (node.pubname) { - output.push(`"${node.pubname}"`); + output.push(QuoteUtils.quoteString(node.pubname)); } if (node.action) { @@ -7328,7 +7291,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'SUBSCRIPTION']; if (node.subname) { - output.push(`"${node.subname}"`); + output.push(QuoteUtils.quoteString(node.subname)); } if (node.kind) { @@ -7380,7 +7343,7 @@ export class Deparser implements DeparserVisitor { } if (node.subname) { - output.push(`"${node.subname}"`); + output.push(QuoteUtils.quoteString(node.subname)); } if (node.behavior) { @@ -7991,7 +7954,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.quoteString(node.indexname)); } } @@ -8070,7 +8033,7 @@ export class Deparser implements DeparserVisitor { } if (node.name) { - output.push(`"${node.name}"`); + output.push(QuoteUtils.quoteString(node.name)); } return output.join(' '); @@ -8117,7 +8080,7 @@ export class Deparser implements DeparserVisitor { throw new Error('CreatedbStmt requires dbname'); } - output.push(`"${node.dbname}"`); + output.push(QuoteUtils.quoteString(node.dbname)); if (node.options && node.options.length > 0) { const options = ListUtils.unwrapList(node.options) @@ -8140,7 +8103,7 @@ export class Deparser implements DeparserVisitor { throw new Error('DropdbStmt requires dbname'); } - output.push(`"${node.dbname}"`); + output.push(QuoteUtils.quoteString(node.dbname)); if (node.options && node.options.length > 0) { const options = ListUtils.unwrapList(node.options) @@ -8336,16 +8299,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.quoteString(node.subname), 'TO'); + } else if (node.renameType === 'OBJECT_DOMCONSTRAINT' && node.subname) { + output.push('RENAME CONSTRAINT', QuoteUtils.quoteString(node.subname), 'TO'); + } else if (node.renameType === 'OBJECT_TABCONSTRAINT' && node.subname) { + output.push('RENAME CONSTRAINT', QuoteUtils.quoteString(node.subname), 'TO'); + } else if (node.renameType === 'OBJECT_ATTRIBUTE' && node.subname) { + output.push('RENAME ATTRIBUTE', QuoteUtils.quoteString(node.subname), 'TO'); + } else if (node.renameType === 'OBJECT_ROLE' && node.subname) { + output.push(QuoteUtils.quoteString(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') { @@ -8677,7 +8640,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['SECURITY LABEL']; if (node.provider) { - output.push('FOR', `"${node.provider}"`); + output.push('FOR', QuoteUtils.quoteString(node.provider)); } output.push('ON'); @@ -9846,32 +9809,32 @@ 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) { + let preservedDefName; + if (QuoteUtils.needsQuotesForString(defName)) { + preservedDefName = QuoteUtils.quoteString(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 QuoteUtils.quoteString(defName); + } + return this.preserveOperatorDefElemCase(defName).toUpperCase(); + } } return this.visit(def, context); }); @@ -10010,20 +9973,20 @@ 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; - } - - // Handle String arguments with single quotes for string literals - if (defValue.String) { - return `${preservedDefName} = '${defValue.String.sval}'`; - } - return `${preservedDefName} = ${this.visit(defValue, context)}`; - } + if (defName && defValue) { + let preservedDefName; + if (QuoteUtils.needsQuotesForString(defName)) { + preservedDefName = QuoteUtils.quoteString(defName); + } else { + preservedDefName = defName; + } + + // 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); }); 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..551af5f5 100644 --- a/packages/deparser/src/utils/quote-utils.ts +++ b/packages/deparser/src/utils/quote-utils.ts @@ -1,20 +1,33 @@ -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 { RESERVED_KEYWORDS, TYPE_FUNC_NAME_KEYWORDS } from '../kwlist'; export class QuoteUtils { + /** + * Checks if a value needs quoting for use in String nodes, DefElem, role names, etc. + * Uses a different algorithm than needsQuotes - checks for mixed case and special characters. + * This was previously Deparser.needsQuotes. + */ + static needsQuotesForString(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) || + RESERVED_KEYWORDS.has(value.toLowerCase()) || + isAllUppercase; + } + + /** + * Quotes a string value if it needs quoting for String nodes. + * Uses needsQuotesForString logic. + */ + static quoteString(value: string): string { + if (QuoteUtils.needsQuotesForString(value)) { + return `"${value}"`; + } + return value; + } + static needsQuotes(value: string): boolean { if (!value || typeof value !== 'string') { return false; @@ -22,7 +35,7 @@ export class QuoteUtils { const lowerValue = value.toLowerCase(); - if (RESERVED_WORDS.has(lowerValue)) { + if (RESERVED_KEYWORDS.has(lowerValue) || TYPE_FUNC_NAME_KEYWORDS.has(lowerValue)) { return true; } @@ -93,4 +106,4 @@ export class QuoteUtils { return !/^\\x[0-9a-fA-F]+$/i.test(value) && value.includes('\\'); } -} \ No newline at end of file +}