Description
The setUpAge() function unconditionally executes LOAD 'age' (or LOAD '$libdir/plugins/age'). The Age.connect() method always calls setUpAge() with no option to skip the LOAD statement.
On managed PostgreSQL services like Azure Database for PostgreSQL, the AGE extension is loaded server-side via shared_preload_libraries. The extension binary may not be accessible at the file path expected by LOAD, causing a psycopg.errors.UndefinedFile error. Users have no way to use Age.connect() in these environments without bypassing the driver's setup entirely.
This is related to #839, which discussed non-superuser access. However, our scenario is different: on Azure managed PostgreSQL the LOAD command fails regardless of privileges because the binary is not at the expected file path. The extension is already loaded and functional — the driver just needs a way to skip the LOAD statement.
Steps to Reproduce
- Set up Azure Database for PostgreSQL (Flexible Server) with AGE extension enabled via
shared_preload_libraries
- Create the extension:
CREATE EXTENSION IF NOT EXISTS age;
- Attempt to connect using the Python driver:
import age
ag = age.connect(
dsn="host=myserver.postgres.database.azure.com port=5432 dbname=mydb user=myuser password=mypass",
graph="my_graph"
)
Expected Behavior
The connection should succeed since AGE is already loaded and available in the database.
Actual Behavior
With load_from_plugins=False (the default):
psycopg.errors.UndefinedFile: could not access file "age": No such file or directory
With load_from_plugins=True:
psycopg.errors.UndefinedFile: could not access file "$libdir/plugins/age": No such file or directory
Neither option works because the binary path is managed by the cloud provider and not directly accessible.
Root Cause
In age.py setUpAge():
def setUpAge(conn, graphName, load_from_plugins=False):
with conn.cursor() as cursor:
if load_from_plugins:
cursor.execute("LOAD '$libdir/plugins/age';")
else:
cursor.execute("LOAD 'age';")
# ... rest of setup (search_path, adapters, graph check)
And in Age.connect():
def connect(self, graph=None, dsn=None, ..., load_from_plugins=False, **kwargs):
conn = psycopg.connect(dsn, cursor_factory=cursor_factory, **kwargs)
setUpAge(conn, graph, load_from_plugins) # always called, no way to skip LOAD
The LOAD statement is only needed when the AGE shared library has not been pre-loaded into the PostgreSQL backend. On managed services, the library is already loaded via shared_preload_libraries, making the LOAD statement unnecessary and harmful.
Suggested Fix
Add a skip_load parameter to both setUpAge() and Age.connect() that skips the LOAD statement while still performing all other setup (search_path, agtype adapter registration, graph creation):
def setUpAge(conn, graphName, load_from_plugins=False, skip_load=False):
with conn.cursor() as cursor:
if not skip_load:
if load_from_plugins:
cursor.execute("LOAD '$libdir/plugins/age';")
else:
cursor.execute("LOAD 'age';")
cursor.execute("SET search_path = ag_catalog, '$user', public;")
# ... rest of adapter registration and graph setup unchanged
def connect(self, graph=None, dsn=None, ..., load_from_plugins=False, skip_load=False, **kwargs):
conn = psycopg.connect(dsn, cursor_factory=cursor_factory, **kwargs)
setUpAge(conn, graph, load_from_plugins, skip_load=skip_load)
# ...
Current Workaround
Users must bypass Age.connect() entirely and manually replicate the setup logic, which is fragile and tightly coupled to internal driver implementation details:
import age
import psycopg
from psycopg.types import TypeInfo
ag = age.Age()
ag.connection = psycopg.connect(dsn, cursor_factory=age.age.ClientCursor)
with ag.connection.cursor() as cur:
cur.execute('SET search_path = ag_catalog, "$user", public;')
ag_info = TypeInfo.fetch(ag.connection, "agtype")
ag.connection.adapters.register_loader(ag_info.oid, age.age.AgeLoader)
ag.connection.adapters.register_loader(ag_info.array_oid, age.age.AgeLoader)
ag.graphName = "my_graph"
age.checkGraphCreated(ag.connection, "my_graph")
ag.connection.commit()
Environment
- Apache AGE: 1.6.0 (also affects master)
- Python driver: master branch (psycopg3 version)
- psycopg: 3.2.x
- Python: 3.13
- Managed PostgreSQL: Azure Database for PostgreSQL – Flexible Server
Description
The
setUpAge()function unconditionally executesLOAD 'age'(orLOAD '$libdir/plugins/age'). TheAge.connect()method always callssetUpAge()with no option to skip theLOADstatement.On managed PostgreSQL services like Azure Database for PostgreSQL, the AGE extension is loaded server-side via
shared_preload_libraries. The extension binary may not be accessible at the file path expected byLOAD, causing apsycopg.errors.UndefinedFileerror. Users have no way to useAge.connect()in these environments without bypassing the driver's setup entirely.This is related to #839, which discussed non-superuser access. However, our scenario is different: on Azure managed PostgreSQL the
LOADcommand fails regardless of privileges because the binary is not at the expected file path. The extension is already loaded and functional — the driver just needs a way to skip theLOADstatement.Steps to Reproduce
shared_preload_librariesCREATE EXTENSION IF NOT EXISTS age;Expected Behavior
The connection should succeed since AGE is already loaded and available in the database.
Actual Behavior
With
load_from_plugins=False(the default):With
load_from_plugins=True:Neither option works because the binary path is managed by the cloud provider and not directly accessible.
Root Cause
In
age.pysetUpAge():And in
Age.connect():The
LOADstatement is only needed when the AGE shared library has not been pre-loaded into the PostgreSQL backend. On managed services, the library is already loaded viashared_preload_libraries, making theLOADstatement unnecessary and harmful.Suggested Fix
Add a
skip_loadparameter to bothsetUpAge()andAge.connect()that skips theLOADstatement while still performing all other setup (search_path, agtype adapter registration, graph creation):Current Workaround
Users must bypass
Age.connect()entirely and manually replicate the setup logic, which is fragile and tightly coupled to internal driver implementation details:Environment