Skip to content

split() treats the delimiter as a regular expression while replace() treats it literally #2568

Description

@hkjiang26

AGE Version: extension 1.8.0 on PostgreSQL 18.6
Installation Method: Docker
API: Cypher and direct SQL

Steps to reproduce (paste into psql as-is)

LOAD 'age';
SET search_path = ag_catalog, public;

-- Cypher path (re-run: DROP GRAPH xdb CASCADE first)
SELECT * FROM create_graph('xdb');
SELECT * FROM cypher('xdb', $$ RETURN split('a.b.c', '.') $$) AS (r agtype);

-- SQL path (same C function, no graph needed)
SELECT age_split('a.b.c', '.');        -- -> ["", "", "", "", "", ""]
SELECT age_split('a1b1c', '1');        -- -> ["a", "b", "c"]   (plain delimiter: works)
SELECT age_replace('a.b.c', '.', '-'); -- -> "a-b-c"
SELECT age_split('a.b.c', '\.');      -- -> ["a", "b", "c"]   (only because it is regex-escaped)
SELECT age_split('a|b', '|');          -- -> ["a", "|", "b"]
SELECT age_split('a+b+c', '+');        -- -> ERROR: invalid regular expression: quantifier operand invalid

Expected behavior

openCypher defines the split delimiter as a literal string: split('a.b.c', '.')['a','b','c']. Neo4j, Memgraph and FalkorDB all return this without any escaping, and replace('a.b.c', '.', '-')'a-b-c' in AGE too — so both functions must interpret '.' identically.

Actual behavior

  • split('a.b.c', '.')["","","","","",""] (six empty strings): the delimiter is compiled as a regular expression, so '.' matches every character.
  • split('a1b1c', '1')["a","b","c"]: plain delimiters work, so the regex interpretation only surfaces for metacharacters — exactly where queries silently break.
  • split('a.b.c', '\.')["a","b","c"]: correct only because the user manually regex-escapes the dot.
  • split('a|b', '|')["a","|","b"]: the empty-alternation regex splits at every position.
  • split('a+b+c', '+')ERROR: invalid regular expression: quantifier operand invalid.
  • replace('a.b.c', '.', '-')"a-b-c": the same engine treats the same delimiter literally.

The engine is internally inconsistent, deviates from openCypher and all sibling engines, and silently corrupts results for common literal delimiters ('.', '|', '+', ...).

Root cause

age_split() (src/backend/utils/adt/agtype.c) passes the delimiter verbatim to PostgreSQL's regexp_split_to_array(), which compiles it as a regular expression; age_replace() performs a literal replacement. Cypher split()/replace() are rewritten to these same SQL functions, so both APIs are affected.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions