Skip to content

Bug: plpgsql_show_dependency_tb misses tables referenced only via %ROWTYPE in DECLARE #221

Description

@racinette

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 (QueryrtableRTE_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions