Skip to main content

String Functions

Every function on this page returns null when any argument is null, unless the function explicitly documents a different fallback. String slicing and length helpers count Unicode code points, not UTF-8 bytes. Regex helpers and string.find report Rust regex/string byte offsets.

Overview​

GoalFunction
Case conversionstring.lower, string.upper
Case style conversionstring.capitalize, string.case, string.swap_case
Trim whitespacestring.trim, string.trim_left, string.trim_right
Substring replacestring.replace
Count / find textstring.find, string.count, string.before, string.after
Substring slicestring.slice
Left / right slicestring.prefix, string.suffix
Split / joinstring.split, string.join, string.words
Blank checkstring.is_blank
Reversestring.reverse
Lengthstring.length, value.size
Padstring.pad_left, string.pad_right
Slug / escape / URLstring.slugify, string.escape, string.url_encode, string.url_decode
Normalise Unicodestring.normalize
Convert typetoString, toInteger, toFloat, toBoolean
Prefix boundstring.prefix_end
Predicate in WHERESTARTS WITH, ENDS WITH, CONTAINS, =~

string.lower / string.upper​

Unicode case conversion.

RETURN string.lower('Ada LoveLace');    // 'ada lovelace'
RETURN string.upper('ada')             // 'ADA'

Non-ASCII letters follow Rust's Unicode case mapping:

RETURN string.lower('Ångström')        // 'ångström'

Case Style​

Use string.capitalize(s) for the first character, or string.capitalize(s, true) to capitalize each whitespace-delimited word. Use string.case(s, style) when generating identifiers or display text.

RETURN string.capitalize('ada lovelace');          // 'Ada lovelace'
RETURN string.capitalize('ada lovelace', true);    // 'Ada Lovelace'
RETURN string.case('hello world', 'camel');        // 'helloWorld'
RETURN string.case('helloWorld', 'snake');         // 'hello_world'
RETURN string.swap_case('LoraDB')                 // 'lORAdb'

Supported string.case styles are 'camel', 'pascal', 'snake', 'kebab', 'screaming_snake' / 'constant', and 'title'.

Case-insensitive matching​

MATCH (u:User)
WHERE string.lower(u.email) = string.lower($search)
RETURN u
MATCH (u:User)
WHERE string.lower(u.name) STARTS WITH string.lower($prefix)
RETURN u

string.trim / string.trim_left / string.trim_right​

Strip whitespace from both ends / left / right.

RETURN string.trim('   hi   ');    // 'hi'
RETURN string.trim_left('   hi   ');   // 'hi   '
RETURN string.trim_right('   hi   ')   // '   hi'

Common for cleaning up user input before storage:

UNWIND $rows AS row
CREATE (:Contact {email: string.lower(string.trim(row.email))})

string.replace​

string.replace(str, find, replacement) — replaces every occurrence.

RETURN string.replace('banana', 'a', 'o');    // 'bonono'
RETURN string.replace('hello', 'x', 'y');     // 'hello'
RETURN string.replace('abc def', ' ', '_')   // 'abc_def'

Multi-step replace​

WITH 'Joe O\'Brien' AS raw
RETURN string.replace(string.replace(raw, ' ', '_'), '\'', '') AS slug
// 'Joe_OBrien'

string.find / string.count / string.before / string.after​

Use these helpers when you need positions or delimiter-based extraction inside one row.

FunctionBehaviour
string.find(s, needle)First byte offset, or -1 when missing
string.find(s, needle, true)All byte offsets as a list
string.count(s, needle)Non-overlapping occurrence count
string.before(s, needle)Text before the first occurrence, or null
string.after(s, needle)Text after the first occurrence, or null
RETURN string.find('banana', 'na');       // 2
RETURN string.count('banana', 'na');      // 2
RETURN string.before('a=b=c', '=');       // 'a'
RETURN string.after('a=b=c', '=');        // 'b=c'
RETURN string.after('abc', '=')          // null

Regex patterns can be used with string.count by wrapping the pattern in slashes:

RETURN string.count('a1 b22 c333', '/\d+/')  // 3

An empty needle returns null; pass a concrete delimiter or regex so the count has a clear meaning.

string.slice​

string.slice(str, start[, length]) — 0-based indices.

RETURN string.slice('loradb', 0, 4);   // 'lora'
RETURN string.slice('loradb', 4);      // 'db'
RETURN string.slice('hello', 1, 3)    // 'ell'

Out-of-range indices return an empty string rather than an error.

RETURN string.slice('hi', 99);         // ''
RETURN string.slice('hi', 0, 99)      // 'hi'

string.prefix / string.suffix​

RETURN string.prefix('graphdb', 5);     // 'graph'
RETURN string.suffix('graphdb', 2)    // 'db'

