Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -588,3 +588,12 @@ BEGIN
RETURN;
END"
`;

exports[`plpgsql-deparser bug fixes uppercase %ROWTYPE/%TYPE references should not quote uppercase %ROWTYPE and %TYPE references 1`] = `
"DECLARE
r myschema.users%ROWTYPE;
n myschema.users.name%TYPE;
BEGIN
RETURN;
END"
`;
24 changes: 24 additions & 0 deletions packages/plpgsql-deparser/__tests__/deparser-fixes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1214,4 +1214,28 @@ END$$`;
expect(deparsed).not.toMatch(/\(m\)\[/);
});
});

describe('uppercase %ROWTYPE/%TYPE references', () => {
it('should not quote uppercase %ROWTYPE and %TYPE references', async () => {
const sql = `CREATE FUNCTION test_rowtype_case() RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
r myschema.users%ROWTYPE;
n myschema.users.name%TYPE;
BEGIN
NULL;
END;
$$`;

await testUtils.expectAstMatch('uppercase rowtype/type refs', sql);

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const deparsed = deparseSync(parsed);
expect(deparsed).toMatchSnapshot();
expect(deparsed).toMatch(/myschema\.users%ROWTYPE/i);
expect(deparsed).not.toContain('"users%ROWTYPE"');
expect(deparsed).not.toContain('"name%TYPE"');
});
});
});
41 changes: 41 additions & 0 deletions packages/plpgsql-deparser/__tests__/hydrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,47 @@ $$`;
expect(deparsedBody).not.toContain('old-schema');
});

it('should preserve array bounds when dehydrating modified type-name nodes', () => {
const sql = `CREATE FUNCTION test_func() RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
v_items "old-schema".mytype[];
v_item "old-schema".mytype;
BEGIN
NULL;
END;
$$`;

const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
const { ast: hydratedAst } = hydratePlpgsqlAst(parsed);

// Rename schema inside the hydrated TypeName AST nodes
const renameTypeNames = (obj: any): void => {
if (obj === null || typeof obj !== 'object') return;
if ('PLpgSQL_type' in obj) {
const typname = obj.PLpgSQL_type.typname;
if (typname && typeof typname === 'object' && typname.kind === 'type-name') {
for (const name of typname.typeNameNode?.names ?? []) {
if (name?.String?.sval === 'old-schema') {
name.String.sval = 'new_schema';
}
}
}
}
for (const value of Object.values(obj)) renameTypeNames(value);
};
renameTypeNames(hydratedAst);

const dehydratedAst = dehydratePlpgsqlAst(hydratedAst);
const deparsedBody = deparseSync(dehydratedAst);

// The renamed array type must keep its [] bounds
expect(deparsedBody).toContain('new_schema.mytype[]');
expect(deparsedBody).toContain('v_item new_schema.mytype;');
expect(deparsedBody).not.toContain('old-schema');
});

it('should deparse modified assign AST nodes (schema renaming in assignments)', () => {
const sql = `CREATE FUNCTION test_func() RETURNS void
LANGUAGE plpgsql
Expand Down
7 changes: 5 additions & 2 deletions packages/plpgsql-deparser/src/hydrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,8 +652,11 @@ function deparseTypeNameNode(typeNameNode: Node, sqlDeparseOptions?: DeparserOpt
}
} as any;
const deparsed = Deparser.deparse(wrappedStmt, sqlDeparseOptions);
// Extract the type name from "SELECT NULL::typename"
const match = deparsed.match(/SELECT\s+NULL::(.+)/i);
// Extract the type name from "SELECT NULL::typename" or
// "SELECT CAST(NULL AS typename)" depending on the deparser's cast style
const match =
deparsed.match(/SELECT\s+NULL::(.+)/i) ||
deparsed.match(/^SELECT\s+CAST\(NULL\s+AS\s+(.+)\)[\s;]*$/i);
if (match) {
return match[1].trim().replace(/;$/, '');
}
Expand Down
5 changes: 3 additions & 2 deletions packages/plpgsql-deparser/src/plpgsql-deparser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,16 +747,17 @@ export class PLpgSQLDeparser {
private deparseType(typeNode: PLpgSQLTypeNode): string {
if ('PLpgSQL_type' in typeNode) {
let typname = typeNode.PLpgSQL_type.typname;
const isRowOrTypeRef = /%(rowtype|type)/i.test(typname);

// Strip pg_catalog. prefix for built-in types, but preserve schema qualification
// for %rowtype and %type references where the schema is part of the table/variable reference
if (!typname.includes('%rowtype') && !typname.includes('%type')) {
if (!isRowOrTypeRef) {
typname = typname.replace(/^"?pg_catalog"?\./, '');
}

// For %rowtype and %type references, preserve as-is after stripping quotes
// These are special PL/pgSQL type references that shouldn't be re-quoted
if (typname.includes('%rowtype') || typname.includes('%type')) {
if (isRowOrTypeRef) {
// Strip quotes and return as-is
return typname.replace(/"/g, '').trim();
}
Expand Down
Loading