Describe the bug
The execCypher() function accepts a cols parameter that is used to build the AS (col1 agtype, col2 agtype, ...) clause in the generated SQL. The _validate_column() / validate_identifier() functions correctly validate that column names match ^[A-Za-z_][A-Za-z0-9_]*$, but they do not check whether the name is a PostgreSQL reserved word.
When a column alias happens to be a reserved word (e.g., count, sum, type, order, group, select), the generated SQL fails with a parse error because the alias is used unquoted in the AS (...) clause.
This is especially problematic when column aliases are derived from Cypher RETURN clauses (e.g., RETURN count(n) AS count).
How are you accessing AGE?
Python driver (psycopg3)
Steps to Reproduce
import age
import psycopg
conn = psycopg.connect("host=localhost dbname=postgres user=postgres",
cursor_factory=age.age.ClientCursor)
age.setUpAge(conn, "test_graph")
# This crashes because "count" is a PostgreSQL reserved word
cursor = age.execCypher(conn, "test_graph",
"MATCH (n:TestNode) RETURN count(n)",
cols=["count"])
Expected behavior
The query should succeed. The driver should quote-escape reserved-word column aliases in the generated AS (...) clause, e.g.:
SELECT * FROM cypher(NULL, NULL) AS ("count" agtype);
Actual behavior
PostgreSQL parse error because count is unquoted:
-- Generated SQL (broken):
SELECT * FROM cypher(NULL, NULL) AS (count agtype);
-- ERROR: syntax error at or near "count"
Current workaround
We maintain a list of 60+ PostgreSQL reserved words and prefix them with col_ before passing to execCypher():
_PG_RESERVED_WORDS = frozenset({"all", "and", "as", "count", "group", "order",
"select", "sum", "type", "where", ...})
def _sanitize_alias(alias):
if alias.lower() in _PG_RESERVED_WORDS:
return f"col_{alias}"
return alias
Suggested fix
In _validate_column() or buildCypher(), wrap column names in double quotes when they match PostgreSQL reserved words. PostgreSQL always accepts double-quoted identifiers, so quoting unconditionally (e.g., f'"{{name}}" {{type_name}}') would be the simplest and safest approach:
# In buildCypher() or _validate_column():
columnExp.append(f'"{{name}}" agtype') # always quote — safe for all identifiers
Alternatively, use psycopg.sql.Identifier() which handles quoting automatically:
from psycopg import sql
# sql.Identifier(name) produces '"name"' only when needed
Environment
- Apache AGE: 1.5.0+ / master
- Python driver: master branch (psycopg3 version)
- psycopg: 3.2.x
- Python: 3.10+
Describe the bug
The
execCypher()function accepts acolsparameter that is used to build theAS (col1 agtype, col2 agtype, ...)clause in the generated SQL. The_validate_column()/validate_identifier()functions correctly validate that column names match^[A-Za-z_][A-Za-z0-9_]*$, but they do not check whether the name is a PostgreSQL reserved word.When a column alias happens to be a reserved word (e.g.,
count,sum,type,order,group,select), the generated SQL fails with a parse error because the alias is used unquoted in theAS (...)clause.This is especially problematic when column aliases are derived from Cypher
RETURNclauses (e.g.,RETURN count(n) AS count).How are you accessing AGE?
Python driver (psycopg3)
Steps to Reproduce
Expected behavior
The query should succeed. The driver should quote-escape reserved-word column aliases in the generated
AS (...)clause, e.g.:Actual behavior
PostgreSQL parse error because
countis unquoted:Current workaround
We maintain a list of 60+ PostgreSQL reserved words and prefix them with
col_before passing toexecCypher():Suggested fix
In
_validate_column()orbuildCypher(), wrap column names in double quotes when they match PostgreSQL reserved words. PostgreSQL always accepts double-quoted identifiers, so quoting unconditionally (e.g.,f'"{{name}}" {{type_name}}') would be the simplest and safest approach:Alternatively, use
psycopg.sql.Identifier()which handles quoting automatically:Environment