EV-ClickHouse
view release on metacpan or search on metacpan
lib/EV/ClickHouse.pm view on Meta::CPAN
=over 4
=item * C<($arrayref_of_arrayrefs)> for select with at least one row
=item * C<(undef)> for DDL/DML on success and for select with zero rows
(both protocols). When in doubt, treat C<undef> and C<[]> equivalently
with C<my @rows = @{$rows // []};>.
=item * C<(undef, $error_message)> on error (server exception or
connection error)
=back
The optional C<\%settings> hashref passes per-query ClickHouse settings
(C<max_execution_time>, C<max_threads>, C<async_insert>, etc.), overriding
connection-level defaults.
The following keys are intercepted by the client and not sent verbatim
to the server:
=over 4
=item C<params =E<gt> \%hash>
Parameterized values for C<{name:Type}> placeholders in the SQL. Encoding
and quoting is the server's job, so values do not need escaping:
$ch->query(
"select * from t where id = {id:UInt64} and name = {n:String}",
{ params => { id => 42, n => "O'Brien" } },
sub { ... },
);
Works on both protocols (HTTP uses URL-encoded C<param_*> query string;
native uses dedicated wire fields).
=item C<query_id =E<gt> $string>
Set the protocol-level query identifier. Retrievable later via
L</last_query_id>.
=item C<raw =E<gt> 1>
HTTP only. The callback receives the raw response body as a scalar string
instead of parsed rows. Use with an explicit C<format> clause:
$ch->query("select * from t format CSV", { raw => 1 }, sub {
my ($body, $err) = @_;
});
Croaks if used with the native protocol.
=item C<query_timeout =E<gt> $seconds>
Per-query timeout, overriding the connection-level C<query_timeout>.
=item C<on_data =E<gt> sub { my ($rows) = @_; ... }>
Native protocol only. A code ref called for each data block as it arrives,
for streaming large result sets. Rows are delivered incrementally and
B<not> accumulated, so the final callback receives C<(undef)> rather than
all rows. The final callback always fires on completion or error, even if
no data block was emitted (empty result, server-side error before the
first block).
$ch->query("select * from big_table",
{ on_data => sub { my ($rows) = @_; process_batch($rows) } },
sub { my (undef, $err) = @_; warn $err if $err },
);
=item C<external =E<gt> \%tables>
Native protocol only. Ships one or more in-memory data blocks that the
query can reference as tables, JOIN against, or filter with C<IN> -
without creating a server-side temporary table. Each entry maps a table
name to C<{ structure =E<gt> [...], data =E<gt> [...] }>:
$ch->query(
"select u.id, u.name from users u where u.id in _wanted",
{ external => {
_wanted => {
structure => [ id => 'UInt64' ],
data => [ [7], [42], [911] ],
},
} },
sub { my ($rows, $err) = @_; ... },
);
C<structure> is a flat list of C<name =E<gt> type> pairs (ClickHouse type
names, e.g. C<UInt64>, C<String>, C<Float64>); C<data> is an arrayref of
row arrayrefs, encoded with the same type machinery as L</insert>. An
empty C<data> arrayref is a valid zero-row table. Several external tables
may be supplied at once. Croaks on the HTTP protocol or on a malformed
spec (odd structure list, non-arrayref row, or a column type that
cannot be encoded).
=back
B<Native protocol type notes:> values come back as typed Perl scalars.
By default C<Date>/C<DateTime> are integers (days since epoch / Unix
timestamps); enable C<decode_datetime> for strings. C<Enum> values are
numeric codes; C<decode_enum> returns labels. C<Decimal> values are
unscaled integers; C<decode_decimal> scales them to floats.
C<SimpleAggregateFunction> is transparently decoded as its inner type.
C<Nested> columns become arrays of tuples. C<LowCardinality> works
correctly across multi-block results with shared dictionaries.
=head2 insert
$ch->insert($table, $data, sub { my (undef, $err) = @_ });
$ch->insert($table, $data, \%settings, sub { my (undef, $err) = @_ });
C<$data> may be either:
=over 4
=item * A pre-formatted TabSeparated string (tabs separate columns,
newlines separate rows, with the standard ClickHouse escapes).
=item * An arrayref of arrayrefs (rows of column values).
lib/EV/ClickHouse.pm view on Meta::CPAN
common transient failure that warrants automatic retry: timeouts,
network errors, memory pressure, replica catch-up, keeper exceptions,
etc. Authoritative-looking source list curated against ClickHouse's
C<src/Common/ErrorCodes.cpp>; expect the set to grow conservatively.
$ch->query($sql, sub {
my ($r, $err) = @_;
if ($err && EV::ClickHouse->is_retryable_error($ch->last_error_code)) {
schedule_retry($sql);
}
});
=head2 server_supports
$ch->server_supports($feature_name)
Returns true if the live native server's protocol revision is high
enough to support the given feature. Feature names map to documented
protocol-revision thresholds so user code can branch cleanly on
capability instead of hard-coding revision numbers. Supported names:
block_info 51903 block_info packet in DATA blocks
server_display_name 54372 ServerHello carries display name
version_patch 54401 ServerHello carries patch version
progress_writes 54420 Progress packets include write counters
server_timezone 54423 Server timezone string in ServerHello
addendum 54458 Native ClientHello addendum block
HTTP connections have no protocol revision (C<server_revision> is C<0>),
so C<server_supports> returns false on HTTP for any feature. Unknown
feature names also return false. Use C<server_revision> directly if you
need the raw integer.
=head2 for_table
$ch->for_table('events', sub {
my ($info, $err) = @_;
die $err if $err;
for my $col (@{ $info->{columns} }) {
printf "%-20s %s\n", $col->{name}, $col->{type};
}
});
Schema introspection: issues C<describe table $name> and delivers
C<{ columns =E<gt> [{name=E<gt>..., type=E<gt>...}, ...] }> to the
callback. Useful for generic insert pipelines that need column types
without hard-coding them. C<$name> may be C<table> or C<db.table>;
non-identifier characters are rejected up-front.
=head2 iterate
my $it = $ch->iterate("select number from numbers(1_000_000)");
while (my $batch = $it->next($timeout)) {
process($_) for @$batch;
}
die $it->error if $it->error;
B<Native protocol only> - relies on the per-block C<on_data> hook and
will croak if invoked on an HTTP connection.
Synchronous-feeling pull iterator over a streaming select. Internally
wraps the native C<on_data> per-block callback and drives the EV loop
from inside C<-E<gt>next> until the next block arrives, the query
completes, or the optional timeout (seconds) expires. Useful for
procedural ETL / export code that doesn't fit a callback shape.
C<-E<gt>error>, C<-E<gt>is_done>, and C<-E<gt>cancel> are also
available on the returned iterator object.
=head2 on_log
$ch->on_log(sub {
my ($entry) = @_;
# $entry: { event_time, host_name, query_id, thread_id,
# priority, source, text }
printf "[CH %s] %s\n", $entry->{priority}, $entry->{text};
});
Native protocol only. Fires once per row inside any C<SERVER_LOG>
packet the server emits. Useful for surfacing
C<send_logs_level =E<gt> 'information'> server-side trace events to
the application's own log stream without polling C<system.text_log>.
The row hash keys mirror the server-side log block schema; missing
keys (older revisions) come through as C<undef>.
=head2 on_query_start
$ch->on_query_start(sub {
my ($query_id) = @_;
log_metric_start($query_id);
});
Optional connection-level hook that fires the moment a query is
dispatched to the wire (after the query_id has been resolved, before
the first send byte). Symmetric with L</on_query_complete>; useful for
deriving accurate "query in flight" durations without depending on
the per-query callback closure. Keepalive PINGs are suppressed, the
same as for C<on_query_complete>. Also accepted as a constructor
argument.
=head2 on_query_complete
$ch->on_query_complete(sub {
my ($query_id, $rows, $bytes, $error_code, $duration_s, $err) = @_;
log_metric(...);
});
Optional connection-level hook that fires after every query (success
or error). Arguments: query_id (or undef), profile_rows, profile_bytes,
last_error_code, wall-clock duration in seconds, error message (or
undef). Useful for statsd/Prometheus-style instrumentation. Also
accepted as a constructor argument.
A per-query override may be passed in the C<\%settings> hashref of
L</query> or L</insert>. When set, it B<replaces> (does not augment)
the connection-level handler for that single call, so per-query
instrumentation doesn't double-count against global metrics:
$ch->query(
$sql,
{ on_query_complete => sub {
my ($qid, $rows, $bytes, $code, $dur, $err) = @_;
record_slow_query($qid, $dur);
} },
$cb,
);
=head2 insert_streamer
my $s = $ch->insert_streamer('events',
batch_size => 5_000,
settings => { query_id => 'ingest-1' }, # optional
on_batch_error => sub { warn "batch err: $_[0]" }, # per-failure
);
while (my $row = next_event()) {
$s->push_row($row);
}
$s->finish(sub {
my (undef, $err) = @_;
die "ingest failed: $err" if $err;
});
Buffered streaming insert for ETL workloads. Rows are buffered until
C<batch_size> is reached, then dispatched as a single C<insert()>.
Dispatches are serialised; push_row keeps buffering while a batch is
in flight (the native protocol cannot pipeline INSERTs). C<finish>
flushes the remaining buffer and fires its callback once all batches
complete; if any batch failed the first error is delivered as
C<$err>. The streamer also offers C<buffered_count> and C<in_flight>
accessors for backpressure logic.
C<<< $streamer->reset >>> discards any rows still in the local buffer
and clears the sticky error so the streamer can be reused after a
permanent error (e.g. a schema fix). Does B<not> touch the underlying
C<$ch> - any batch already on the wire still completes normally. Any
callback registered via C<finish> or C<await_drain> that has not yet
fired is invoked with a C<'streamer reset'> error rather than being
silently dropped.
C<high_water> + C<on_high_water> trigger a one-shot notification when
the buffered row count crosses the watermark, intended as a hint to
slow the producer. Set C<high_water> below C<batch_size>; if
C<high_water E<gt> batch_size>, the buffer drains via C<batch_size>
flushes before the watermark is reached and C<on_high_water> never
fires. When C<high_water == batch_size> the watermark and the flush
threshold coincide: C<on_high_water> fires once per batch, right after
the flush dispatches (so C<in_flight> is already 1). The notification
re-arms only after the buffer drops below C<high_water>.
C<<< $streamer->await_drain($cb) >>> registers a callback that fires
once the buffer drops to C<low_water> (default C<high_water / 2>; 0
when C<high_water> isn't set). Pairs with C<on_high_water> to close
the backpressure loop:
my $s = $ch->insert_streamer($table,
batch_size => 5_000,
high_water => 10_000,
low_water => 3_000,
on_high_water => sub { $producer->pause },
);
$s->await_drain(sub { $producer->resume });
Fires synchronously if the buffer is already at/below C<low_water>.
Re-arms each call: register again from inside the callback if you
want to keep watching. The callback receives one argument: C<undef>
on a normal drain, or an error string if the streamer was C<reset>
before the buffer drained (so C<my ($err) = @_> distinguishes them).
B<Named-row mode:> pass C<columns =E<gt> [@col_names]> at construction
to accept hashref rows instead of positional arrayrefs. The streamer
reorders each pushed hash into the declared column order, so producer
code does not have to know where each column lives in the table.
my $s = $ch->insert_streamer('events',
columns => [qw(ts user_id action payload)],
batch_size => 5_000,
);
$s->push_row({ user_id => 7, action => 'click', ts => time });
$s->push_row([ 1735, 7, 'view', '...' ]); # arrayref still works
Hash keys missing from a row become C<undef>; extra keys are ignored.
Mixing arrayref and hashref pushes is allowed.
lib/EV/ClickHouse.pm view on Meta::CPAN
=item Insert silently dropped (counts don't match)
Likely C<insert_deduplication_token> dedupe; either you're reusing a token
across distinct batches, or the table is C<ReplicatedMergeTree> with the
default dedupe window. See F<eg/idempotent_insert.pl>.
=item Hangs on connect when host is a hostname
Without L<EV::cares>, DNS resolution falls back to blocking
C<getaddrinfo>. Install L<EV::cares> for non-blocking lookup; otherwise
use an IP literal or a local caching resolver (nscd / systemd-resolved).
=item C<connect_timeout> doesn't fire
It does across TCP connect, TLS handshake, and native ServerHello. If
the timer doesn't fire, the underlying issue is usually a synchronous
DNS stall (see above) which happens before C<start_connect> arms the
timer; install L<EV::cares> to push DNS off the loop.
=item Per-query C<query_timeout> is ignored
Set it inside the C<\%settings> hashref, not as a top-level argument:
C<<< $ch->query($sql, { query_timeout =E<gt> 5 }, $cb) >>>.
=item Which host am I currently pointed at after failover?
C<<< $ch->current_host >>> and C<<< $ch->current_port >>> reflect the
live target after a multi-host rotation. Use C<<< on_failover =E<gt>
sub { ... } >>> to get notified at the moment of each rotation.
=item How do I retry only on transient errors?
C<<< EV::ClickHouse->is_retryable_error($code) >>> returns true for the
common transient codes (timeouts, network errors, replica catch-up,
keeper exceptions, ...). Inspect C<<< $ch->last_error_code >>> from
inside your query callback and schedule a retry only when the predicate
fires - permanent errors (auth failures, missing tables) won't qualify.
Sample skeleton:
$ch->query($sql, sub {
my ($r, $err) = @_;
if ($err && EV::ClickHouse->is_retryable_error($ch->last_error_code)) {
schedule_retry($sql);
} elsif ($err) { warn "permanent: $err" }
});
=item Idempotent insert silently drops some rows
C<<< idempotent =E<gt> 1 >>> auto-mints
C<insert_deduplication_token>; if your producer issues the SAME logical
batch twice (e.g. retry after a transient network blip) only the first
write lands, by design. To force two distinct logical batches through,
either pass an explicit C<<< idempotent =E<gt> $token >>> per batch or
omit the option for fresh inserts. See F<eg/idempotent_insert.pl>.
=item C<on_data> vs C<iterate> - which should I pick?
C<<< on_data =E<gt> sub { } >>> in the per-query settings is the
lowest-overhead streaming path: each native data block is delivered as
soon as the parser has it, no per-row allocation overhead beyond the
batch arrayref. C<iterate> is a synchronous-feeling pull wrapper around
the same machinery - useful when the surrounding code is procedural
(ETL scripts, exporters) and a callback shape doesn't fit. Both are
native-only.
=item Connection in front of nginx / reverse proxy strips X-ClickHouse-* headers
Pass C<<< http_basic_auth =E<gt> 1 >>> to send the credentials as
C<Authorization: Basic ...> instead. Most HTTP gateways forward
Authorization verbatim while filtering proprietary headers.
=back
=head1 TUNING
=over 4
=item Native vs HTTP
Native (port 9000) is typically 2-5x faster for insert and select-of-many-rows
because rows ship as binary columns instead of TSV text. Use HTTP only when
the network path requires HTTPS-only or when you need C<raw =E<gt> 1> CSV /
JSONEachRow / Parquet bodies.
=item C<compress =E<gt> 1>
Enables LZ4 (native) or gzip (HTTP). LZ4 cost is small and saves ~50-70%
on text-heavy columns. Gzip is heavier; turn on only if you're bandwidth-bound.
=item C<insert_streamer> batch_size
Default 10_000 is a good baseline. Smaller (1k-2k) reduces memory pressure
on the producer; larger (50k-100k) reduces server-side merge cost on
MergeTree. Match to your row width: ~1 MB per batch is a sweet spot.
=item C<keepalive>
Enable on long-lived idle connections (HTTP behind a load balancer or
NAT, or a native connection that may sit minutes between queries). 15-30s
is typical.
=item C<reconnect_max_attempts>
Always set in production. Default is unlimited; a permanent failure
(wrong host, wrong port, dead server) will spin C<on_error> forever
otherwise.
=item C<progress_period>
Coalesce on_progress packets to one fire per N seconds. Big SELECTs can
emit hundreds per second; throttle to 1-5s for monitoring dashboards.
=item Pull-iterator vs C<on_data>
C<on_data> has lower per-block overhead. C<iterate> trades that for a
synchronous-feeling API; use it when the surrounding code is procedural.
=item C<EV::ClickHouse::Pool>
( run in 1.386 second using v1.01-cache-2.11-cpan-995e09ba956 )