Skip to content

Commit cd117b6

Browse files
committed
fix(plpgsql-deparser): use QuoteUtils for schema-qualified type names
For schema-qualified types (e.g., schema.typename), the deparser now uses QuoteUtils from pgsql-deparser for proper identifier quoting. This ensures consistent quoting behavior between the SQL deparser and PL/pgSQL deparser. Changes: - Import QuoteUtils from pgsql-deparser - Add isSchemaQualifiedType() to detect types with dots - Add parseQualifiedTypeName() to parse dotted type names - Update deparseType() to use QuoteUtils.quoteTypeDottedName() for schema-qualified types - Preserve original format for simple types to maintain round-trip consistency - Handle array type suffixes (e.g., []) separately from the base type
1 parent b932871 commit cd117b6

File tree

1 file changed

+106
-5
lines changed

1 file changed

+106
-5
lines changed

packages/plpgsql-deparser/src/plpgsql-deparser.ts

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* CREATE FUNCTION statement.
1212
*/
1313

14-
import { Deparser as SqlDeparser } from 'pgsql-deparser';
14+
import { Deparser as SqlDeparser, QuoteUtils } from 'pgsql-deparser';
1515
import {
1616
PLpgSQLParseResult,
1717
PLpgSQLFunctionNode,
@@ -695,22 +695,123 @@ export class PLpgSQLDeparser {
695695

696696
/**
697697
* Deparse a type reference
698+
*
699+
* For schema-qualified types (containing a dot), uses QuoteUtils from pgsql-deparser
700+
* for proper identifier quoting. For simple types, preserves the original format
701+
* to maintain round-trip consistency.
698702
*/
699703
private deparseType(typeNode: PLpgSQLTypeNode): string {
700704
if ('PLpgSQL_type' in typeNode) {
701705
let typname = typeNode.PLpgSQL_type.typname;
702-
// Remove quotes
703-
typname = typname.replace(/"/g, '');
706+
704707
// Strip pg_catalog. prefix for built-in types, but preserve schema qualification
705708
// for %rowtype and %type references where the schema is part of the table/variable reference
706709
if (!typname.includes('%rowtype') && !typname.includes('%type')) {
707-
typname = typname.replace(/^pg_catalog\./, '');
710+
typname = typname.replace(/^"?pg_catalog"?\./, '');
708711
}
709-
return typname.trim();
712+
713+
// For %rowtype and %type references, preserve as-is after stripping quotes
714+
// These are special PL/pgSQL type references that shouldn't be re-quoted
715+
if (typname.includes('%rowtype') || typname.includes('%type')) {
716+
// Strip quotes and return as-is
717+
return typname.replace(/"/g, '').trim();
718+
}
719+
720+
// Check if this is a schema-qualified type (contains a dot outside of quotes)
721+
// Only apply QuoteUtils for schema-qualified types to ensure consistent quoting
722+
// For simple types, preserve the original format for round-trip consistency
723+
const isSchemaQualified = this.isSchemaQualifiedType(typname);
724+
725+
if (!isSchemaQualified) {
726+
// Simple type - just strip quotes and return as-is
727+
return typname.replace(/"/g, '').trim();
728+
}
729+
730+
// Schema-qualified type - apply proper quoting
731+
const trimmedTypname = typname.trim();
732+
733+
// Handle array types - extract the array suffix (e.g., [], [3], [][])
734+
// Array notation should not be quoted, only the base type
735+
const arrayMatch = trimmedTypname.match(/(\[[\d]*\])+$/);
736+
const arraySuffix = arrayMatch ? arrayMatch[0] : '';
737+
const baseTypeName = arraySuffix ? trimmedTypname.slice(0, -arraySuffix.length) : trimmedTypname;
738+
739+
// Parse the base type name into parts, handling quoted identifiers
740+
// Type names can be: "schema"."type", schema.type, or just type
741+
const parts = this.parseQualifiedTypeName(baseTypeName);
742+
743+
// Use QuoteUtils to properly quote the type name parts
744+
const quotedType = QuoteUtils.quoteTypeDottedName(parts);
745+
746+
// Re-add the array suffix (unquoted)
747+
return quotedType + arraySuffix;
710748
}
711749
return '';
712750
}
713751

752+
/**
753+
* Check if a type name is schema-qualified (contains a dot outside of quotes).
754+
*/
755+
private isSchemaQualifiedType(typname: string): boolean {
756+
let inQuotes = false;
757+
for (let i = 0; i < typname.length; i++) {
758+
const ch = typname[i];
759+
if (ch === '"') {
760+
if (inQuotes && typname[i + 1] === '"') {
761+
i++; // Skip escaped quote
762+
} else {
763+
inQuotes = !inQuotes;
764+
}
765+
} else if (ch === '.' && !inQuotes) {
766+
return true;
767+
}
768+
}
769+
return false;
770+
}
771+
772+
/**
773+
* Parse a qualified type name into its component parts.
774+
* Handles both quoted ("schema"."type") and unquoted (schema.type) identifiers.
775+
*
776+
* @param typname - The type name string, possibly with quotes and dots
777+
* @returns Array of unquoted identifier parts
778+
*/
779+
private parseQualifiedTypeName(typname: string): string[] {
780+
const parts: string[] = [];
781+
let current = '';
782+
let inQuotes = false;
783+
784+
for (let i = 0; i < typname.length; i++) {
785+
const ch = typname[i];
786+
787+
if (ch === '"') {
788+
if (inQuotes && typname[i + 1] === '"') {
789+
// Escaped quote ("") inside quoted identifier
790+
current += '"';
791+
i++; // Skip the next quote
792+
} else {
793+
// Toggle quote state
794+
inQuotes = !inQuotes;
795+
}
796+
} else if (ch === '.' && !inQuotes) {
797+
// Dot outside quotes - separator
798+
if (current) {
799+
parts.push(current.trim());
800+
current = '';
801+
}
802+
} else {
803+
current += ch;
804+
}
805+
}
806+
807+
// Add the last part
808+
if (current) {
809+
parts.push(current.trim());
810+
}
811+
812+
return parts;
813+
}
814+
714815
/**
715816
* Deparse an expression
716817
*/

0 commit comments

Comments
 (0)