Skip to content

create_graph() fails when ag_catalog is not in search_path: operator class "graphid_ops" does not exist #2504

Description

@NotHimmel

What happens

On a session whose search_path does not include ag_catalog, creating a graph fails — even though the call is fully schema qualified:

LOAD 'age';
SHOW search_path;          --  "$user", public

SELECT ag_catalog.create_graph('g');
-- ERROR:  operator class "graphid_ops" does not exist for access method "btree"

The whole transaction rolls back, so no partial graph is left behind.

The same failure hits every entry point that reaches create_label():

  • ag_catalog.create_graph()
  • ag_catalog.create_vlabel() / ag_catalog.create_elabel()
  • ag_catalog.load_labels_from_file() / load_edges_from_file() (via get_or_create_label())
  • a CREATE or MERGE clause that mentions a label for the first time — this one fails in the middle of a running query

Expected

create_graph() should not depend on the caller's search_path. The error is also misleading: graphid_ops does exist (ag_catalog, sql/age_main.sql), it is simply not visible, and the message says does not exist with no hint about search_path. Combined with the 42704 undefined_object code, this sends people looking at privileges (GRANT USAGE ON SCHEMA ag_catalog, GRANT EXECUTE ...), which changes nothing.

Root cause

create_index_on_column() in src/backend/commands/label_commands.c passes the operator class as an unqualified name:

index_col->opclass = list_make1(makeString("graphid_ops"));

ResolveOpClass()OpclassnameGetOpcid() resolves an unqualified opclass name through search_path, so the lookup fails when ag_catalog is not on it.

This is the only unqualified name in that code path. Everything else the generated DDL references is already immune:

reference how it is built search_path dependent
column types graphid / agtype GRAPHIDOID / AGTYPEOID (OIDs) no
_graphid() default list_make2(makeString("ag_catalog"), makeString("_graphid")) no
agtype_build_map() default list_make2(makeString("ag_catalog"), ...) no
_label_id() list_make2(makeString("ag_catalog"), ...) no
int4 / regclass SystemTypeName()pg_catalog.* no
graphid_ops list_make1(makeString(...)) yes

So this looks like an oversight rather than a deliberate choice — the same function qualifies three other ag_catalog names explicitly.

This is a regression

Before the id-column indexes were added in #2117, create_label() created no index and therefore resolved no name through search_path, so create_graph() worked under any search_path.

Commits carrying the unqualified name:

branch commit
master, PG19, release/PG19/1.8.0 5aed9ec (#2117)
PG18, release/PG18/1.7.0, release/PG18/1.8.0 2f36b1c (#2117)
PG17, release/PG17/1.7.0 858a0b7 (#2117)
PG16 8c74fd2 (#2375)

Affected: 1.7.0 and later. 1.6.0 and earlier are not affected.

Why no existing test catches it

Every file in regress/sql/ that creates a graph sets SET search_path TO ag_catalog; — 35/35 on release/PG18/1.7.0, 46/47 on master, the single exception being agehash.sql, which only calls ag_catalog._agehash_self_test() and creates nothing. So no test exercises the unqualified-lookup path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions