Applies to VAST Cluster 5.5.
Functions › Text basics › Unicode and URLs. Percent-encodes a string so it can be placed in a URL: spaces, reserved characters such as &, =, / and ?, and characters outside ASCII become % followed by two hexadecimal digits for each UTF-8 byte. It has no aliases; url_decode reverses it.
Syntax
url_encode(s)
| Argument | Type | Description |
|---|---|---|
s |
VARCHAR |
The string to encode. |
Returns: VARCHAR.
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_url_encode. 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 file names. The last row has no name:
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_url_encode"'
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}.files"
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, 'report-2026.pdf'), "
"(2, 'Q3 results.xlsx'), "
"(3, 'R&D plan.txt'), "
"(4, 'caf\u00e9 menu.pdf'), "
"(5, NULL)")
[]
[]
[(5,)]
What it does
url_encode turns the string into UTF-8 bytes and writes each byte either as itself or as % followed by its value in two upper-case hexadecimal digits:
- Kept as they are: ASCII letters, digits and the four characters
-,.,_and~. - Encoded: everything else, including the space, the characters that have a meaning in a URL (
/,?,#,&,=,+,%) and every byte of a character outside ASCII.
| Call | Result |
|---|---|
url_encode('Hello') |
'Hello' |
url_encode('AZaz0189') |
'AZaz0189': letters and digits are kept |
url_encode('~-._') |
'~-._' |
url_encode('a b') |
'a%20b': a space is %20, not + |
url_encode('a+b') |
'a%2Bb' |
url_encode('?#%') |
'%3F%23%25' |
url_encode('a b&c=d/é') |
'a%20b%26c%3Dd%2F%C3%A9': é is the two bytes C3 A9 |
url_encode('e\u0301 🇫🇷 ẞ') |
'e%CC%81%20%F0%9F%87%AB%F0%9F%87%B7%20%E1%BA%9E': the combining accent is CC 81, the flag 8 bytes, ẞ 3 bytes |
Because / is encoded too, encode each part of a URL separately (a path segment, a query parameter's value) and join the parts with the separators, rather than encoding a whole URL.
The encoding depends on the bytes, so the same-looking text in two Unicode forms gives two encodings: the decomposed 'e\u0301' gives e%CC%81, the precomposed é gives %C3%A9. Apply nfc_normalize first if you need one encoding for both.
NULLs and edge cases
| Input | Result |
|---|---|
NULL |
NULL |
'' (empty string) |
'' |
'~-._' |
'~-._' |
'a+b' |
'a%2Bb' |
'?#%' |
'%3F%23%25' |
'AZaz0189' |
'AZaz0189' |
'e\u0301' and 'é' |
'e%CC%81' and '%C3%A9' |
A number, decimal, date, boolean, BLOB or list |
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 url_encode(NULL), url_encode(''), url_encode('~-._'), url_encode('a+b'), url_encode('?#%'), "
"url_encode('AZaz0189')")
run("SELECT url_encode('e\u0301'), url_encode('\u00e9'), url_encode('e\u0301 🇫🇷 ẞ')")
run("SELECT url_encode(123)")
[(None, '', '~-._', 'a%2Bb', '%3F%23%25', 'AZaz0189')]
[('e%CC%81', '%C3%A9', 'e%CC%81%20%F0%9F%87%AB%F0%9F%87%B7%20%E1%BA%9E')]
InternalError: Binder Error: No function matches the given name and argument types 'url_encode(INTEGER_LITERAL)'. You might need to add explicit type casts.
Examples
A download link for each file. The name is encoded as one path segment and joined to the fixed part of the URL with ||; the NULL name gives a NULL link:
run(f"SELECT id, 'https://files.example.com/shared/' || url_encode(name) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 'https://files.example.com/shared/report-2026.pdf'), (2, 'https://files.example.com/shared/Q3%20results.xlsx'), (3, 'https://files.example.com/shared/R%26D%20plan.txt'), (4, 'https://files.example.com/shared/caf%C3%A9%20menu.pdf'), (5, None)]
Find the names that change when they're encoded, that is, the ones that contain characters that aren't safe in a URL:
run(f"SELECT id, name FROM {TABLE} WHERE url_encode(name) <> name ORDER BY id LIMIT 10")
[(2, 'Q3 results.xlsx'), (3, 'R&D plan.txt'), (4, 'café menu.pdf')]
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 url_encode() is always computed by the Query Engine:
- In the
SELECTlist, the scan reads the column and the Query Engine computesurl_encode()for each row it returns. - In
WHERE, the scan sends the column for every row, and aFILTERstep in the Query Engine computesurl_encode()and keeps the rows that match. No rows are skipped in storage.
The plan of the WHERE example shows the FILTER step above the scan:
plan(f"SELECT id FROM {TABLE} WHERE url_encode(name) <> name")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ #1 │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ FILTER │
│ ──────────────────── │
│ (url_encode(name) != name)│
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ │
│ Projections: │
│ name │
│ id │
│ │
└───────────────────────────┘
Make it faster
- Filter on the raw column, encode in the
SELECTlist. Encoding is for output: compare the stored name, not its encoding, inWHERE. A comparison of the raw column with a constant is pushed down to VAST DataBase, so rows that don't match never leave storage, andurl_encode()runs only on the rows returned. The plan forname = 'R&D plan.txt'has noFILTERstep:
plan(f"SELECT 'https://files.example.com/shared/' || url_encode(name) FROM {TABLE} WHERE name = 'R&D plan.txt'")
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ ('https://files.example │
│ .com/shared/' || │
│ url_encode(name)) │
│ │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ VAST_SCAN │
│ ──────────────────── │
│ Function: VAST_SCAN │
│ Projections: name │
│ │
└───────────────────────────┘
- Combine it with a pushed-down condition. In
id > 100 AND url_encode(name) <> name,id > 100is still evaluated in VAST DataBase, sourl_encode()runs only on the rows that pass it.
Limitations and errors
- Strings only. A number, decimal, date, boolean,
BLOBor list fails withNo function matches the given name and argument types. Cast a number first:url_encode(CAST(n AS VARCHAR)). - One encoding, no options. Spaces are always
%20, never+, and/is always encoded. There's no argument to keep characters unencoded. - Unicode form matters. See What it does.
run("SELECT url_encode(123)")
run("SELECT url_encode('\\xAA'::BLOB)")
run("SELECT url_encode(CAST(123 AS VARCHAR))")
InternalError: Binder Error: No function matches the given name and argument types 'url_encode(INTEGER_LITERAL)'. You might need to add explicit type casts.
InternalError: Binder Error: No function matches the given name and argument types 'url_encode(BLOB)'. You might need to add explicit type casts.
[('123',)]
Similar functions
| Function | What it does | Result |
|---|---|---|
url_encode |
percent-encodes a string | url_encode('a b') gives 'a%20b' |
| url_decode | reverses url_encode |
url_decode('a%20b') gives 'a b'; url_decode(url_encode('a b/é')) gives 'a b/é' |
to_base64 (Binary, hash and UUID functions) |
encodes a BLOB as Base64 text |
to_base64('a'::BLOB) gives 'YQ==' |
run("SELECT url_encode('a b'), url_decode('a%20b'), url_decode(url_encode('a b/\u00e9'))")
run("SELECT to_base64('a'::BLOB)")
[('a%20b', 'a b', 'a b/é')]
[('YQ==',)]
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.
- Where filters run and pushdowns.
- EXPLAIN: how to read a plan.
- url_decode and nfc_normalize.