--- description: "AgensGraph 2.18 release notes (2.18.6.1), by SKAI Worldwide (formerly Bitnine): PostgreSQL 18 base, GQL, typed property columns, row-level security." --- # AgensGraph 2.18 ## 2.18.6.1 Release 2.18.6.1 changes no query, catalog or storage behaviour. It is built on PostgreSQL 18.6, as 2.18.6.0 is, and starts on a 2.18.6.0 data directory as it is. * `agens --help` and `ag_ctl --help` send bug reports to and name as the home page. 2.18.6.0 named `bitnine.net`, a domain the company no longer holds. ## 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](#migrating-to-218) below before upgrading, and follow the procedure in [Upgrading to 2.18](upgrade-to-2.18.md) — 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`, `INSERT` and `FOR`; `CALL { }` subqueries and `CALL … YIELD`; `OPTIONAL CALL`; the subquery value expressions `EXISTS { }`, `COUNT { }`, `COLLECT { }`, `ARRAY { }`, `VALUE { }` and `x IN { }`; standalone `ORDER BY`, `SKIP`, `OFFSET` and `LIMIT`; the `XOR` operator; and `CURRENT_GRAPH` / `CURRENT_PROPERTY_GRAPH`. See [GQL conformance](../appendix/gql-conformance.md). * **Typed-column property promotion.** `ALTER VLABEL movie ADD COLUMN year int GENERATED` gives 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](../querying/typed-columns.md). * **Graph elements in native arrays.** `collect()` over a vertex, edge or path now builds a `vertex[]`, `edge[]` or `graphpath[]` 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 | | --- | --- | | [`LET`](../reference/clauses/let.md) | Adds computed values to the row without dropping anything in scope. | | [`NEXT`](../reference/clauses/next.md) | Chains one query onto the result of another. | | [`FILTER`](../reference/clauses/filter.md) | Filters rows; `FILTER` and `FILTER WHERE` forms. | | [`FINISH`](../reference/clauses/finish.md) | Ends a query that projects nothing. | | [`INSERT`](../reference/clauses/insert.md) | Creates elements; the GQL spelling of `CREATE`. | | [`FOR`](../reference/clauses/for.md) | Iterates a list, with `WITH OFFSET` for the index. | | [`CALL { }`](../reference/clauses/call-subquery.md) | A subquery, with an explicit variable import list. | | [`CALL … YIELD`](../reference/clauses/call-yield.md) | Calls a function and names its output columns. | | [`OPTIONAL CALL`](../reference/clauses/optional-call.md) | As `CALL`, but a call yielding nothing still produces a row. | | [Subquery expressions](../reference/clauses/subquery-expressions.md) | `EXISTS { }` and `VALUE { }` over a subquery. `COUNT { }`, `COLLECT { }`, `ARRAY { }` and `x IN { }` do the same job but are AgensGraph extensions, not GQL. | 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: ```cypher 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: ```cypher 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](../querying/typed-columns.md) 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 new `ag_graph.graphmeta_valid` column that gates scan pruning. See [System catalogs](../system_catalogs/catalogs.md). * Three new settings: `enable_graph_ddl`, `enable_property_promotion` and `enable_graph_endpoint_elision`. See [Server settings](../reference/guc.md). * `log_statement = mod` now records Cypher writes. Previously a server asked to log data-modifying statements logged none of them — a SQL `INSERT` was recorded and a Cypher `CREATE` beside it was not. ## Migrating to 2.18 These are the changes most likely to break something that works today. [Upgrading to 2.18](upgrade-to-2.18.md) 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](../appendix/keywords.md) 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](../appendix/keywords.md). ### 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** returns `vertex[]`, `edge[]` or `graphpath[]` rather than a jsonb array of pictures. Reading a property off a collected element now works, where before it silently answered empty. A `collect` over a *property* still returns a jsonb list, so stored data keeps its shape. * **`min()` and `max()` over a vertex or edge are now an error**: ``` ERROR: function min(vertex) is not unique ``` PostgreSQL 18 added `min(record)` and `max(record)`, which now compete with `min(jsonb)`/`max(jsonb)` through the implicit `vertex → jsonb` cast. Cast explicitly — `min(v::jsonb)` — or aggregate the identity, `min(id(v))`. * **`range()`** returns a jsonb array rather than an integer array. * **`toStringList`, `toIntegerList` and `toFloatList`** were rewritten. Each now has two overloads: called on a real array, from SQL, they return `text[]`, `bigint[]` and `double precision[]`; called on a Cypher list, which is `jsonb`, they return `jsonb`. 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 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 TABLE` may 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, and `enable_graph_ddl` lifts the restriction. Changing the type of `id`, `start`, `end` or `properties` is refused even then. * SQL `MERGE` against a label now needs `enable_graph_dml`, as `INSERT`, `UPDATE` and `DELETE` already did. * **Row-level security is now actually enforced on graph writes.** The `USING` clause of an `UPDATE` or `DELETE` policy was attached to a range-table entry that is never scanned, so it filtered nothing — a role with no `DELETE` policy at all deleted rows anyway. `INSERT` policies 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 `WITH` may not carry the same name twice, an `ORDER BY` key 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 a `CALL` subquery 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 ` 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 refuses `CREATE`, `SET` and the other write clauses in a subquery body. `CALL { } IN TRANSACTIONS` does 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`.