AgensGraph 2.18.6.0#
Release 2.18.6.0 moves AgensGraph onto PostgreSQL 18 and adds a substantial part of the
ISO/IEC 39075 (GQL) standard. It also brings typed-column property promotion, which lets a
hot jsonb property become a real indexable column without changing how queries are written.
Several changes will break a working 2.17 application. Read Migrating to 2.18 below before upgrading, and follow the procedure in Upgrading to 2.18 — a 2.17 data directory cannot be started by 2.18 binaries.
Overview#
PostgreSQL 18 base. 2.17 was built on PostgreSQL 17.10; 2.18.6.0 is built on 18.6, so everything new in PostgreSQL 18 comes with it.
GQL. New statements
LET,NEXT,FILTER,FINISH,INSERTandFOR;CALL { }subqueries andCALL … YIELD;OPTIONAL CALL; the subquery value expressionsEXISTS { },COUNT { },COLLECT { },ARRAY { },VALUE { }andx IN { }; standaloneORDER BY,SKIP,OFFSETandLIMIT; theXORoperator; andCURRENT_GRAPH/CURRENT_PROPERTY_GRAPH. See GQL conformance.Typed-column property promotion.
ALTER VLABEL movie ADD COLUMN year int GENERATEDgives a property a real, indexable column of a declared type, and a Cypher property read resolves to it without the query changing. See Promoting a property to a column.Graph elements in native arrays.
collect()over a vertex, edge or path now builds avertex[],edge[]orgraphpath[]instead of a jsonb picture, so the collected elements stay usable.Planner work on traversal, including scan pruning driven by
ag_graphmeta, elision of unused pattern endpoints, and better estimates for variable-length edges.Fifteen crash classes fixed, several of them reachable with read privileges alone.
New features#
GQL statements#
Statement |
What it does |
|---|---|
Adds computed values to the row without dropping anything in scope. |
|
Chains one query onto the result of another. |
|
Filters rows; |
|
Ends a query that projects nothing. |
|
Creates elements; the GQL spelling of |
|
Iterates a list, with |
|
A subquery, with an explicit variable import list. |
|
Calls a function and names its output columns. |
|
As |
|
|
Standalone ORDER BY, SKIP, OFFSET and LIMIT became clauses in their own right rather
than parts of RETURN and WITH, so they can sit anywhere in a pipeline. They cannot end a
query: a statement still has to finish with RETURN, FINISH or a write clause.
Also new: the XOR operator, NULLIF, IS [NOT] UNKNOWN, RETURN ALL / WITH ALL,
exists(property), size(value), cardinality(), toInteger(), toFloat(), and SQL value
functions inside Cypher.
Casts can now name a keyword type: ::int, ::numeric, ::varchar, ::bit, ::interval,
::boolean, ::date, ::timestamptz and the rest. Several of those had no working spelling
at all before. ::timestamp and ::time remain unwritable in Cypher; wrap the expression in
SQL for those.
Typed-column property promotion#
A property lives in a per-element jsonb document, which means a read walks a JSON document
and cannot use an index on the value’s own type. Promotion gives one property a generated,
stored column of a declared type:
ALTER VLABEL movie ADD COLUMN year int GENERATED;
CREATE PROPERTY INDEX ON movie (year);
Queries do not change — MATCH (m:movie) WHERE m.year > 2010 now resolves m.year to the
column and can use the index. Promotion also works at creation time, and a column can take
its value from a differently named property:
CREATE VLABEL doc (words int GENERATED (word_count), score numeric GENERATED);
The column carries the property named in the parentheses, so the read that resolves to
words is d.word_count — the property key, not the column name.
Extension types work too, which is the case that matters for retrieval: with pgvector
installed, CREATE VLABEL doc (emb vector(1024) GENERATED) gives an embedding a real
indexable column instead of a jsonb array cast on every read.
A promoted key is no longer schemaless: a write whose value does not fit the column’s type aborts. Promoting a property to a column covers the trade-offs.
Other additions#
ALTER VLABEL … ADD COLUMN/DROP COLUMN.A new catalog,
ag_label_property, recording which column carries which property, and a newag_graph.graphmeta_validcolumn that gates scan pruning. See System catalogs.Three new settings:
enable_graph_ddl,enable_property_promotionandenable_graph_endpoint_elision. See Server settings.log_statement = modnow records Cypher writes. Previously a server asked to log data-modifying statements logged none of them — a SQLINSERTwas recorded and a CypherCREATEbeside it was not.
Migrating to 2.18#
These are the changes most likely to break something that works today. Upgrading to 2.18 has the full list and the procedure.
New reserved words#
xor, current_graph and current_property_graph are now reserved. An identifier, label,
variable or alias with any of those names needs quoting, and quoting means double quotes,
in Cypher as in SQL — MATCH (n:"xor"). A backtick is not a quoting character here, so
`xor` is a syntax error.
insert, finish and let are also reserved, but two uses that worked on 2.16 and 2.17
were deliberately restored: insert as a variable, and insert or finish as a label in a
pattern. Creating such a label still needs quoting — CREATE VLABEL insert; is a syntax
error, CREATE VLABEL "insert"; works, and the unquoted pattern spelling then finds it.
What still needs quoting in every position is let or finish as a variable name, let
as a label, and sorting by an alias named let or finish.
Keywords has the full matrix of which word fails in which position.
CALL was left unreserved, so call remains usable as a column, table, alias, label,
property key and variable. Exactly two spellings change meaning and need quoting: call(...)
— a function literally named call invoked with arguments — and (call { ... }), a node
pattern whose variable is named call and which carries a property map but no label.
See Keywords.
A condition takes only a boolean#
WHERE n.count and FILTER n.name used to read a property for non-zero or non-empty and
select rows accordingly. They now raise a cast error:
ERROR: cannot cast "Jack" (jsonb type string) to boolean
This applies in WHERE, FILTER, AND, OR, NOT, XOR, CASE WHEN and dijkstra()’s
qualifier. An explicit ::boolean still converts, and a genuine boolean, a jsonb null and an
absent key are unaffected. The change is deliberate: ISO/IEC 39075 defines a search condition
as a boolean value expression.
Changed result types#
collect()over a vertex, edge or path returnsvertex[],edge[]orgraphpath[]rather than a jsonb array of pictures. Reading a property off a collected element now works, where before it silently answered empty. Acollectover a property still returns a jsonb list, so stored data keeps its shape.min()andmax()over a vertex or edge are now an error:ERROR: function min(vertex) is not uniquePostgreSQL 18 added
min(record)andmax(record), which now compete withmin(jsonb)/max(jsonb)through the implicitvertex → jsonbcast. Cast explicitly —min(v::jsonb)— or aggregate the identity,min(id(v)).range()returns a jsonb array rather than an integer array.toStringList,toIntegerListandtoFloatListwere rewritten. Each now has two overloads: called on a real array, from SQL, they returntext[],bigint[]anddouble precision[]; called on a Cypher list, which isjsonb, they returnjsonb. The old versions gave several silent wrong answers. A bigint lost its high bits, a non-numeric string converted to 0, a one-element list came back empty, and an element longer than 64 characters ran past a fixed buffer.
Changed result sets#
WITH … WHERE with SKIP or LIMIT now pages after the filter. Previously the paging
happened first, so WITH x WHERE x > 1 ORDER BY x LIMIT 2 could return one row. The order is
now WHERE, then ORDER BY, then SKIP/LIMIT. This changes result sets.
RETURN <expr> AS d ORDER BY d sorts the value, not its jsonb form. Rows and ordering are
unchanged for every ordered type except a non-finite float, which used to sort among strings.
The gain is that an index can serve the ordering.
A write clause’s changes are visible to a later read in the same statement.
MATCH (p) SET p.age = 99 WITH p MATCH (q) RETURN q.age returned the pre-write value, and
CREATE (:Q) WITH 1 MATCH (x:Q) RETURN count(x) returned 0 while silently discarding the
create.
ALL, ANY, NONE and SINGLE follow three-valued logic. any([null] WHERE p) is now
null rather than false. Empty lists keep their vacuous results.
Stricter validation#
Each of these used to succeed, and in most cases used to produce a wrong answer:
ALTER TABLEmay no longer reshape a graph label — add or drop a column, change inheritance, or rename a label column. The label DDL does the same job, andenable_graph_ddllifts the restriction. Changing the type ofid,start,endorpropertiesis refused even then.SQL
MERGEagainst a label now needsenable_graph_dml, asINSERT,UPDATEandDELETEalready did.Row-level security is now actually enforced on graph writes. The
USINGclause of anUPDATEorDELETEpolicy was attached to a range-table entry that is never scanned, so it filtered nothing — a role with noDELETEpolicy at all deleted rows anyway.INSERTpolicies were checked against the wrong row, and edge creation was never checked, so an edge label with row security accepted every insert. If you rely on RLS over a graph, test it again on 2.18: writes that used to pass will now be refused.A
WITHmay not carry the same name twice, anORDER BYkey may not compute with a volatile item, a pattern variable may not name a column already in scope, a graph write may not replace a property map with a scalar, and aCALLsubquery may not return a name already bound outside it.
Diagnostics#
EXPLAIN output changed for count() over a graph element, for property reads, and for
variable-length traversal nodes, which now name the edge label they walk. Refresh any
regression baselines that compare plan text.
Fixes#
Fifteen crash classes were fixed. Several were reachable by a user with nothing but read privileges, and took down every session on the instance:
head('{}'::text[])crashed the server while planning.RETURN cardinality(['a','b'])crashed at parse time.MATCH ()-[r:e*1..2 = 5]->()— a non-map constraint on a variable-length edge.CREATE PROPERTY INDEX ON doc (tags[0]).An aggregate inside a list comprehension.
A list element longer than 64 characters ran past a fixed buffer.
Four more fixes, each of which had been giving a wrong answer rather than an error.
vertex ordering and edge equality were wrong in 2.17.0, so <, <=, >, >= on a
vertex and = on an edge did not mean what they said. min(jsonb) and max(jsonb) returned
an arbitrary element. DELETE returned an empty value for the element it removed, and
emptied other variables bound to it. And x IN <list> over a graph element compared full
serializations rather than identity.
A PostgreSQL 18 regression is also fixed inside this release: DISTINCT, GROUP BY,
ORDER BY, min() and max() over a property read failed with “could not determine which
collation to use”. Such a key now takes the database default, the same collation jsonb itself
uses, so results are unchanged.
Not in this release#
To be explicit, because these have been asked about:
Write clauses inside
CALL { }. The parser refusesCREATE,SETand the other write clauses in a subquery body.CALL { } IN TRANSACTIONSdoes not exist.Sessions and schemas from GQL —
SESSION_SET,USE— and typed graphs / graph types.CREATE OR REPLACE CONSTRAINT, which parses and is then refused.Non-stored and non-generated promoted columns. A promoted column must be
GENERATED … STORED.