NAME
DBIO::ClickHouse::Introspect::Util - ClickHouse type-string parser shared by the introspect submodules
VERSION
version 0.900004
DESCRIPTION
Leaf module holding the pure-data helpers shared by the system.* readers (DBIO::ClickHouse::Introspect::Tables, DBIO::ClickHouse::Introspect::Columns, DBIO::ClickHouse::Introspect::Indexes) and by DBIO::ClickHouse::Introspect itself. None of these subs talk to the database or hold state, so they are unit-testable without a server -- which matters here more than in the other drivers, because ClickHouse has no embedded mode and every live assertion costs a running server.
The centrepiece is "normalize_type", the inverse of the type map in DBIO::ClickHouse::DDL. Everything in this module exists so that the introspected model of a table the DDL layer created describes the same column metadata the DDL layer started from -- otherwise the diff layer sees phantom changes on every run.
Round-trip fidelity
normalize_type is written so that feeding its result back through DBIO::ClickHouse::DDL's _clickhouse_column_type reproduces the native type string it was given. Two cases cannot round-trip and are called out here rather than hidden:
Stringis many-to-one. The DDL layer collapses the whole varchar / char / text / blob / json family ontoString, so introspection can only pick one representative. It pickstext(unsized, whichStringis). A schema that declaredvarchar => 255introspects back astextwith nosize. This is only a phantom change if a desired-state model is built directly fromcolumn_info; when the desired state is produced by deploying the generated DDL and introspecting it (the family's test-deploy-and-compare strategy) both sides normalize identically.Explicitly numbered enums.
DBIO::ClickHouse::DDLnumbers enum members1..Nin declaration order. AnEnum8whose values are anything else (starting at 0, sparse, reordered) cannot be expressed asdata_type => 'enum', so the whole type string is kept verbatim -- which does round-trip, via the DDL layer's pass-through for parameterized types -- and the member names are still exposed inextra->{list}for readers that want them.
Bare Date and bare DateTime used to be a third case: they are returned verbatim here, and DBIO::ClickHouse::DDL used to lowercase data_type before its type-map lookup and widen them to Date32 / DateTime64(3). It now treats an exact native spelling as a passthrough, so they round-trip like any other native name.
METHODS
normalize_type
my $n = DBIO::ClickHouse::Introspect::Util->normalize_type('LowCardinality(Nullable(String))');
# { native_type => 'LowCardinality(Nullable(String))',
# data_type => 'text',
# is_nullable => 1,
# extra => { low_cardinality => 1 } }
Parses one native ClickHouse type string (as system.columns.type reports it) into the portable DBIO column metadata that produced it. Always returns a hashref with native_type, data_type, is_nullable and extra; size is present only when the type carries one.
The wrappers are peeled in the order DBIO::ClickHouse::DDL nests them: LowCardinality outside Nullable outside the base type. Array(T) is handled by the base-type parser, because Array is not a nullability wrapper -- it is the type.
Recognised shapes:
Int8 Int16 Int32 Int64 -> tinyint smallint integer bigint
UInt8 UInt16 UInt32 UInt64 -> same, extra => { unsigned => 1 }
Float32 / Float64 -> real / double precision
String -> text
FixedString(N) -> char, size N, extra => { fixed_string => 1 }
Decimal(P, S) -> decimal, size [P, S]
Decimal32/64/128/256(S) -> decimal, size [9|18|38|76, S]
Date32 -> date
DateTime64(3) -> datetime, size 3
DateTime64(3, 'UTC') -> timestamptz, size 3, extra => { timezone }
DateTime64(N[, 'TZ']) -> verbatim, size N, extra => { timezone }
Bool -> boolean
UUID -> uuid
Enum8(...) / Enum16(...) -> enum, extra => { list => [...] }
Array(T) -> "<T>[]"
anything else -> verbatim
Verbatim is not a failure mode: DBIO::ClickHouse::DDL passes an unmapped data_type through untouched, so IPv6, Int128, Map(String, UInt64) and friends survive a full round trip unchanged.
view_select
my $select = DBIO::ClickHouse::Introspect::Util->view_select($create_table_query);
The SELECT of a view, cut out of the CREATE [MATERIALIZED] VIEW ... AS SELECT ... statement system.tables.create_table_query holds (ClickHouse has no view_definition worth reading). Returns undef for anything that is not such a statement.
split_top_level
my @entries = DBIO::ClickHouse::Introspect::Util->split_top_level("toYYYYMM(ts), id");
# ('toYYYYMM(ts)', 'id')
Splits a comma-separated ClickHouse expression list on its top-level commas, ignoring commas nested in parentheses, brackets or single-quoted string literals. Entries are trimmed and empty ones dropped.
This is what turns system.tables' sorting_key / partition_key / primary_key strings and an Enum8 member list into usable lists -- a plain split /,/ would break Decimal(10, 2) and toYYYYMM(ts) apart.
unquote_string
my $value = DBIO::ClickHouse::Introspect::Util->unquote_string(q{'it\\'s'});
# "it's"
The inverse of "quote_string" in DBIO::ClickHouse::DDL::Emit: strips the surrounding single quotes and unescapes backslash-escaped backslashes and quotes. Returns undef when $string is not a single complete quoted literal -- 'a' || 'b' is an expression, not a string default, and must not be silently truncated to a.
SEE ALSO
DBIO::ClickHouse::DDL - the forward type map this module inverts
DBIO::ClickHouse::Introspect - the model builder that consumes these helpers
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.