Applies to VAST Cluster 5.5.
Functions › Text basics › Characters and code points. Returns the Unicode code point of the first character of a string, as an integer. Despite its name, it isn't limited to ASCII. ascii has no aliases; ord (also unicode) is the same except on an empty string.
Syntax
ascii(s)
| Argument | Type | Description |
|---|---|---|
s |
VARCHAR |
The string whose first character to read. |
Returns: INTEGER.
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_ascii. 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 names: one that starts with a capital letter, one in lower case, one that starts with an accented letter, an empty name and a NULL:
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_ascii"'
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}.people"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, name VARCHAR)")
run(f"INSERT INTO {TABLE} VALUES "
"(1, 'Ada Lovelace'), "
"(2, 'grace hopper'), "
"(3, 'Émile Borel'), "
"(4, ''), "
"(5, NULL)")
[]
[]
[(5,)]
What it does
ascii returns the code point of the first character of s: the number that Unicode gives that character. For ASCII characters it's the ASCII code; for other characters it's their Unicode code point, so the result can be larger than 127.
| Call | Result | Why |
|---|---|---|
ascii('A') |
65 |
The ASCII code of A. |
ascii('AB') |
65 |
Only the first character counts. |
ascii('é') |
233 |
U+00E9, the precomposed é. |
ascii('€') |
8364 |
U+20AC. |
ascii('e\u0301') |
101 |
An e followed by a combining accent: the first code point is the e. |
ascii('') |
0 |
An empty string has no first character. |
A code point isn't always what a reader sees as the first character. 'e\u0301' is displayed as é but starts with a plain e, so ascii returns 101, the code of e. To make such strings start with one precomposed character where one exists, apply nfc_normalize first.
NULLs and edge cases
| Input | Call | Result |
|---|---|---|
NULL |
ascii(NULL) |
NULL |
'' (empty string) |
ascii('') |
0 |
| An ASCII character | ascii('A') |
65 |
| A string of several characters | ascii('AB') |
65, the code point of the first one |
A precomposed é (U+00E9) |
ascii('é') |
233 |
| A character outside Latin-1 | ascii('€') |
8364 |
e followed by U+0301 |
ascii('e\u0301') |
101, the code point of e |
| The same, normalised first | ascii(nfc_normalize('e\u0301')) |
233 |
| A number | ascii(123) |
Error: No function matches the given name and argument types |
A BLOB |
ascii('\xAA'::BLOB) |
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 ascii(NULL), ascii(''), ascii('A'), ascii('AB'), ascii('é'), ascii('€'), ascii('e\u0301')")
run("SELECT ascii(nfc_normalize('e\u0301'))")
run("SELECT ascii(123)")
run("SELECT ascii('\\xAA'::BLOB)")
[(None, 0, 65, 65, 233, 8364, 101)]
[(233,)]
InternalError: Binder Error: No function matches the given name and argument types 'ascii(INTEGER_LITERAL)'. You might need to add explicit type casts.
InternalError: Binder Error: No function matches the given name and argument types 'ascii(BLOB)'. You might need to add explicit type casts.
Examples
The code point of the first character of each name. The empty name gives 0, and NULL gives NULL:
run(f"SELECT id, name, ascii(name) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 'Ada Lovelace', 65), (2, 'grace hopper', 103), (3, 'Émile Borel', 201), (4, '', 0), (5, None, None)]
Keep the names that start with a capital ASCII letter, A to Z (codes 65 to 90). grace hopper starts with a lower-case letter and Émile Borel with a letter outside ASCII, so neither matches:
run(f"SELECT id, name FROM {TABLE} WHERE ascii(name) BETWEEN 65 AND 90 ORDER BY id LIMIT 10")
[(1, 'Ada Lovelace')]
Keep the names that start with a character outside ASCII:
run(f"SELECT id, name FROM {TABLE} WHERE ascii(name) > 127 ORDER BY id LIMIT 10")
[(3, 'Émile Borel')]
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 ascii() is always computed by the Query Engine:
- In the
SELECTlist, the scan reads the column and the Query Engine computesascii()for each row it returns. - In
WHERE, the scan sends the column for every row, and aFILTERstep in the Query Engine computesascii()and keeps the rows that match. No rows are skipped in storage.
The plan of the first WHERE example shows the FILTER step above the scan:
plan(f"SELECT id FROM {TABLE} WHERE ascii(name) BETWEEN 65 AND 90")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ (ascii(name) BETWEEN 65 │
│ AND 90) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ name │
│ id │
│ │
└───────────────────────────┘
Make it faster
- Test for empty strings with
= '', notascii(s) = 0. A comparison of the raw column with a constant is pushed down to VAST DataBase, so rows that don't match never leave storage. The plan forname = ''has noFILTERstep:
plan(f"SELECT id FROM {TABLE} WHERE name = ''")
┌───────────────────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: id │
│ │
└───────────────────────────┘
- Store the first code point if you filter on it often. Compute
ascii(name)when you write the rows and keep it in its own column:initial_code BETWEEN 65 AND 90is pushed down,ascii(name) BETWEEN 65 AND 90isn't. - Combine it with a pushed-down condition. In
id > 100 AND ascii(name) > 127,id > 100is still evaluated in VAST DataBase, soascii()runs only on the rows that pass it.
Limitations and errors
- Strings only. A number, a decimal, a
BLOB, a date, a boolean or a list fails withNo function matches the given name and argument types. Cast it toVARCHARfirst. - Not limited to ASCII.
asciireturns any Unicode code point, such as 8364 for€. To test whether a string starts with an ASCII character, compare the result with 127. - The first code point, not the first character a reader sees. See What it does.
- 0 for an empty string. 0 is also the code point of the NUL character, so
asciican't tell an empty string from a string that starts with NUL. ord returns -1 for an empty string.
run("SELECT ascii(DATE '2026-10-16')")
InternalError: Binder Error: No function matches the given name and argument types 'ascii(DATE)'. You might need to add explicit type casts.
Similar functions
| Function | Returns | 'Hello' |
'é' |
'e\u0301' |
'' |
|---|---|---|---|---|---|
ascii |
the first code point; 0 for an empty string | 72 |
233 |
101 |
0 |
| ord | the first code point; -1 for an empty string | 72 |
233 |
101 |
-1 |
unicode |
the same as ord |
72 |
233 |
101 |
-1 |
| chr | the reverse: the character for a code point | chr(233) is 'é' |
ascii(chr(0)), the NUL character, also returns 0, the same as ascii(''). ord returns 0 for NUL and -1 for an empty string, so use ord when you need to tell them apart.
run("SELECT ascii('Hello'), ord('Hello'), unicode('Hello')")
run("SELECT ascii('é'), ord('é'), unicode('é'), chr(233)")
run("SELECT ascii('e\u0301'), ord('e\u0301'), unicode('e\u0301')")
run("SELECT ascii(''), ord(''), unicode(''), ascii(chr(0))")
[(72, 72, 72)]
[(233, 233, 233, 'é')]
[(101, 101, 101)]
[(0, -1, -1, 0)]
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 chr.
- length: code points and graphemes.
- Where filters run and pushdowns.
- EXPLAIN: how to read a plan.