Welcome to the new DOCS.vastdata.com. We've added new menus and a version selector, plus new persona and resource pages to help customers get started. Looking for a previous article? Use the docs search.

Regular-expression syntax and options

Prev Next

Applies to VAST Cluster 5.5.

Functions › Regular expressions › Regular expressions. The regular-expression functions (regexp_matches, regexp_full_match and ~, regexp_extract, regexp_extract_all, regexp_replace and the split functions) share one pattern syntax and one options string. This page describes both, and the pattern features that aren't available.

Syntax

A pattern and an options string are arguments of these functions:

regexp_matches(s, pattern)
regexp_matches(s, pattern, options)
regexp_full_match(s, pattern, options)
regexp_extract(s, pattern, group, options)
regexp_extract_all(s, pattern, group, options)
regexp_replace(s, pattern, replacement, options)
Argument Type Description
pattern VARCHAR The regular expression. A constant, or an expression such as a column.
options VARCHAR Optional. A constant string of one-letter flags, such as 'i' or 'gi'.

Set up the examples

The examples connect with the ADBC driver, as in VAST Query Engine (ADBC), and work in a schema called docs_sql_fn_string_regexp_syntax. Set QE_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and VASTDB_BUCKET (SQL reference), and run the blocks in order in one Python session. Below each block is the output you should see.

This block connects, defines the run() and plan() helpers, and creates a table of five log lines. Line 4 has a line break in it, and the last row has no values:

import os
import re

import adbc_driver_manager
import adbc_driver_manager.dbapi as dbapi
import adbc_driver_vastdb

BUCKET = os.environ["VASTDB_BUCKET"]
SCHEMA = f'"{BUCKET}/docs_sql_fn_string_regexp_syntax"'

conn = dbapi.connect(
    driver=adbc_driver_vastdb.get_driver_path(),
    db_kwargs={
        "vast.db.endpoint": os.environ["QE_ENDPOINT"],
        "vast.db.access_key": os.environ["AWS_ACCESS_KEY_ID"],
        "vast.db.secret_key": os.environ["AWS_SECRET_ACCESS_KEY"],
    },
    autocommit=True,
)

def server_message(e):
    """The server's message in a driver error, without the request ID or the echoed statement."""
    text = re.sub(r"\[req=0x[0-9a-f]+\]: ", "", str(e))
    found = (re.search(r'message: "(.*?)", request_id', text, re.S)
             or re.search(r"description: (.*)\)", text, re.S))
    message = found.group(1) if found else text.strip().splitlines()[-1]
    message = re.sub(r"^Plan query: '.*?' failed with: ", "", message, flags=re.S)
    message = re.split(r"\\n|\n", message)[0].replace('\\"', '"')
    return message.replace(BUCKET, "<bucket>")

def run(sql):
    """Run one statement. Print the rows it returns, or the error."""
    try:
        with conn.cursor() as cur:
            cur.execute(sql)
            rows = cur.fetchall() if cur.description else []
    except (adbc_driver_manager.Error, OSError) as e:
        print(f"{type(e).__name__}: {server_message(e)}")
    else:
        print(rows)

def plan(sql):
    """Print the plan without its ~N Rows lines: the table's row count, which can still be 0 just after an insert."""
    with conn.cursor() as cur:
        cur.execute("EXPLAIN " + sql)
        steps = cur.fetchall()[0][1]
    print("\n".join(line for line in steps.splitlines() if "Rows" not in line))

TABLE = f"{SCHEMA}.logs"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, line VARCHAR)")
run(f"INSERT INTO {TABLE} VALUES "
    "(1, 'ERROR 2026-10-16 disk full'), "
    "(2, 'warn: Disk 93% full'), "
    "(3, 'caf\u00e9 cr\u00e8me'), "
    "(4, 'Line1\nLine2'), "
    "(5, NULL)")
[]
[]
[(5,)]

What it does

Writing a pattern. In a SQL string literal a backslash is an ordinary character, so '\d+' in SQL is the pattern \d+. In a Python string, write it '\\d+', as on this page.

Pattern element Meaning Notes
. any one character (code point) Not a line break, unless option s or (?s). e with a combining accent (U+0301) is two characters, and so is a flag emoji.
^, $ start and end of the string Not at line breaks unless (?m). $ doesn't match before a final line break: 'a' + line break doesn't match 'a$'.
\A, \z, \b start of the string, end of the string, a word boundary
[abc], [^abc], [a-z] a character set
\d, \w, \s an ASCII digit, an ASCII word character, white space \d doesn't match other digits such as '٣', \w doesn't match 'é', and \s matches a tab but not a no-break space (U+00A0).
[[:digit:]], [[:alpha:]] and other POSIX classes ASCII classes [[:alpha:]] doesn't match 'é'. An unknown class such as [[:foo:]] fails.
\pL, \pN, \p{Greek}, \p{Han}, \p{Latin} Unicode letters, numbers and scripts Use these for text outside ASCII.
\x41, \x{1F600} a character by its code
\Q...\E the text between is literal '\Qa.b\E' matches a.b only.
*, +, ?, {n}, {n,m} repetition {n} up to 1000. Lazy forms such as *? are accepted.
(...), (?:...), (?P<name>...) a group, a group that doesn't capture, a named group
a\|b either
(?i), (?-i), (?s), (?m) inline flags: ignore case, stop ignoring case, . matches a line break, ^ and $ at line breaks An inline flag overrides the options string.

