create_graph() fails when ag_catalog is not in search_path: operator class "graphid_ops" does not exist
- Dominant language
- C
- Stars
- 4.8k
- Forks
- 523
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 9
Description
### 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:
```sql
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:
```c
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.
Contributor guide
Research direction
Start in src/backend/commands/label_commands.c at create_index_on_column(), then inspect the existing regression cases under regress/sql/. Reproduce graph creation with ag_catalog absent from search_path and add coverage for that scenario; done means create_graph() and the listed label entry points succeed without requiring callers to modify search_path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, postgresql
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100