Applies to VAST Cluster 5.5.
Functions › Text basics › Characters and code points. Returns the one-character string whose Unicode code point is the given integer. It's the reverse of ord. chr has no aliases.
Syntax
chr(code_point)
| Argument | Type | Description |
|---|---|---|
code_point |
INTEGER |
A Unicode code point, from 0 to 1114111 (U+10FFFF). The surrogates, 55296 to 57343 (U+D800 to U+DFFF), aren't valid code points. A BIGINT isn't accepted: cast it to INTEGER. |
Returns: VARCHAR, one character long.
Aliases: none.
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_chr. 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 code points with their names. The code column is an INTEGER, the type chr takes. 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_chr"'
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}.code_points"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, code INTEGER, name VARCHAR)")
run(f"INSERT INTO {TABLE} VALUES "
"(1, 65, 'LATIN CAPITAL LETTER A'), "
"(2, 233, 'LATIN SMALL LETTER E WITH ACUTE'), "
"(3, 8364, 'EURO SIGN'), "
"(4, 128512, 'GRINNING FACE'), "
"(5, NULL, NULL)")
[]
[]
[(5,)]
What it does
chr returns the character that Unicode gives the number code_point. For 0 to 127 that's the ASCII character with that code; above 127 it's any Unicode character, up to 1114111 (U+10FFFF), the highest code point.
| Call | Result | Why |
|---|---|---|
chr(65) |
'A' |
ASCII code 65. |
chr(233) |
'é' |
U+00E9, the precomposed é. |
chr(8364) |
'€' |
U+20AC. |
chr(128512) |
'😀' |
U+1F600, outside the Basic Multilingual Plane. |
chr(0) |
'\x00' |
The NUL character: a string of length 1, not an empty string. |
chr(1114111) |
'\U0010ffff' |
The highest code point. |
chr('65') |
'A' |
A string literal of digits is converted to INTEGER. Other text, such as chr('abc'), fails with Could not convert string 'abc' to INT32. |
chr returns one code point, which isn't always a whole character as a reader sees it: a flag, for example, is two code points. Build such a string with chr(127467) || chr(127479), or write it as a literal.
NULLs and edge cases
| Input | Call | Result |
|---|---|---|
NULL |
chr(NULL) |
NULL |
| An ASCII code | chr(65) |
'A' |
| A code point above 127 | chr(233) |
'é' |
| A code point above 127 | chr(8364) |
'€' |
| A code point outside the Basic Multilingual Plane | chr(128512) |
'😀' |
0 |
chr(0) |
'\x00', the NUL character |
| The length of NUL | length(chr(0)) |
1 |
| The highest code point | chr(1114111) |
'\U0010ffff' |
| A string of digits | chr('65') |
'A' |
| A negative number | chr(-1) |
Error: Invalid UTF8 Codepoint -1 |
| A surrogate (U+D800) | chr(55296) |
Error: Invalid UTF8 Codepoint 55296 |
| Above 1114111 | chr(1114112) |
Error: Invalid UTF8 Codepoint 1114112 |
| A decimal | chr(65.0) |
Error: No function matches the given name and argument types |
The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:
run("SELECT chr(NULL), chr(65), chr(233), chr(8364), chr(128512)")
run("SELECT chr(0), length(chr(0)), chr(1114111), chr('65')")
run("SELECT chr(-1)")
run("SELECT chr(55296)")
run("SELECT chr(1114112)")
run("SELECT chr(65.0)")
[(None, 'A', 'é', '€', '😀')]
[('\x00', 1, '\U0010ffff', 'A')]
OSError: Invalid UTF8 Codepoint -1
OSError: Invalid UTF8 Codepoint 55296
OSError: Invalid UTF8 Codepoint 1114112
InternalError: Binder Error: No function matches the given name and argument types 'chr(DECIMAL(3,1))'. You might need to add explicit type casts.
Examples
The character for each code point:
run(f"SELECT id, code, chr(code), name FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 65, 'A', 'LATIN CAPITAL LETTER A'), (2, 233, 'é', 'LATIN SMALL LETTER E WITH ACUTE'), (3, 8364, '€', 'EURO SIGN'), (4, 128512, '😀', 'GRINNING FACE'), (5, None, None, None)]
Build a label from a character and its name. || returns NULL when either side is NULL:
run(f"SELECT id, chr(code) || ' ' || lower(name) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 'A latin capital letter a'), (2, 'é latin small letter e with acute'), (3, '€ euro sign'), (4, '😀 grinning face'), (5, None)]
Keep the rows whose character is €:
run(f"SELECT id, name FROM {TABLE} WHERE chr(code) = '€' ORDER BY id LIMIT 10")
[(3, 'EURO SIGN')]
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 can evaluate a comparison of a column with a constant, but not a function of a column, so chr() is always computed by the Query Engine:
- In the
SELECTlist, the scan reads the column and the Query Engine computeschr()for each row it returns. - In
WHERE, the scan sends the column for every row, and aFILTERstep in the Query Engine computeschr()and keeps the rows that match. No rows are skipped in storage.
Because chr() runs on every row the scan sends, one row with an invalid code point fails the whole query, in SELECT and in WHERE alike. See Limitations and errors.
The plan of the WHERE example shows the FILTER step above the scan:
plan(f"SELECT id FROM {TABLE} WHERE chr(code) = '€'")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ (chr(code) = '€') │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ code │
│ id │
│ │
└───────────────────────────┘
Make it faster
- Compare the code, not the character.
chr(code) = '€'keeps the same rows ascode = 8364, but only the second is a comparison of a raw column with a constant, which is pushed down to VAST DataBase, so rows that don't match never leave storage. Its plan has noFILTERstep:
plan(f"SELECT id FROM {TABLE} WHERE code = 8364")
┌───────────────────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: id │
│ │
└───────────────────────────┘
- Combine it with a pushed-down condition. In
id > 100 AND chr(code) = '€',id > 100is still evaluated in VAST DataBase, sochr()runs only on the rows that pass it.
Limitations and errors
- Valid code points only. A negative number, a surrogate (55296 to 57343, U+D800 to U+DFFF) or a number above 1114111 fails with
Invalid UTF8 Codepointand the number. On a column, one such row fails the whole query. - Guard a column that can hold invalid values. Return
NULLfor them withCASE, so that the query doesn't fail:
run(f"INSERT INTO {TABLE} VALUES (6, -1, 'NOT A CODE POINT')")
run(f"SELECT id, chr(code) FROM {TABLE} ORDER BY id LIMIT 10")
run(f"SELECT id, CASE WHEN code BETWEEN 0 AND 1114111 AND code NOT BETWEEN 55296 AND 57343 "
f"THEN chr(code) END FROM {TABLE} ORDER BY id LIMIT 10")
[(1,)]
OSError: Invalid UTF8 Codepoint -1
[(1, 'A'), (2, 'é'), (3, '€'), (4, '😀'), (5, None), (6, None)]
INTEGERonly. A decimal, such as65.0, fails withNo function matches the given name and argument types, and so does aBIGINT, such as aBIGINTcolumn orchr(id + 64)on one. Cast it toINTEGERfirst:chr(CAST(65.0 AS INTEGER)). A string that isn't a number fails withCould not convert string 'abc' to INT32.- One code point per call. To build a string of several code points, concatenate the results with
||or concat. - ord returns -1 for an empty string, and
chr(-1)fails. Check for -1 before you pass the result ofordtochr.
run("SELECT chr(CAST(65.0 AS INTEGER)), chr(127467) || chr(127479)")
[('A', '🇫🇷')]
Similar functions
| Function | Direction | Call | Result |
|---|---|---|---|
chr |
code point to character | chr(233) |
'é' |
| ord | first character to code point; -1 for '' |
ord('é') |
233 |
unicode |
the same as ord |
unicode('é') |
233 |
| ascii | first character to code point; 0 for '' |
ascii('é') |
233 |
run("SELECT chr(233), ord('é'), unicode('é'), ascii('é')")
run("SELECT chr(ord('é')) = 'é'")
[('é', 233, 233, 233)]
[(True,)]
Clean up:
run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]
See also
- Text basics: all the text functions.
- ord and ascii.
- CASE: choosing a value.
- Where filters run and pushdowns.
- EXPLAIN: how to read a plan.