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.
What happens
On a session whose
search_pathdoes not includeag_catalog, creating a graph fails — even though the call is fully schema qualified: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()(viaget_or_create_label())CREATEorMERGEclause that mentions a label for the first time — this one fails in the middle of a running queryExpected
create_graph()should not depend on the caller'ssearch_path. The error is also misleading:graphid_opsdoes exist (ag_catalog,sql/age_main.sql), it is simply not visible, and the message saysdoes not existwith no hint aboutsearch_path. Combined with the42704 undefined_objectcode, this sends people looking at privileges (GRANT USAGE ON SCHEMA ag_catalog,GRANT EXECUTE ...), which changes nothing.Root cause
create_index_on_column()insrc/backend/commands/label_commands.cpasses the operator class as an unqualified name:ResolveOpClass()→OpclassnameGetOpcid()resolves an unqualified opclass name throughsearch_path, so the lookup fails whenag_catalogis not on it.This is the only unqualified name in that code path. Everything else the generated DDL references is already immune:
graphid/agtypeGRAPHIDOID/AGTYPEOID(OIDs)_graphid()defaultlist_make2(makeString("ag_catalog"), makeString("_graphid"))agtype_build_map()defaultlist_make2(makeString("ag_catalog"), ...)_label_id()list_make2(makeString("ag_catalog"), ...)int4/regclassSystemTypeName()→pg_catalog.*graphid_opslist_make1(makeString(...))So this looks like an oversight rather than a deliberate choice — the same function qualifies three other
ag_catalognames explicitly.This is a regression
Before the id-column indexes were added in #2117,
create_label()created no index and therefore resolved no name throughsearch_path, socreate_graph()worked under anysearch_path.Commits carrying the unqualified name:
master,PG19,release/PG19/1.8.05aed9ec(#2117)PG18,release/PG18/1.7.0,release/PG18/1.8.02f36b1c(#2117)PG17,release/PG17/1.7.0858a0b7(#2117)PG168c74fd2(#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 setsSET search_path TO ag_catalog;— 35/35 onrelease/PG18/1.7.0, 46/47 onmaster, the single exception beingagehash.sql, which only callsag_catalog._agehash_self_test()and creates nothing. So no test exercises the unqualified-lookup path.