Applies to VAST Cluster 5.5.
Functions › Parsing and JSON functions › Check and inspect. Returns true if a JSON document contains another JSON value anywhere inside it: a scalar value, an object's key/value pairs, or an array's elements.
Syntax
json_contains(haystack, needle)
| Argument | Type | Description |
|---|---|---|
haystack |
VARCHAR or JSON |
The JSON document to search. |
needle |
VARCHAR or JSON |
The JSON value to look for, written as JSON text: '"ada"' for a string, '3' for a number, '{"city": "Oslo"}' for an object. |
Returns: BOOLEAN.
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_json_json_contains. 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 four events with a JSON payload stored as VARCHAR. Row 3 holds an array, and row 4 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_json_json_contains"'
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}.events"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, payload VARCHAR, user_name VARCHAR)")
run(f"INSERT INTO {TABLE} VALUES "
"(1, '{\"user\": \"ada\", \"n\": 3, \"tags\": [\"a\", \"b\"], \"geo\": {\"city\": \"Oslo\", \"lat\": 59.9}}', 'ada'), "
"(2, '{\"user\": \"alan\", \"n\": 20, \"tags\": [], \"geo\": null}', 'alan'), "
"(3, '[10, 20, [30, 40]]', NULL), "
"(4, NULL, NULL)")
[]
[]
[(4,)]
What it does
json_contains parses both arguments as JSON and looks for the needle at any depth of the haystack:
| Needle | Matches when the haystack has… | Example on {"user": "ada", "n": 3, "tags": ["a", "b", "c"], "geo": {"city": "Oslo", "lat": 59.9}, "z": null} |
Result |
|---|---|---|---|
A string, number, boolean or null |
an equal value, at any depth | '"Oslo"', '3', 'null' |
true |
| a value of another JSON type or spelling | '"3"', '3.0' |
false |
|
| part of a string only | '"ad"' |
false |
|
| An object | an object, at any depth, with all of the needle's key/value pairs, in any order | '{"city": "Oslo"}', '{"lat": 59.9, "city": "Oslo"}', '{"geo": {"city": "Oslo"}}' |
true |
| the same keys, with a value that differs in case | '{"city": "oslo"}' |
false |
|
| An array | an array with all of the needle's elements, in any order | '["b", "a"]' |
true |
{} or [] |
(the sample document has objects and arrays) | '{}', '[]' |
true |
String comparison is exact and case-sensitive. A number matches only a number written the same way: 3 doesn't match 3.0.
NULLs and edge cases
| Input | Result |
|---|---|
NULL haystack or needle |
NULL |
json_contains('[1, 2]', '2') |
true |
json_contains('[[1, 2]]', '[1]') |
true: the needle's elements are found in the inner array |
json_contains('1', '1') (a scalar document) |
true |
A needle that isn't JSON, such as 'ada' without quotes |
Error: Malformed JSON at byte 0 of input: unexpected character. Input: ada |
| A haystack that isn't JSON | Error: Malformed JSON at byte 0 of input: invalid literal. Input: not json |
| A number as the haystack | Error: No function matches the given name and argument types 'json_contains(INTEGER_LITERAL, STRING_LITERAL)' |
The cases of both tables, as the cluster returns them. Each line shows the needle and the result. They use the helpers from Set up the examples:
DOC = '{"user": "ada", "n": 3, "tags": ["a", "b", "c"], "geo": {"city": "Oslo", "lat": 59.9}, "z": null}'
for needle in ['"Oslo"', '3', 'null', '"3"', '3.0', '"ad"', '{"city": "Oslo"}', '{"lat": 59.9, "city": "Oslo"}',
'{"geo": {"city": "Oslo"}}', '{"city": "oslo"}', '["b", "a"]', '{}', '[]']:
run(f"SELECT '{needle}', json_contains('{DOC}', '{needle}')")
run("SELECT json_contains(NULL, '1'), json_contains('[1, 2]', NULL), json_contains('[1, 2]', '2'), "
"json_contains('[[1, 2]]', '[1]'), json_contains('1', '1')")
run(f"SELECT json_contains('{DOC}', 'ada')")
run("SELECT json_contains('not json', '1')")
run("SELECT json_contains(123, '1')")
[('"Oslo"', True)]
[('3', True)]
[('null', True)]
[('"3"', False)]
[('3.0', False)]
[('"ad"', False)]
[('{"city": "Oslo"}', True)]
[('{"lat": 59.9, "city": "Oslo"}', True)]
[('{"geo": {"city": "Oslo"}}', True)]
[('{"city": "oslo"}', False)]
[('["b", "a"]', True)]
[('{}', True)]
[('[]', True)]
[(None, None, True, True, True)]
OSError: Malformed JSON at byte 0 of input: unexpected character. Input: ada
OSError: Malformed JSON at byte 0 of input: invalid literal. Input: not json
InternalError: Binder Error: No function matches the given name and argument types 'json_contains(INTEGER_LITERAL, STRING_LITERAL)'. You might need to add explicit type casts.
Examples
Which events mention the user ada, a city of Oslo, or the number 20. Row 2 has "n": 20, and row 3 is an array with 20 as one of its elements:
run("SELECT id, json_contains(payload, '\"ada\"'), json_contains(payload, '{\"city\": \"Oslo\"}'), "
f"json_contains(payload, '20') FROM {TABLE} ORDER BY id LIMIT 10")
[(1, True, True, False), (2, False, False, True), (3, False, False, True), (4, None, None, None)]
Keep the events that have an object with "city": "Oslo":
run("SELECT id FROM " + TABLE + " WHERE json_contains(payload, '{\"city\": \"Oslo\"}') ORDER BY id LIMIT 10")
[(1,)]
How it runs in VAST
The Query Engine runs on the cluster's CNodes, and a query on a table starts with a scan in VAST DataBase (VAST_SCAN). VAST DataBase stores the payload as text and can't look inside it, so json_contains always runs in the Query Engine and parses the document of every row it gets:
- In the
SELECTlist, the scan reads the column and the Query Engine computesjson_containsfor each row it returns. - In
WHERE, the scan sends the column for every row, and aFILTERstep in the Query Engine parses each document and keeps the rows that match.
The plan of a WHERE condition shows the FILTER step above the scan:
plan(f"SELECT id FROM {TABLE} WHERE json_contains(payload, '\"ada\"')")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ json_contains(payload, ' │
│ "ada"') │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ payload │
│ id │
│ │
└───────────────────────────┘
Make it faster
- Keep the values you search for in their own columns. A comparison of a plain column with a constant is pushed down to VAST DataBase, so rows that don't match never leave storage. The plan for
user_name = 'ada'has noFILTERstep:
plan(f"SELECT id FROM {TABLE} WHERE user_name = 'ada'")
┌───────────────────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: id │
│ │
└───────────────────────────┘
- Compare the whole document when you can.
payload = '[]'is pushed down too;payload LIKE '%ada%'isn't, and runs in aFILTERstep likejson_contains. - Write each document once, valid. Every row is parsed on every query; see Make JSON queries faster.
Limitations and errors
- The needle is JSON, not a plain value. Write a string with its double quotes,
'"ada"'. Without them the needle isn't valid JSON and the query fails withMalformed JSON at byte 0 of input: unexpected character.to_json turns a SQL value into JSON text:to_json('ada')returns"ada". - One invalid document fails the query. If any row the function reads isn't valid JSON, the query stops with
Malformed JSON at byte .... Keep such rows out with a pushed-down condition: see invalid JSON in a column. - Text or JSON arguments. A number fails with
No function matches the given name and argument types; cast it, or write it as text. - It doesn't say where the value is. To test one path, use json_extract_string or json_exists.
Similar functions
| Function | Tests | Example |
|---|---|---|
| json_exists | a path exists, whatever its value | json_exists(payload, '$.geo') |
| json_keys with list_contains | a top-level key exists | list_contains(json_keys(payload), 'geo') |
| json_extract_string | the value at one path | json_extract_string(payload, '$.user') = 'ada' |
LIKE |
the text of the document, keys and partial words included | payload LIKE '%ada%' |
The four tests and json_contains on each row:
run("SELECT id, json_contains(payload, '\"ada\"'), json_exists(payload, '$.geo'), "
"list_contains(json_keys(payload), 'geo'), json_extract_string(payload, '$.user') = 'ada', "
f"payload LIKE '%ada%' FROM {TABLE} ORDER BY id LIMIT 10")
[(1, True, True, True, True, True), (2, False, True, True, False, False), (3, False, False, False, None, False), (4, None, None, None, None, None)]
Clean up:
run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]
See also
- Parsing and JSON functions: all the JSON functions, JSON paths and invalid JSON.
- Where filters run and pushdowns.
- EXPLAIN: how to read a plan.