The options string is a constant of one-letter flags:

Option Effect
i Ignore case, including outside ASCII ('ÉCOLE' matches 'école'). One letter never matches two: 'STRASSE' doesn't match 'straße'.
c Case-sensitive (the default). With both i and c, the last one wins: 'ic' is case-sensitive, 'ci' isn't.
s . also matches a line break.
l The pattern is literal text, not a regular expression. Combine it with i: 'li'.
g Replace every match (regexp_replace only; other functions fail with Option 'g' (global replace) is only valid for regexp_replace).
m, n, p Accepted. They don't make ^ and $ match at line breaks, or . match one; use (?m) or s.
'' The default.

Options are lower-case: 'I', 'G', 'x' and any other letter fail with Unrecognized Regex option.

NULLs and edge cases

Input Result
A NULL string or pattern NULL
A NULL options string Error: Regex options field must not be NULL (but regexp_replace returns NULL)
Options from a column Error: Regex options field must be a constant
An empty pattern '' Matches every string, the empty one too
regexp_matches('ab', '(?i)A(?-i)b') true
regexp_matches('ABC', '(?i)b', 'c') (the inline flag wins) true
regexp_matches('ABC', 'b', 'ic'), regexp_matches('ABC', 'b', 'ci') false, true
regexp_matches('a.c', 'a.c', 'l'), regexp_matches('abc', 'a.c', 'l') true, false
regexp_matches('e\u0301', '^.$'), regexp_matches('e\u0301', '^..$') false, true

The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:

run("SELECT regexp_matches(NULL, 'a'), regexp_matches('abc', NULL), regexp_matches('abc', ''), regexp_matches('', '')")
run("SELECT regexp_matches('abc', 'b', NULL)")
run(f"SELECT regexp_matches(line, 'a', CAST(id AS VARCHAR)) FROM {TABLE} LIMIT 1")
run("SELECT regexp_matches('ab', '(?i)A(?-i)b'), regexp_matches('ABC', '(?i)b', 'c'), "
    "regexp_matches('ABC', 'b', 'ic'), regexp_matches('ABC', 'b', 'ci')")
run("SELECT regexp_matches('a.c', 'a.c', 'l'), regexp_matches('abc', 'a.c', 'l'), "
    "regexp_matches('e\u0301', '^.$'), regexp_matches('e\u0301', '^..$')")
[(None, None, True, True)]
InternalError: Invalid Input Error: Regex options field must not be NULL
InternalError: Invalid Input Error: Regex options field must be a constant
[(True, True, False, True)]
[(True, False, False, True)]

Examples

Character classes: \d for digits, \w and \pL for letters. \w+$ doesn't match the last word of café crème, because \w is ASCII only; \pL+$ does:

run(f"SELECT id, regexp_matches(line, '\\d{{4}}-\\d{{2}}-\\d{{2}}'), regexp_matches(line, '\\s\\w+$'), "
    f"regexp_matches(line, '\\s\\pL+$') FROM {TABLE} ORDER BY id LIMIT 10")
[(1, True, True, True), (2, False, True, True), (3, False, False, True), (4, False, True, False), (5, None, None, None)]

Options and inline flags: 'disk' with 'i' also matches Disk. In line 4, ^Line2 matches only with (?m), and 1.L (a dot for the line break) only with 's':

run(f"SELECT id, regexp_matches(line, 'disk', 'i'), regexp_matches(line, '^Line2'), "
    f"regexp_matches(line, '(?m)^Line2'), regexp_matches(line, '1.L'), regexp_matches(line, '1.L', 's') "
    f"FROM {TABLE} ORDER BY id LIMIT 10")
[(1, True, False, False, False, False), (2, True, False, False, False, False), (3, False, False, False, False, False), (4, False, False, True, False, True), (5, None, None, None, None, None)]

How it runs in VAST

