mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
180 lines
6.3 KiB
Python
180 lines
6.3 KiB
Python
import pytest
|
|
|
|
from helpers.cluster import ClickHouseCluster
|
|
|
|
cluster = ClickHouseCluster(__file__)
|
|
|
|
# Instance without allow_experimental_webassembly_udf (default is disabled)
|
|
node0 = cluster.add_instance("node0")
|
|
|
|
# Instance with allow_experimental_webassembly_udf enabled
|
|
node1 = cluster.add_instance(
|
|
"node1",
|
|
main_configs=["configs/wasm_udf.xml"],
|
|
stay_alive=True,
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def start_cluster():
|
|
try:
|
|
cluster.start()
|
|
yield cluster
|
|
finally:
|
|
cluster.shutdown()
|
|
|
|
|
|
# A small WebAssembly modules encoded in base64
|
|
|
|
# https://github.com/eliben/wasm-wat-samples/blob/125f27fa4bf7fb3c5aab5479136be4596150420a/prime-test/isprime.wat
|
|
ISPRIME_WASM_BASE64 = (
|
|
"AGFzbQEAAAABBgFgAX8BfwMCAQAHDAEIaXNfcHJpbWUAAApUAVIBAX8gAEECSQRAQQAPCyAAQQJG"
|
|
"BEBBAQ8LIABBAnBBAEYEQEEADwtBAyEBA0ACQCABIABPDQAgACABcEEARgRAQQAPCyABQQJqIQEM"
|
|
"AQsLQQEL"
|
|
)
|
|
|
|
# https://github.com/eliben/wasm-wat-samples/blob/125f27fa4bf7fb3c5aab5479136be4596150420a/select/select.wat
|
|
ADD_OR_SUB_WASM_BASE64 = (
|
|
"AGFzbQEAAAABCAFgA39/fwF/AwIBAAcOAQphZGRfb3Jfc3ViAAAKFAESACAAIAFqIAAgAWsgAkEAThsL"
|
|
)
|
|
|
|
|
|
def test_disabled_not_available(start_cluster):
|
|
"""Without the flag, `system.webassembly_modules` must not exist and WASM functions must not be creatable."""
|
|
|
|
# Table must not appear in system.tables
|
|
result = node0.query(
|
|
"SELECT name FROM system.tables WHERE database = 'system' AND name = 'webassembly_modules'"
|
|
)
|
|
assert result.strip() == ""
|
|
|
|
# SELECT on the table must fail
|
|
error = node0.query_and_get_error("SELECT name FROM system.webassembly_modules")
|
|
assert "webassembly_modules" in error
|
|
|
|
# INSERT must fail
|
|
error = node0.query_and_get_error(
|
|
f"INSERT INTO system.webassembly_modules (name, code) "
|
|
f"SELECT 'module1', base64Decode('{ISPRIME_WASM_BASE64}')"
|
|
)
|
|
assert "webassembly_modules" in error
|
|
|
|
# CREATE FUNCTION must fail
|
|
error = node0.query_and_get_error(
|
|
"CREATE FUNCTION is_prime LANGUAGE WASM ABI ROW_DIRECT "
|
|
"FROM 'module1' ARGUMENTS (num UInt32) RETURNS UInt32"
|
|
)
|
|
assert "WebAssembly support is not enabled" in error
|
|
|
|
|
|
def test_enabled(start_cluster):
|
|
"""With the flag enabled: table is visible, modules and functions can be loaded, and they persist after restart."""
|
|
|
|
if node1.is_built_with_memory_sanitizer():
|
|
pytest.skip("Wasmtime is disabled in MSAN builds")
|
|
|
|
# Table must appear in system.tables
|
|
result = node1.query(
|
|
"SELECT name FROM system.tables WHERE database = 'system' AND name = 'webassembly_modules'"
|
|
)
|
|
assert result.strip() == "webassembly_modules"
|
|
|
|
node1.query("DROP FUNCTION IF EXISTS is_prime")
|
|
node1.query("DELETE FROM system.webassembly_modules WHERE name = 'isprime'")
|
|
|
|
node1.query(
|
|
f"INSERT INTO system.webassembly_modules (name, code) "
|
|
f"SELECT 'isprime', base64Decode('{ISPRIME_WASM_BASE64}')"
|
|
)
|
|
|
|
result = node1.query(
|
|
"SELECT name FROM system.webassembly_modules WHERE name = 'isprime'"
|
|
)
|
|
assert result.strip() == "isprime"
|
|
|
|
node1.query(
|
|
"CREATE FUNCTION is_prime LANGUAGE WASM ABI ROW_DIRECT "
|
|
"FROM 'isprime' ARGUMENTS (num UInt32) RETURNS UInt32"
|
|
)
|
|
|
|
# 2, 3, 5, 7 are prime; 4, 6, 8, 9 are not
|
|
result = node1.query(
|
|
"SELECT number, is_prime(number :: UInt32) FROM system.numbers "
|
|
"WHERE number BETWEEN 2 AND 9 ORDER BY number"
|
|
)
|
|
assert result == "2\t1\n3\t1\n4\t0\n5\t1\n6\t0\n7\t1\n8\t0\n9\t0\n"
|
|
|
|
assert (
|
|
"Cannot delete WebAssembly module 'isprime' while it is in use. Drop all functions referring to it first"
|
|
in node1.query_and_get_error(
|
|
"DELETE FROM system.webassembly_modules WHERE name = 'isprime'"
|
|
)
|
|
)
|
|
|
|
# After restart, module and function must still be available
|
|
node1.restart_clickhouse()
|
|
|
|
result = node1.query(
|
|
"SELECT name FROM system.webassembly_modules WHERE name = 'isprime'"
|
|
)
|
|
assert result.strip() == "isprime"
|
|
|
|
result = node1.query(
|
|
"SELECT number, is_prime(number :: UInt32) FROM system.numbers "
|
|
"WHERE number BETWEEN 2 AND 9 ORDER BY number"
|
|
)
|
|
assert result == "2\t1\n3\t1\n4\t0\n5\t1\n6\t0\n7\t1\n8\t0\n9\t0\n"
|
|
|
|
# A .wasm file placed directly on disk (without INSERT INTO system.webassembly_modules)
|
|
# must be loadable when referenced by CREATE FUNCTION.
|
|
wasm_dir = "/var/lib/clickhouse/user_scripts/wasm"
|
|
node1.exec_in_container(
|
|
[
|
|
"python3",
|
|
"-c",
|
|
f"import base64; open('{wasm_dir}/add_or_sub.wasm', 'wb')"
|
|
f".write(base64.b64decode('{ADD_OR_SUB_WASM_BASE64}'))",
|
|
]
|
|
)
|
|
node1.query(
|
|
"CREATE FUNCTION add_or_sub LANGUAGE WASM ABI ROW_DIRECT FROM 'add_or_sub'"
|
|
"ARGUMENTS (a UInt32, b UInt32, control Int32) RETURNS UInt32"
|
|
)
|
|
result = node1.query(
|
|
"SELECT add_or_sub(10 :: UInt32, 5 :: UInt32, 1 :: Int32), add_or_sub(10 :: UInt32, 5 :: UInt32, -1 :: Int32)"
|
|
)
|
|
assert result == "15\t5\n"
|
|
|
|
# If the .wasm file is broken, CREATE FUNCTION must fail with an error,
|
|
# and the broken module must not be added to system.webassembly_modules
|
|
node1.exec_in_container(
|
|
[
|
|
"python3",
|
|
"-c",
|
|
f"open('{wasm_dir}/broken.wasm', 'wb')"
|
|
f".write(b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00')",
|
|
]
|
|
)
|
|
error = node1.query_and_get_error(
|
|
"CREATE FUNCTION some_func LANGUAGE WASM ABI ROW_DIRECT FROM 'broken'"
|
|
"ARGUMENTS (a UInt32) RETURNS UInt32"
|
|
)
|
|
assert "Cannot load WebAssembly module" in error
|
|
|
|
result = node1.query("SELECT name FROM system.webassembly_modules ORDER BY name")
|
|
assert result == "add_or_sub\nisprime\n"
|
|
|
|
# DELETE from system.webassembly_modules with a non-identifier predicate
|
|
# (e.g. WHERE 1=1) must return an error, not crash the server.
|
|
error = node1.query_and_get_error(
|
|
"DELETE FROM system.webassembly_modules WHERE 1=1"
|
|
)
|
|
assert "Only deletion by module name is supported" in error
|
|
assert node1.query("SELECT 1").strip() == "1"
|
|
|
|
# Cleanup
|
|
node1.query("DROP FUNCTION IF EXISTS is_prime")
|
|
node1.query("DROP FUNCTION IF EXISTS add_or_sub")
|
|
node1.query("DELETE FROM system.webassembly_modules WHERE name = 'isprime'")
|
|
node1.query("DELETE FROM system.webassembly_modules WHERE name = 'add_or_sub'")
|