ClickHouse-Encoder
view release on metacpan or search on metacpan
lib/ClickHouse/Encoder.pm view on Meta::CPAN
}
sub for_table {
my ($class, $table, %opts) = @_;
_validate_table_name($table);
return $class->_for_describe("describe table $table", %opts);
}
sub _for_describe {
my ($class, $describe_sql, %opts) = @_;
my $via = $opts{via} // 'client';
my @lines;
if ($via eq 'http') {
my ($url, $hdr) = _http_url_headers(
"$describe_sql format tabseparated", %opts);
my $resp = _http_tiny(%opts, timeout => $opts{timeout} // 10)
->get($url, { headers => $hdr });
die "HTTP describe table failed (status $resp->{status}): $resp->{content}\n"
unless $resp->{success};
@lines = split /\n/, $resp->{content};
}
elsif ($via eq 'client') {
my $host = $opts{host} // 'localhost';
my $port = $opts{port} // 9000;
my $database = $opts{database} // 'default';
my $user = $opts{user} // 'default';
my $password = $opts{password} // '';
my $client = $opts{client} // 'clickhouse-client';
my @cmd = (
$client,
'--host', $host,
'--port', $port,
'--database', $database,
'--user', $user,
'--query', "$describe_sql format tabseparated",
);
# Pass password via env so it doesn't show up in `ps`. Always
# set $password (even empty) so an empty `password => ''` arg
# also overrides any inherited CLICKHOUSE_PASSWORD.
local $ENV{CLICKHOUSE_PASSWORD} = $password;
open my $fh, '-|', @cmd
or die "Failed to run clickhouse-client: $!";
@lines = <$fh>;
close $fh;
die "clickhouse-client failed: exit code " . ($? >> 8) if $?;
}
else {
die "Unknown via='$via' (expected 'client' or 'http')";
}
my @columns;
# ClickHouse's TabSeparated format escapes \\ \n \t \r \0 in field
# values. Multi-line type expressions (e.g. named Tuple, Nested) come
# out as a single TSV line with embedded \n and indentation; un-escape
# so the XS parser sees the canonical type string. The default branch
# ($1) handles \\ -> \ and any other backslash-escape forms transparently.
my $unesc = sub {
my $s = shift // '';
$s =~ s{\\(.)}{ $1 eq 'n' ? "\n"
: $1 eq 't' ? "\t"
: $1 eq 'r' ? "\r"
: $1 eq '0' ? "\0"
: $1 }ge;
$s;
};
for my $line (@lines) {
chomp $line;
next if $line eq '';
my ($name, $type) = split /\t/, $line;
push @columns, [$unesc->($name), $unesc->($type)];
}
die "No columns found for: $describe_sql" unless @columns;
return $class->new(columns => \@columns);
}
sub columns {
my $self = shift;
return $self->_columns;
}
sub validate_rows {
my ($self, $rows) = @_;
my @errors;
for my $i (0 .. $#$rows) {
local $@;
unless (eval { $self->encode([$rows->[$i]]); 1 }) {
(my $msg = "$@") =~ s/\s*at .+ line \d+\.\s*\z//;
push @errors, { row => $i, error => $msg };
}
}
return \@errors;
}
sub compressed_writer {
my ($class, $mode, $writer) = @_;
return $writer if !defined $mode || $mode eq 'raw';
if ($mode eq 'zstd') {
require Compress::Zstd;
return sub {
my $out = Compress::Zstd::compress($_[0]);
defined $out or die "zstd compression failed";
$writer->($out);
};
}
if ($mode eq 'gzip') {
require IO::Compress::Gzip;
return sub {
my $out;
IO::Compress::Gzip::gzip(\$_[0], \$out)
or die "gzip failed: $IO::Compress::Gzip::GzipError";
$writer->($out);
};
}
die "Unknown compress mode '$mode' (expected 'zstd', 'gzip', or 'raw')";
}
lib/ClickHouse/Encoder.pm view on Meta::CPAN
=over 4
=item *
Integers: C<Int8>, C<Int16>, C<Int32>, C<Int64>, C<UInt8>, C<UInt16>,
C<UInt32>, C<UInt64>.
=item *
Floats: C<Float32>, C<Float64>, C<BFloat16> (CH 24.x; 2-byte truncated
Float32). C<Inf>, C<-Inf>, and C<NaN> are preserved.
=item *
Strings: C<String> (length-prefixed bytes), C<FixedString(N)> (N bytes,
null-padded). Both pass the SV's bytes through unchanged: a UTF-8 string
encodes its UTF-8 bytes, a binary blob encodes its bytes, and truncation
is by byte not codepoint.
=item *
Dates: C<Date>, C<Date32>, C<DateTime>, C<DateTime('tz')> (timezone is part
of the schema, not the value), C<DateTime64(P)> with C<P> in 0..9.
=item *
Decimals: C<Decimal32(S)> (S in 0..9), C<Decimal64(S)> (0..18),
C<Decimal128(S)> (0..38), C<Decimal256(S)> (0..76), and
C<Decimal(P, S)> with C<P> in 1..38 (auto-routed to 32/64/128).
=item *
Enums: C<Enum8('a' = 1, ...)>, C<Enum16(...)>.
=item *
C<Bool> / C<Boolean> (1 byte; truthy/falsy in Perl sense).
=item *
C<UUID> (16 bytes; accept either the standard
C<xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx> string or 16 raw bytes).
=item *
C<IPv4> (UInt32 LE; accept dotted-quad string or integer),
C<IPv6> (16 bytes network-order; accept colon-hex string or 16 raw bytes).
=item *
C<Map(K, V)> (wire-equivalent to C<Array(Tuple(K, V))>; accept either a
hashref or an arrayref of pairs).
=item *
C<Variant(T1, T2, ...)> (CH 24.1+; tagged union). Each row is either
C<undef> (null) or C<[$variant_idx, $value]>, where C<$variant_idx> is
the 0-based position in the declared C<Variant(...)> type list (so
C<Variant(String, UInt32)> uses 0 for String and 1 for UInt32). Up to 254
variants. ClickHouse stores Variant arms alphabetically by type name on
the wire; the encoder remaps your declared index transparently, so you
always use declaration order in your code. C<describe table> returns the
arms already alphabetized, and C<for_table> uses that form, so indices
match either way.
=item *
C<LowCardinality(String)>, C<LowCardinality(FixedString(N))>,
C<LowCardinality(Nullable(String))>, C<LowCardinality(Nullable(FixedString(N)))>.
=item *
Geo: C<Point>, C<Ring>, C<LineString>, C<MultiLineString>, C<Polygon>,
C<MultiPolygon> (aliases over C<Tuple>/C<Array>). L</parse_wkt> turns
a Well-Known-Text string into the nested-arrayref input these
columns expect.
=item *
Composites: C<Array(T)>, C<Tuple(T1, T2, ...)>, C<Nullable(T)>. Arbitrary
nesting; C<Nullable(Nullable(T))> is rejected, matching ClickHouse. Named
tuple elements (C<Tuple(a Int32, b String)>) are accepted; the wire
format itself ignores names, but for a named tuple the encoder will
also accept rows as hashrefs (C<<< { a => 1, b => "x" } >>>) in addition
to arrayrefs (C<[1, "x"]>). Missing keys in a hashref encode as the
type's null placeholder.
=item *
C<SimpleAggregateFunction(func, T)>: parsed as plain C<T>; the
aggregation function name only affects how readers consume the column,
not the wire-level binary format. Full C<AggregateFunction> (with
function-specific binary state) is not supported; use
C<SimpleAggregateFunction> where the inner type matches the on-the-wire
representation.
=item *
C<JSON> / C<Object('json')> (the stable CH 24.8+ JSON type). Each row
is a hashref of leaf values; nested hashrefs are auto-flattened into
dotted-path subcolumns matching CH's internal storage (so
C<<< { user => { name => "alice" } } >>> stores under the C<user.name>
path on the wire and round-trips back as the same nested structure
on decode). Leaf value typing is inferred per-path from Perl SV flags:
=over 4
=item *
Numeric SV with the integer flag set but not the float flag
(C<SvIOK && !SvNOK>) -> C<Int64>.
=item *
Numeric SV with the float flag set (C<SvNOK>): non-integer value ->
C<Float64>; a NV that happens to equal an integer (e.g. C<1.0>)
collapses to C<Int64> on the wire, mirroring CH's own C<JSONEachRow>
inference. Round-trip then decodes back as an integer; to keep an
integer-valued float as C<Float64> use a dedicated C<Float64> column
or a C<<< Variant(Float64, ...) >>>. C<NaN> and C<Inf> stay C<Float64>.
lib/ClickHouse/Encoder.pm view on Meta::CPAN
is integer-only).
=item *
C<Decimal*>: a number goes through C<double> (lossy past 2^53); a
B<string> matching C<[+-]?digits[.digits]?> is parsed digit-by-digit and
scaled exactly. B<If precision matters, pass strings.>
=item *
C<Enum8> / C<Enum16>: accept either the declared name or its integer
value; mixing the two within a column is fine.
=item *
C<Nullable(T)>: C<undef> writes a null-bitmap entry plus a type-shaped
placeholder (zero scalar, empty array, or a recursive zero tuple).
=back
=head1 EXAMPLES
The F<eg/> directory ships runnable scripts:
=over 4
=item F<eg/insert_http.pl>
End-to-end insert over HTTP via L<HTTP::Tiny>. The shortest path to "insert
real data into a real ClickHouse".
=item F<eg/insert_streaming.pl>
Reuse one encoder across many batches, piping each batch to
C<clickhouse-client>. Demonstrates the intended one-encoder-many-batches
pattern.
=item F<eg/for_table.pl>
Schema discovery via C<for_table>.
=item F<eg/from_csv.pl>
Read a CSV with L<Text::CSV_XS>, map columns to a ClickHouse schema, and
insert via HTTP.
=item F<eg/insert_clickhouse_local.pl>
Server-less ETL: encode rows, pipe Native bytes into C<clickhouse-local>,
have it write a Parquet (or ORC, etc.) file.
=item F<eg/etl_dbi.pl>
Read rows from a source database via L<DBI>, encode to Native, insert into
ClickHouse via HTTP. Reuses one encoder across all fetched batches.
=item F<eg/insert_compressed.pl>
insert with on-the-wire compression (zstd via L<Compress::Zstd>, falling
back to gzip via core L<IO::Compress::Gzip>). Sets C<Content-Encoding>
so ClickHouse decompresses transparently.
=item F<eg/insert_async_ev.pl>
Non-blocking concurrent inserts using L<EV>'s event loop with raw HTTP
sockets, paired with this encoder's L</streamer>. Demonstrates the
"many in-flight inserts without blocking on each round-trip" pattern.
=item F<eg/insert_with_lowcardinality.pl>
Measures the wire-size reduction (~50% on event/log data) and encoding
throughput of C<LowCardinality(String)> versus plain C<String> for the
typical case where a column has few distinct values that repeat across
many rows.
=item F<eg/json_lines_ingest.pl>
Reads NDJSON from STDIN or a file, maps each object's fields onto a
ClickHouse table's columns (discovered via C<for_table>), and inserts
batched blocks over HTTP.
=item F<eg/streaming_aggregate.pl>
Pre-aggregates an event stream in Perl (per minute, per key) and
flushes rolled-up counters to a C<SummingMergeTree> on a wall-clock
timer. The classic pattern when the firehose is too high-cardinality
to store as raw rows.
=item F<eg/postgres_to_clickhouse.pl>
Replicates a PostgreSQL table to ClickHouse using L<DBD::Pg> on the
source side and this encoder's streamer on the destination side.
Memory is bounded by the batch size, so it scales to hundreds of
millions of rows.
=item F<eg/clickhouse_replication.pl>
Replicates one ClickHouse table to another (potentially on a different
server) by streaming Native bytes end-to-end via a temp-file spool.
=item F<eg/parallel_loader.pl>
Forks N worker processes, each ingesting one slice of the input.
Workers share nothing; each opens its own HTTP connection. Scales
network-bound ingestion linearly with worker count.
=item F<eg/redis_to_clickhouse.pl>
Drains a Redis stream (XREADGROUP) or list (BRPOP) into a ClickHouse
table, with idle-flush so the destination doesn't see arbitrarily
delayed batches when the source is quiet.
=item F<eg/syslog_ingest.pl>
Reads RFC 5424 syslog lines from STDIN, parses lossily (any
unparseable line still goes through with the raw text in C<msg>),
and inserts into a fixed schema.
=item F<eg/json_streaming.pl>
NDJSON from STDIN into a C<JSON> column via the encoder's streaming
( run in 3.827 seconds using v1.01-cache-2.11-cpan-7fcb06a456a )