Summary
plpgsql_show_dependency_tb does not report a table dependency when the table is referenced only via %ROWTYPE in a PL/pgSQL DECLARE block. The dependency is real and dynamic — %ROWTYPE variables track the table's live composite type at runtime — but the function is not surfaced as depending on the table.
Tables referenced via SQL statements (SELECT, INSERT, PERFORM, etc.) in the function body are reported correctly. The gap is specific to %ROWTYPE declarations that are the only reference to a table (no SQL statement in the body touches the table directly).
Impact
Tools that rely on plpgsql_show_dependency_tb for dependency tracking will miss %ROWTYPE-only table dependencies. If the table's structure changes (column dropped), the function breaks at runtime — and plpgsql_check_function_tb does catch the error — but the dependency list gave no indication that the function depended on the table, so a consumer wouldn't know to re-check it.
Reproduction
CREATE EXTENSION IF NOT EXISTS plpgsql_check;
-- Table referenced ONLY via %ROWTYPE (no SQL statement touches it in the function body).
CREATE TABLE public.rto (
id integer,
val text,
extra boolean
);
-- Function declares r public.rto%ROWTYPE and uses r.extra in the body,
-- but never executes a SQL statement that references public.rto.
CREATE FUNCTION public.demo_fn() RETURNS text
LANGUAGE plpgsql AS $$
DECLARE
r public.rto%ROWTYPE;
BEGIN
-- r.extra is used (not just declared), creating a real structural dependency:
IF r.extra IS NULL THEN
r.val := 'default';
ELSE
r.val := 'set';
END IF;
RETURN r.val;
END;
$$;
-- Expected: public.rto appears as a RELATION dependency.
-- Actual: empty result — no dependencies reported at all.
SELECT type, schema, name
FROM plpgsql_show_dependency_tb('public.demo_fn()')
ORDER BY name;
-- -- Expected output --
-- type | schema | name
-- ----------+--------+------
-- RELATION | public | rto
-- -- Actual output --
-- (0 rows)
-- dropping a column used via the %ROWTYPE variable breaks the function.
ALTER TABLE public.rto DROP COLUMN extra;
-- plpgsql_check_function_tb catches the breakage — proving the function
-- depends on the table's structure. But plpgsql_show_dependency_tb above
-- didn't report the dependency, so a consumer wouldn't know to re-check.
SELECT message, level
FROM plpgsql_check_function_tb('public.demo_fn()');
-- -- Output --
-- message | level
-- -------------------------------------+-------
-- record "r" has no field "extra" | error
-- Cleanup
DROP FUNCTION public.demo_fn();
DROP TABLE public.rto;
Environment
- plpgsql_check 2.10.1
- PostgreSQL 18 (tested via PGlite WASM build; behavior is not WASM-specific)
Root cause (from source inspection)
plpgsql_show_dependency_tb calls detect_dependency_walker (src/expr_walk.c:43), which walks the planned query tree (Query → rtable → RTE_RELATION entries). %ROWTYPE in DECLARE is resolved by the PL/pgSQL compiler to a composite type OID stored on the PLpgSQL_rec datum — it does not generate a RangeTblEntry in any SQL query plan. Therefore the dependency walker never sees it.
A fix would need to additionally walk the function's datum list (PLpgSQL_function->datums) and, for each PLpgSQL_rec variable whose type is a table's composite type (typrelid != InvalidOid), emit a RELATION dependency for typrelid.
Contrast with %TYPE
%TYPE (in DECLARE or function signatures) is frozen at CREATE time — it resolves to a scalar type OID and does not create a dynamic structural dependency. Missing %TYPE references from the dependency list is acceptable (the type doesn't change at runtime). %ROWTYPE is different — it's dynamic, and the omission is a real gap.
Summary
plpgsql_show_dependency_tbdoes not report a table dependency when the table is referenced only via%ROWTYPEin a PL/pgSQL DECLARE block. The dependency is real and dynamic —%ROWTYPEvariables track the table's live composite type at runtime — but the function is not surfaced as depending on the table.Tables referenced via SQL statements (SELECT, INSERT, PERFORM, etc.) in the function body are reported correctly. The gap is specific to
%ROWTYPEdeclarations that are the only reference to a table (no SQL statement in the body touches the table directly).Impact
Tools that rely on
plpgsql_show_dependency_tbfor dependency tracking will miss%ROWTYPE-only table dependencies. If the table's structure changes (column dropped), the function breaks at runtime — andplpgsql_check_function_tbdoes catch the error — but the dependency list gave no indication that the function depended on the table, so a consumer wouldn't know to re-check it.Reproduction
Environment
Root cause (from source inspection)
plpgsql_show_dependency_tbcallsdetect_dependency_walker(src/expr_walk.c:43), which walks the planned query tree (Query→rtable→RTE_RELATIONentries).%ROWTYPEin DECLARE is resolved by the PL/pgSQL compiler to a composite type OID stored on thePLpgSQL_recdatum — it does not generate aRangeTblEntryin any SQL query plan. Therefore the dependency walker never sees it.A fix would need to additionally walk the function's datum list (
PLpgSQL_function->datums) and, for eachPLpgSQL_recvariable whose type is a table's composite type (typrelid != InvalidOid), emit aRELATIONdependency fortyprelid.Contrast with
%TYPE%TYPE(in DECLARE or function signatures) is frozen at CREATE time — it resolves to a scalar type OID and does not create a dynamic structural dependency. Missing%TYPEreferences from the dependency list is acceptable (the type doesn't change at runtime).%ROWTYPEis different — it's dynamic, and the omission is a real gap.