NAME
DBIO::ClickHouse::Test - ClickHouse-specific test utilities for DBIO
VERSION
version 0.900004
SYNOPSIS
use DBIO::ClickHouse::Test;
# In a live test file:
plan skip_all => 'Set DBIO_TEST_CLICKHOUSE_DSN to run live tests'
unless $ENV{DBIO_TEST_CLICKHOUSE_DSN};
my $schema = DBIO::ClickHouse::Test->init_schema;
my $rs = $schema->resultset('Hit');
DESCRIPTION
Test helpers for the DBIO::ClickHouse driver distribution: driver tests call DBIO::ClickHouse::Test->init_schema and get a connected, deployed schema backed by a live ClickHouse server.
Unlike embedded drivers, ClickHouse has no in-memory / :memory: mode, so there is no zero-config default. Everything here is gated on DBIO_TEST_CLICKHOUSE_DSN (plus optional DBIO_TEST_CLICKHOUSE_DBUSER / DBIO_TEST_CLICKHOUSE_DBPASS) and dies cleanly when it is unset. Live tests should plan skip_all on that env var themselves; the die is the backstop.
METHODS
_database
Returns ($dsn, $user, $pass, \%opts) for $schema->connect(). Requires DBIO_TEST_CLICKHOUSE_DSN; ClickHouse has no embedded mode, so there is no fallback DSN. DBIO_TEST_CLICKHOUSE_DBUSER / DBIO_TEST_CLICKHOUSE_DBPASS default to the empty string. Croaks with a clear message when the DSN is unset.
Any extra arguments are merged into the connect attributes, minus the "init_schema" control keys.
When the DSN is a dbi:mysql:... one, mysql_enable_utf8mb4 => 1 is added to the connect attributes, so the suite asserts against decoded Unicode on both transports -- the contract DBD::mysql is documented with and the one DBD::MariaDB enforces unconditionally. See docs/adr/0001.
test_database
my $name = DBIO::ClickHouse::Test->test_database;
The name of this process's test database, dbio_ch_test_<pid>. See "EVERY TEST PROCESS GETS ITS OWN DATABASE".
test_connect_info
my ($dsn, $user, $pass, $opts) = DBIO::ClickHouse::Test->test_connect_info;
my ($dsn, $user, $pass, $opts)
= DBIO::ClickHouse::Test->test_connect_info(mutations_sync => 0);
"_database", with the DSN pointed at "test_database" instead of at the database the environment names. This is what a live test needs in order to open a second connection to the same schema -- a different mutations_sync, a different session setting -- without landing in a different database than "init_schema" deployed into.
init_schema
my $schema = DBIO::ClickHouse::Test->init_schema(%opts);
Creates this process's test database, connects DBIO::ClickHouse::Test::Schema to it with +DBIO::ClickHouse::Storage pinned, deploys the schema and populates the fixture. Returns the connected schema.
Options:
no_deploy-- connect only; do not create any table.no_populate-- deploy, but insert no fixture rows.schema_class-- deploy a different schema class instead of DBIO::ClickHouse::Test::Schema, which is how a live file that needs one table of its own shape still gets the per-process database and its cleanup. Impliesno_populate: "populate_schema" writes the standard fixture and has no idea what another schema's sources are called.
Anything else is passed through to DBI->connect as a connect attribute, which is how a test reaches connect-time driver options such as mutations_sync => 0.
populate_schema
DBIO::ClickHouse::Test->populate_schema($schema);
Writes the fixture DBIO::ClickHouse::Test::Schema is designed around. Every source is filled with one void-context populate, i.e. one multi-row INSERT statement -- both because that is the only insert shape ClickHouse actually wants ("INSERTS AND BATCHING" in DBIO::ClickHouse::Storage) and because it means the fixture itself exercises "insert_multi" in DBIO::ClickHouse::SQLMaker over every column type in the schema.
Hit rows deliberately omit the id column, so the server-side DEFAULT generateUUIDv4() fills it -- the one path on which this driver lets the database generate a key.
probe_row
my $row = DBIO::ClickHouse::Test->probe_row;
The single fixture row for DBIO::ClickHouse::Test::Schema::Probe, as a hashref ready for populate. It is a method rather than a literal in "populate_schema" so that t/18-types-live.t can assert against the same values without repeating them, which is the whole point: the assertions there are about what the server gave back, not about what the test typed twice.
Two cells are literal SQL. Array(T) has no bind type on any MySQL-wire DBD -- "insert_multi" in DBIO::ClickHouse::SQLMaker throws on a bare arrayref rather than guessing -- so an array value has to be written as ClickHouse array syntax.
Values are chosen to be significant, not merely valid: d_wide is before 1970 (the range Date32 exists for and Date cannot hold), ts_ms carries milliseconds (the precision DateTime64(3) exists for), and state is the enum member whose apostrophe has to survive the wire.
EVERY TEST PROCESS GETS ITS OWN DATABASE
"init_schema" does not deploy into the database named in the DSN. It creates dbio_ch_test_<pid>, connects the schema there, deploys into it, and drops it again in an END block. The DSN's own database is only ever used to issue the CREATE / DROP DATABASE pair.
That is not tidiness, it is what makes two of the live assertions possible at all:
The deploy round trip is a whole-database comparison.
$deploy->diffintrospectscurrentDatabase()and compares it against a scratch database holding nothing but the schema. Any other table in the database -- a leftover from an aborted run, an unrelated table someone created on the shared server -- shows up as aDROP TABLEthe diff wants to perform, andt/30-deploy.t's central assertion ("a freshly deployed schema has no diff against itself") fails for a reason that has nothing to do with the code under test.The suite has to survive being re-run. A live file that dies half-way leaves its tables behind. Scoping to a per-process database, and dropping and recreating it at the start of each
init_schema, means the next run starts from a known state instead of inheriting the previous failure.
The cost is that the connecting user needs CREATE DATABASE and DROP DATABASE. DBIO::ClickHouse::Deploy already requires both for its scratch database, so the live suite could not run without them anyway.
It also means a live file that creates a raw table of its own needs no cleanup of its own: the table is inside the per-process database and goes with it. Do not write a DROP TABLE on the last line of the file and call that cleanup -- the last line is precisely what a failed assertion never reaches.
A database left behind by a process that was killed outright (kill -9, an OOM) is named dbio_ch_test_* and is safe to drop by hand; the k8s workflow in the dbio-clickhouse-database skill deletes the whole pod at the end, which takes them all with it.
Why this does not call DBIO::Test->init_schema
"init_schema" in DBIO::Test is hardcoded to DBIO::Test::Schema -- both the class it clones and the fixture populate_schema writes -- and that schema cannot be deployed against ClickHouse in any useful sense. The reasons are recorded in "WHY NOT DBIO::Test::Schema" in DBIO::ClickHouse::Test::Schema; the short version is that its integer is_auto_increment primary keys are precisely the identity model "last_insert_id" in DBIO::ClickHouse::Storage exists to refuse.
Deployment goes through "install" in DBIO::ClickHouse::Deploy rather than $schema->deploy on purpose. DBIO::Storage::DBI::deploy carps on a statement that fails and carries on, so a schema that only half deployed would be reported as deployed and the failure would surface as a mystified assertion three files later. install runs the statements with RaiseError, so a rejected CREATE TABLE stops the file where it happened.
SEE ALSO
DBIO::ClickHouse::Test::Schema - the schema deployed here, and why it is not DBIO::Test::Schema
DBIO::ClickHouse::Deploy - the deploy path used by "init_schema"
AUTHOR
DBIO Authors
COPYRIGHT AND LICENSE
Copyright (C) 2026 DBIO Authors
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.