App-Sqitch

 view release on metacpan or  search on metacpan

lib/App/Sqitch/Engine/clickhouse.pm  view on Meta::CPAN

package App::Sqitch::Engine::clickhouse;

use 5.010;
use strict;
use warnings;
use utf8;
use Try::Tiny;
use App::Sqitch::X qw(hurl);
use Locale::TextDomain qw(App-Sqitch);
use App::Sqitch::Plan::Change;
use Path::Class;
use Scalar::Util qw(looks_like_number);
use Moo;
use App::Sqitch::Types qw(DBH URIDB ArrayRef Str HashRef);
use namespace::autoclean;
use List::MoreUtils qw(firstidx);

extends 'App::Sqitch::Engine';

our $VERSION = 'v1.6.1'; # VERSION

has uri => (
    is       => 'ro',
    isa      => URIDB,
    lazy     => 1,
    default  => \&_setup_uri,
);

sub _setup_uri {
    my $self = shift;
    my $uri = $self->SUPER::uri;
    my $cfg = $self->_clickcnf;
    if (!$uri->host && (my $host = $ENV{CLICKHOUSE_HOST} || $cfg->{host})) {
        $uri->host($host);
    }
    if (!$uri->dbname && (my $db = $cfg->{database})) {
        $uri->dbname($db);
    }

    # Use HTTPS port if CLI using native TLS port.
    # https://clickhouse.com/docs/guides/sre/network-ports
    $uri->port(8443) if !$uri->_port && ($cfg->{port} || 0) == 9440;

    # Always require secure connections when required.
    # https://github.com/ClickHouse/ClickHouse/blob/faf6d05/src/Client/ConnectionParameters.cpp#L27-L43
    if (
        $cfg->{secure}
        || ($cfg->{port} || 0) == 9440 # assume both native and http should be secure or not.
        || ($uri->host || '') =~ /\.clickhouse(?:-staging)?\.cloud\z/
    ) {
        $uri->query_param( SSLMode => 'require' )
            unless $uri->query_param( 'SSLMode' );
    }

    # Add ODBC params for TLS configs.
    # https://clickhouse.com/docs/operations/server-configuration-parameters/settings
    # https://github.com/clickHouse/clickhouse-odbc?tab=readme-ov-file#configuration
    if ( my $tls = $cfg->{tls} ) {
        for my $map (
            [ privateKeyFile  => 'PrivateKeyFile'  ],
            [ certificateFile => 'CertificateFile' ],
            [ caConfig        => 'CALocation'      ],
        ) {
            if ( my $val = $tls->{ $map->[0] } ) {
                if ( my $p = $uri->query_param( $map->[1] ) ) {
                    # Ideally the ODBC param would override the config,
                    # bug there is currently no way to pass TLS options to
                    # the CLI.
                    hurl engine => __x(
                        'Client config {cfg_key} value "{cfg_val}" conflicts with ODBC param {odb_param} value "{odbc_val}"',
                        cfg_key    => "openSSL.client.$map->[0]",
                        cfg_val    => $val,
                        odbc_param => $map->[1],
                        odbc_val   => $p,
                    ) if $p ne $val;
                }
                $uri->query_param( $map->[1] => $val );
            }
        }

        # verificationMode | SSLMode
        # -----------------|---------------
        # none             | [nonexistent]
        # relaxed          | allow
        # strict           | require
        # once             | require
        if (
            (my $mode = $tls->{verificationMode})
            && !$uri->query_param( 'SSLMode' )
        ) {
            if ($mode eq 'strict' || $mode eq 'once') {

lib/App/Sqitch/Engine/clickhouse.pm  view on Meta::CPAN

# or else localhost. Then look for that name in a connection under
# `connections_credentials`. If it exists, copy/overwrite `hostname`, `port`,
# `secure`, `user`, `password`, and `database`. Fall back on root object
# values `host` (not `hostname`) `port`, `secure`, `user`, `password`, and
# `database`.
#
# https://github.com/ClickHouse/ClickHouse/blob/d0facf0/programs/client/Client.cpp#L139-L212
sub _conn_cfg {
    my ($cfg, $host) = @_;

    # Copy root-level configs.
    my $conn = {
        (exists $cfg->{secure} ? (secure => _is_true $cfg->{secure}) : ()),
        map { ( $_ => $cfg->{$_}) } grep { $cfg->{$_} } qw(host port user password database),
    };

    # Copy client TLS config if exists.
    if (my $tls = $cfg->{openSSL}) {
        $conn->{tls} = $tls->{client} if $tls->{client};
    }

    # Copy connection credentials for this host if they exists.
    $host ||= $cfg->{host} || 'localhost';
    my $creds = $cfg->{connections_credentials} or return $conn;
    my $conns = $creds->{connection} or return $conn;
    for my $c (@{ ref $conns eq 'ARRAY' ? $conns : [$conns] }) {
        next unless ($c->{name} || '') eq $host;
        if (exists $c->{secure}) {
            $conn->{secure} = _is_true $c->{secure}
        }
        $conn->{host} = $c->{hostname} if $c->{hostname};
        $conn->{$_} = $c->{$_} for grep { $c->{$_} } qw(port user password database);
    }
    return $conn;
}

has _clickcnf => (
    is      => 'rw',
    isa     => HashRef,
    lazy    => 1,
    default => \&_load_cfg,
);

sub _load_cfg {
    my $self = shift;
    # https://clickhouse.com/docs/interfaces/cli#configuration_files
    # https://github.com/ClickHouse/ClickHouse/blob/master/src/Common/Config/getClientConfigPath.cpp
    for my $spec (
        ['.', 'clickhouse-client'],
        [App::Sqitch::Config->home_dir, '.clickhouse-client'],
        ['etc', 'clickhouse-client'],
    ) {
        for my $ext (qw(xml yaml yml)) {
            my $path = file $spec->[0], "$spec->[1].$ext";
            next unless -f $path;
            my $config = $ext eq 'xml' ? _load_xml $path : do {
                require YAML::Tiny;
                YAML::Tiny->read($path)->[0];
            };
            # We want the hostname specified by the user, if present.
            my $host = $ENV{CLICKHOUSE_HOST} || $self->SUPER::uri->host;
            return _conn_cfg $config, $host;
        }
    }
    return {};
}

sub _def_user { $ENV{CLICKHOUSE_USER}     || $_[0]->_clickcnf->{user}     }
sub _def_pass { $ENV{CLICKHOUSE_PASSWORD} || shift->_clickcnf->{password} }

sub _dsn {
    # Always set the host name to the default if it's not set. Otherwise
    # URI::db::_odbc returns the DSN `dbi:ODBC:DSN=sqitch;Driver=ClickHouse`.
    # We don't want that, because no such DSN exists. By setting the host
    # name, it instead returns
    # `dbi:ODBC:Server=localhost;Database=sqitch;Driver=ClickHouse`, almost
    # certainly more correct.
    my $uri = shift->registry_uri;
    unless ($uri->host) {
        $uri = $uri->clone;
        $uri->host('localhost');
    }
    return $uri->dbi_dsn
}

has dbh => (
    is      => 'rw',
    isa     => DBH,
    lazy    => 1,
    default => sub {
        my $self = shift;
        $self->use_driver;
        return DBI->connect($self->_dsn, $self->username, $self->password, {
            PrintError   => 0,
            RaiseError   => 0,
            AutoCommit   => 1,
            HandleError  => $self->error_handler,
            odbc_utf8_on => 1,
        });
    }
);

has _ts_default => (
    is      => 'ro',
    isa     => Str,
    lazy    => 1,
    default => sub { q{now64(6, 'UTC')} },
);

# Need to wait until dbh and _ts_default are defined.
with 'App::Sqitch::Role::DBIEngine';

has _cli => (
    is      => 'ro',
    isa     => ArrayRef,
    lazy    => 1,
    default => \&_load_cli,
);

sub _load_cli {
    my $self = shift;



( run in 0.612 second using v1.01-cache-2.11-cpan-5b529ec07f3 )