apache / apache/age

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

Open
#2,568 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
C
Stars
4.8k
Forks
523
Avg merge
1d 2h
Merged PRs (30d)
9

Description

**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)

```sql
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.

Contributor guide

Open the contributing guide

Research direction

Read src/backend/utils/adt/agtype.c, starting at age_split(), and compare its delimiter handling with age_replace(). Run the supplied psql reproduction through both the Cypher and direct SQL paths. Done means literal delimiters such as '.', '|', and '+' split correctly without regex escaping, while replacement behavior remains consistent.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, postgresql
Domain
databases
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
82/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.