| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 |
- import { POSTGRESQL_RESERVED_WORDS } from '@supabase/pg-meta/src/pg-format/reserved'
- import type { ColumnDef, ColumnRef, Node, RangeVar, ResTarget } from 'libpg-query'
- /**
- * Recursively traverse a libpg-query AST to extract all identifiers.
- * Collects table names from RangeVar and column names from ColumnRef/ResTarget.
- */
- export function extractIdentifiers(ast: Node | Node[]): string[] {
- const identifiers: string[] = []
- function extractFromRangeVar(rv: RangeVar): void {
- if (rv.relname) identifiers.push(rv.relname)
- if (rv.schemaname) identifiers.push(rv.schemaname)
- }
- function traverse(node: unknown): void {
- if (!node || typeof node !== 'object') return
- const obj = node as Record<string, unknown>
- // RangeVar - table references (wrapped form in SELECT)
- if ('RangeVar' in obj) {
- extractFromRangeVar(obj.RangeVar as RangeVar)
- }
- // relation - table references (unwrapped form in INSERT/UPDATE/DELETE)
- if ('relation' in obj && obj.relation && typeof obj.relation === 'object') {
- extractFromRangeVar(obj.relation as RangeVar)
- }
- // ColumnRef - column references in expressions
- if ('ColumnRef' in obj) {
- const cr = obj.ColumnRef as ColumnRef
- for (const field of cr.fields ?? []) {
- if ('String' in field) {
- const str = field.String as { sval?: string }
- if (str.sval) identifiers.push(str.sval)
- }
- }
- }
- // ResTarget - column targets in INSERT/UPDATE
- if ('ResTarget' in obj) {
- const rt = obj.ResTarget as ResTarget
- if (rt.name) identifiers.push(rt.name)
- }
- // ColumnDef - column definitions in CREATE TABLE
- if ('ColumnDef' in obj) {
- const cd = obj.ColumnDef as ColumnDef
- if (cd.colname) identifiers.push(cd.colname)
- }
- // Recurse into all values
- for (const value of Object.values(obj)) {
- if (Array.isArray(value)) {
- value.forEach(traverse)
- } else {
- traverse(value)
- }
- }
- }
- traverse(ast)
- return identifiers
- }
- export function needsQuoting(identifier: string): boolean {
- if (POSTGRESQL_RESERVED_WORDS.has(identifier.toUpperCase())) {
- return true
- }
- // Matches valid unquoted identifiers: starts with underscore or lowercase letter,
- // followed by digits, dollar signs, underscores, or lowercase letters
- // Examples: "users", "user_id", "_private", "col$name"
- const validUnquotedPattern = /^[_a-z][\d$_a-z]*$/
- if (validUnquotedPattern.test(identifier)) {
- return false
- }
- return true
- }
- export function isQuotedInSql(sql: string, identifier: string): boolean {
- // Escapes special regex characters so they're treated literally
- // Examples: "table.name" -> "table\.name", "col(value)" -> "col\(value\)"
- const escapedForRegex = identifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
- // PostgreSQL escapes quotes inside quoted identifiers as ""
- const escapedIdentifier = escapedForRegex.replace(/"/g, '""')
- // Matches quoted identifier: "identifier" (case-insensitive)
- // Examples: "MyTable", "my""table" (for identifier "my"table")
- const quotedPattern = new RegExp(`"${escapedIdentifier}"`, 'i')
- return quotedPattern.test(sql)
- }
|