Length exceeding the input returns the whole string:

RETURN string.prefix('ab', 99)         // 'ab'

string.split / string.join / string.words​

RETURN string.split('a,b,c,d', ',');         // ['a', 'b', 'c', 'd']
RETURN string.split('one two three', ' ');   // ['one', 'two', 'three']
RETURN string.split('x', ',');               // ['x']
RETURN string.split('p12', '');              // ['p', '1', '2']
RETURN string.join(['red', 'green'], ', ');  // 'red, green'
RETURN string.words('  red green\tblue ')   // ['red', 'green', 'blue']

Empty input returns ['']. An empty delimiter splits into Unicode code points, not grapheme clusters (and split('', '') is []): a precomposed é stays one element, an e followed by a combining accent becomes two.

A list of delimiters splits on any of them, as in Neo4j 5. Each is literal text (never a regex), and at each position the first one in the list that matches wins.

RETURN split('a,b;c', [',', ';']);      // ['a', 'b', 'c']
RETURN split('a--b-c', ['--', '-'])     // ['a', 'b', 'c']

string.words uses Unicode whitespace and drops empty fields, which is usually what you want for tokenizing human-entered text.

Split + UNWIND​

Turn comma-separated values into rows:

UNWIND string.split('red,green,blue', ',') AS color
CREATE (:Swatch {color: color})

Join values for display​

string.join(list, separator) accepts strings, numbers, booleans, and null values. null becomes an empty field.

MATCH (u:User)
RETURN u.name, string.join(u.tags, ', ') AS tags_csv

string.reverse / value.reverse​

Works on both strings and lists.

RETURN string.reverse('hello');       // 'olleh'
RETURN value.reverse([1, 2, 3])      // [3, 2, 1]

reverse(x) is a compatibility alias for value.reverse(x), which works for both strings and lists. Use string.reverse(s) when you want to be explicit that the input should be text.

string.length / value.size​

FunctionMeasures
string.length(s)Unicode code-point count
value.size(s) / size(s)Polymorphic size helper; strings return their length
path.length(p) / length(p)Hop count for paths, not a string helper
RETURN string.length('abc');   // 3
RETURN string.length('café');  // 4
RETURN value.size('abc')      // 3

For paths, use path.length(p).

string.is_blank​

string.is_blank(s) returns true when a string is empty or contains only whitespace. It is useful for imports where a missing field arrives as spaces instead of null.

RETURN string.is_blank('   ');       // true
RETURN string.is_blank('\t\n');      // true
RETURN string.is_blank(' data ')    // false

string.pad_left / string.pad_right​

string.pad_left(str, length, padding) / string.pad_right(str, length, padding) — pads to the target length using the padding character repeated.

RETURN string.pad_left('7',   3, '0');   // '007'
RETURN string.pad_right('7',   3, '0');   // '700'
RETURN string.pad_left('abc', 5, '.');   // '..abc'
RETURN string.pad_right('abc', 5, '.')   // 'abc..'

If the input is already longer than length, it's returned unchanged.

Fixed-width formatting​

MATCH (r:Record)
RETURN string.pad_left(toString(r.id), 6, '0') AS padded_id

Encoding and Escaping​

Use string.slugify for simple URL slugs, string.escape when embedding text into another format, and URL helpers for query-string safe values.

RETURN string.slugify('Hello, World! 2026');      // 'hello-world-2026'
RETURN string.escape('Tom & "Ada"', 'html');      // 'Tom & "Ada"'
RETURN string.escape('Tom & "Ada"', 'json');      // '"Tom & \"Ada\""'
RETURN string.url_encode('a b/c');                // 'a%20b%2Fc'
RETURN string.url_decode('a%20b%2Fc')            // 'a b/c'

string.escape supports 'json', 'html', and 'cypher' / 'lora'. string.slugify is intentionally conservative: it lowercases ASCII letters and collapses non-alphanumeric runs to -.

string.normalize​

string.normalize(s[, form]) applies Unicode normalization. The default form is 'nfc'. Supported forms are 'nfc', 'nfd', 'nfkc', and 'nfkd'.

RETURN string.normalize('Café');                   // 'Café'   (NFC)
RETURN string.length(string.normalize('é', 'nfd')) // 2

Normalize text before storing it when users may submit visually identical strings in different Unicode forms. Pair with string.lower and string.trim for stable email or slug comparisons.

Type conversion​

These helpers are kept for Cypher compatibility and quick scalar parsing. For explicit typed construction, prefer casts: '2024-01-15'::DATE, {x: 1, y: 2}::POINT, [1, 2, 3]::VECTOR<INTEGER>(3), or CAST(value AS TYPE). Use TRY_CAST(value AS TYPE) when invalid input should become null.

