From 15b953c205ed0aa819cbba7af7c1afcde1622610 Mon Sep 17 00:00:00 2001 From: himmel Date: Wed, 12 Aug 2026 06:51:58 +0000 Subject: [PATCH 1/2] Fix create_graph failing when ag_catalog is not in search_path create_index_on_column() passed "graphid_ops" as an unqualified operator class name, so it was resolved against the caller's search_path. Whenever ag_catalog was not on the search_path, creating a label failed with ERROR: operator class "graphid_ops" does not exist for access method "btree" even for a fully qualified call such as SELECT ag_catalog.create_graph('g'); The error is misleading: the operator class does exist, it is simply not visible. This affected create_graph(), create_vlabel(), create_elabel(), the CSV loader, and label creation from a CREATE or MERGE clause at query time, since all of them reach create_label(). The unqualified name arrived with the id column indexes in #2117, so this is a regression. Before that commit create_label() built no index and resolved no name through the search_path. Every other object referenced by the DDL that create_label() generates is already reached either by OID or by an ag_catalog qualified name, so qualifying this one restores the previous behaviour. --- src/backend/commands/label_commands.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/label_commands.c b/src/backend/commands/label_commands.c index ac789ecce..780578ef3 100644 --- a/src/backend/commands/label_commands.c +++ b/src/backend/commands/label_commands.c @@ -509,7 +509,8 @@ static void create_index_on_column(char *schema_name, index_col->expr = NULL; index_col->indexcolname = NULL; index_col->collation = NIL; - index_col->opclass = list_make1(makeString("graphid_ops")); + index_col->opclass = list_make2(makeString("ag_catalog"), + makeString("graphid_ops")); index_col->opclassopts = NIL; index_col->ordering = SORTBY_DEFAULT; index_col->nulls_ordering = SORTBY_NULLS_DEFAULT; From 897257117c66bf8af9ee39ed1c8d5b312b7b54d3 Mon Sep 17 00:00:00 2001 From: himmel Date: Mon, 24 Aug 2026 05:42:43 +0000 Subject: [PATCH 2/2] Add regression test for graphid_ops search_path qualification Cover create_label() with ag_catalog off the search_path. The block calls create_graph(), create_vlabel(), create_elabel() and a cypher CREATE clause, all through ag_catalog qualified names, so it exercises both create_index_on_column() branches (the unique index on a vertex label's id column and the non-unique indexes on an edge label's start_id and end_id columns) plus label creation at query time. Without the qualified operator class name the first statement fails with ERROR: operator class "graphid_ops" does not exist for access method "btree" --- regress/expected/catalog.out | 52 ++++++++++++++++++++++++++++++++++++ regress/sql/catalog.sql | 18 +++++++++++++ 2 files changed, 70 insertions(+) diff --git a/regress/expected/catalog.out b/regress/expected/catalog.out index a15fa4698..7cf67cdac 100644 --- a/regress/expected/catalog.out +++ b/regress/expected/catalog.out @@ -607,3 +607,55 @@ NOTICE: graph "graph" has been dropped (1 row) +-- +-- create_label() must not resolve graphid_ops through the search_path +-- +-- Every AGE object is referenced with an ag_catalog qualified name here, so +-- these statements have to work with ag_catalog off the search_path. +-- +SET search_path TO public; +SELECT ag_catalog.create_graph('graphid_ops_search_path'); +NOTICE: graph "graphid_ops_search_path" has been created + create_graph +-------------- + +(1 row) + +-- vertex labels index id, edge labels index start_id and end_id +SELECT ag_catalog.create_vlabel('graphid_ops_search_path', 'v'); +NOTICE: VLabel "v" has been created + create_vlabel +--------------- + +(1 row) + +SELECT ag_catalog.create_elabel('graphid_ops_search_path', 'e'); +NOTICE: ELabel "e" has been created + create_elabel +--------------- + +(1 row) + +-- label creation at query time goes through the same path +SELECT * FROM ag_catalog.cypher('graphid_ops_search_path', + $$CREATE (:query_time_v)-[:query_time_e]->(:query_time_v)$$) + AS (result ag_catalog.agtype); + result +-------- +(0 rows) + +SELECT ag_catalog.drop_graph('graphid_ops_search_path', true); +NOTICE: drop cascades to 6 other objects +DETAIL: drop cascades to table graphid_ops_search_path._ag_label_vertex +drop cascades to table graphid_ops_search_path._ag_label_edge +drop cascades to table graphid_ops_search_path.v +drop cascades to table graphid_ops_search_path.e +drop cascades to table graphid_ops_search_path.query_time_v +drop cascades to table graphid_ops_search_path.query_time_e +NOTICE: graph "graphid_ops_search_path" has been dropped + drop_graph +------------ + +(1 row) + +SET search_path TO ag_catalog; diff --git a/regress/sql/catalog.sql b/regress/sql/catalog.sql index bb72c3495..87a8bf76e 100644 --- a/regress/sql/catalog.sql +++ b/regress/sql/catalog.sql @@ -235,3 +235,21 @@ SELECT count(*) FROM ag_label; -- dropping the graphs SELECT drop_graph('issue_2245', true); SELECT drop_graph('graph', true); + +-- +-- create_label() must not resolve graphid_ops through the search_path +-- +-- Every AGE object is referenced with an ag_catalog qualified name here, so +-- these statements have to work with ag_catalog off the search_path. +-- +SET search_path TO public; +SELECT ag_catalog.create_graph('graphid_ops_search_path'); +-- vertex labels index id, edge labels index start_id and end_id +SELECT ag_catalog.create_vlabel('graphid_ops_search_path', 'v'); +SELECT ag_catalog.create_elabel('graphid_ops_search_path', 'e'); +-- label creation at query time goes through the same path +SELECT * FROM ag_catalog.cypher('graphid_ops_search_path', + $$CREATE (:query_time_v)-[:query_time_e]->(:query_time_v)$$) + AS (result ag_catalog.agtype); +SELECT ag_catalog.drop_graph('graphid_ops_search_path', true); +SET search_path TO ag_catalog;