An invalid constant pattern fails the query with Invalid Input Error: and the reason, such as missing ): (. A pattern from a column is checked for each row: one invalid value fails the query, with the reason but without the Invalid Input Error: prefix. regexp_extract and regexp_replace behave differently with an invalid constant pattern: see their pages.

Patterns are always evaluated in the Query Engine. Before it plans a query, the Query Engine rewrites some regexp_matches patterns into cheaper tests:

Pattern in WHERE regexp_matches(s, ...) Becomes Runs in
'^literal$' (the whole string, no special characters) s = 'literal' VAST DataBase (pushed down; no FILTER step)
'^literal' prefix(s, 'literal') a FILTER step in the Query Engine
'literal$' suffix(s, 'literal') a FILTER step
'literal' contains(s, 'literal') a FILTER step
anything else, such as '^(a\|b)' or '\d+' regexp_matches(...) a FILTER step

The plans of a whole-string pattern, a prefix and a general pattern:

plan(f"SELECT id FROM {TABLE} WHERE regexp_matches(line, '^Hello World$')")
plan(f"SELECT id FROM {TABLE} WHERE regexp_matches(line, '^ERROR')")
plan(f"SELECT id FROM {TABLE} WHERE regexp_matches(line, '\\d+')")
┌───────────────────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│      Projections: id      │
│                           │
└───────────────────────────┘
┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│             #1            │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           FILTER          │
│    ────────────────────   │
│   prefix(line, 'ERROR')   │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│                           │
│        Projections:       │
│            line           │
│             id            │
│                           │
└───────────────────────────┘
┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│             #1            │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           FILTER          │
│    ────────────────────   │
│regexp_matches(line, '\d+')│
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│                           │
│        Projections:       │
│            line           │
│             id            │
│                           │
└───────────────────────────┘

Make it faster

  • Prefer a literal or anchored pattern. '^ERROR' runs as prefix() and 'disk' as contains(); a whole-string pattern is pushed down as an equality.
  • Use a range for a prefix. line >= 'ERROR' AND line < 'ERROS' is pushed down; see LIKE.
  • Use a constant pattern. A mistake in it fails the query whatever the data, instead of only when a bad value turns up.
  • Extract at load time. If you filter on a part of a string often, store it in its own column with regexp_extract when you write the data.

Limitations and errors

These pattern features aren't available. A constant pattern that uses one fails when the query is planned:

Feature Error Instead
Backreferences: (ab)\1 invalid escape sequence: \\1 Extract the group and compare it in a second step
Lookahead and lookbehind: (?=, (?!, (?<=, (?<! invalid perl operator: (?= (and so on) Combine two tests with AND or AND NOT, or check in client code
Named groups as (?<name>...) invalid perl operator: (?< (?P<name>...)
Possessive repetition a++ and atomic groups (?>...) bad repetition operator: ++, invalid perl operator: (?> a+
Free-spacing mode (?x) and option x invalid perl operator: (?x, Unrecognized Regex option x Write the pattern without spaces
Repetition over 1000: a{1001} invalid repetition size: {1001} Split the pattern, or test the length with length

Other pattern errors name the problem: missing ): (, unexpected ):, missing ]: [, no argument for repetition operator: *, trailing \, invalid escape sequence: \\k.

Nested repetition is safe to use: (a*)*c on a string of 50,001 characters returns false without error.

run("SELECT regexp_matches('abab', '(ab)\\1')")
run("SELECT regexp_matches('ab', 'a(?=b)')")
run("SELECT regexp_matches('ab', '(?<x>a)b'), regexp_matches('ab', '(?P<x>a)b')")
run("SELECT regexp_matches('abc', 'b', 'x')")
run("SELECT regexp_matches('abc', '(')")
run("SELECT regexp_matches(repeat('a', 50000) || 'b', '(a*)*c')")
InternalError: Invalid Input Error: invalid escape sequence: \\1
InternalError: Invalid Input Error: invalid perl operator: (?=
InternalError: Invalid Input Error: invalid perl operator: (?<
InternalError: Invalid Input Error: Unrecognized Regex option x
InternalError: Invalid Input Error: missing ): (
[(False,)]

Similar functions

The other pattern languages, and how each matches one character in 'abc':

Function Pattern language Matches Case
LIKE 'a_c' % and _ the whole string sensitive
GLOB 'a?c' *, ? and [...] the whole string sensitive
SIMILAR TO 'a.c' regular expression the whole string sensitive
regexp_full_match 'a.c' regular expression the whole string option i
regexp_matches '^a.c$' regular expression anywhere, unless anchored option i
regexp_escape turns text into a pattern that matches it literally
run("SELECT 'abc' LIKE 'a_c', 'abc' GLOB 'a?c', 'abc' SIMILAR TO 'a.c', regexp_full_match('abc', 'a.c'), "
    "regexp_matches('abc', '^a.c$')")
run("SELECT regexp_matches('a.b', 'a.b', 'l'), regexp_matches('axb', 'a.b', 'l'), "
    "regexp_matches('axb', regexp_escape('a.b'))")
[(True, True, True, True, True)]
[(True, False, False)]

Clean up:

run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]

See also