FunctionAcceptsReturns
toString(x)anyString; null → null
toInteger(x) / toIntegerOrNull(x)Int, Float (truncates), String, BoolInt; OrNull form returns null on parse failure
toFloat(x) / toFloatOrNull(x)Int, Float, StringFloat; OrNull form returns null on parse failure
toBoolean(x) / toBooleanOrNull(x)Bool, String ("true"/"false"), Int (0 / non-0)Bool; OrNull form returns null on parse failure
RETURN toString(42);              // '42'
RETURN toString(true);            // 'true'
RETURN toString('2024-01-15'::DATE);  // '2024-01-15'

RETURN toInteger('007');          // 7
RETURN toInteger(3.9);            // 3       (truncates)
RETURN toInteger(true);           // 1
RETURN toIntegerOrNull('not a number'); // null    (parse fails)

RETURN toFloat('3.14');           // 3.14
RETURN toFloat(42);               // 42.0

RETURN toBoolean('TRUE');         // true
RETURN toBoolean(0);              // false
RETURN toBooleanOrNull('maybe')  // null

Safe conversion pattern​

Combine with coalesce for a default on parse failure:

MATCH (p:Product) RETURN coalesce(toInteger(p.stock), 0) AS stock

For imports where the target type is not one of the simple scalar helper names, keep the conversion explicit:

UNWIND $rows AS row
WITH row, TRY_CAST(row.shipped_on AS DATE) AS shipped_on
WHERE shipped_on IS NOT NULL
CREATE (:Shipment {id: row.id, shipped_on: shipped_on})

string.prefix_end​

The smallest string greater than every string that starts with the argument: its last character that can be incremented is, and what follows is dropped. null when there is none (the empty string, or one of only U+10FFFF characters). x STARTS WITH s is the range s <= x < string.prefix_end(s), which is how a RANGE index answers STARTS WITH.

RETURN string.prefix_end('ab')      // 'ac'

String operators (in WHERE)​

Covered in the WHERE page — included here for completeness:

OperatorCase-sensitiveDescription
STARTS WITHyesPrefix match
ENDS WITHyesSuffix match
CONTAINSyesSubstring match
=~yesRegex match (Rust regex, RE2-style — no backreferences)
MATCH (u:User) WHERE u.email ENDS WITH '@loradb.com' RETURN u;
MATCH (u:User) WHERE string.lower(u.email) =~ '.*@loradb\\.com$' RETURN u;
MATCH (u:User) WHERE u.name CONTAINS 'Admin' RETURN u

Regex vs CONTAINS​

Regex is more expressive but slower and strict-anchored (=~ 'foo' matches only the full string foo). Prefer CONTAINS for simple substring matches.

Common patterns​

Slugify​

WITH 'Hello, World! 2024' AS raw
RETURN string.slugify(raw) AS slug
// 'hello-world-2024'

For international slugs, normalize first and decide host-side whether to transliterate non-ASCII characters.

Initials​

MATCH (p:Person) WHERE p.name IS NOT NULL
RETURN p.name,
     reduce(acc = '', part IN string.split(p.name, ' ') |
            acc + string.prefix(part, 1)) AS initials

Domain from email​

MATCH (u:User) WHERE u.email CONTAINS '@'
RETURN u.email,
     string.slice(u.email, value.size(string.split(u.email, '@')[0]) + 1) AS domain

Normalise for comparison​

MATCH (u:User)
WHERE string.lower(string.trim(u.email)) = string.lower(string.trim($candidate))
RETURN u

Join a list into a string​

MATCH (u:User)
RETURN u.name, string.join(u.tags, ', ') AS tags_csv

For conditional joins or per-element formatting, use reduce so each element can be transformed before it is appended.

Parse key=value pairs​

WITH 'a=1;b=2;c=3' AS s
RETURN [pair IN string.split(s, ';') |
{key: string.split(pair, '=')[0], value: string.split(pair, '=')[1]}
] AS parsed

Values are strings — wrap each parsed value with toInteger if you need numeric types.

Truncate for preview​

MATCH (p:Post)
RETURN p.id,
     CASE WHEN string.length(p.body) > 100
          THEN string.prefix(p.body, 97) + '...'
          ELSE p.body
     END AS preview

The conditional here is a CASE expression — LoraDB's ternary. See that page for the full reference.

Limitations​

  • string.lower / string.upper use Unicode case mapping, not locale-specific case folding. Turkish dotted/dotless i and similar locale-sensitive cases should still be handled host-side when exact locale behavior matters.
  • string.find returns byte offsets. Use string.slice for code-point slicing once you already know the desired character positions.
  • string.normalize handles Unicode normalization forms, but it is not a locale-aware collation or fuzzy matching function.
  • Use string.length when you need Unicode code-point counts.

See also​