EV-ClickHouse
view release on metacpan or search on metacpan
lib/EV/ClickHouse.pm view on Meta::CPAN
package EV::ClickHouse;
use strict;
use warnings;
use EV;
use Scalar::Util qw(refaddr weaken);
# Identifier validation regexes â single source of truth for the table
# / column / function names that get spliced into SQL via the helpers.
my $RE_IDENT = qr/\A[A-Za-z_][A-Za-z0-9_]*\z/;
my $RE_TABLE = qr/\A[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?\z/;
BEGIN {
our $VERSION = '0.05';
use XSLoader;
XSLoader::load __PACKAGE__, $VERSION;
}
# Holds in-flight EV::cares resolvers so they aren't garbage-collected
# before their callback fires. Keyed by refaddr of the resolver itself
# (not the connection) â see new() for the rationale. Each entry is
# deleted from inside its own resolved callback via a deferred timer.
# Plain package hash (not Hash::Util::FieldHash) so module load doesn't
# add tied/magic SVs that Test::LeakTrace would flag.
our %_failover;
*q = \&query;
*reconnect = \&reset;
*disconnect = \&finish;
*ddl = \&query; # readability at call sites for DDL/DML
sub _uri_unescape { my $s = $_[0]; $s =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/ge; $s }
# Parse a URI query string into a hash. Bare keys (no `=`) are stored
# as 1 (standard URL flag convention); existing keys in $h are preserved
# (path-derived host/port/etc win over query-string overrides).
sub _uri_qs_into {
my ($qs, $h) = @_;
return unless defined $qs && length $qs;
for my $pair (split /&/, $qs) {
my ($k, $v) = split /=/, $pair, 2;
next unless defined $k && length $k;
$h->{$k} //= defined $v ? _uri_unescape($v) : 1;
}
}
sub new {
my ($class, %args) = @_;
# Connection URI: clickhouse://user:pass@host:port/database
# host accepts a bracketed IPv6 literal (e.g. clickhouse://[::1]:9000/db)
if (my $uri = delete $args{uri}) {
if ($uri =~ m{^clickhouse(?:\+(\w+))?://(?:([^:@]*?)(?::([^@]*))?\@)?(\[[^\]]+\]|[^/:?]+)(?::(\d+))?(?:/([^?]*))?(?:\?(.*))?$}) {
my ($proto, $u, $pw, $h, $p, $db, $qs) = ($1, $2, $3, $4, $5, $6, $7);
$h =~ s/^\[(.*)\]$/$1/;
$args{protocol} //= $proto if $proto;
$args{user} //= _uri_unescape($u) if defined $u && $u ne '';
$args{password} //= _uri_unescape($pw) if defined $pw;
$args{host} //= $h;
$args{port} //= $p if defined $p;
$args{database} //= _uri_unescape($db) if defined $db && $db ne '';
_uri_qs_into($qs, \%args);
} else {
die "EV::ClickHouse: invalid URI '$uri'\n";
}
}
lib/EV/ClickHouse.pm view on Meta::CPAN
my $setter = "_set_$opt";
$self->$setter($val);
}
for my $opt (qw(session_id tls_ca_file tls_cert_file tls_key_file)) {
defined(my $val = delete $args{$opt}) or next;
my $setter = "_set_$opt";
$self->$setter($val);
}
# query_log_comment: 1 = auto-generate "ev_ch user=$ENV{USER} pid=$$"; any
# other defined non-empty string (including "0") is taken literally;
# undef / not present / empty string is disabled.
{
my $qlc = delete $args{query_log_comment};
if (defined $qlc && length $qlc) {
my $cmt = (!ref($qlc) && "$qlc" ne '1')
? $qlc
: sprintf 'ev_ch user=%s pid=%d', $ENV{USER} // 'na', $$;
$cmt =~ s{\*/}{*\\/}g;
$self->_set_query_log_comment($cmt);
}
}
# decode_flags bitmask (DT_STR=1, DEC_SCALE=2, ENUM_STR=4, NAMED_ROWS=8)
my $decode_flags = (delete $args{decode_datetime} ? 1 : 0)
| (delete $args{decode_decimal} ? 2 : 0)
| (delete $args{decode_enum} ? 4 : 0)
| (delete $args{named_rows} ? 8 : 0);
$self->_set_decode_flags($decode_flags) if $decode_flags;
if (my $settings = delete $args{settings}) { $self->_set_settings($settings) }
warn "EV::ClickHouse->new: unknown parameter(s): " . join(', ', sort keys %args) . "\n"
if %args;
if ($hosts_list) {
$self->_set_failover($hosts_list, $port);
}
# Async DNS via EV::cares when available â non-IP hostnames are
# resolved off-loop so the constructor returns immediately and the
# main EV loop never blocks on getaddrinfo. Pre-connect-queued
# queries fire once the resolved-address connect completes. Falls
# back to the XS blocking resolver if EV::cares isn't installed
# or the host is already an IP literal.
if ($host !~ /^[\d.]+$|^\[?[0-9a-fA-F:]+\]?$/
&& eval { require EV::cares; 1 }) {
# Stash the resolver in %_failover, keyed by refaddr of the
# resolver itself (NOT of $self). Two reasons:
# - never delete the resolver from inside its own callback â
# ares_destroy from a c-ares cb corrupts the channel heap.
# We defer the delete via EV::timer(0,...) so it runs from
# a clean stack frame.
# - keying by refaddr($self) was racy: A's deferred-delete
# could fire after A's struct was freed and B got the same
# refaddr, dropping B's resolver. refaddr($r) is unique
# while $r is alive in %_failover.
my $r = EV::cares->new;
my $key = refaddr($r);
$_failover{$key} = $r;
my $weak2 = $self; weaken $weak2;
$self->_set_dns_pending(1);
$r->resolve($host, sub {
my ($status, @addrs) = @_;
my $w; $w = EV::timer(0, 0, sub { undef $w; delete $_failover{$key} });
# Skip if the connection has been DESTROYed or the user
# finished it while DNS was in flight (cleanup_connection
# clears dns_pending; if it's 0 here, finish ran already).
return unless $weak2 && $weak2->_take_dns_pending;
if ($status != 0 || !@addrs) {
$weak2->skip_pending;
# Warn if the handler itself throws â matches the XS
# emit_error path (WARN_AND_CLEAR_ERRSV); a bare eval here
# would make a DNS failure vanish under the default
# `on_error => sub { die @_ }`.
eval { $weak2->on_error->("DNS resolution failed for '$host'"); 1 }
or warn "EV::ClickHouse: exception in error handler: $@";
return;
}
my ($v4) = grep /^[\d.]+$/, @addrs;
$weak2->connect($v4 // $addrs[0], $port, $user, $password, $database);
});
} else {
$self->connect($host, $port, $user, $password, $database);
}
$self;
}
sub _split_host_port {
my ($entry, $default_port) = @_;
if ($entry =~ /^\[([^\]]+)\](?::(\d+))?$/) {
return ($1, $2 // $default_port); # IPv6 literal in brackets
}
if ($entry =~ /^([^:]+):(\d+)$/) {
return ($1, $2);
}
return ($entry, $default_port);
}
# Pull-based result iterator: $it = $ch->iterate($sql, [\%settings])
# while (my $batch = $it->next($timeout)) { ... }
# Wraps the native on_data per-block callback in a synchronous-feeling
# pull interface for procedural code. The iterator drives the EV loop
# from inside ->next until the next block arrives, the query completes,
# or the optional timeout (seconds) expires.
sub iterate {
my ($self, $sql, $settings) = @_;
my $it = bless {
ch => $self,
batches => [],
done => 0,
err => undef,
}, 'EV::ClickHouse::Iterator';
my $on_data = sub {
push @{ $it->{batches} }, $_[0];
EV::break;
};
my %s = $settings ? %$settings : ();
$s{on_data} = $on_data;
$self->query($sql, \%s, sub {
( run in 1.665 second using v1.01-cache-2.11-cpan-995e09ba956 )