NAME
DBIO::ClickHouse::Diff::Table - Diff operations for ClickHouse tables, and the no-silent-rebuild policy
VERSION
version 0.900004
DESCRIPTION
Table-level diff operations for ClickHouse -- CREATE TABLE and DROP TABLE -- plus "check_rebuild_required", the policy that refuses to paper over a change ClickHouse cannot make in place.
A create op carries the target table's columns and data-skipping indexes so that "as_sql" emits the whole definition as one statement. That is not a stylistic preference: a ClickHouse table cannot be built up with ALTER TABLE ADD COLUMN from nothing, because ENGINE and the sorting key have to be present at CREATE time and can never be set afterwards.
METHODS
throw_exception
$class->throw_exception($message);
Raises a DBIO::Exception. The diff layer has no storage to route through (a diff is computed from two model hashrefs and may never touch a connection), so this is the family's throw_exception contract reduced to what is available here. It exists so that no error path in this distribution reaches for a bare die or croak.
check_rebuild_required
DBIO::ClickHouse::Diff::Table->check_rebuild_required($source_tables, $target_tables);
Walks the tables present in both models and throws on the first immutable property that differs. Called from "diff", so every route into the diff -- $deploy->diff, $diff->operations, a direct Diff::Table->diff in a test -- is covered by the same check.
The comparison honours core's desired-state contract ("changed_fields" in DBIO::Diff::Compare): a property the target side leaves undefined was not prescribed by the desired state and is not compared. A model assembled by hand that only carries table_name therefore never trips the policy, while a model introspected from a live server always carries all four.
engine_expression is compared rather than engine_full, deliberately. engine_full also carries SETTINGS, and the server fills those in with its own defaults (index_granularity = 8192 and friends), so comparing it would throw on every table of every schema that never mentioned a setting.
diff
my @ops = DBIO::ClickHouse::Diff::Table->diff(
$source_tables, $target_tables,
$target_columns, $target_skipping_indexes,
$storage, # optional, drives IF EXISTS
$declared_settings, # optional, { table => \%settings }
);
Runs "check_rebuild_required" first, then the ordinary create/drop walk: a table only in the target is created (with its columns and skipping indexes inline), a table only in the source is dropped. Tables present on both sides produce no table-level operation -- their columns and indexes are handled by DBIO::ClickHouse::Diff::Column and DBIO::ClickHouse::Diff::Index.
$storage is optional: when given, the resulting ops stash it and "as_sql" will pass an if_not_exists / if_exists flag to the Emit functions based on "should_emit_if_exists" in DBIO::Diff::Op. Without it, the returned SQL stays bare -- the legacy shape t/40 / t/41 already pin against.
$declared_settings is optional; a table only in the target picks its entry up for the SETTINGS clause of its CREATE TABLE. Tables present on both sides are not compared on settings.
Views must be filtered out by the caller; see "Views are only created" in DBIO::ClickHouse::Diff.
as_sql
Renders DROP TABLE or the full CREATE TABLE through DBIO::ClickHouse::DDL::Emit, so the diff's CREATE TABLE is char-for-char the shape DBIO::ClickHouse::DDL emits for a fresh install.
The engine clauses are emitted in the order SHOW CREATE TABLE prints them: ENGINE, PARTITION BY, PRIMARY KEY, ORDER BY. Two of them are conditional:
PRIMARY KEYis emitted only when it actually differs from the sorting key. ClickHouse fills the primary key in fromORDER BYwhen it was not declared separately, so restating it would make every table look like it declared a redundant prefix.SETTINGSis emitted only from the settings the schema declares (clickhouse_table({ settings => {...} }), handed in asdeclared_settings), rendered by "settings_clause" in DBIO::ClickHouse::DDL::Emit exactly as the install DDL does -- so a table created by an upgrade is the table a fresh install makes. The model'sengine_fullis never used: it reports the server's own defaults, and freezing those into a migration pins a server default into user DDL.
The two statement forms accept an optional if_not_exists / if_exists flag from DBIO::ClickHouse::DDL::Emit -- the flag is populated once here, from "should_emit_if_exists" in DBIO::Diff::Op on the op's stashed storage, and passed through to the "create_table" in DBIO::ClickHouse::DDL::Emit or "drop_table" in DBIO::ClickHouse::DDL::Emit call. A storage that did not declare the F12 capability (or none given) keeps the legacy bare shape, exactly as t/40 / t/41 already pin.
summary
One line naming the table and whether it is created (+) or dropped (-).
THE REBUILD POLICY
Four properties of a MergeTree table are fixed the moment it is created and cannot be changed by any ALTER:
the engine (
MergeTreevsReplacingMergeTree(version), ...)the sorting key --
ORDER BY, the thing ClickHouse calls the primary keythe primary key prefix, when one was declared separately
the partition key --
PARTITION BY
The only way to change any of them is to build a new table and move the data. This module does not do that, and it does not silently do nothing either. "check_rebuild_required" throws a DBIO::Exception naming the table, the property, both values, and the four statements the operator has to run.
Why refuse rather than emit DROP TABLE + CREATE TABLE: on every other engine in this family a table-level diff op is a schema change; here it would be a data deletion. DROP TABLE on ClickHouse takes billions of rows with it, there is no transaction to roll back (docs/adr/0003-no-transactions-policy.md), and the failure would be discovered after the fact. An upgrade that stops with an actionable message is strictly better than one that succeeds by deleting the warehouse.
Why refuse rather than emit nothing: the plain create/drop walk only looks at table names, so a table present on both sides with a different sorting key produces no operation at all. The upgrade would report success, the sorting key would still be the old one, and every query written for the new one would do a full scan. Silence is the worst of the three options.
SEE ALSO
DBIO::ClickHouse::Diff - the orchestrator that drives this
DBIO::ClickHouse::DDL::Emit - the statement templates rendered here
DBIO::ClickHouse::Introspect::Tables - produces the model section
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.