From a96dc06fbdbf68a49554178138eb49fad901506b Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 11 Dec 2025 02:15:11 -0800 Subject: [PATCH 01/11] versions --- PUBLISH.md | 4 ++-- pnpm-workspace.yaml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) 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/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/*' From 1f9fb34e4e3e0ac6b8c4bff1cf433e180fd076fa Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 21 Dec 2025 17:12:09 -0800 Subject: [PATCH 02/11] kwlist --- packages/deparser/package.json | 3 +- packages/deparser/scripts/keywords.ts | 114 ++++++ packages/deparser/src/kwlist.ts | 537 ++++++++++++++++++++++++++ 3 files changed, 653 insertions(+), 1 deletion(-) create mode 100644 packages/deparser/scripts/keywords.ts create mode 100644 packages/deparser/src/kwlist.ts 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/kwlist.ts b/packages/deparser/src/kwlist.ts new file mode 100644 index 00000000..a4d8abd0 --- /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 = 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"; +} From bfc4b39260f7e8316870a513c99ece2e8a3252d3 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Mon, 22 Dec 2025 01:19:28 +0000 Subject: [PATCH 03/11] refactor: centralize quote-utils to use generated kwlist.ts - Remove hardcoded RESERVED_WORDS from quote-utils.ts - Import RESERVED_KEYWORDS and TYPE_FUNC_NAME_KEYWORDS from kwlist.ts - Add explicit Set type annotations to kwlist.ts exports for proper TypeScript compatibility - QuoteUtils.needsQuotes() now uses the centralized keyword sets Co-Authored-By: Dan Lynch --- packages/deparser/src/kwlist.ts | 8 ++++---- packages/deparser/src/utils/quote-utils.ts | 20 +++----------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/deparser/src/kwlist.ts b/packages/deparser/src/kwlist.ts index a4d8abd0..64c55207 100644 --- a/packages/deparser/src/kwlist.ts +++ b/packages/deparser/src/kwlist.ts @@ -522,10 +522,10 @@ export const kwlist = { ] } 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 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(); diff --git a/packages/deparser/src/utils/quote-utils.ts b/packages/deparser/src/utils/quote-utils.ts index babb5384..a9855efc 100644 --- a/packages/deparser/src/utils/quote-utils.ts +++ b/packages/deparser/src/utils/quote-utils.ts @@ -1,18 +1,4 @@ -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 { static needsQuotes(value: string): boolean { @@ -22,7 +8,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 +79,4 @@ export class QuoteUtils { return !/^\\x[0-9a-fA-F]+$/i.test(value) && value.includes('\\'); } -} \ No newline at end of file +} From fb6d159f0b2a4c34e045eecec7c0069d3bf43f5d Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Mon, 22 Dec 2025 02:07:27 +0000 Subject: [PATCH 04/11] fix: update keywords.ts generator to include Set type annotations The generated kwlist.ts needs explicit Set type annotations for TypeScript compatibility when calling .has() with string arguments. Co-Authored-By: Dan Lynch --- packages/deparser/scripts/keywords.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/deparser/scripts/keywords.ts b/packages/deparser/scripts/keywords.ts index 46e5557b..3bd2ad68 100644 --- a/packages/deparser/scripts/keywords.ts +++ b/packages/deparser/scripts/keywords.ts @@ -88,10 +88,10 @@ export type KeywordKind = 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 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(); From ba5bac2981441049f4a385eadf18a3e223118867 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Mon, 22 Dec 2025 02:54:43 +0000 Subject: [PATCH 05/11] refactor: unify reserved words and move Deparser.needsQuotes to QuoteUtils - Remove Deparser.RESERVED_WORDS and Deparser.needsQuotes from deparser.ts - Add QuoteUtils.needsQuotesForString() and QuoteUtils.quoteString() methods - Update all call sites to use QuoteUtils instead of Deparser methods - Both QuoteUtils.needsQuotes and QuoteUtils.needsQuotesForString now use RESERVED_KEYWORDS from kwlist.ts as the single source of truth - Add Set type annotations to kwlist.ts exports for TypeScript compatibility Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 44 ++++------------------ packages/deparser/src/utils/quote-utils.ts | 27 +++++++++++++ 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 52c52db0..d25b686f 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 { @@ -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); } @@ -5788,7 +5760,7 @@ export class Deparser implements DeparserVisitor { ? `'${argValue}'` : argValue; - const quotedDefname = node.defname.includes(' ') || node.defname.includes('-') || Deparser.needsQuotes(node.defname) + const quotedDefname = node.defname.includes(' ') || node.defname.includes('-') || QuoteUtils.needsQuotesForString(node.defname) ? `"${node.defname}"` : node.defname; @@ -5968,7 +5940,7 @@ export class Deparser implements DeparserVisitor { if (this.getNodeType(item) === 'String') { // Check if this identifier needs quotes to preserve case const value = itemData.sval; - if (Deparser.needsQuotes(value)) { + if (QuoteUtils.needsQuotesForString(value)) { return `"${value}"`; } return value; @@ -6245,7 +6217,7 @@ export class Deparser implements DeparserVisitor { } // Handle CREATE AGGREGATE quoted identifiers - preserve quotes when needed - if (Deparser.needsQuotes(node.defname)) { + if (QuoteUtils.needsQuotesForString(node.defname)) { const quotedDefname = `"${node.defname}"`; if (node.arg) { if (this.getNodeType(node.arg) === 'String') { @@ -9848,7 +9820,7 @@ export class Deparser implements DeparserVisitor { if (defName && defValue) { let preservedDefName; - if (Deparser.needsQuotes(defName)) { + if (QuoteUtils.needsQuotesForString(defName)) { preservedDefName = `"${defName}"`; } else { preservedDefName = this.preserveOperatorDefElemCase(defName); @@ -10012,7 +9984,7 @@ export class Deparser implements DeparserVisitor { if (defName && defValue) { let preservedDefName; - if (Deparser.needsQuotes(defName)) { + if (QuoteUtils.needsQuotesForString(defName)) { preservedDefName = `"${defName}"`; } else { preservedDefName = defName; diff --git a/packages/deparser/src/utils/quote-utils.ts b/packages/deparser/src/utils/quote-utils.ts index a9855efc..551af5f5 100644 --- a/packages/deparser/src/utils/quote-utils.ts +++ b/packages/deparser/src/utils/quote-utils.ts @@ -1,6 +1,33 @@ 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; From 7caeee93579ce3aa7d5db1e8679590d10aba0f50 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Mon, 22 Dec 2025 03:06:33 +0000 Subject: [PATCH 06/11] refactor: centralize all inline quoting to use QuoteUtils methods Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 233 ++++++++++++++---------------- 1 file changed, 112 insertions(+), 121 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index d25b686f..cce3302d 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -4162,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 @@ -4239,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: @@ -5731,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) { @@ -5760,9 +5760,7 @@ export class Deparser implements DeparserVisitor { ? `'${argValue}'` : argValue; - const quotedDefname = node.defname.includes(' ') || node.defname.includes('-') || QuoteUtils.needsQuotesForString(node.defname) - ? `"${node.defname}"` - : node.defname; + const quotedDefname = QuoteUtils.quoteString(node.defname); if (node.defaction === 'DEFELEM_ADD') { return `ADD ${quotedDefname} ${finalValue}`; @@ -5787,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}`; @@ -5855,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}`; } @@ -5937,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 (QuoteUtils.needsQuotesForString(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('.')}`; @@ -6208,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 (QuoteUtils.needsQuotesForString(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 @@ -6278,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(); } @@ -7146,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) { @@ -7207,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) { @@ -7231,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'); @@ -7260,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) { @@ -7300,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) { @@ -7352,7 +7343,7 @@ export class Deparser implements DeparserVisitor { } if (node.subname) { - output.push(`"${node.subname}"`); + output.push(QuoteUtils.quoteString(node.subname)); } if (node.behavior) { @@ -7963,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)); } } @@ -8042,7 +8033,7 @@ export class Deparser implements DeparserVisitor { } if (node.name) { - output.push(`"${node.name}"`); + output.push(QuoteUtils.quoteString(node.name)); } return output.join(' '); @@ -8089,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) @@ -8112,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) @@ -8308,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') { @@ -8649,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'); @@ -9818,32 +9809,32 @@ export class Deparser implements DeparserVisitor { const defName = defElem.defname; const defValue = defElem.arg; - if (defName && defValue) { - let preservedDefName; - if (QuoteUtils.needsQuotesForString(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); }); @@ -9982,20 +9973,20 @@ export class Deparser implements DeparserVisitor { const defName = defElem.defname; const defValue = defElem.arg; - if (defName && defValue) { - let preservedDefName; - if (QuoteUtils.needsQuotesForString(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); }); From 30cbd40c666ec7434a5584adace4276430ba3c1b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 04:53:51 +0000 Subject: [PATCH 07/11] feat: add quoteIdentifier and quoteQualifiedIdentifier from PostgreSQL ruleutils.c Port PostgreSQL's quote_identifier() and quote_qualified_identifier() functions to TypeScript. These functions properly: - Quote identifiers only when needed (lowercase letters, digits, underscores) - Escape embedded double quotes as "" - Check against keyword categories (quote all except UNRESERVED_KEYWORD) References: - https://github.com/postgres/postgres/blob/fab5cd3dd1323f9e66efeb676c4bb212ff340204/src/backend/utils/adt/ruleutils.c#L13055-L13137 - https://github.com/postgres/postgres/blob/fab5cd3dd1323f9e66efeb676c4bb212ff340204/src/backend/utils/adt/ruleutils.c#L13139-L13156 Co-Authored-By: Dan Lynch --- packages/deparser/src/utils/quote-utils.ts | 85 +++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/packages/deparser/src/utils/quote-utils.ts b/packages/deparser/src/utils/quote-utils.ts index 551af5f5..12228feb 100644 --- a/packages/deparser/src/utils/quote-utils.ts +++ b/packages/deparser/src/utils/quote-utils.ts @@ -1,4 +1,4 @@ -import { RESERVED_KEYWORDS, TYPE_FUNC_NAME_KEYWORDS } from '../kwlist'; +import { RESERVED_KEYWORDS, TYPE_FUNC_NAME_KEYWORDS, UNRESERVED_KEYWORDS, keywordKindOf } from '../kwlist'; export class QuoteUtils { /** @@ -105,5 +105,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); + } } From 18f323154b9b03ba700ddb476e7574bd6065459b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 05:13:24 +0000 Subject: [PATCH 08/11] refactor: migrate quoteString usages to quoteIdentifier and remove deprecated methods - Replace all quoteString usages in deparser.ts with quoteIdentifier - Replace all needsQuotesForString usages with quoteIdentifier checks - Remove deprecated quoteString and needsQuotesForString methods from quote-utils.ts - Update quoteIfNeeded() and String() methods to use quoteIdentifier - Migrate DefElem contexts and VariableSetStmt to use quoteIdentifier The quoteIdentifier function is more correct because it: - Properly escapes embedded double quotes as "" - Checks all keyword categories (not just RESERVED_KEYWORDS) - Follows PostgreSQL's exact quote_identifier() logic from ruleutils.c Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 82 ++++++++++------------ packages/deparser/src/utils/quote-utils.ts | 29 +------- 2 files changed, 38 insertions(+), 73 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index cce3302d..cf6c5698 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -2458,7 +2458,7 @@ export class Deparser implements DeparserVisitor { } quoteIfNeeded(value: string): string { - return QuoteUtils.quoteString(value); + return QuoteUtils.quoteIdentifier(value); } preserveOperatorDefElemCase(defName: string): string { @@ -2500,7 +2500,7 @@ export class Deparser implements DeparserVisitor { } } - return QuoteUtils.quoteString(value); + return QuoteUtils.quoteIdentifier(value); } Integer(node: t.Integer, context: DeparserContext): string { @@ -4163,16 +4163,16 @@ export class Deparser implements DeparserVisitor { }).join(', ') : ''; // Handle args - always include TO clause if args exist (even if empty string) - const paramName = QuoteUtils.quoteString(node.name); + 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.quoteString(node.name); + const defaultParamName = QuoteUtils.quoteIdentifier(node.name); return `SET ${defaultParamName} TO DEFAULT`; case 'VAR_SET_CURRENT': - const currentParamName = QuoteUtils.quoteString(node.name); + const currentParamName = QuoteUtils.quoteIdentifier(node.name); return `SET ${currentParamName} FROM CURRENT`; case 'VAR_SET_MULTI': if (node.name === 'TRANSACTION' || node.name === 'SESSION CHARACTERISTICS') { @@ -4240,7 +4240,7 @@ export class Deparser implements DeparserVisitor { return `SET ${assignments}`; } case 'VAR_RESET': - const resetParamName = QuoteUtils.quoteString(node.name); + const resetParamName = QuoteUtils.quoteIdentifier(node.name); return `RESET ${resetParamName}`; case 'VAR_RESET_ALL': return 'RESET ALL'; @@ -5687,7 +5687,7 @@ export class Deparser implements DeparserVisitor { } if (node.role) { - const roleName = QuoteUtils.quoteString(node.role); + const roleName = QuoteUtils.quoteIdentifier(node.role); output.push(roleName); } @@ -5735,7 +5735,7 @@ export class Deparser implements DeparserVisitor { 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 QuoteUtils.quoteIdentifier(node.defname); } return node.defname.charAt(0).toUpperCase() + node.defname.slice(1).toLowerCase(); } @@ -5760,7 +5760,7 @@ export class Deparser implements DeparserVisitor { ? `'${argValue}'` : argValue; - const quotedDefname = QuoteUtils.quoteString(node.defname); + const quotedDefname = QuoteUtils.quoteIdentifier(node.defname); if (node.defaction === 'DEFELEM_ADD') { return `ADD ${quotedDefname} ${finalValue}`; @@ -5785,7 +5785,7 @@ export class Deparser implements DeparserVisitor { return `SET ${node.defname} ${quotedValue}`; } - const quotedDefname = QuoteUtils.quoteString(node.defname); + const quotedDefname = QuoteUtils.quoteIdentifier(node.defname); return `${quotedDefname} ${quotedValue}`; } else if (node.defaction === 'DEFELEM_DROP') { // Handle DROP without argument @@ -5851,7 +5851,7 @@ export class Deparser implements DeparserVisitor { const quotedValue = typeof argValue === 'string' ? QuoteUtils.escape(argValue) : argValue; - const quotedDefname = QuoteUtils.quoteString(node.defname); + const quotedDefname = QuoteUtils.quoteIdentifier(node.defname); return `${quotedDefname} ${quotedValue}`; } @@ -5934,7 +5934,7 @@ export class Deparser implements DeparserVisitor { if (this.getNodeType(item) === 'String') { // Check if this identifier needs quotes to preserve case const value = itemData.sval; - return QuoteUtils.quoteString(value); + return QuoteUtils.quoteIdentifier(value); } return this.visit(item, context); }); @@ -6202,14 +6202,14 @@ export class Deparser implements DeparserVisitor { // 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 QuoteUtils.quoteIdentifier(node.defname); } return preservedName.toUpperCase(); } // Handle CREATE AGGREGATE quoted identifiers - preserve quotes when needed - if (QuoteUtils.needsQuotesForString(node.defname)) { - const quotedDefname = QuoteUtils.quoteString(node.defname); + 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); @@ -6273,7 +6273,7 @@ export class Deparser implements DeparserVisitor { 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 QuoteUtils.quoteIdentifier(node.defname); } } @@ -7137,7 +7137,7 @@ export class Deparser implements DeparserVisitor { output.push('SERVER'); if (node.servername) { - output.push(QuoteUtils.quoteString(node.servername)); + output.push(QuoteUtils.quoteIdentifier(node.servername)); } if (node.options && node.options.length > 0) { @@ -7198,7 +7198,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['CREATE', 'PUBLICATION']; if (node.pubname) { - output.push(QuoteUtils.quoteString(node.pubname)); + output.push(QuoteUtils.quoteIdentifier(node.pubname)); } if (node.pubobjects && node.pubobjects.length > 0) { @@ -7222,7 +7222,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['CREATE', 'SUBSCRIPTION']; if (node.subname) { - output.push(QuoteUtils.quoteString(node.subname)); + output.push(QuoteUtils.quoteIdentifier(node.subname)); } output.push('CONNECTION'); @@ -7251,7 +7251,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'PUBLICATION']; if (node.pubname) { - output.push(QuoteUtils.quoteString(node.pubname)); + output.push(QuoteUtils.quoteIdentifier(node.pubname)); } if (node.action) { @@ -7291,7 +7291,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['ALTER', 'SUBSCRIPTION']; if (node.subname) { - output.push(QuoteUtils.quoteString(node.subname)); + output.push(QuoteUtils.quoteIdentifier(node.subname)); } if (node.kind) { @@ -7343,7 +7343,7 @@ export class Deparser implements DeparserVisitor { } if (node.subname) { - output.push(QuoteUtils.quoteString(node.subname)); + output.push(QuoteUtils.quoteIdentifier(node.subname)); } if (node.behavior) { @@ -7954,7 +7954,7 @@ export class Deparser implements DeparserVisitor { output.push(this.RangeVar(node.relation, context)); if (node.indexname) { - output.push('USING', QuoteUtils.quoteString(node.indexname)); + output.push('USING', QuoteUtils.quoteIdentifier(node.indexname)); } } @@ -8033,7 +8033,7 @@ export class Deparser implements DeparserVisitor { } if (node.name) { - output.push(QuoteUtils.quoteString(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } return output.join(' '); @@ -8080,7 +8080,7 @@ export class Deparser implements DeparserVisitor { throw new Error('CreatedbStmt requires dbname'); } - output.push(QuoteUtils.quoteString(node.dbname)); + output.push(QuoteUtils.quoteIdentifier(node.dbname)); if (node.options && node.options.length > 0) { const options = ListUtils.unwrapList(node.options) @@ -8103,7 +8103,7 @@ export class Deparser implements DeparserVisitor { throw new Error('DropdbStmt requires dbname'); } - output.push(QuoteUtils.quoteString(node.dbname)); + output.push(QuoteUtils.quoteIdentifier(node.dbname)); if (node.options && node.options.length > 0) { const options = ListUtils.unwrapList(node.options) @@ -8300,15 +8300,15 @@ export class Deparser implements DeparserVisitor { } if (node.renameType === 'OBJECT_COLUMN' && node.subname) { - output.push('RENAME COLUMN', QuoteUtils.quoteString(node.subname), 'TO'); + output.push('RENAME COLUMN', QuoteUtils.quoteIdentifier(node.subname), 'TO'); } else if (node.renameType === 'OBJECT_DOMCONSTRAINT' && node.subname) { - output.push('RENAME CONSTRAINT', QuoteUtils.quoteString(node.subname), 'TO'); + output.push('RENAME CONSTRAINT', QuoteUtils.quoteIdentifier(node.subname), 'TO'); } else if (node.renameType === 'OBJECT_TABCONSTRAINT' && node.subname) { - output.push('RENAME CONSTRAINT', QuoteUtils.quoteString(node.subname), 'TO'); + output.push('RENAME CONSTRAINT', QuoteUtils.quoteIdentifier(node.subname), 'TO'); } else if (node.renameType === 'OBJECT_ATTRIBUTE' && node.subname) { - output.push('RENAME ATTRIBUTE', QuoteUtils.quoteString(node.subname), 'TO'); + output.push('RENAME ATTRIBUTE', QuoteUtils.quoteIdentifier(node.subname), 'TO'); } else if (node.renameType === 'OBJECT_ROLE' && node.subname) { - output.push(QuoteUtils.quoteString(node.subname), 'RENAME TO'); + 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') { @@ -8640,7 +8640,7 @@ export class Deparser implements DeparserVisitor { const output: string[] = ['SECURITY LABEL']; if (node.provider) { - output.push('FOR', QuoteUtils.quoteString(node.provider)); + output.push('FOR', QuoteUtils.quoteIdentifier(node.provider)); } output.push('ON'); @@ -9810,12 +9810,8 @@ export class Deparser implements DeparserVisitor { const defValue = defElem.arg; if (defName && defValue) { - let preservedDefName; - if (QuoteUtils.needsQuotesForString(defName)) { - preservedDefName = QuoteUtils.quoteString(defName); - } else { - preservedDefName = this.preserveOperatorDefElemCase(defName); - } + 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); @@ -9831,7 +9827,7 @@ export class Deparser implements DeparserVisitor { } else if (defName && !defValue) { // Handle boolean flags like HASHES, MERGES - preserve original case if (defName === 'Hashes' || defName === 'Merges') { - return QuoteUtils.quoteString(defName); + return QuoteUtils.quoteIdentifier(defName); } return this.preserveOperatorDefElemCase(defName).toUpperCase(); } @@ -9974,12 +9970,8 @@ export class Deparser implements DeparserVisitor { const defValue = defElem.arg; if (defName && defValue) { - let preservedDefName; - if (QuoteUtils.needsQuotesForString(defName)) { - preservedDefName = QuoteUtils.quoteString(defName); - } else { - preservedDefName = defName; - } + const quotedDefName = QuoteUtils.quoteIdentifier(defName); + const preservedDefName = quotedDefName !== defName ? quotedDefName : defName; // Handle String arguments with single quotes for string literals if (defValue.String) { diff --git a/packages/deparser/src/utils/quote-utils.ts b/packages/deparser/src/utils/quote-utils.ts index 12228feb..d710f34f 100644 --- a/packages/deparser/src/utils/quote-utils.ts +++ b/packages/deparser/src/utils/quote-utils.ts @@ -1,33 +1,6 @@ -import { RESERVED_KEYWORDS, TYPE_FUNC_NAME_KEYWORDS, UNRESERVED_KEYWORDS, keywordKindOf } from '../kwlist'; +import { RESERVED_KEYWORDS, TYPE_FUNC_NAME_KEYWORDS, keywordKindOf } 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; From fb278ad34cf34066464a3552b1a5fac940fc4596 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 05:33:41 +0000 Subject: [PATCH 09/11] refactor: migrate all quote() usages to quoteIdentifier and remove deprecated methods - Replace all QuoteUtils.quote() usages with QuoteUtils.quoteIdentifier() - Remove deprecated needsQuotes() and quote() methods from quote-utils.ts - QuoteUtils now only exports: escape, escapeEString, formatEString, needsEscapePrefix, quoteIdentifier, quoteQualifiedIdentifier This completes the migration to PostgreSQL-accurate identifier quoting. Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 284 ++++++++++----------- packages/deparser/src/utils/quote-utils.ts | 44 +--- 2 files changed, 143 insertions(+), 185 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index cf6c5698..e6b28331 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,7 +1883,7 @@ export class Deparser implements DeparserVisitor { return output.join(' '); } - const quotedTypeName = QuoteUtils.quote(typeName); + const quotedTypeName = QuoteUtils.quoteIdentifier(typeName); let result = mods(quotedTypeName, args); if (node.arrayBounds && node.arrayBounds.length > 0) { @@ -1972,7 +1972,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 +2017,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.quoteIdentifier(node.schemaname) + '.' + QuoteUtils.quoteIdentifier(node.relname); } else { - tableName = QuoteUtils.quote(node.relname); + tableName = QuoteUtils.quoteIdentifier(node.relname); } output.push(tableName); @@ -2281,7 +2281,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)); @@ -2690,7 +2690,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) { @@ -2741,7 +2741,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) { @@ -3673,7 +3673,7 @@ export class Deparser implements DeparserVisitor { } if (node.idxname) { - output.push(QuoteUtils.quote(node.idxname)); + output.push(QuoteUtils.quoteIdentifier(node.idxname)); } output.push('ON'); @@ -3716,7 +3716,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(' '); @@ -3726,7 +3726,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))); } @@ -3785,7 +3785,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))); } @@ -4053,19 +4053,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': @@ -4524,7 +4524,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()); @@ -4559,12 +4559,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(objectName)} USING ${accessMethod}`; } return items.join('.'); } @@ -4609,7 +4609,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()); @@ -4670,7 +4670,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 @@ -4837,7 +4837,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) { @@ -4901,7 +4901,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) { @@ -4959,7 +4959,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'); @@ -4974,7 +4974,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) { @@ -5000,7 +5000,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': @@ -5017,7 +5017,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'); @@ -5052,7 +5052,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'); @@ -5064,7 +5064,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) { @@ -5075,7 +5075,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': @@ -5102,21 +5102,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()); } @@ -5128,7 +5128,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) { @@ -5144,7 +5144,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) { @@ -5160,7 +5160,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) { @@ -5170,31 +5170,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': @@ -5212,31 +5212,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'); @@ -5289,7 +5289,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'); @@ -5301,7 +5301,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) { @@ -5311,14 +5311,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; @@ -5351,7 +5351,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'); } @@ -5360,7 +5360,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)); } @@ -5381,7 +5381,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) { @@ -5434,7 +5434,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) { @@ -5444,7 +5444,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) { @@ -5454,7 +5454,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') { @@ -5602,7 +5602,7 @@ export class Deparser implements DeparserVisitor { } if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } if (node.argType) { @@ -6497,7 +6497,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: @@ -6564,7 +6564,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'); @@ -6573,7 +6573,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': @@ -6590,7 +6590,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'); @@ -6599,7 +6599,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': @@ -7011,7 +7011,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(' ')); @@ -7090,7 +7090,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) { @@ -7632,7 +7632,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'); } @@ -7688,7 +7688,7 @@ export class Deparser implements DeparserVisitor { } if (node.portalname) { - output.push(QuoteUtils.quote(node.portalname)); + output.push(QuoteUtils.quoteIdentifier(node.portalname)); } return output.join(' '); @@ -7770,7 +7770,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) { @@ -7797,7 +7797,7 @@ export class Deparser implements DeparserVisitor { } if (node.servername) { - output.push(QuoteUtils.quote(node.servername)); + output.push(QuoteUtils.quoteIdentifier(node.servername)); } if (node.servertype) { @@ -7809,7 +7809,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) { @@ -7828,7 +7828,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) { @@ -7859,7 +7859,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) { @@ -7890,7 +7890,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(' '); @@ -7900,7 +7900,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) { @@ -7929,13 +7929,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) { @@ -8204,7 +8204,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': @@ -8263,7 +8263,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) { @@ -8282,7 +8282,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)); } @@ -8321,7 +8321,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') { @@ -8349,7 +8349,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)); } @@ -8815,7 +8815,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) { @@ -8861,7 +8861,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('('); @@ -8899,7 +8899,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()) { @@ -9073,7 +9073,7 @@ export class Deparser implements DeparserVisitor { } if (node.name) { - output.push(QuoteUtils.quote(node.name)); + output.push(QuoteUtils.quoteIdentifier(node.name)); } return output.join(' '); @@ -9083,7 +9083,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'); @@ -9117,7 +9117,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) { @@ -9286,13 +9286,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) { @@ -9323,10 +9323,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('.')); } @@ -9364,10 +9364,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('.')); } @@ -9772,7 +9772,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); } @@ -10151,7 +10151,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) { @@ -10166,7 +10166,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'); @@ -10178,7 +10178,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) { @@ -10193,7 +10193,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 @@ -10242,7 +10242,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'); @@ -10275,7 +10275,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'); @@ -10346,7 +10346,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(' '); @@ -10533,7 +10533,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) { @@ -10690,9 +10690,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()'; } @@ -10708,7 +10708,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)); @@ -10805,7 +10805,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}`); } @@ -10829,26 +10829,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 ''; } @@ -10932,7 +10932,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'); @@ -11012,44 +11012,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); }); @@ -11130,7 +11130,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) { @@ -11218,7 +11218,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(domainName)}`); } else { output.push(this.visit(node.object as any, context)); } @@ -11228,7 +11228,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(typeName)}`); } else { output.push(this.visit(node.object as any, context)); } @@ -11238,7 +11238,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(conversionName)}`); } else { output.push(this.visit(node.object as any, context)); } @@ -11248,7 +11248,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(parserName)}`); } else { output.push(this.visit(node.object as any, context)); } @@ -11258,7 +11258,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(configName)}`); } else { output.push(this.visit(node.object as any, context)); } @@ -11268,7 +11268,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(templateName)}`); } else { output.push(this.visit(node.object as any, context)); } @@ -11278,7 +11278,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(dictionaryName)}`); } else { output.push(this.visit(node.object as any, context)); } @@ -11288,12 +11288,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(opClassName)} USING ${accessMethod}`); } else { output.push(this.visit(node.object as any, context)); } @@ -11303,12 +11303,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(opFamilyName)} USING ${accessMethod}`); } else { output.push(this.visit(node.object as any, context)); } @@ -11320,7 +11320,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(' '); @@ -11397,7 +11397,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/utils/quote-utils.ts b/packages/deparser/src/utils/quote-utils.ts index d710f34f..a2668690 100644 --- a/packages/deparser/src/utils/quote-utils.ts +++ b/packages/deparser/src/utils/quote-utils.ts @@ -1,48 +1,6 @@ -import { RESERVED_KEYWORDS, TYPE_FUNC_NAME_KEYWORDS, keywordKindOf } from '../kwlist'; +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_KEYWORDS.has(lowerValue) || TYPE_FUNC_NAME_KEYWORDS.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, "''")}'`; } From 742a31822382df4cf3bc4d71d94a925cf0d4b6ea Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 05:36:58 +0000 Subject: [PATCH 10/11] fix: don't quote type names - they are not identifiers Type names like 'int' and 'timestamp' should not be quoted even though they are TYPE_FUNC_NAME_KEYWORDS. The quoteIdentifier function is meant for SQL identifiers (table names, column names, etc.), not for type names which have their own grammar rules in PostgreSQL. Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index e6b28331..160e2942 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -1883,8 +1883,7 @@ export class Deparser implements DeparserVisitor { return output.join(' '); } - const quotedTypeName = QuoteUtils.quoteIdentifier(typeName); - let result = mods(quotedTypeName, args); + let result = mods(typeName, args); if (node.arrayBounds && node.arrayBounds.length > 0) { result += formatArrayBounds(node.arrayBounds); From 8016a7dbf76dba0d434c9399a298683f48519555 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 05:51:45 +0000 Subject: [PATCH 11/11] refactor: use quoteQualifiedIdentifier for schema.name patterns Replaced 11 instances of manual schema.name concatenation with quoteQualifiedIdentifier() for cleaner, more maintainable code: - RangeVar: schema.table patterns - DropStmt: operator class/family with schema - AlterObjectSchemaStmt: domain, type, conversion, parser, config, template, dictionary, operator class/family patterns Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 160e2942..19ab64bb 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -2022,7 +2022,7 @@ export class Deparser implements DeparserVisitor { } tableName += '.' + QuoteUtils.quoteIdentifier(node.relname); } else if (node.schemaname) { - tableName = QuoteUtils.quoteIdentifier(node.schemaname) + '.' + QuoteUtils.quoteIdentifier(node.relname); + tableName = QuoteUtils.quoteQualifiedIdentifier(node.schemaname, node.relname); } else { tableName = QuoteUtils.quoteIdentifier(node.relname); } @@ -4563,7 +4563,7 @@ export class Deparser implements DeparserVisitor { const accessMethod = items[0]; const schemaName = items[1]; const objectName = items[2]; - return `${QuoteUtils.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(objectName)} USING ${accessMethod}`; + return `${QuoteUtils.quoteQualifiedIdentifier(schemaName, objectName)} USING ${accessMethod}`; } return items.join('.'); } @@ -11217,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(domainName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, domainName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11227,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(typeName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, typeName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11237,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(conversionName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, conversionName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11247,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(parserName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, parserName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11257,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(configName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, configName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11267,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(templateName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, templateName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11277,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.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(dictionaryName)}`); + output.push(QuoteUtils.quoteQualifiedIdentifier(schemaName, dictionaryName)); } else { output.push(this.visit(node.object as any, context)); } @@ -11292,7 +11292,7 @@ export class Deparser implements DeparserVisitor { const accessMethod = items[0].String?.sval || ''; const schemaName = items[1].String?.sval || ''; const opClassName = items[2].String?.sval || ''; - output.push(`${QuoteUtils.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(opClassName)} USING ${accessMethod}`); + output.push(`${QuoteUtils.quoteQualifiedIdentifier(schemaName, opClassName)} USING ${accessMethod}`); } else { output.push(this.visit(node.object as any, context)); } @@ -11307,7 +11307,7 @@ export class Deparser implements DeparserVisitor { const accessMethod = items[0].String?.sval || ''; const schemaName = items[1].String?.sval || ''; const opFamilyName = items[2].String?.sval || ''; - output.push(`${QuoteUtils.quoteIdentifier(schemaName)}.${QuoteUtils.quoteIdentifier(opFamilyName)} USING ${accessMethod}`); + output.push(`${QuoteUtils.quoteQualifiedIdentifier(schemaName, opFamilyName)} USING ${accessMethod}`); } else { output.push(this.visit(node.object as any, context)); }