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. Implies no_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->diff introspects currentDatabase() 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 a DROP TABLE the diff wants to perform, and t/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

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.