NAME
DBIO::ClickHouse::Diff - Compare two introspected ClickHouse models
VERSION
version 0.900004
DESCRIPTION
DBIO::ClickHouse::Diff compares two introspected ClickHouse models (produced by DBIO::ClickHouse::Introspect) and emits a list of structured diff operations that can be rendered to SQL or to a human-readable summary.
my $diff = DBIO::ClickHouse::Diff->new(
source => $current_model,
target => $desired_model,
);
if ($diff->has_changes) {
print $diff->as_sql;
print $diff->summary;
}
Operations are emitted in dependency order: tables, index drops, columns, the other data-skipping index operations, then views. An index drop comes before the column walk because the server refuses to drop a column an index still reads.
BOTH SIDES MUST BE INTROSPECTED
The comparison is done on the native ClickHouse type strings the model carries (LowCardinality(Nullable(String)), Decimal(10, 2)), not on portable DBIO type names. That is what makes it exact -- and it means a model hand-assembled from column_info is not a valid input: the DDL layer maps the whole varchar / char / text / json family onto String, so such a target would differ from the live side on every string column and never converge. Build the desired side the way DBIO::ClickHouse::Deploy does: deploy the install DDL into a scratch database and introspect that.
WHAT THIS DIFF WILL NOT DO
It will not silently rebuild a table
ENGINE, ORDER BY, PRIMARY KEY and PARTITION BY are fixed at CREATE TABLE time on ClickHouse. A change to any of them throws a DBIO::Exception naming the table, the property and the statements to run by hand, rather than emitting a DROP TABLE that would delete the data or -- worse -- emitting nothing at all and reporting success. The policy, and why it is a refusal rather than a rebuild, is documented in "THE REBUILD POLICY" in DBIO::ClickHouse::Diff::Table.
Because the check runs while the operation list is built, it fires from has_changes, as_sql and summary alike -- there is no route into the diff that skips it.
It drops every skipping index the schema does not declare
A DROP INDEX is emitted for each data-skipping index a surviving table has and the target lacks, also when the target declares none for that table: the column drop of a column whose last index reads it needs it. An index created by hand is therefore removed unless the schema declares it. See "Every skipping index the target lacks is dropped" in DBIO::ClickHouse::Diff::Index.
Views follow the schema's declaration
Views are filtered out of both models before the table, column and index walks. That keeps DBIO::ClickHouse::Diff::Column from emitting ALTER TABLE ... MODIFY COLUMN against a view, and a view's CREATE TABLE from being rendered out of its kind. Their own operations come from DBIO::ClickHouse::Diff::View, against the optional declared_views constructor argument ({ view => $select_sql }, built by "declared_views" in DBIO::ClickHouse::DDL from the schema itself, the selection install_ddl uses):
a declared view the live model lacks gets a
CREATE VIEW;a declared view whose live definition differs gets a
CREATE OR REPLACE VIEW(compared as the server holds the text, see "What "differs" means" in DBIO::ClickHouse::Diff::View);a live view the schema does not declare gets a
DROP VIEW, emitted before the table operations; create and replace come after all table, column and index operations (no opt-in -- a view holds no data).
A materialized view holds data, so a live one that the schema does not declare, or declares as a plain view, is not dropped or replaced: the diff throws a DBIO::Exception naming it, while the operation list is built and so before any statement runs. Without declared_views no view operation is produced and no view is refused.
NOT PART OF THE MODEL, NOT PART OF THE DIFF
Three things the introspector reads but this diff deliberately ignores, because the desired side can never carry them and comparing them would delete or freeze live state on the first upgrade: table and column comments, per-column compression codecs, and the engine's SETTINGS (the server fills those with its own defaults; an existing table's settings are never compared). One exception, for a table the diff creates: the optional declared_settings constructor argument ({ table => \%settings }, built by "declared_settings" in DBIO::ClickHouse::DDL) is rendered as the SETTINGS clause of that CREATE TABLE, so the table does not come into being with server defaults the schema never asked for. Foreign keys and unique constraints do not appear at all -- ClickHouse has neither, and the model's foreign_keys and indexes sections are always empty (docs/adr/0004-no-foreign-keys.md).
SEE ALSO
DBIO::ClickHouse::Deploy - builds the desired model and applies the diff
DBIO::ClickHouse::Introspect - produces both models
DBIO::ClickHouse::DDL::Emit - the statement shapes the ops render through
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.