From eebd6caf3a65475b6aea82f2286bcc2572ee6734 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 16:23:46 +0600 Subject: [PATCH 01/58] fix --- Gemfile | 2 + Gemfile.lock | 21 ++ bin/clickhouse-server | 19 ++ lib/tasks/clickhouse_connect.rake | 17 ++ lib/umbrellio-utils.rb | 4 + lib/umbrellio_utils.rb | 3 + lib/umbrellio_utils/click_house.rb | 178 ++++++++++++++++ lib/umbrellio_utils/database.rb | 18 +- lib/umbrellio_utils/migrations.rb | 271 ++++++++++++++++++++++++ lib/umbrellio_utils/misc.rb | 5 + lib/umbrellio_utils/sql.rb | 118 +++++++++++ spec/support/clickhouse.rb | 23 ++ spec/support/database.rb | 11 + spec/support/sequel_patches.rb | 4 +- spec/umbrellio_utils/clickhouse_spec.rb | 121 +++++++++++ spec/umbrellio_utils/migrations_spec.rb | 163 ++++++++++++++ spec/umbrellio_utils/sql_spec.rb | 149 +++++++++++++ 17 files changed, 1118 insertions(+), 9 deletions(-) create mode 100755 bin/clickhouse-server create mode 100644 lib/tasks/clickhouse_connect.rake create mode 100644 lib/umbrellio_utils/click_house.rb create mode 100644 lib/umbrellio_utils/migrations.rb create mode 100644 lib/umbrellio_utils/sql.rb create mode 100644 spec/support/clickhouse.rb create mode 100644 spec/umbrellio_utils/clickhouse_spec.rb create mode 100644 spec/umbrellio_utils/migrations_spec.rb create mode 100644 spec/umbrellio_utils/sql_spec.rb diff --git a/Gemfile b/Gemfile index 797fd4c..59ad9ef 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,8 @@ gemspec gem "activesupport" gem "bundler" gem "ci-helper" +gem "click_house", github: "umbrellio/click_house", branch: "master" +gem "csv" gem "http" gem "net-pop" gem "nokogiri" diff --git a/Gemfile.lock b/Gemfile.lock index cbcddd6..4113c00 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,12 @@ +GIT + remote: https://github.com/umbrellio/click_house.git + revision: 1bbf8cb909a248b401d0ba9a9f6f1de2e2c068db + branch: master + specs: + click_house (2.1.2) + activesupport + faraday (>= 1.7, < 3) + PATH remote: . specs: @@ -100,6 +109,7 @@ GEM concurrent-ruby (1.3.5) connection_pool (2.5.4) crass (1.0.6) + csv (3.3.5) date (3.4.1) diff-lcs (1.6.2) docile (1.4.1) @@ -109,6 +119,12 @@ GEM erb (4.0.4) cgi (>= 0.3.3) erubi (1.13.1) + faraday (2.14.0) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.2) + net-http (~> 0.5) ffi (1.17.2-aarch64-linux-gnu) ffi (1.17.2-aarch64-linux-musl) ffi (1.17.2-arm-linux-gnu) @@ -163,6 +179,8 @@ GEM method_source (1.1.0) mini_mime (1.1.5) minitest (5.25.5) + net-http (0.8.0) + uri (>= 0.11.1) net-imap (0.5.10) date net-protocol @@ -364,6 +382,7 @@ GEM unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.1.0) + uri (1.1.1) useragent (0.16.11) websocket-driver (0.8.0) base64 @@ -388,6 +407,8 @@ DEPENDENCIES activesupport bundler ci-helper + click_house! + csv http net-pop nokogiri diff --git a/bin/clickhouse-server b/bin/clickhouse-server new file mode 100755 index 0000000..af3bd25 --- /dev/null +++ b/bin/clickhouse-server @@ -0,0 +1,19 @@ +#!/usr/bin/env -S bash -eu + +docker stop clickhouse-server || true +docker rm clickhouse-server || true + +docker run \ + --detach \ + --network host \ + --name clickhouse-server \ + --ulimit nofile=262144:262144 \ + $CLICKHOUSE_IMAGE_TAG + +# Wait for ClickHouse server to become available +until docker exec clickhouse-server clickhouse-client --query "SELECT 1" &>/dev/null; do + echo "Waiting for ClickHouse to be ready..." + sleep 1 +done + +rails ch:create ch:migrate diff --git a/lib/tasks/clickhouse_connect.rake b/lib/tasks/clickhouse_connect.rake new file mode 100644 index 0000000..dbdc5ff --- /dev/null +++ b/lib/tasks/clickhouse_connect.rake @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +namespace :ch do + desc "run clickhouse client" + task connect: :environment do + params = { + host: ENV.fetch("CLICKHOUSE_HOST", UmbrellioUtils::ClickHouse.config.host), + user: ENV.fetch("CLICKHOUSE_USER", UmbrellioUtils::ClickHouse.config.username), + password: ENV.fetch("CLICKHOUSE_PASSWORD", UmbrellioUtils::ClickHouse.config.password), + database: ENV.fetch("CLICKHOUSE_DATABASE", UmbrellioUtils::ClickHouse.config.database), + **UmbrellioUtils::ClickHouse.config.global_params, + }.compact_blank + + cmd = Shellwords.join(["clickhouse", "client", *params.map { |k, v| "--#{k}=#{v}" }]) + exec(cmd) + end +end diff --git a/lib/umbrellio-utils.rb b/lib/umbrellio-utils.rb index 37910e1..91d9c47 100644 --- a/lib/umbrellio-utils.rb +++ b/lib/umbrellio-utils.rb @@ -1,3 +1,7 @@ # frozen_string_literal: true require_relative "umbrellio_utils" + +if defined?(Rake) + Dir[File.join(__dir__, "tasks/**/*.rake")].each { |f| load f } +end diff --git a/lib/umbrellio_utils.rb b/lib/umbrellio_utils.rb index d7da4ce..8b922a3 100644 --- a/lib/umbrellio_utils.rb +++ b/lib/umbrellio_utils.rb @@ -48,18 +48,21 @@ def synchronize(&) require_relative "umbrellio_utils/cards" require_relative "umbrellio_utils/checks" +require_relative "umbrellio_utils/click_house" require_relative "umbrellio_utils/constants" require_relative "umbrellio_utils/control" require_relative "umbrellio_utils/database" require_relative "umbrellio_utils/formatting" require_relative "umbrellio_utils/http_client" require_relative "umbrellio_utils/jobs" +require_relative "umbrellio_utils/migrations" require_relative "umbrellio_utils/misc" require_relative "umbrellio_utils/parsing" require_relative "umbrellio_utils/passwords" require_relative "umbrellio_utils/random" require_relative "umbrellio_utils/request_wrapper" require_relative "umbrellio_utils/rounding" +require_relative "umbrellio_utils/sql" require_relative "umbrellio_utils/semantic_logger/tiny_json_formatter" require_relative "umbrellio_utils/store" require_relative "umbrellio_utils/vault" diff --git a/lib/umbrellio_utils/click_house.rb b/lib/umbrellio_utils/click_house.rb new file mode 100644 index 0000000..d062443 --- /dev/null +++ b/lib/umbrellio_utils/click_house.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +module UmbrellioUtils + module ClickHouse + class << self + include Memery + + delegate :create_database, :drop_database, :config, to: :client + + def insert(table_name, db_name: self.db_name, rows: []) + client.insert(full_table_name(table_name, db_name), rows, format: "JSONEachRow") + end + + def from(source, db_name: self.db_name) + ds = + case source + when Symbol + DB.from(db_name == self.db_name ? SQL[source] : SQL[db_name][source]) + when nil + DB.dataset + else + DB.from(source) + end + + ds.clone(ch: true) + end + + def execute(sql, host: nil, **opts) + log_errors(sql) do + client(host).execute(sql, params: opts) + end + end + + def query(dataset, host: nil, **) + sql = sql_for(dataset) + + log_errors(sql) do + select_all(sql, host:, **).map { Misc::StrictHash[it.symbolize_keys] } + end + end + + def query_value(dataset, host: nil, **) + sql = sql_for(dataset) + + log_errors(sql) do + select_value(sql, host:, **) + end + end + + def count(dataset) + query_value(dataset.select(SQL.ch_count)) + end + + def optimize_table!(table_name, db_name: self.db_name) + execute("OPTIMIZE TABLE #{db_name}.#{table_name} ON CLUSTER click_cluster FINAL") + end + + def truncate_table!(table_name, db_name: self.db_name) + execute("TRUNCATE TABLE #{db_name}.#{table_name} ON CLUSTER click_cluster SYNC") + end + + def drop_table!(table_name, db_name: self.db_name) + execute("DROP TABLE #{db_name}.#{table_name} ON CLUSTER click_cluster SYNC") + end + + def describe_table(table_name, db_name: self.db_name) + sql = "DESCRIBE TABLE #{full_table_name(table_name, db_name)} FORMAT JSON" + + log_errors(sql) do + select_all(sql).map { Misc::StrictHash[it.symbolize_keys] } + end + end + + def db_name + client.config.database.to_sym + end + + def parse_value(value, type:) + case type + when /String/ + value&.to_s + when /DateTime/ + Time.zone.parse(value) if value + else + value + end + end + + def server_version + select_value("SELECT version()").to_f + end + + def pg_table_connection(table) + host = DB.opts[:host].presence || "localhost" + port = DB.opts[:port] || 5432 + database = DB.opts[:database] || ENV.fetch("USER") + username = DB.opts[:user] || ENV.fetch("USER") + password = DB.opts[:password] || ENV.fetch("PASSWORD") + + Sequel.function(:postgresql, "#{host}:#{port}", database, table, username, password) + end + + def with_temp_table(dataset, primary_key: [:id], primary_key_types: [:integer], temp_table_name:, **, &) + UmbrellioUtils::Database.create_temp_table(nil, primary_key:, primary_key_types:, temp_table_name:, &) + populate_temp_table!(temp_table_name, dataset) + UmbrellioUtils::Database.with_temp_table(nil, primary_key:, temp_table_name:, **, &) + end + + private + + def client(host = nil) + cfg = ::ClickHouse.config + cfg.host = resolve(host) if host + ::ClickHouse::Connection.new(cfg) + end + memoize :client, ttl: 1.minute + + def resolve(host) + IPSocket.getaddress(host) + rescue => e + Exceptions.notify!(e, raise_errors: false) + config.host + end + + def logger + client.config.logger + end + + def log_errors(sql) + yield + rescue ::ClickHouse::Error => e + logger.error("ClickHouse error: #{e.inspect}\nSQL: #{sql}") + raise e + end + + def sql_for(dataset) + unless ch_dataset?(dataset) + raise "Non-ClickHouse dataset: #{dataset.inspect}. You should use `CH.from` instead of `DB`" + end + + dataset.sql + end + + def ch_dataset?(dataset) + case dataset + when Sequel::Dataset + dataset.opts[:ch] && Array(dataset.opts[:from]).all? { ch_dataset?(it) } + when Sequel::SQL::AliasedExpression + ch_dataset?(dataset.expression) + when Sequel::SQL::Identifier, Sequel::SQL::QualifiedIdentifier + true + else + raise "Unknown dataset type: #{dataset.inspect}" + end + end + + def full_table_name(table_name, db_name) + table_name = table_name.value if table_name.is_a?(Sequel::SQL::Identifier) + "#{db_name}.#{table_name}" + end + + def select_all(sql, host: nil, **) + response = client(host).get(body: sql, query: { default_format: "JSON", ** }) + ::ClickHouse::Response::Factory.response(response, client(host).config) + end + + def select_value(...) + select_all(...).first.to_a.dig(0, -1) + end + + def populate_temp_table!(temp_table_name, dataset) + execute(<<~SQL.squish) + INSERT INTO TABLE FUNCTION #{DB.literal(pg_table_connection(temp_table_name))} #{dataset.sql} + SQL + end + end + end +end diff --git a/lib/umbrellio_utils/database.rb b/lib/umbrellio_utils/database.rb index 9a35dc6..a9fa4bf 100644 --- a/lib/umbrellio_utils/database.rb +++ b/lib/umbrellio_utils/database.rb @@ -79,26 +79,28 @@ def with_temp_table( end # rubocop:enable Metrics/ParameterLists - def create_temp_table(dataset, primary_key: nil, temp_table_name: nil) + def create_temp_table(dataset, primary_key: nil, primary_key_types: nil, temp_table_name: nil) time = Time.current - model = dataset.model + query_table_name = dataset&.model&.table_name - temp_table_name ||= :"temp_#{model.table_name}_#{time.to_i}_#{time.nsec}" + temp_table_name ||= :"temp_#{query_table_name}_#{time.to_i}_#{time.nsec}" return temp_table_name if DB.table_exists?(temp_table_name) primary_key = primary_key_from(dataset, primary_key:) + primary_key_types ||= primary_key.map { |x| dataset.model.db_schema[x][:db_type] } DB.create_table(temp_table_name, unlogged: true) do - primary_key.each do |field| - type = model.db_schema[field][:db_type] - column(field, type) + primary_key.each.with_index do |field, i| + column(field, primary_key_types[i]) end primary_key(primary_key) end - insert_ds = dataset.select(*qualified_pk(model.table_name, primary_key)) - DB[temp_table_name].disable_insert_returning.insert(insert_ds) + unless dataset.nil? + insert_ds = dataset.select(*qualified_pk(query_table_name, primary_key)) + DB[temp_table_name].disable_insert_returning.insert(insert_ds) + end temp_table_name end diff --git a/lib/umbrellio_utils/migrations.rb b/lib/umbrellio_utils/migrations.rb new file mode 100644 index 0000000..66cb954 --- /dev/null +++ b/lib/umbrellio_utils/migrations.rb @@ -0,0 +1,271 @@ +# frozen_string_literal: true + +module UmbrellioUtils + module Migrations # rubocop:disable Metrics/ModuleLength + extend self + + def create_new_id_bigint_column(table_name) + DB.run(<<~SQL.squish) + LOCK TABLE #{table_name} IN ACCESS EXCLUSIVE MODE; + + CREATE OR REPLACE FUNCTION id_trigger() + RETURNS trigger + AS + $BODY$ + DECLARE + BEGIN + NEW.id_bigint := NEW.id; + RETURN NEW; + END; + $BODY$ LANGUAGE plpgsql; + + ALTER TABLE #{table_name} ADD id_bigint BIGINT; + + CREATE TRIGGER #{table_name}_bigint + BEFORE INSERT OR UPDATE + ON #{table_name} + FOR EACH ROW + EXECUTE FUNCTION id_trigger(); + SQL + end + + def drop_old_id_column(table_name, associations = {}, skip_fk_create: false) # rubocop:disable Metrics/MethodLength + query_start = <<~SQL.squish + LOCK TABLE #{table_name} IN ACCESS EXCLUSIVE MODE; + DROP TRIGGER #{table_name}_bigint ON #{table_name}; + ALTER TABLE #{table_name} RENAME id TO id_integer; + ALTER TABLE #{table_name} RENAME id_bigint TO id; + + CREATE SEQUENCE IF NOT EXISTS new_#{table_name}_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + SELECT setval( + 'new_#{table_name}_id_seq', + COALESCE((SELECT MAX(id) + 1 FROM #{table_name}), 1), + false + ); + ALTER TABLE #{table_name} + ALTER COLUMN id SET DEFAULT nextval('new_#{table_name}_id_seq'); + SQL + + fkey_query = "" + associations.map do |assoc_table, assoc_name| + constraint_name = "#{assoc_table}_#{assoc_name}_fkey" + + fkey_query += <<~SQL.squish + ALTER TABLE #{assoc_table} + DROP CONSTRAINT IF EXISTS #{constraint_name} + SQL + if skip_fk_create + fkey_query += ";" + next + end + + fkey_query += <<~SQL.squish + , ADD CONSTRAINT #{constraint_name} + FOREIGN KEY (#{assoc_name}) REFERENCES #{table_name}(id) NOT VALID; + SQL + end + + query_end = <<~SQL.squish + ALTER TABLE #{table_name} DROP id_integer; + ALTER TABLE #{table_name} ADD CONSTRAINT #{table_name}_pkey PRIMARY KEY + USING INDEX #{table_name}_id_bigint_index; + SQL + + query = query_start + fkey_query + query_end + DB.run(query) + end + + def drop_foreign_keys(_table_name, associations) + associations.map do |assoc_table, assoc_name| + constraint_name = "#{assoc_table}_#{assoc_name}_fkey" + fkey_query = <<~SQL.squish + ALTER TABLE #{assoc_table} DROP CONSTRAINT IF EXISTS #{constraint_name}; + SQL + DB.run(fkey_query) + end + end + + def create_foreign_keys(table_name, associations) + associations.map do |assoc_table, assoc_name| + constraint_name = "#{assoc_table}_#{assoc_name}_fkey" + fkey_query = <<~SQL.squish + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = '#{constraint_name}' + ) THEN + ALTER TABLE #{assoc_table} ADD CONSTRAINT #{constraint_name} + FOREIGN KEY (#{assoc_name}) REFERENCES #{table_name}(id) NOT VALID; + END IF; + END$$; + SQL + DB.run(fkey_query) + end + end + + def create_new_foreign_key_column(table_name, column_name) + DB.run(<<~SQL.squish) + LOCK TABLE #{table_name} IN ACCESS EXCLUSIVE MODE; + + CREATE OR REPLACE FUNCTION #{column_name}_bigint_trigger() + RETURNS trigger + AS + $BODY$ + DECLARE + BEGIN + NEW.#{column_name}_bigint := NEW.#{column_name}; + RETURN NEW; + END; + $BODY$ LANGUAGE plpgsql; + + ALTER TABLE #{table_name} ADD #{column_name}_bigint BIGINT; + + CREATE TRIGGER #{table_name}_#{column_name}_bigint + BEFORE INSERT OR UPDATE + ON #{table_name} + FOR EACH ROW + EXECUTE FUNCTION #{column_name}_bigint_trigger(); + SQL + end + + def check_id_consistency(table_name, col_name = "id") + res = DB[table_name].where( + Sequel[col_name.to_sym] !~ SQL.coalesce(Sequel[:"#{col_name}_bigint"], 0), + ).count + raise "Inconsistent ids in #{table_name}: #{res} records" if res.positive? + true + end + + # rubocop:disable Metrics/MethodLength + def drop_old_foreign_key_column(table_name, column_name, skip_constraint: false, + primary_key: [], uniq_constr: false) + query_start = <<~SQL.squish + LOCK TABLE #{table_name} IN ACCESS EXCLUSIVE MODE; + DROP TRIGGER #{table_name}_#{column_name}_bigint ON #{table_name}; + ALTER TABLE #{table_name} RENAME #{column_name} TO #{column_name}_integer; + ALTER TABLE #{table_name} RENAME #{column_name}_bigint TO #{column_name}; + SQL + + fkey_query = "" + unless skip_constraint + constraint_name = "#{table_name}_#{column_name}_fkey" + ref_table_name = column_name.to_s.delete_suffix("_id").pluralize + fkey_query = <<~SQL.squish + ALTER TABLE #{table_name} + DROP CONSTRAINT IF EXISTS #{constraint_name}, + ADD CONSTRAINT #{constraint_name} + FOREIGN KEY (#{column_name}) REFERENCES #{ref_table_name}(id) NOT VALID; + SQL + end + + drop_query = <<~SQL.squish + ALTER TABLE #{table_name} DROP #{column_name}_integer; + SQL + + constr_query = "" + if uniq_constr + constr_query = <<~SQL.squish + ALTER TABLE #{table_name} + ADD CONSTRAINT #{table_name}_#{column_name}_key UNIQUE (#{column_name}); + SQL + end + + pkey_query = "" + if primary_key.present? + pkey_query = <<~SQL.squish + ALTER TABLE #{table_name} ADD CONSTRAINT #{table_name}_pkey PRIMARY KEY + USING INDEX #{table_name}_#{primary_key.join("_")}_index; + SQL + end + + query = query_start + fkey_query + drop_query + constr_query + pkey_query + DB.run(query) + end + # rubocop:enable Metrics/MethodLength + + def check_associations(model, method, reverse_method) + model.dataset.limit(10).all.each do |record| + res = record.public_send(method).public_send(reverse_method) + raise StandardError if res.blank? + end + true + end + + def create_distributed_table!(table_name, sharding_key, db_name: UmbrellioUtils::ClickHouse.db_name) + UmbrellioUtils::ClickHouse.execute(<<~SQL.squish) + DROP TABLE IF EXISTS #{db_name}.#{table_name}_distributed + ON CLUSTER click_cluster + SQL + + UmbrellioUtils::ClickHouse.execute(<<~SQL.squish) + CREATE TABLE #{db_name}.#{table_name}_distributed + ON CLUSTER click_cluster + AS #{db_name}.#{table_name} + ENGINE = Distributed(click_cluster, #{db_name}, #{table_name}, #{sharding_key}) + SQL + end + + # @example + # add_columns_to_view( + # "orders_clickhouse_view", + # Sequel[:orders][:data].pg_jsonb.get_text("some_data_column").as(:some_column), + # Sequel[:orders][:column].as(:some_other_column), + # ) + def add_columns_to_view(view_name, *sequel_columns) + sequel_columns.each do |column| + unless column.is_a?(Sequel::SQL::AliasedExpression) + raise ArgumentError.new("not Sequel::SQL::AliasedExpression") + end + end + + DB.transaction do + DB.run("LOCK TABLE #{view_name}") + definition = view_definition(view_name) + sql = sequel_columns.map { |x| DB.literal(x) }.join(", ") + new_definition = definition.sub("FROM", ", #{sql} FROM") + DB.run("CREATE OR REPLACE VIEW #{view_name} AS #{new_definition}") + end + end + + # @example + # drop_columns_from_view("orders_clickhouse_view", "id", "guid") + def drop_columns_from_view(view_name, *columns) + DB.transaction do + DB.run("LOCK TABLE #{view_name}") + definition = view_definition(view_name) + parsed_columns = parse_columns(definition) + parsed_columns.reject! { |name, _| name.in?(columns) } + sql = parsed_columns.map { |_, sql| sql }.join(", ") + new_definition = definition.sub(/SELECT(.*?)FROM/i, "SELECT #{sql} FROM") + DB.run("DROP VIEW #{view_name}") + DB.run("CREATE VIEW #{view_name} AS #{new_definition}") + end + end + + private + + def parse_columns(definition) + fields_sql = definition[/SELECT(.*?)FROM/i, 1].strip + fields = fields_sql.scan(/(?:[^,(]+|\([^)]*\))+/).map(&:strip) + field_names = fields.map do |field| + field[/as (.*)/i, 1] || field[/\.(.*)\z/, 1] + end + field_names.zip(fields) + end + + def view_definition(view) + DB[:pg_views] + .where(viewname: view.to_s) + .select(:definition).first[:definition] + .squish + end + end +end diff --git a/lib/umbrellio_utils/misc.rb b/lib/umbrellio_utils/misc.rb index 62e157b..d66713e 100644 --- a/lib/umbrellio_utils/misc.rb +++ b/lib/umbrellio_utils/misc.rb @@ -4,6 +4,11 @@ module UmbrellioUtils module Misc extend self + class StrictHash < Hash + alias get [] + alias [] fetch + end + def table_sync(scope, delay: 1, routing_key: nil) scope.in_batches do |batch| batch_for_sync = batch.all.reject { |model| model.try(:skip_table_sync?) } diff --git a/lib/umbrellio_utils/sql.rb b/lib/umbrellio_utils/sql.rb new file mode 100644 index 0000000..3c446a0 --- /dev/null +++ b/lib/umbrellio_utils/sql.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +module UmbrellioUtils + module SQL + extend self + + UniqueConstraintViolation = Sequel::UniqueConstraintViolation + + def [](*) + Sequel[*] + end + + def func(...) + Sequel.function(...) + end + + def cast(...) + Sequel.cast(...) + end + + def case(...) + Sequel.case(...) + end + + def pg_jsonb(...) + Sequel.pg_jsonb(...) + end + + def and(*conditions) + Sequel.&(*Array(conditions.flatten.presence || true)) + end + + def not(...) + Sequel.~(...) + end + + def or(*conditions) + Sequel.|(*Array(conditions.flatten.presence || true)) + end + + def range(from, to, **) + Sequel::Postgres::PGRange.new(from, to, **) + end + + def max(expr) + func(:max, expr) + end + + def min(expr) + func(:min, expr) + end + + def sum(expr) + func(:sum, expr) + end + + def count(expr = nil) + expr ? func(:count, expr) : func(:count).* + end + + def ch_count(*) + Sequel.function(:count, *) + end + + def avg(expr) + func(:avg, expr) + end + + def pg_percentile(expr, percentile) + func(:percentile_cont, percentile).within_group(expr) + end + + def pg_median(expr) + pg_percentile(expr, 0.5) + end + + def ch_median(expr) + func(:median, expr) + end + + def abs(expr) + func(:abs, expr) + end + + def coalesce(*exprs) + func(:coalesce, *exprs) + end + + def least(*exprs) + func(:least, *exprs) + end + + def greatest(*exprs) + func(:greatest, *exprs) + end + + def date_trunc(truncate, expr) + func(:date_trunc, truncate.to_s, expr) + end + + def ch_timestamp(time) + time&.strftime("%F %T.%6N") + end + + def ch_timestamp_expr(time) + time = Time.zone.parse(time) if time.is_a?(String) + SQL.func(:toDateTime64, SQL[ch_timestamp(time)], 6) + end + + def true + Sequel.lit("true") + end + + def false + Sequel.lit("false") + end + end +end diff --git a/spec/support/clickhouse.rb b/spec/support/clickhouse.rb new file mode 100644 index 0000000..1cc087b --- /dev/null +++ b/spec/support/clickhouse.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "logger" +require "csv" +require "click_house" + +config = ClickHouse.config do |config| + config.assign(host: "localhost") + config.logger = Logger.new("log/ch.log") +end + +client = ClickHouse::Connection.new(config) +client.execute(<<~SQL) + CREATE DATABASE IF NOT EXISTS umbrellio_utils_test; +SQL + +config.database = "umbrellio_utils_test" + +client.execute(<<~SQL) + CREATE TABLE IF NOT EXISTS test (id Int32) + ENGINE = MergeTree() + ORDER BY id; +SQL diff --git a/spec/support/database.rb b/spec/support/database.rb index 36125a1..5ef33a1 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -16,6 +16,8 @@ Sequel::Model.db = DB DB.extension :batches +DB.extension :pg_json +DB.extension :pg_range DB.drop_table? :users, cascade: true DB.create_table :users do @@ -68,3 +70,12 @@ def skip_table_sync? false end end + +class TestMigration < Sequel::Model(:test_migrations) + one_to_many :test_migration_references, + class_name: "TestMigrationReference", key: :test_migration_id, primary_key: :id +end + +class TestMigrationReference < Sequel::Model(:test_migration_references) + many_to_one :test_migration, class_name: "TestMigration", key: :test_migration_id +end diff --git a/spec/support/sequel_patches.rb b/spec/support/sequel_patches.rb index 45a90e3..0558bee 100644 --- a/spec/support/sequel_patches.rb +++ b/spec/support/sequel_patches.rb @@ -6,8 +6,10 @@ class Sequel::Postgres::Dataset # rubocop:disable Lint/ConstantDefinitionInBlock def select_sql return super if @opts[:_skip_order_patch] + return super if @opts[:ch] && @opts[:order].present? order = @opts[:order].dup || [] - order << Sequel.function(:random) + fn = @opts.key?(:ch) ? :rand : :random + order << Sequel.function(fn) clone(order:, _skip_order_patch: true).select_sql end end diff --git a/spec/umbrellio_utils/clickhouse_spec.rb b/spec/umbrellio_utils/clickhouse_spec.rb new file mode 100644 index 0000000..49bf402 --- /dev/null +++ b/spec/umbrellio_utils/clickhouse_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +describe UmbrellioUtils::ClickHouse do + let(:ch) { described_class } + + before do + ch.truncate_table!("test") + ch.insert("test", rows: [{ id: 1 }, { id: 2 }, { id: 3 }]) + ch.optimize_table!("test") # just for coverage + end + + describe "#from" do + context "with another db" do + specify do + expect(ch.from(:test, db_name: :test).sql).to eq( + 'SELECT * FROM "test"."test" ORDER BY rand()', + ) + end + end + + context "with nil" do + specify do + expect(ch.from(nil).sql).to eq("SELECT * ORDER BY rand()") + end + end + + context "with another source" do + specify do + expect(ch.from(ch.from(:test)).sql).to eq( + 'SELECT * FROM (SELECT * FROM "test" ORDER BY rand()) AS "t1" ORDER BY rand()'.b, + ) + end + end + end + + describe "#query" do + specify do + query = ch.from(:test).order(:id).select(:id) + expect(ch.query(query)).to eq([{ id: 1 }, { id: 2 }, { id: 3 }]) + end + end + + describe "#query_value" do + specify do + query = ch.from(:test).order(:id).select(:id) + expect(ch.query_value(query)).to eq(1) + end + end + + describe "#count" do + specify do + query = ch.from(:test) + expect(ch.count(query)).to eq(3) + end + end + + describe "#describe_table" do + specify do + expect(ch.describe_table("test")).to eq( + [ + codec_expression: "", + comment: "", + default_expression: "", + default_type: "", + name: "id", + ttl_expression: "", + type: "Int32", + ], + ) + end + end + + describe "#db_name" do + specify do + expect(ch.db_name).to eq(:umbrellio_utils_test) + end + end + + describe "#server_version" do + specify do + expect(ch.server_version).to match(Numeric) + end + end + + describe "#with_temp_table" do + specify do + result = [] + dataset = ch.from(:test).order(:id) + ch.with_temp_table(dataset, temp_table_name: "some_test_table", page_size: 1) do |batch| + result << batch + end + expect(result).to eq([[3], [2], [1]]) + end + end + + describe "#execute" do + after { ch.drop_table!("test2") } + + specify do + ch.execute("CREATE TABLE test2 (id Int64) ENGINE=MergeTree() ORDER BY id;") + end + end + + describe "#parse_value" do + it "parses string" do + expect(ch.parse_value("123", type: "String")).to eq("123") + end + + it "parses nil as string" do + expect(ch.parse_value(nil, type: "String")).to be_nil + end + + it "parses time" do + expect(ch.parse_value("2020-01-01", type: "DateTime")).to eq(Time.zone.parse("2020-01-01")) + end + + it "parses integer" do + expect(ch.parse_value(123, type: "Int32")).to eq(123) + end + end +end diff --git a/spec/umbrellio_utils/migrations_spec.rb b/spec/umbrellio_utils/migrations_spec.rb new file mode 100644 index 0000000..cbc4976 --- /dev/null +++ b/spec/umbrellio_utils/migrations_spec.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +describe UmbrellioUtils::Migrations do + before do + DB.drop_table? :test_migrations, cascade: true + DB.create_table :test_migrations do + primary_key :id + column :test, :text + end + + DB.run("CREATE OR REPLACE VIEW test_migrations_view AS SELECT id from users") + + DB.drop_table? :test_migration_references + DB.create_table :test_migration_references do + primary_key :id + foreign_key :test_migration_id, :test_migrations + end + end + + before do + DB[:test_migrations].multi_insert(test_data) + DB[:test_migration_references].multi_insert(test_reference_data) + end + + let(:test_data) do + Array.new(10) { |index| Hash[id: index + 1, test: index.to_s] } + end + + let(:test_reference_data) do + Array.new(10) { |index| Hash[id: index + 1, test_migration_id: index + 1] } + end + + context "with migrate to bigint" do + def expected_foreign_key + DB.foreign_key_list(:test_migration_references).first + end + + def check_contains_fk! + expect(expected_foreign_key).to include(columns: [:test_migration_id], table: :test_migrations) + end + + def check_contains_no_fk! + expect(expected_foreign_key).to be_nil + end + + let(:associations) { Hash[test_migration_references: :test_migration_id] } + + it "migrates to bigint column" do + described_class.create_new_id_bigint_column(:test_migrations) + expect(DB[:test_migrations].columns).to eq(%i[id test id_bigint]) # creates id_bigint column + + # contains trigger which copy from id column + DB[:test_migrations].insert(id: 11, test: 11) + expect(DB[:test_migrations].first(id: 11)).to eq(id: 11, test: "11", id_bigint: 11) + + DB[:test_migrations].update(id_bigint: :id) + DB.alter_table(:test_migrations) { add_index :id_bigint, unique: true } + described_class.check_id_consistency(:test_migrations) + + described_class.drop_old_id_column(:test_migrations, associations) + DB[:test_migrations].send(:clear_columns_cache) + expect(DB[:test_migrations].columns).to eq(%i[test id]) + expect(DB.schema(:test_migrations).to_h[:id]).to include(db_type: "bigint", primary_key: true) + check_contains_fk! + end + + it "updates foreign key to bigint" do + described_class.create_new_foreign_key_column(:test_migration_references, :test_migration_id) + expect(DB[:test_migration_references].columns).to eq( # creates bigint column + %i[id test_migration_id test_migration_id_bigint], + ) + + # contains trigger which copy from test_migration_id column + DB[:test_migrations].insert(id: 11, test: 11) + DB[:test_migration_references].insert(id: 11, test_migration_id: 11) + expect(DB[:test_migration_references].first(id: 11)).to eq( + id: 11, test_migration_id: 11, test_migration_id_bigint: 11, + ) + + DB[:test_migration_references].update(test_migration_id: :id) + + described_class.drop_old_foreign_key_column(:test_migration_references, :test_migration_id) + DB[:test_migration_references].send(:clear_columns_cache) + expect(DB[:test_migration_references].columns).to eq(%i[id test_migration_id]) + expect(DB[:test_migration_references].first(id: 11)).to eq(id: 11, test_migration_id: 11) + type = DB.schema(:test_migration_references).to_h.dig(:test_migration_id, :db_type) + expect(type).to eq("bigint") + check_contains_fk! + end + + context "with skip create fk" do + it "doesn't create fk" do + described_class.create_new_id_bigint_column(:test_migrations) + DB[:test_migrations].update(id_bigint: :id) + DB.alter_table(:test_migrations) { add_index :id_bigint, unique: true } + described_class.drop_old_id_column(:test_migrations, associations, skip_fk_create: true) + + check_contains_no_fk! + end + end + + context "with drop and create foreign keys" do + specify do + described_class.drop_foreign_keys(:test_migration_references, associations) + check_contains_no_fk! + described_class.create_foreign_keys(:test_migrations, associations) + check_contains_fk! + end + end + end + + describe "#check_id_consistency" do + specify do + described_class.create_new_id_bigint_column(:test_migrations) + expect { described_class.check_id_consistency(:test_migrations) }.to raise_error( + RuntimeError, /Inconsistent ids in test_migrations: 10 records/ + ) + end + end + + describe "#check_associations" do + specify do + result = described_class.check_associations( + TestMigrationReference, :test_migration, :test_migration_references + ) + expect(result).to be_truthy + end + + context "with invalid" do + it "raises error" do + stub_const("TestMigrationReference", Class.new(Sequel::Model(:test_migrations)) do + def test_migration + Struct.new(:test_migration_references).new(test_migration_references: []) + end + end) + + expect do + described_class.check_associations( + TestMigrationReference, :test_migration, :test_migration_references + ) + end.to raise_error(StandardError) + end + end + end + + describe "#create_distributed_table" do + specify do + described_class.create_distributed_table!("test", "id") + expect(UmbrellioUtils::ClickHouse.describe_table("test_distributed")).to be_present + end + end + + context "with view" do + specify do + described_class.add_columns_to_view("test_migrations_view", Sequel[:email].as(:test)) + expect(DB[:test_migrations_view].columns).to eq(%i[id test]) + + described_class.drop_columns_from_view("test_migrations_view", "test") + DB[:test_migrations_view].send(:clear_columns_cache) + expect(DB[:test_migrations_view].columns).to eq(%i[id]) + end + end +end diff --git a/spec/umbrellio_utils/sql_spec.rb b/spec/umbrellio_utils/sql_spec.rb new file mode 100644 index 0000000..948a1d7 --- /dev/null +++ b/spec/umbrellio_utils/sql_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +describe UmbrellioUtils::SQL do + let(:sql) { described_class } + + subject(:result) { DB.literal(expr) } + + describe "#[]" do + let(:expr) { sql[:test] } + + specify { expect(result).to eq('"test"') } + end + + describe "#func" do + let(:expr) { sql.func(:some_function, :test) } + + specify { expect(result).to eq('some_function("test")') } + end + + describe "#cast" do + let(:expr) { sql.cast(:test, :integer) } + + specify { expect(result).to eq('CAST("test" AS integer)') } + end + + describe "#case" do + let(:expr) { sql.case({ sql[:test] =~ sql[:test2] => 1 }, 2) } + + specify { expect(result).to eq('(CASE WHEN ("test" = "test2") THEN 1 ELSE 2 END)') } + end + + describe "#pg_jsonb" do + let(:expr) { sql.pg_jsonb(test: 123) } + + specify { expect(result).to eq(%('{"test":123}'::jsonb)) } + end + + describe "#and" do + let(:expr) { sql.and({ test: 123 }, { test2: 321 }) } + + specify { expect(result).to eq('(("test" = 123) AND ("test2" = 321))') } + end + + describe "#or" do + let(:expr) { sql.or({ test: 123 }, { test2: 321 }) } + + specify { expect(result).to eq('(("test" = 123) OR ("test2" = 321))') } + end + + describe "#range" do + let(:expr) { sql.range(Time.zone.parse("2014-01-01"), Time.zone.parse("2015-01-01")) } + + specify do + expect(result).to eq("'[2014-01-01 00:00:00.000000+0000,2015-01-01 00:00:00.000000+0000]'") + end + end + + %w[max min sum avg abs coalesce least greatest].each do |function| + describe "##{function}" do + let(:expr) { sql.public_send(function, :test) } + + specify { expect(result).to eq(%(#{function}("test"))) } + end + end + + describe "#count" do + context "with expression" do + let(:expr) { sql.count(:test) } + + specify { expect(result).to eq('count("test")') } + end + + context "without expression" do + let(:expr) { sql.count } + + specify { expect(result).to eq("count(*)") } + end + end + + describe "#ch_count" do + let(:expr) { sql.ch_count(:test) } + + specify { expect(result).to eq('count("test")') } + end + + describe "#pg_percentile" do + let(:expr) { sql.pg_percentile(:test, 0.1) } + + specify { expect(result).to eq('percentile_cont(0.1) WITHIN GROUP (ORDER BY "test")') } + end + + describe "#pg_median" do + let(:expr) { sql.pg_median(:test) } + + specify { expect(result).to eq('percentile_cont(0.5) WITHIN GROUP (ORDER BY "test")') } + end + + describe "#ch_median" do + let(:expr) { sql.ch_median(:test) } + + specify { expect(result).to eq('median("test")') } + end + + describe "#date_trunc" do + let(:expr) { sql.date_trunc("hour", :test) } + + specify { expect(result).to eq(%(date_trunc('hour', "test"))) } + end + + describe "#ch_timestamp" do + context "with time" do + let(:expr) { sql.ch_timestamp(Time.zone.parse("2020-01-01")) } + + specify { expect(result).to eq("'2020-01-01 00:00:00.000000'") } + end + + context "with nil" do + let(:expr) { sql.ch_timestamp(nil) } + + specify { expect(result).to eq("NULL") } + end + end + + describe "#ch_timestamp_expr" do + context "with time" do + let(:expr) { sql.ch_timestamp_expr(Time.zone.parse("2020-01-01")) } + + specify { expect(result).to eq("toDateTime64('2020-01-01 00:00:00.000000', 6)") } + end + + context "with string" do + let(:expr) { sql.ch_timestamp_expr("2020-01-01") } + + specify { expect(result).to eq("toDateTime64('2020-01-01 00:00:00.000000', 6)") } + end + end + + describe "#true" do + let(:expr) { sql.true } + + specify { expect(result).to eq("true") } + end + + describe "#false" do + let(:expr) { sql.false } + + specify { expect(result).to eq("false") } + end +end From 86f554d02dc36c58b031fe4115633cd7628fe2dd Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 16:37:30 +0600 Subject: [PATCH 02/58] fix lint --- .rubocop.yml | 3 +++ lib/umbrellio_utils/click_house.rb | 32 +++++++++++++++---------- lib/umbrellio_utils/database.rb | 2 -- lib/umbrellio_utils/migrations.rb | 2 +- lib/umbrellio_utils/sql.rb | 12 +++++----- spec/umbrellio_utils/migrations_spec.rb | 4 +++- 6 files changed, 32 insertions(+), 23 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 1367e0a..783ce52 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,6 +12,9 @@ Naming/MethodParameterName: RSpec/EmptyLineAfterHook: Enabled: false +Metrics/ModuleLength: + Enabled: false + Naming/FileName: Exclude: - lib/umbrellio-utils.rb diff --git a/lib/umbrellio_utils/click_house.rb b/lib/umbrellio_utils/click_house.rb index d062443..35fa648 100644 --- a/lib/umbrellio_utils/click_house.rb +++ b/lib/umbrellio_utils/click_house.rb @@ -31,19 +31,19 @@ def execute(sql, host: nil, **opts) end end - def query(dataset, host: nil, **) + def query(dataset, host: nil, **opts) sql = sql_for(dataset) log_errors(sql) do - select_all(sql, host:, **).map { Misc::StrictHash[it.symbolize_keys] } + select_all(sql, host:, **opts).map { |x| Misc::StrictHash[x.symbolize_keys] } end end - def query_value(dataset, host: nil, **) + def query_value(dataset, host: nil, **opts) sql = sql_for(dataset) log_errors(sql) do - select_value(sql, host:, **) + select_value(sql, host:, **opts) end end @@ -67,7 +67,7 @@ def describe_table(table_name, db_name: self.db_name) sql = "DESCRIBE TABLE #{full_table_name(table_name, db_name)} FORMAT JSON" log_errors(sql) do - select_all(sql).map { Misc::StrictHash[it.symbolize_keys] } + select_all(sql).map { |x| Misc::StrictHash[x.symbolize_keys] } end end @@ -100,10 +100,14 @@ def pg_table_connection(table) Sequel.function(:postgresql, "#{host}:#{port}", database, table, username, password) end - def with_temp_table(dataset, primary_key: [:id], primary_key_types: [:integer], temp_table_name:, **, &) - UmbrellioUtils::Database.create_temp_table(nil, primary_key:, primary_key_types:, temp_table_name:, &) + def with_temp_table( + dataset, temp_table_name:, primary_key: [:id], primary_key_types: [:integer], **opts, & + ) + UmbrellioUtils::Database.create_temp_table( + nil, primary_key:, primary_key_types:, temp_table_name:, & + ) populate_temp_table!(temp_table_name, dataset) - UmbrellioUtils::Database.with_temp_table(nil, primary_key:, temp_table_name:, **, &) + UmbrellioUtils::Database.with_temp_table(nil, primary_key:, temp_table_name:, **opts, &) end private @@ -135,7 +139,8 @@ def log_errors(sql) def sql_for(dataset) unless ch_dataset?(dataset) - raise "Non-ClickHouse dataset: #{dataset.inspect}. You should use `CH.from` instead of `DB`" + raise "Non-ClickHouse dataset: #{dataset.inspect}. " \ + "You should use `CH.from` instead of `DB`" end dataset.sql @@ -144,7 +149,7 @@ def sql_for(dataset) def ch_dataset?(dataset) case dataset when Sequel::Dataset - dataset.opts[:ch] && Array(dataset.opts[:from]).all? { ch_dataset?(it) } + dataset.opts[:ch] && Array(dataset.opts[:from]).all? { |x| ch_dataset?(x) } when Sequel::SQL::AliasedExpression ch_dataset?(dataset.expression) when Sequel::SQL::Identifier, Sequel::SQL::QualifiedIdentifier @@ -159,8 +164,8 @@ def full_table_name(table_name, db_name) "#{db_name}.#{table_name}" end - def select_all(sql, host: nil, **) - response = client(host).get(body: sql, query: { default_format: "JSON", ** }) + def select_all(sql, host: nil, **opts) + response = client(host).get(body: sql, query: { default_format: "JSON", **opts }) ::ClickHouse::Response::Factory.response(response, client(host).config) end @@ -170,7 +175,8 @@ def select_value(...) def populate_temp_table!(temp_table_name, dataset) execute(<<~SQL.squish) - INSERT INTO TABLE FUNCTION #{DB.literal(pg_table_connection(temp_table_name))} #{dataset.sql} + INSERT INTO TABLE FUNCTION #{DB.literal(pg_table_connection(temp_table_name))} + #{dataset.sql} SQL end end diff --git a/lib/umbrellio_utils/database.rb b/lib/umbrellio_utils/database.rb index a9fa4bf..2d1c1d7 100644 --- a/lib/umbrellio_utils/database.rb +++ b/lib/umbrellio_utils/database.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# rubocop:disable Metrics/ModuleLength module UmbrellioUtils module Database extend self @@ -156,4 +155,3 @@ def pop_next_pk_batch(temp_table_name, primary_key, batch_size) end end end -# rubocop:enable Metrics/ModuleLength diff --git a/lib/umbrellio_utils/migrations.rb b/lib/umbrellio_utils/migrations.rb index 66cb954..0b8fa2f 100644 --- a/lib/umbrellio_utils/migrations.rb +++ b/lib/umbrellio_utils/migrations.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module UmbrellioUtils - module Migrations # rubocop:disable Metrics/ModuleLength + module Migrations extend self def create_new_id_bigint_column(table_name) diff --git a/lib/umbrellio_utils/sql.rb b/lib/umbrellio_utils/sql.rb index 3c446a0..1a6bbec 100644 --- a/lib/umbrellio_utils/sql.rb +++ b/lib/umbrellio_utils/sql.rb @@ -6,8 +6,8 @@ module SQL UniqueConstraintViolation = Sequel::UniqueConstraintViolation - def [](*) - Sequel[*] + def [](*args) + Sequel[*args] end def func(...) @@ -38,8 +38,8 @@ def or(*conditions) Sequel.|(*Array(conditions.flatten.presence || true)) end - def range(from, to, **) - Sequel::Postgres::PGRange.new(from, to, **) + def range(from_value, to_value, **opts) + Sequel::Postgres::PGRange.new(from_value, to_value, **opts) end def max(expr) @@ -58,8 +58,8 @@ def count(expr = nil) expr ? func(:count, expr) : func(:count).* end - def ch_count(*) - Sequel.function(:count, *) + def ch_count(*args) + Sequel.function(:count, *args) end def avg(expr) diff --git a/spec/umbrellio_utils/migrations_spec.rb b/spec/umbrellio_utils/migrations_spec.rb index cbc4976..2895ced 100644 --- a/spec/umbrellio_utils/migrations_spec.rb +++ b/spec/umbrellio_utils/migrations_spec.rb @@ -36,7 +36,9 @@ def expected_foreign_key end def check_contains_fk! - expect(expected_foreign_key).to include(columns: [:test_migration_id], table: :test_migrations) + expect(expected_foreign_key).to include( + columns: [:test_migration_id], table: :test_migrations, + ) end def check_contains_no_fk! From 7f2aa7e10925ed847d5a8b9181d0ba9c04cb193d Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 16:49:31 +0600 Subject: [PATCH 03/58] fix --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 39145c3..f859e26 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,9 @@ jobs: PGUSER: root steps: + - name: "Setup ClickHouse Server" + uses: getsentry/action-clickhouse-in-ci@v1.6 + - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 From b50784c3f35cba2faaae780cdb6d8c216e663178 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 16:53:23 +0600 Subject: [PATCH 04/58] fix --- spec/support/database.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/support/database.rb b/spec/support/database.rb index 5ef33a1..5de3eb2 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -45,6 +45,18 @@ foreign_key :user_id, :users end +DB.drop_table? :test_migrations, cascade: true +DB.create_table :test_migrations do + primary_key :id + column :test, :text +end + +DB.drop_table? :test_migration_references +DB.create_table :test_migration_references do + primary_key :id + foreign_key :test_migration_id, :test_migrations +end + class User < Sequel::Model(:users) def skip_table_sync? false From 5e2111b696b352916b84085ef3a834efb2d6e8cd Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 17:34:34 +0600 Subject: [PATCH 05/58] fix --- .github/workflows/test.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f859e26..ac3c861 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,14 +30,24 @@ jobs: --health-retries 5 ports: - 5432:5432 + + clickhouse: + image: clickhouse/clickhouse-server:latest + env: + CLICKHOUSE_CLUSTER_NAME: click_cluster + ports: + - 8123:8123 + - 9000:9000 + options: >- + --health-cmd "curl -f http://localhost:8123/ping || exit 1" + --health-interval 1s + --health-timeout 5s + --health-retries 30 env: PGHOST: localhost PGUSER: root steps: - - name: "Setup ClickHouse Server" - uses: getsentry/action-clickhouse-in-ci@v1.6 - - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 From bcfa0ad9db5dea5e63a4c1706239074c9c7d7554 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 17:37:14 +0600 Subject: [PATCH 06/58] fix --- .github/workflows/test.yml | 3 +++ spec/support/clickhouse.rb | 7 +------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac3c861..260eaac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,9 @@ jobs: clickhouse: image: clickhouse/clickhouse-server:latest env: + CLICKHOUSE_USER: test + CLICKHOUSE_PASSWORD: test + CLICKHOUSE_DB: umbrellio_utils_test CLICKHOUSE_CLUSTER_NAME: click_cluster ports: - 8123:8123 diff --git a/spec/support/clickhouse.rb b/spec/support/clickhouse.rb index 1cc087b..404dfb9 100644 --- a/spec/support/clickhouse.rb +++ b/spec/support/clickhouse.rb @@ -5,16 +5,11 @@ require "click_house" config = ClickHouse.config do |config| - config.assign(host: "localhost") + config.assign(host: "localhost", database: "umbrellio_utils_test") config.logger = Logger.new("log/ch.log") end client = ClickHouse::Connection.new(config) -client.execute(<<~SQL) - CREATE DATABASE IF NOT EXISTS umbrellio_utils_test; -SQL - -config.database = "umbrellio_utils_test" client.execute(<<~SQL) CREATE TABLE IF NOT EXISTS test (id Int32) From 6b0421b4e5aa6d3c97e2d54c4a4deb63e80a1ce7 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 17:43:40 +0600 Subject: [PATCH 07/58] fix --- .github/clickhouse/config.d/click_cluster.xml | 12 ++++++++++++ .github/workflows/test.yml | 2 ++ 2 files changed, 14 insertions(+) create mode 100644 .github/clickhouse/config.d/click_cluster.xml diff --git a/.github/clickhouse/config.d/click_cluster.xml b/.github/clickhouse/config.d/click_cluster.xml new file mode 100644 index 0000000..2d7496b --- /dev/null +++ b/.github/clickhouse/config.d/click_cluster.xml @@ -0,0 +1,12 @@ + + + + + + localhost + 9000 + + + + + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 260eaac..e3eccce 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,6 +41,8 @@ jobs: ports: - 8123:8123 - 9000:9000 + volumes: + - ./.github/clickhouse/config.d:/etc/clickhouse-server/config.d options: >- --health-cmd "curl -f http://localhost:8123/ping || exit 1" --health-interval 1s From 6212c04b1c220958dfbaa81ab50cc9eebfe6379b Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 17:56:30 +0600 Subject: [PATCH 08/58] fix --- .github/clickhouse/config.d/click_cluster.xml | 12 -- .github/clickhouse/config.d/config.xml | 104 ++++++++++++++++++ .github/workflows/test.yml | 5 +- 3 files changed, 105 insertions(+), 16 deletions(-) delete mode 100644 .github/clickhouse/config.d/click_cluster.xml create mode 100644 .github/clickhouse/config.d/config.xml diff --git a/.github/clickhouse/config.d/click_cluster.xml b/.github/clickhouse/config.d/click_cluster.xml deleted file mode 100644 index 2d7496b..0000000 --- a/.github/clickhouse/config.d/click_cluster.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - localhost - 9000 - - - - - diff --git a/.github/clickhouse/config.d/config.xml b/.github/clickhouse/config.d/config.xml new file mode 100644 index 0000000..655ba09 --- /dev/null +++ b/.github/clickhouse/config.d/config.xml @@ -0,0 +1,104 @@ + + + click_cluster + 8123 + 9000 + 9004 + 9009 + + error + /var/log/clickhouse-server/clickhouse-server.log + /var/log/clickhouse-server/clickhouse-server.err.log + 1000M + 10 + 1 + + + false + + 3 + 4096 + 100 + 0 + 10000 + 0.9 + 4194304 + 0 + 8589934592 + 5368709120 + /var/lib/clickhouse/ + /var/lib/clickhouse/tmp/ + /var/lib/clickhouse/user_files/ + + + users.xml + + + /var/lib/clickhouse/access/ + + + default + default + + true + false + + shard1 + node1 + + 3600 + 3600 + + 60 + + system + query_log
+ toYYYYMM(event_date) + 7500 +
+ + system + trace_log
+ toYYYYMM(event_date) + 7500 +
+ + system + query_thread_log
+ toYYYYMM(event_date) + 7500 +
+ + system + metric_log
+ 7500 + 1000 +
+ + system + asynchronous_metric_log
+ 60000 +
+ + + engine MergeTree + partition by toYYYYMM(finish_date) + order by (finish_date, finish_time_us, trace_id) + + system + opentelemetry_span_log
+ 7500 +
+ + system + crash_log
+ + 1000 +
+ *_dictionary.xml + + /clickhouse/task_queue/ddl + + /var/lib/clickhouse/format_schemas/ + 1 +
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3eccce..6479e26 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,12 +32,9 @@ jobs: - 5432:5432 clickhouse: - image: clickhouse/clickhouse-server:latest + image: clickhouse/clickhouse-server:25.3.6.56-alpine env: - CLICKHOUSE_USER: test - CLICKHOUSE_PASSWORD: test CLICKHOUSE_DB: umbrellio_utils_test - CLICKHOUSE_CLUSTER_NAME: click_cluster ports: - 8123:8123 - 9000:9000 From 0a1a30196e1cb80cc9ba7b5ac747d61277be1e4b Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 17:59:06 +0600 Subject: [PATCH 09/58] fix --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6479e26..4167a8b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,9 +42,9 @@ jobs: - ./.github/clickhouse/config.d:/etc/clickhouse-server/config.d options: >- --health-cmd "curl -f http://localhost:8123/ping || exit 1" - --health-interval 1s + --health-interval 10s --health-timeout 5s - --health-retries 30 + --health-retries 5 env: PGHOST: localhost PGUSER: root From 56e8678259b35960ce47937d1b102ba6bd727f76 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:05:39 +0600 Subject: [PATCH 10/58] fix --- .github/clickhouse/config.d/click_cluster.xml | 12 ++ .github/clickhouse/config.d/config.xml | 104 ------------------ .github/workflows/test.yml | 3 +- 3 files changed, 14 insertions(+), 105 deletions(-) create mode 100644 .github/clickhouse/config.d/click_cluster.xml delete mode 100644 .github/clickhouse/config.d/config.xml diff --git a/.github/clickhouse/config.d/click_cluster.xml b/.github/clickhouse/config.d/click_cluster.xml new file mode 100644 index 0000000..2d7496b --- /dev/null +++ b/.github/clickhouse/config.d/click_cluster.xml @@ -0,0 +1,12 @@ + + + + + + localhost + 9000 + + + + + diff --git a/.github/clickhouse/config.d/config.xml b/.github/clickhouse/config.d/config.xml deleted file mode 100644 index 655ba09..0000000 --- a/.github/clickhouse/config.d/config.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - click_cluster - 8123 - 9000 - 9004 - 9009 - - error - /var/log/clickhouse-server/clickhouse-server.log - /var/log/clickhouse-server/clickhouse-server.err.log - 1000M - 10 - 1 - - - false - - 3 - 4096 - 100 - 0 - 10000 - 0.9 - 4194304 - 0 - 8589934592 - 5368709120 - /var/lib/clickhouse/ - /var/lib/clickhouse/tmp/ - /var/lib/clickhouse/user_files/ - - - users.xml - - - /var/lib/clickhouse/access/ - - - default - default - - true - false - - shard1 - node1 - - 3600 - 3600 - - 60 - - system - query_log
- toYYYYMM(event_date) - 7500 -
- - system - trace_log
- toYYYYMM(event_date) - 7500 -
- - system - query_thread_log
- toYYYYMM(event_date) - 7500 -
- - system - metric_log
- 7500 - 1000 -
- - system - asynchronous_metric_log
- 60000 -
- - - engine MergeTree - partition by toYYYYMM(finish_date) - order by (finish_date, finish_time_us, trace_id) - - system - opentelemetry_span_log
- 7500 -
- - system - crash_log
- - 1000 -
- *_dictionary.xml - - /clickhouse/task_queue/ddl - - /var/lib/clickhouse/format_schemas/ - 1 -
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4167a8b..9757272 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,13 +35,14 @@ jobs: image: clickhouse/clickhouse-server:25.3.6.56-alpine env: CLICKHOUSE_DB: umbrellio_utils_test + CLICKHOUSE_SKIP_USER_SETUP: 1 ports: - 8123:8123 - 9000:9000 volumes: - ./.github/clickhouse/config.d:/etc/clickhouse-server/config.d options: >- - --health-cmd "curl -f http://localhost:8123/ping || exit 1" + --health-cmd 'clickhouse-client --query "SELECT * FROM system.build_options"' --health-interval 10s --health-timeout 5s --health-retries 5 From a03a6eebfdaae464b9b11b135537716c5f55d4a8 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:06:43 +0600 Subject: [PATCH 11/58] fix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9757272..4888c3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,7 @@ jobs: volumes: - ./.github/clickhouse/config.d:/etc/clickhouse-server/config.d options: >- - --health-cmd 'clickhouse-client --query "SELECT * FROM system.build_options"' + --health-cmd "clickhouse-client --query \"SELECT * FROM system.build_options\"" --health-interval 10s --health-timeout 5s --health-retries 5 From 44533371bfd3bfdab765c26b553a5960c7e327f5 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:10:49 +0600 Subject: [PATCH 12/58] fix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4888c3c..6edcec4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: - 8123:8123 - 9000:9000 volumes: - - ./.github/clickhouse/config.d:/etc/clickhouse-server/config.d + - ./.github/clickhouse/config.d:/etc/clickhouse-server/config.d:ro options: >- --health-cmd "clickhouse-client --query \"SELECT * FROM system.build_options\"" --health-interval 10s From a7b1769624a8f369e03b8c7f9d0d059e561bb7e7 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:12:18 +0600 Subject: [PATCH 13/58] fikx --- .github/workflows/test.yml | 2 +- {.github/clickhouse => clickhouse}/config.d/click_cluster.xml | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {.github/clickhouse => clickhouse}/config.d/click_cluster.xml (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6edcec4..3199c9c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: - 8123:8123 - 9000:9000 volumes: - - ./.github/clickhouse/config.d:/etc/clickhouse-server/config.d:ro + - ./clickhouse/config.d:/etc/clickhouse-server/config.d:ro options: >- --health-cmd "clickhouse-client --query \"SELECT * FROM system.build_options\"" --health-interval 10s diff --git a/.github/clickhouse/config.d/click_cluster.xml b/clickhouse/config.d/click_cluster.xml similarity index 100% rename from .github/clickhouse/config.d/click_cluster.xml rename to clickhouse/config.d/click_cluster.xml From 3504fd14a50aa2a6f56c5952f4f086b41a86a651 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:15:09 +0600 Subject: [PATCH 14/58] fix --- .../workflows/clickhouse}/config.d/click_cluster.xml | 0 .github/workflows/test.yml | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) rename {clickhouse => .github/workflows/clickhouse}/config.d/click_cluster.xml (100%) diff --git a/clickhouse/config.d/click_cluster.xml b/.github/workflows/clickhouse/config.d/click_cluster.xml similarity index 100% rename from clickhouse/config.d/click_cluster.xml rename to .github/workflows/clickhouse/config.d/click_cluster.xml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3199c9c..b91fbdf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,8 +40,9 @@ jobs: - 8123:8123 - 9000:9000 volumes: - - ./clickhouse/config.d:/etc/clickhouse-server/config.d:ro + - ./.github/clickhouse/config.d:/etc/clickhouse-server/config.d options: >- + --user=clickhouse --health-cmd "clickhouse-client --query \"SELECT * FROM system.build_options\"" --health-interval 10s --health-timeout 5s From 71070ca953e3da6464fe6ad5f44c75c0e5349e17 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:17:45 +0600 Subject: [PATCH 15/58] fix --- .../{workflows => }/clickhouse/config.d/click_cluster.xml | 0 .github/workflows/test.yml | 7 ++++++- 2 files changed, 6 insertions(+), 1 deletion(-) rename .github/{workflows => }/clickhouse/config.d/click_cluster.xml (100%) diff --git a/.github/workflows/clickhouse/config.d/click_cluster.xml b/.github/clickhouse/config.d/click_cluster.xml similarity index 100% rename from .github/workflows/clickhouse/config.d/click_cluster.xml rename to .github/clickhouse/config.d/click_cluster.xml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b91fbdf..ac4c503 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: - 8123:8123 - 9000:9000 volumes: - - ./.github/clickhouse/config.d:/etc/clickhouse-server/config.d + - /tmp/clickhouse-config:/etc/clickhouse-server/config.d:ro options: >- --user=clickhouse --health-cmd "clickhouse-client --query \"SELECT * FROM system.build_options\"" @@ -52,6 +52,11 @@ jobs: PGUSER: root steps: + - name: Prepare ClickHouse config + run: | + mkdir -p /tmp/clickhouse-config + cp .github/clickhouse/config.d/*.xml /tmp/clickhouse-config/ + - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 From 907a622b23eb9a404e75b0f3e863228a71ba98b7 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:21:02 +0600 Subject: [PATCH 16/58] fix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac4c503..bf6467e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: - name: Prepare ClickHouse config run: | mkdir -p /tmp/clickhouse-config - cp .github/clickhouse/config.d/*.xml /tmp/clickhouse-config/ + cp ./.github/clickhouse/config.d/*.xml /tmp/clickhouse-config/ - uses: actions/checkout@v2 From 0986addd205d7dbd6e481ac29aeed1a792471cde Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:21:56 +0600 Subject: [PATCH 17/58] fix --- .github/clickhouse/config.d/click_cluster.xml | 12 ------------ .github/workflows/test.yml | 2 -- 2 files changed, 14 deletions(-) delete mode 100644 .github/clickhouse/config.d/click_cluster.xml diff --git a/.github/clickhouse/config.d/click_cluster.xml b/.github/clickhouse/config.d/click_cluster.xml deleted file mode 100644 index 2d7496b..0000000 --- a/.github/clickhouse/config.d/click_cluster.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - localhost - 9000 - - - - - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf6467e..4ded6c0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,8 +39,6 @@ jobs: ports: - 8123:8123 - 9000:9000 - volumes: - - /tmp/clickhouse-config:/etc/clickhouse-server/config.d:ro options: >- --user=clickhouse --health-cmd "clickhouse-client --query \"SELECT * FROM system.build_options\"" From 665bf0df674d79a0a862da26c5d476eb2bf136d3 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:23:30 +0600 Subject: [PATCH 18/58] fix --- .github/workflows/test.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ded6c0..afc4c96 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,11 +50,6 @@ jobs: PGUSER: root steps: - - name: Prepare ClickHouse config - run: | - mkdir -p /tmp/clickhouse-config - cp ./.github/clickhouse/config.d/*.xml /tmp/clickhouse-config/ - - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 From e059821d47a4f11e04a3ceac5f7b5b940bde0454 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:28:39 +0600 Subject: [PATCH 19/58] fix --- .github/clickhouse/config.d/click_cluster.xml | 12 ++++++++++++ .github/workflows/test.yml | 2 ++ 2 files changed, 14 insertions(+) create mode 100644 .github/clickhouse/config.d/click_cluster.xml diff --git a/.github/clickhouse/config.d/click_cluster.xml b/.github/clickhouse/config.d/click_cluster.xml new file mode 100644 index 0000000..2d7496b --- /dev/null +++ b/.github/clickhouse/config.d/click_cluster.xml @@ -0,0 +1,12 @@ + + + + + + localhost + 9000 + + + + + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index afc4c96..b91fbdf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,8 @@ jobs: ports: - 8123:8123 - 9000:9000 + volumes: + - ./.github/clickhouse/config.d:/etc/clickhouse-server/config.d options: >- --user=clickhouse --health-cmd "clickhouse-client --query \"SELECT * FROM system.build_options\"" From 3b52714a8ce79c329c0bca98e58121b1e2ff0ee2 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:40:10 +0600 Subject: [PATCH 20/58] fix --- .github/workflows/test.yml | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b91fbdf..fe27e18 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,23 +30,6 @@ jobs: --health-retries 5 ports: - 5432:5432 - - clickhouse: - image: clickhouse/clickhouse-server:25.3.6.56-alpine - env: - CLICKHOUSE_DB: umbrellio_utils_test - CLICKHOUSE_SKIP_USER_SETUP: 1 - ports: - - 8123:8123 - - 9000:9000 - volumes: - - ./.github/clickhouse/config.d:/etc/clickhouse-server/config.d - options: >- - --user=clickhouse - --health-cmd "clickhouse-client --query \"SELECT * FROM system.build_options\"" - --health-interval 10s - --health-timeout 5s - --health-retries 5 env: PGHOST: localhost PGUSER: root @@ -54,6 +37,14 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Start ClickHouse with mounted config + run: | + docker run -d --name ch \ + CLICKHOUSE_SKIP_USER_SETUP=1 CLICKHOUSE_DB=umbrellio_utils_test -p 8123:8123 -p 9000:9000 + -v ${{ github.workspace }}/.github/clickhouse/config.d:/etc/clickhouse-server/config.d \ + -v ${{ github.workspace }}/.github/clickhouse/users.d:/etc/clickhouse-server/users.d \ + clickhouse/clickhouse-server:25.3.6.56-alpine + - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} From 6078d1d934a7029ad471dbe6336389307b35086e Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:46:25 +0600 Subject: [PATCH 21/58] fix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe27e18..083cefc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: - name: Start ClickHouse with mounted config run: | docker run -d --name ch \ - CLICKHOUSE_SKIP_USER_SETUP=1 CLICKHOUSE_DB=umbrellio_utils_test -p 8123:8123 -p 9000:9000 + -e CLICKHOUSE_SKIP_USER_SETUP=1 -e CLICKHOUSE_DB=umbrellio_utils_test -p 8123:8123 -p 9000:9000 -v ${{ github.workspace }}/.github/clickhouse/config.d:/etc/clickhouse-server/config.d \ -v ${{ github.workspace }}/.github/clickhouse/users.d:/etc/clickhouse-server/users.d \ clickhouse/clickhouse-server:25.3.6.56-alpine From 7fd2bb8afb9eba8efb5becb883bb403030b89e31 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 18:47:14 +0600 Subject: [PATCH 22/58] fix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 083cefc..0eef9ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: - name: Start ClickHouse with mounted config run: | docker run -d --name ch \ - -e CLICKHOUSE_SKIP_USER_SETUP=1 -e CLICKHOUSE_DB=umbrellio_utils_test -p 8123:8123 -p 9000:9000 + -e CLICKHOUSE_SKIP_USER_SETUP=1 -e CLICKHOUSE_DB=umbrellio_utils_test -p 8123:8123 -p 9000:9000 \ -v ${{ github.workspace }}/.github/clickhouse/config.d:/etc/clickhouse-server/config.d \ -v ${{ github.workspace }}/.github/clickhouse/users.d:/etc/clickhouse-server/users.d \ clickhouse/clickhouse-server:25.3.6.56-alpine From eb94fde6dea4ef19d57442963ce55b05f3ed6f48 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 19:08:17 +0600 Subject: [PATCH 23/58] fix --- .github/clickhouse/config.d/click_cluster.xml | 12 -- .github/clickhouse/config.xml | 138 ++++++++++++++++++ .github/workflows/test.yml | 3 +- 3 files changed, 139 insertions(+), 14 deletions(-) delete mode 100644 .github/clickhouse/config.d/click_cluster.xml create mode 100644 .github/clickhouse/config.xml diff --git a/.github/clickhouse/config.d/click_cluster.xml b/.github/clickhouse/config.d/click_cluster.xml deleted file mode 100644 index 2d7496b..0000000 --- a/.github/clickhouse/config.d/click_cluster.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - localhost - 9000 - - - - - diff --git a/.github/clickhouse/config.xml b/.github/clickhouse/config.xml new file mode 100644 index 0000000..ad88129 --- /dev/null +++ b/.github/clickhouse/config.xml @@ -0,0 +1,138 @@ + + + + click_cluster + + 8123 + 9000 + 9004 + 9009 + + + error + /var/log/clickhouse-server/clickhouse-server.log + /var/log/clickhouse-server/clickhouse-server.err.log + 1000M + 10 + 1 + + + + false + + + 3 + + 4096 + 100 + 0 + 10000 + 0.9 + + 4194304 + 0 + + 8589934592 + + 5368709120 + + /var/lib/clickhouse/ + + /var/lib/clickhouse/tmp/ + + /var/lib/clickhouse/user_files/ + + + + users.xml + + + /var/lib/clickhouse/access/ + + + + default + default + + + + true + false + + + shard1 + node1 + + + 3600 + + 3600 + + 60 + + + system + query_log
+ + toYYYYMM(event_date) + + 7500 +
+ + + system + trace_log
+ + toYYYYMM(event_date) + 7500 +
+ + + system + query_thread_log
+ toYYYYMM(event_date) + 7500 +
+ + + system + metric_log
+ 7500 + 1000 +
+ + + system + asynchronous_metric_log
+ 60000 +
+ + + + engine MergeTree + partition by toYYYYMM(finish_date) + order by (finish_date, finish_time_us, trace_id) + + system + opentelemetry_span_log
+ 7500 +
+ + + system + crash_log
+ + + 1000 +
+ + *_dictionary.xml + + + /clickhouse/task_queue/ddl + + + /var/lib/clickhouse/format_schemas/ + + 1 +
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0eef9ca..39c1c7b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,8 +41,7 @@ jobs: run: | docker run -d --name ch \ -e CLICKHOUSE_SKIP_USER_SETUP=1 -e CLICKHOUSE_DB=umbrellio_utils_test -p 8123:8123 -p 9000:9000 \ - -v ${{ github.workspace }}/.github/clickhouse/config.d:/etc/clickhouse-server/config.d \ - -v ${{ github.workspace }}/.github/clickhouse/users.d:/etc/clickhouse-server/users.d \ + -v ${{ github.workspace }}/.github/clickhouse/config.xml:/etc/clickhouse-server/config.xml \ clickhouse/clickhouse-server:25.3.6.56-alpine - uses: ruby/setup-ruby@v1 From 1e2ed8cb8794ffa0228e4bbadbc78a6e7b1b1a9f Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 19:11:51 +0600 Subject: [PATCH 24/58] fix --- .github/clickhouse/clickhouse_keeper.xml | 42 ++++++++++++++++++++++++ .github/workflows/test.yml | 1 + 2 files changed, 43 insertions(+) create mode 100644 .github/clickhouse/clickhouse_keeper.xml diff --git a/.github/clickhouse/clickhouse_keeper.xml b/.github/clickhouse/clickhouse_keeper.xml new file mode 100644 index 0000000..6331870 --- /dev/null +++ b/.github/clickhouse/clickhouse_keeper.xml @@ -0,0 +1,42 @@ + + + + + true + + localhost + 9000 + + + + + + + 9181 + 0 + 1 + /var/lib/clickhouse/coordination/log + /var/lib/clickhouse/coordination/snapshots + + + 10000 + 30000 + trace + + + + + 1 + localhost + 9234 + + + + + + + localhost + 9181 + + + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 39c1c7b..4d1dd3f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,7 @@ jobs: docker run -d --name ch \ -e CLICKHOUSE_SKIP_USER_SETUP=1 -e CLICKHOUSE_DB=umbrellio_utils_test -p 8123:8123 -p 9000:9000 \ -v ${{ github.workspace }}/.github/clickhouse/config.xml:/etc/clickhouse-server/config.xml \ + -v ${{ github.workspace }}/.github/clickhouse/clickhouse_keeper.xml:/etc/clickhouse-server/config.d/clickhouse_keeper.xml \ clickhouse/clickhouse-server:25.3.6.56-alpine - uses: ruby/setup-ruby@v1 From 921d60f4b1daa9564c00686dee26534c27614a12 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 19:19:00 +0600 Subject: [PATCH 25/58] fix --- lib/umbrellio_utils/click_house.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/umbrellio_utils/click_house.rb b/lib/umbrellio_utils/click_house.rb index 35fa648..803d07a 100644 --- a/lib/umbrellio_utils/click_house.rb +++ b/lib/umbrellio_utils/click_house.rb @@ -95,7 +95,7 @@ def pg_table_connection(table) port = DB.opts[:port] || 5432 database = DB.opts[:database] || ENV.fetch("USER") username = DB.opts[:user] || ENV.fetch("USER") - password = DB.opts[:password] || ENV.fetch("PASSWORD") + password = DB.opts[:password] || ENV.fetch("PASSWORD", "") Sequel.function(:postgresql, "#{host}:#{port}", database, table, username, password) end From d4c947ef409477cf5872cf4c0fa77e537332f6fb Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 19:40:59 +0600 Subject: [PATCH 26/58] fix --- .github/clickhouse/config.xml | 138 ----------------------------- .github/workflows/test.yml | 59 ++++++++---- lib/umbrellio_utils/click_house.rb | 6 +- spec/support/database.rb | 8 +- 4 files changed, 51 insertions(+), 160 deletions(-) delete mode 100644 .github/clickhouse/config.xml diff --git a/.github/clickhouse/config.xml b/.github/clickhouse/config.xml deleted file mode 100644 index ad88129..0000000 --- a/.github/clickhouse/config.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - click_cluster - - 8123 - 9000 - 9004 - 9009 - - - error - /var/log/clickhouse-server/clickhouse-server.log - /var/log/clickhouse-server/clickhouse-server.err.log - 1000M - 10 - 1 - - - - false - - - 3 - - 4096 - 100 - 0 - 10000 - 0.9 - - 4194304 - 0 - - 8589934592 - - 5368709120 - - /var/lib/clickhouse/ - - /var/lib/clickhouse/tmp/ - - /var/lib/clickhouse/user_files/ - - - - users.xml - - - /var/lib/clickhouse/access/ - - - - default - default - - - - true - false - - - shard1 - node1 - - - 3600 - - 3600 - - 60 - - - system - query_log
- - toYYYYMM(event_date) - - 7500 -
- - - system - trace_log
- - toYYYYMM(event_date) - 7500 -
- - - system - query_thread_log
- toYYYYMM(event_date) - 7500 -
- - - system - metric_log
- 7500 - 1000 -
- - - system - asynchronous_metric_log
- 60000 -
- - - - engine MergeTree - partition by toYYYYMM(finish_date) - order by (finish_date, finish_time_us, trace_id) - - system - opentelemetry_span_log
- 7500 -
- - - system - crash_log
- - - 1000 -
- - *_dictionary.xml - - - /clickhouse/task_queue/ddl - - - /var/lib/clickhouse/format_schemas/ - - 1 -
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d1dd3f..f6cdd05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,25 +17,50 @@ jobs: matrix: ruby: ["3.1", "3.2", "3.3", "3.4"] - services: - postgres: - image: postgres - env: - POSTGRES_USER: root - POSTGRES_HOST_AUTH_METHOD: trust - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 env: PGHOST: localhost - PGUSER: root - + PGUSER: user steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Create docker network + run: docker network create dbnet + + - name: Start PostgreSQL + run: | + docker run -d \ + --name pg \ + --network dbnet \ + -e POSTGRES_PASSWORD=pass \ + -e POSTGRES_USER=user \ + -e POSTGRES_DB=umbrellio_utils_test \ + -p 5432:5432 \ + postgres:14 + + - name: Start ClickHouse + run: | + docker run -d \ + --name ch \ + --network dbnet \ + -p 9000:9000 -p 8123:8123 \ + -v ${{ github.workspace }}/.github/clickhouse/clickhouse_keeper.xml:/etc/clickhouse-server/config.d/keeper.xml \ + clickhouse/clickhouse-server:25 + + - name: Wait for Postgres + run: | + for i in {1..30}; do + if docker exec pg pg_isready -U user; then exit 0; fi + sleep 1 + done + exit 1 + + - name: Wait for ClickHouse + run: | + for i in {1..30}; do + if docker exec ch clickhouse-client --query "SELECT 1"; then exit 0; fi + sleep 1 + done + exit 1 - name: Start ClickHouse with mounted config run: | @@ -51,8 +76,6 @@ jobs: rubygems: latest bundler-cache: true - - run: psql -c 'CREATE DATABASE umbrellio_utils_test' - - name: Run Linter run: bundle exec ci-helper RubocopLint diff --git a/lib/umbrellio_utils/click_house.rb b/lib/umbrellio_utils/click_house.rb index 803d07a..8e90de8 100644 --- a/lib/umbrellio_utils/click_house.rb +++ b/lib/umbrellio_utils/click_house.rb @@ -93,9 +93,9 @@ def server_version def pg_table_connection(table) host = DB.opts[:host].presence || "localhost" port = DB.opts[:port] || 5432 - database = DB.opts[:database] || ENV.fetch("USER") - username = DB.opts[:user] || ENV.fetch("USER") - password = DB.opts[:password] || ENV.fetch("PASSWORD", "") + database = DB.opts[:database] + username = DB.opts[:user] + password = DB.opts[:password] Sequel.function(:postgresql, "#{host}:#{port}", database, table, username, password) end diff --git a/spec/support/database.rb b/spec/support/database.rb index 5de3eb2..382bdc2 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -4,7 +4,13 @@ begin db_name = "umbrellio_utils_test" - DB = Sequel.connect(ENV.fetch("DB_URL", "postgres:///#{db_name}")) + DB = Sequel.postgres( + "umbrellio_utils_test", + user: "user", + password: "pass", + host: "pg", + port: 5432, + ) rescue Sequel::DatabaseConnectionError => error puts error abort "You probably need to create a test database. " \ From 84f9e0ce867ea539861caf75295216f8fe6a4255 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 19:42:56 +0600 Subject: [PATCH 27/58] fix --- .github/workflows/test.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f6cdd05..b672f3d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: --network dbnet \ -p 9000:9000 -p 8123:8123 \ -v ${{ github.workspace }}/.github/clickhouse/clickhouse_keeper.xml:/etc/clickhouse-server/config.d/keeper.xml \ - clickhouse/clickhouse-server:25 + clickhouse/clickhouse-server:25.3.6.56-alpine - name: Wait for Postgres run: | @@ -62,14 +62,6 @@ jobs: done exit 1 - - name: Start ClickHouse with mounted config - run: | - docker run -d --name ch \ - -e CLICKHOUSE_SKIP_USER_SETUP=1 -e CLICKHOUSE_DB=umbrellio_utils_test -p 8123:8123 -p 9000:9000 \ - -v ${{ github.workspace }}/.github/clickhouse/config.xml:/etc/clickhouse-server/config.xml \ - -v ${{ github.workspace }}/.github/clickhouse/clickhouse_keeper.xml:/etc/clickhouse-server/config.d/clickhouse_keeper.xml \ - clickhouse/clickhouse-server:25.3.6.56-alpine - - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} From 5b332c60506e5e9bc554caf1fea0c3b1bda68048 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 19:45:05 +0600 Subject: [PATCH 28/58] fix --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b672f3d..2f0f6cb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,7 @@ jobs: docker run -d \ --name ch \ --network dbnet \ + -e CLICKHOUSE_SKIP_USER_SETUP=1 -e CLICKHOUSE_DB=umbrellio_utils_test \ -p 9000:9000 -p 8123:8123 \ -v ${{ github.workspace }}/.github/clickhouse/clickhouse_keeper.xml:/etc/clickhouse-server/config.d/keeper.xml \ clickhouse/clickhouse-server:25.3.6.56-alpine From 8f45b58334042105a116aca792ca882cbeab376e Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 19:49:11 +0600 Subject: [PATCH 29/58] fix --- .github/workflows/test.yml | 2 +- lib/umbrellio_utils/click_house.rb | 2 +- spec/support/database.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2f0f6cb..4c82ae3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: ruby: ["3.1", "3.2", "3.3", "3.4"] env: - PGHOST: localhost + PGHOST: pg PGUSER: user steps: - uses: actions/checkout@v4 diff --git a/lib/umbrellio_utils/click_house.rb b/lib/umbrellio_utils/click_house.rb index 8e90de8..97d8a0c 100644 --- a/lib/umbrellio_utils/click_house.rb +++ b/lib/umbrellio_utils/click_house.rb @@ -91,7 +91,7 @@ def server_version end def pg_table_connection(table) - host = DB.opts[:host].presence || "localhost" + host = ENV["PGHOST"] || DB.opts[:host].presence || "localhost" port = DB.opts[:port] || 5432 database = DB.opts[:database] username = DB.opts[:user] diff --git a/spec/support/database.rb b/spec/support/database.rb index 382bdc2..8fdd321 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -8,7 +8,7 @@ "umbrellio_utils_test", user: "user", password: "pass", - host: "pg", + host: "localhost", port: 5432, ) rescue Sequel::DatabaseConnectionError => error From 7a4e9da5c885a767b46b8315cc9e436da70a7f1e Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 20:06:49 +0600 Subject: [PATCH 30/58] fix --- {bin => exe}/clickhouse-server | 0 spec/support/database.rb | 4 ++-- spec/umbrellio_utils/migrations_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename {bin => exe}/clickhouse-server (100%) diff --git a/bin/clickhouse-server b/exe/clickhouse-server similarity index 100% rename from bin/clickhouse-server rename to exe/clickhouse-server diff --git a/spec/support/database.rb b/spec/support/database.rb index 8fdd321..0f3a3a2 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -6,8 +6,8 @@ db_name = "umbrellio_utils_test" DB = Sequel.postgres( "umbrellio_utils_test", - user: "user", - password: "pass", + user: "igor", + password: "password", host: "localhost", port: 5432, ) diff --git a/spec/umbrellio_utils/migrations_spec.rb b/spec/umbrellio_utils/migrations_spec.rb index 2895ced..c42d117 100644 --- a/spec/umbrellio_utils/migrations_spec.rb +++ b/spec/umbrellio_utils/migrations_spec.rb @@ -132,7 +132,7 @@ def check_contains_no_fk! it "raises error" do stub_const("TestMigrationReference", Class.new(Sequel::Model(:test_migrations)) do def test_migration - Struct.new(:test_migration_references).new(test_migration_references: []) + Struct.new(:test_migration_references).new([]) end end) From a187ee3ae5a505197e3b4ec8d2e9d1e1cc3f1f92 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Mon, 1 Dec 2025 20:08:16 +0600 Subject: [PATCH 31/58] fix --- spec/support/database.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/support/database.rb b/spec/support/database.rb index 0f3a3a2..8fdd321 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -6,8 +6,8 @@ db_name = "umbrellio_utils_test" DB = Sequel.postgres( "umbrellio_utils_test", - user: "igor", - password: "password", + user: "user", + password: "pass", host: "localhost", port: 5432, ) From 62cf94433ce340cfc52edb34631bc889d611a447 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 12:42:38 +0600 Subject: [PATCH 32/58] bump version --- lib/umbrellio_utils/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/umbrellio_utils/version.rb b/lib/umbrellio_utils/version.rb index 9613d8d..495981b 100644 --- a/lib/umbrellio_utils/version.rb +++ b/lib/umbrellio_utils/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module UmbrellioUtils - VERSION = "1.9.0" + VERSION = "1.10.0" end From 5c5d92536d5103b6d35d975de21500f218a716bf Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 12:59:08 +0600 Subject: [PATCH 33/58] fix --- Gemfile.lock | 2 +- {exe => bin}/clickhouse-server | 0 umbrellio_utils.gemspec | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename {exe => bin}/clickhouse-server (100%) diff --git a/Gemfile.lock b/Gemfile.lock index 4113c00..6574b16 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,7 +10,7 @@ GIT PATH remote: . specs: - umbrellio-utils (1.9.0) + umbrellio-utils (1.10.0) memery (~> 1) GEM diff --git a/exe/clickhouse-server b/bin/clickhouse-server similarity index 100% rename from exe/clickhouse-server rename to bin/clickhouse-server diff --git a/umbrellio_utils.gemspec b/umbrellio_utils.gemspec index f9566d6..d46ce19 100644 --- a/umbrellio_utils.gemspec +++ b/umbrellio_utils.gemspec @@ -23,8 +23,8 @@ Gem::Specification.new do |spec| spec.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end - spec.bindir = "exe" - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.bindir = "bin" + spec.executables = ["clickhouse-server"] spec.require_paths = ["lib"] spec.add_dependency "memery", "~> 1" From 6b2ad4fcc09b797a9d33829aba7665d4f5353512 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 13:17:21 +0600 Subject: [PATCH 34/58] fix --- bin/clickhouse-server | 2 +- lib/umbrellio_utils/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/clickhouse-server b/bin/clickhouse-server index af3bd25..ba6a040 100755 --- a/bin/clickhouse-server +++ b/bin/clickhouse-server @@ -1,4 +1,4 @@ -#!/usr/bin/env -S bash -eu +#!/usr/bin/env bash docker stop clickhouse-server || true docker rm clickhouse-server || true diff --git a/lib/umbrellio_utils/version.rb b/lib/umbrellio_utils/version.rb index 495981b..7dbd65d 100644 --- a/lib/umbrellio_utils/version.rb +++ b/lib/umbrellio_utils/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module UmbrellioUtils - VERSION = "1.10.0" + VERSION = "1.10.1" end From 9a6793f005b13a9e0d635c8643b1d392c539694d Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 13:20:11 +0600 Subject: [PATCH 35/58] fix --- Gemfile.lock | 2 +- bin/clickhouse-server | 3 ++- lib/umbrellio_utils/version.rb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6574b16..21edf7c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,7 +10,7 @@ GIT PATH remote: . specs: - umbrellio-utils (1.10.0) + umbrellio-utils (1.10.1) memery (~> 1) GEM diff --git a/bin/clickhouse-server b/bin/clickhouse-server index ba6a040..221c58b 100755 --- a/bin/clickhouse-server +++ b/bin/clickhouse-server @@ -1,4 +1,5 @@ -#!/usr/bin/env bash +#!/bin/bash +set -eu docker stop clickhouse-server || true docker rm clickhouse-server || true diff --git a/lib/umbrellio_utils/version.rb b/lib/umbrellio_utils/version.rb index 7dbd65d..9c27714 100644 --- a/lib/umbrellio_utils/version.rb +++ b/lib/umbrellio_utils/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module UmbrellioUtils - VERSION = "1.10.1" + VERSION = "1.10.2" end From 1fc3981f9d2065753eb2479533b5cf870c4bc59d Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 13:20:41 +0600 Subject: [PATCH 36/58] fix --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 21edf7c..224fa31 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,7 +10,7 @@ GIT PATH remote: . specs: - umbrellio-utils (1.10.1) + umbrellio-utils (1.10.2) memery (~> 1) GEM From cc1534645003ecc797df386101009b1b2bc17e14 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 13:45:02 +0600 Subject: [PATCH 37/58] fix --- Gemfile.lock | 2 +- lib/umbrellio_utils/click_house.rb | 272 ++++++++++++++--------------- lib/umbrellio_utils/version.rb | 2 +- spec/support/database.rb | 4 +- 4 files changed, 140 insertions(+), 140 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 224fa31..6574b16 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,7 +10,7 @@ GIT PATH remote: . specs: - umbrellio-utils (1.10.2) + umbrellio-utils (1.10.0) memery (~> 1) GEM diff --git a/lib/umbrellio_utils/click_house.rb b/lib/umbrellio_utils/click_house.rb index 97d8a0c..21618e4 100644 --- a/lib/umbrellio_utils/click_house.rb +++ b/lib/umbrellio_utils/click_house.rb @@ -2,183 +2,183 @@ module UmbrellioUtils module ClickHouse - class << self - include Memery + include Memery - delegate :create_database, :drop_database, :config, to: :client + extend self - def insert(table_name, db_name: self.db_name, rows: []) - client.insert(full_table_name(table_name, db_name), rows, format: "JSONEachRow") - end + delegate :create_database, :drop_database, :config, to: :client - def from(source, db_name: self.db_name) - ds = - case source - when Symbol - DB.from(db_name == self.db_name ? SQL[source] : SQL[db_name][source]) - when nil - DB.dataset - else - DB.from(source) - end - - ds.clone(ch: true) - end + def insert(table_name, db_name: self.db_name, rows: []) + client.insert(full_table_name(table_name, db_name), rows, format: "JSONEachRow") + end - def execute(sql, host: nil, **opts) - log_errors(sql) do - client(host).execute(sql, params: opts) + def from(source, db_name: self.db_name) + ds = + case source + when Symbol + DB.from(db_name == self.db_name ? SQL[source] : SQL[db_name][source]) + when nil + DB.dataset + else + DB.from(source) end - end - def query(dataset, host: nil, **opts) - sql = sql_for(dataset) + ds.clone(ch: true) + end - log_errors(sql) do - select_all(sql, host:, **opts).map { |x| Misc::StrictHash[x.symbolize_keys] } - end + def execute(sql, host: nil, **opts) + log_errors(sql) do + client(host).execute(sql, params: opts) end + end - def query_value(dataset, host: nil, **opts) - sql = sql_for(dataset) + def query(dataset, host: nil, **opts) + sql = sql_for(dataset) - log_errors(sql) do - select_value(sql, host:, **opts) - end + log_errors(sql) do + select_all(sql, host:, **opts).map { |x| Misc::StrictHash[x.symbolize_keys] } end + end - def count(dataset) - query_value(dataset.select(SQL.ch_count)) - end + def query_value(dataset, host: nil, **opts) + sql = sql_for(dataset) - def optimize_table!(table_name, db_name: self.db_name) - execute("OPTIMIZE TABLE #{db_name}.#{table_name} ON CLUSTER click_cluster FINAL") + log_errors(sql) do + select_value(sql, host:, **opts) end + end - def truncate_table!(table_name, db_name: self.db_name) - execute("TRUNCATE TABLE #{db_name}.#{table_name} ON CLUSTER click_cluster SYNC") - end + def count(dataset) + query_value(dataset.select(SQL.ch_count)) + end - def drop_table!(table_name, db_name: self.db_name) - execute("DROP TABLE #{db_name}.#{table_name} ON CLUSTER click_cluster SYNC") - end + def optimize_table!(table_name, db_name: self.db_name) + execute("OPTIMIZE TABLE #{db_name}.#{table_name} ON CLUSTER click_cluster FINAL") + end - def describe_table(table_name, db_name: self.db_name) - sql = "DESCRIBE TABLE #{full_table_name(table_name, db_name)} FORMAT JSON" + def truncate_table!(table_name, db_name: self.db_name) + execute("TRUNCATE TABLE #{db_name}.#{table_name} ON CLUSTER click_cluster SYNC") + end - log_errors(sql) do - select_all(sql).map { |x| Misc::StrictHash[x.symbolize_keys] } - end - end + def drop_table!(table_name, db_name: self.db_name) + execute("DROP TABLE #{db_name}.#{table_name} ON CLUSTER click_cluster SYNC") + end - def db_name - client.config.database.to_sym - end + def describe_table(table_name, db_name: self.db_name) + sql = "DESCRIBE TABLE #{full_table_name(table_name, db_name)} FORMAT JSON" - def parse_value(value, type:) - case type - when /String/ - value&.to_s - when /DateTime/ - Time.zone.parse(value) if value - else - value - end + log_errors(sql) do + select_all(sql).map { |x| Misc::StrictHash[x.symbolize_keys] } end + end - def server_version - select_value("SELECT version()").to_f + def db_name + client.config.database.to_sym + end + + def parse_value(value, type:) + case type + when /String/ + value&.to_s + when /DateTime/ + Time.zone.parse(value) if value + else + value end + end - def pg_table_connection(table) - host = ENV["PGHOST"] || DB.opts[:host].presence || "localhost" - port = DB.opts[:port] || 5432 - database = DB.opts[:database] - username = DB.opts[:user] - password = DB.opts[:password] + def server_version + select_value("SELECT version()").to_f + end - Sequel.function(:postgresql, "#{host}:#{port}", database, table, username, password) - end + def pg_table_connection(table) + host = ENV["PGHOST"] || DB.opts[:host].presence || "localhost" + port = DB.opts[:port] || 5432 + database = DB.opts[:database] + username = DB.opts[:user] + password = DB.opts[:password] + + Sequel.function(:postgresql, "#{host}:#{port}", database, table, username, password) + end - def with_temp_table( - dataset, temp_table_name:, primary_key: [:id], primary_key_types: [:integer], **opts, & + def with_temp_table( + dataset, temp_table_name:, primary_key: [:id], primary_key_types: [:integer], **opts, & + ) + UmbrellioUtils::Database.create_temp_table( + nil, primary_key:, primary_key_types:, temp_table_name:, & ) - UmbrellioUtils::Database.create_temp_table( - nil, primary_key:, primary_key_types:, temp_table_name:, & - ) - populate_temp_table!(temp_table_name, dataset) - UmbrellioUtils::Database.with_temp_table(nil, primary_key:, temp_table_name:, **opts, &) - end + populate_temp_table!(temp_table_name, dataset) + UmbrellioUtils::Database.with_temp_table(nil, primary_key:, temp_table_name:, **opts, &) + end - private + private - def client(host = nil) - cfg = ::ClickHouse.config - cfg.host = resolve(host) if host - ::ClickHouse::Connection.new(cfg) - end - memoize :client, ttl: 1.minute + def client(host = nil) + cfg = ::ClickHouse.config + cfg.host = resolve(host) if host + ::ClickHouse::Connection.new(cfg) + end + memoize :client, ttl: 1.minute - def resolve(host) - IPSocket.getaddress(host) - rescue => e - Exceptions.notify!(e, raise_errors: false) - config.host - end + def resolve(host) + IPSocket.getaddress(host) + rescue => e + Exceptions.notify!(e, raise_errors: false) + config.host + end - def logger - client.config.logger - end + def logger + client.config.logger + end - def log_errors(sql) - yield - rescue ::ClickHouse::Error => e - logger.error("ClickHouse error: #{e.inspect}\nSQL: #{sql}") - raise e - end + def log_errors(sql) + yield + rescue ::ClickHouse::Error => e + logger.error("ClickHouse error: #{e.inspect}\nSQL: #{sql}") + raise e + end - def sql_for(dataset) - unless ch_dataset?(dataset) - raise "Non-ClickHouse dataset: #{dataset.inspect}. " \ + def sql_for(dataset) + unless ch_dataset?(dataset) + raise "Non-ClickHouse dataset: #{dataset.inspect}. " \ "You should use `CH.from` instead of `DB`" - end - - dataset.sql end - def ch_dataset?(dataset) - case dataset - when Sequel::Dataset - dataset.opts[:ch] && Array(dataset.opts[:from]).all? { |x| ch_dataset?(x) } - when Sequel::SQL::AliasedExpression - ch_dataset?(dataset.expression) - when Sequel::SQL::Identifier, Sequel::SQL::QualifiedIdentifier - true - else - raise "Unknown dataset type: #{dataset.inspect}" - end - end + dataset.sql + end - def full_table_name(table_name, db_name) - table_name = table_name.value if table_name.is_a?(Sequel::SQL::Identifier) - "#{db_name}.#{table_name}" + def ch_dataset?(dataset) + case dataset + when Sequel::Dataset + dataset.opts[:ch] && Array(dataset.opts[:from]).all? { |x| ch_dataset?(x) } + when Sequel::SQL::AliasedExpression + ch_dataset?(dataset.expression) + when Sequel::SQL::Identifier, Sequel::SQL::QualifiedIdentifier + true + else + raise "Unknown dataset type: #{dataset.inspect}" end + end - def select_all(sql, host: nil, **opts) - response = client(host).get(body: sql, query: { default_format: "JSON", **opts }) - ::ClickHouse::Response::Factory.response(response, client(host).config) - end + def full_table_name(table_name, db_name) + table_name = table_name.value if table_name.is_a?(Sequel::SQL::Identifier) + "#{db_name}.#{table_name}" + end - def select_value(...) - select_all(...).first.to_a.dig(0, -1) - end + def select_all(sql, host: nil, **opts) + response = client(host).get(body: sql, query: { default_format: "JSON", **opts }) + ::ClickHouse::Response::Factory.response(response, client(host).config) + end - def populate_temp_table!(temp_table_name, dataset) - execute(<<~SQL.squish) - INSERT INTO TABLE FUNCTION #{DB.literal(pg_table_connection(temp_table_name))} - #{dataset.sql} - SQL - end + def select_value(...) + select_all(...).first.to_a.dig(0, -1) + end + + def populate_temp_table!(temp_table_name, dataset) + execute(<<~SQL.squish) + INSERT INTO TABLE FUNCTION #{DB.literal(pg_table_connection(temp_table_name))} + #{dataset.sql} + SQL end end end diff --git a/lib/umbrellio_utils/version.rb b/lib/umbrellio_utils/version.rb index 9c27714..495981b 100644 --- a/lib/umbrellio_utils/version.rb +++ b/lib/umbrellio_utils/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module UmbrellioUtils - VERSION = "1.10.2" + VERSION = "1.10.0" end diff --git a/spec/support/database.rb b/spec/support/database.rb index 8fdd321..0f3a3a2 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -6,8 +6,8 @@ db_name = "umbrellio_utils_test" DB = Sequel.postgres( "umbrellio_utils_test", - user: "user", - password: "pass", + user: "igor", + password: "password", host: "localhost", port: 5432, ) From 883c58d780c74b0c1d04b0ca8cb6ea444add2fd3 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 13:46:26 +0600 Subject: [PATCH 38/58] fix --- lib/umbrellio_utils/click_house.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/umbrellio_utils/click_house.rb b/lib/umbrellio_utils/click_house.rb index 21618e4..c731866 100644 --- a/lib/umbrellio_utils/click_house.rb +++ b/lib/umbrellio_utils/click_house.rb @@ -141,7 +141,7 @@ def log_errors(sql) def sql_for(dataset) unless ch_dataset?(dataset) raise "Non-ClickHouse dataset: #{dataset.inspect}. " \ - "You should use `CH.from` instead of `DB`" + "You should use `CH.from` instead of `DB`" end dataset.sql From 0c8828d6caaf8f521115e241a2ae20b7cc0647be Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 13:47:41 +0600 Subject: [PATCH 39/58] fix --- lib/umbrellio_utils/click_house.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/umbrellio_utils/click_house.rb b/lib/umbrellio_utils/click_house.rb index c731866..b42fa86 100644 --- a/lib/umbrellio_utils/click_house.rb +++ b/lib/umbrellio_utils/click_house.rb @@ -6,7 +6,7 @@ module ClickHouse extend self - delegate :create_database, :drop_database, :config, to: :client + delegate :create_database, :drop_database, :tables, :config, to: :client def insert(table_name, db_name: self.db_name, rows: []) client.insert(full_table_name(table_name, db_name), rows, format: "JSONEachRow") From dc299e74e30405ee6e13feb3163ef45758a3bc5e Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 13:48:15 +0600 Subject: [PATCH 40/58] fix --- spec/support/database.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/support/database.rb b/spec/support/database.rb index 0f3a3a2..8fdd321 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -6,8 +6,8 @@ db_name = "umbrellio_utils_test" DB = Sequel.postgres( "umbrellio_utils_test", - user: "igor", - password: "password", + user: "user", + password: "pass", host: "localhost", port: 5432, ) From 0ac31c2a5a5e61a4e4ae96ce52855e4972c4e2a1 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 14:04:20 +0600 Subject: [PATCH 41/58] fix --- lib/umbrellio-utils.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/umbrellio-utils.rb b/lib/umbrellio-utils.rb index 91d9c47..37910e1 100644 --- a/lib/umbrellio-utils.rb +++ b/lib/umbrellio-utils.rb @@ -1,7 +1,3 @@ # frozen_string_literal: true require_relative "umbrellio_utils" - -if defined?(Rake) - Dir[File.join(__dir__, "tasks/**/*.rake")].each { |f| load f } -end From 08a1a6152eae404a47fb4a1e10cec07237dd2749 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 14:05:19 +0600 Subject: [PATCH 42/58] fix --- lib/umbrellio-utils.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/umbrellio-utils.rb b/lib/umbrellio-utils.rb index 37910e1..91d9c47 100644 --- a/lib/umbrellio-utils.rb +++ b/lib/umbrellio-utils.rb @@ -1,3 +1,7 @@ # frozen_string_literal: true require_relative "umbrellio_utils" + +if defined?(Rake) + Dir[File.join(__dir__, "tasks/**/*.rake")].each { |f| load f } +end From 443b3e9cd865f48e759d3154f5d112b48a797631 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 15:17:41 +0600 Subject: [PATCH 43/58] fix --- lib/umbrellio_utils/sql.rb | 78 +++++++++++++++++++++- spec/support/database.rb | 6 +- spec/umbrellio_utils/sql_spec.rb | 107 ++++++++++++++++++++++++++++++- 3 files changed, 184 insertions(+), 7 deletions(-) diff --git a/lib/umbrellio_utils/sql.rb b/lib/umbrellio_utils/sql.rb index 1a6bbec..c54081c 100644 --- a/lib/umbrellio_utils/sql.rb +++ b/lib/umbrellio_utils/sql.rb @@ -26,6 +26,15 @@ def pg_jsonb(...) Sequel.pg_jsonb(...) end + def to_utc(date) + func(:timezone, "UTC", date) + end + + def to_timezone(zone, date) + utc_date = to_utc(date) + func(:timezone, zone, cast(utc_date, :timestamptz)) + end + def and(*conditions) Sequel.&(*Array(conditions.flatten.presence || true)) end @@ -38,10 +47,14 @@ def or(*conditions) Sequel.|(*Array(conditions.flatten.presence || true)) end - def range(from_value, to_value, **opts) + def pg_range(from_value, to_value, **opts) Sequel::Postgres::PGRange.new(from_value, to_value, **opts) end + def pg_range_by_range(range) + Sequel::Postgres::PGRange.from_range(range) + end + def max(expr) func(:max, expr) end @@ -86,6 +99,18 @@ def coalesce(*exprs) func(:coalesce, *exprs) end + def coalesce0(*args) + coalesce(*args, 0) + end + + def nullif(main_expr, checking_expr) + func(:nullif, main_expr, checking_expr) + end + + def distinct(expr) + func(:distinct, expr) + end + def least(*exprs) func(:least, *exprs) end @@ -104,7 +129,50 @@ def ch_timestamp(time) def ch_timestamp_expr(time) time = Time.zone.parse(time) if time.is_a?(String) - SQL.func(:toDateTime64, SQL[ch_timestamp(time)], 6) + func(:toDateTime64, Sequel[ch_timestamp(time)], 6) + end + + def ch_time_range(range) + Range.new(ch_timestamp(range.begin), ch_timestamp(range.end), range.exclude_end?) + end + + def jsonb_dig(jsonb, path) + path.reduce(jsonb) { |acc, cur| acc[cur] } + end + + def jsonb_typeof(jsonb) + func(:jsonb_typeof, jsonb) + end + + def empty_jsonb + Sequel.pg_jsonb({}) + end + + def round(value, precision = 0) + func(:round, value, precision) + end + + def row(*values) + func(:row, *values) + end + + def map_to_expr(hash) + hash.map { |aliaz, expr| expr.as(aliaz) } + end + + def intersect(left_expr, right_expr) + Sequel.lit("SELECT ? INTERSECT SELECT ?", left_expr, right_expr) + end + + # can rewrite scalar values + def jsonb_unsafe_set(jsonb, path, value) + parent_path = path.slice(..-2) + raw_parent = jsonb_dig(jsonb, parent_path) + parent = jsonb_rewrite_scalar(raw_parent) + last_path = path.slice(-1..-1) + updated_parent = parent.set(last_path, value) + result = self.case({ { value => nil } => parent }, updated_parent) + jsonb.set(parent_path, result) end def true @@ -114,5 +182,11 @@ def true def false Sequel.lit("false") end + + private + + def jsonb_rewrite_scalar(jsonb) + self.case({ { jsonb_typeof(jsonb) => %w[object array] } => jsonb }, empty_jsonb).pg_jsonb + end end end diff --git a/spec/support/database.rb b/spec/support/database.rb index 8fdd321..4df0724 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -6,8 +6,8 @@ db_name = "umbrellio_utils_test" DB = Sequel.postgres( "umbrellio_utils_test", - user: "user", - password: "pass", + user: "igor", + password: "password", host: "localhost", port: 5432, ) @@ -25,6 +25,8 @@ DB.extension :pg_json DB.extension :pg_range +Sequel.extension :pg_json_ops + DB.drop_table? :users, cascade: true DB.create_table :users do primary_key :id diff --git a/spec/umbrellio_utils/sql_spec.rb b/spec/umbrellio_utils/sql_spec.rb index 948a1d7..f42126b 100644 --- a/spec/umbrellio_utils/sql_spec.rb +++ b/spec/umbrellio_utils/sql_spec.rb @@ -47,15 +47,53 @@ specify { expect(result).to eq('(("test" = 123) OR ("test2" = 321))') } end - describe "#range" do - let(:expr) { sql.range(Time.zone.parse("2014-01-01"), Time.zone.parse("2015-01-01")) } + describe "#to_utc" do + let(:expr) { sql.to_utc("2020-01-01 00:00:00.000000") } + + specify { expect(result).to eq("timezone('UTC', '2020-01-01 00:00:00.000000')") } + end + + describe "#to_timezone" do + let(:expr) { sql.to_timezone("UTC+6", "2020-01-01 00:00:00.000000") } + + specify do + expect(result).to eq( + "timezone('UTC+6', CAST(timezone('UTC', '2020-01-01 00:00:00.000000') AS timestamptz))", + ) + end + end + + describe "#pg_range" do + let(:expr) { sql.pg_range(Time.zone.parse("2014-01-01"), Time.zone.parse("2015-01-01")) } + + specify do + expect(result).to eq("'[2014-01-01 00:00:00.000000+0000,2015-01-01 00:00:00.000000+0000]'") + end + end + + describe "#pg_range_by_range" do + let(:expr) do + sql.pg_range_by_range(Time.zone.parse("2014-01-01")..Time.zone.parse("2015-01-01")) + end specify do expect(result).to eq("'[2014-01-01 00:00:00.000000+0000,2015-01-01 00:00:00.000000+0000]'") end end - %w[max min sum avg abs coalesce least greatest].each do |function| + describe "#coalesce0" do + let(:expr) { sql.coalesce0("test") } + + specify { expect(result).to eq("coalesce('test', 0)") } + end + + describe "#nullif" do + let(:expr) { sql.nullif(1, 1) } + + specify { expect(result).to eq("nullif(1, 1)") } + end + + %w[max min sum avg abs coalesce least greatest distinct jsonb_typeof row].each do |function| describe "##{function}" do let(:expr) { sql.public_send(function, :test) } @@ -135,6 +173,69 @@ end end + describe "#ch_time_range" do + let(:expr) { sql.ch_time_range(Time.zone.parse("2020-01-01")..Time.zone.parse("2020-01-02")) } + + specify { expect(result).to eq("'[2020-01-01 00:00:00.000000,2020-01-02 00:00:00.000000]'") } + end + + describe "#jsonb_dig" do + let(:expr) { sql.jsonb_dig(sql[:test].pg_jsonb, %w[test test2]) } + + specify { expect(result).to eq(%("test"['test']['test2'])) } + end + + describe "#empty_jsonb" do + let(:expr) { sql.empty_jsonb } + + specify { expect(result).to eq("'{}'::jsonb") } + end + + describe "#round" do + let(:expr) { sql.round(:test, 5) } + + specify { expect(result).to eq('round("test", 5)') } + end + + describe "#map_to_expr" do + let(:expr) { sql.map_to_expr({ test: Sequel[:test1], test2: Sequel[:some] }) } + + specify { expect(result).to eq('("test1" AS "test", "some" AS "test2")') } + end + + describe "#intersect" do + let(:expr) { sql.intersect(:test, :test2) } + + specify { expect(result).to eq('SELECT "test" INTERSECT SELECT "test2"') } + end + + describe "#jsonb_unsafe_set" do + let(:expr) { sql.jsonb_unsafe_set(sql[:test].pg_jsonb, %w[test test2], 123) } + + specify do + expect(result).to eq( + <<~SQL.squish.gsub("( ", "(").gsub(" )", ")"), + jsonb_set( + "test", ('test'), + (CASE WHEN (123 IS NULL) THEN ( + CASE WHEN ( + jsonb_typeof("test"['test']) IN ('object', 'array') + ) THEN "test"['test'] ELSE '{}'::jsonb END + ) ELSE jsonb_set( + (CASE WHEN ( + jsonb_typeof("test"['test']) IN ('object', 'array') + ) THEN "test"['test'] ELSE '{}'::jsonb END), + ('test2'), + 123, + true + ) END + ), + true) + SQL + ) + end + end + describe "#true" do let(:expr) { sql.true } From 9945230c146ca4eaa1ed228d330625309d01964f Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 15:20:50 +0600 Subject: [PATCH 44/58] fix --- spec/support/database.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/support/database.rb b/spec/support/database.rb index 4df0724..f673f11 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -6,8 +6,8 @@ db_name = "umbrellio_utils_test" DB = Sequel.postgres( "umbrellio_utils_test", - user: "igor", - password: "password", + user: "user", + password: "pass", host: "localhost", port: 5432, ) From 1e78684c7ef7f82cbead784f05f933c7c59b729f Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 15:42:31 +0600 Subject: [PATCH 45/58] fix --- lib/umbrellio_utils/click_house.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/umbrellio_utils/click_house.rb b/lib/umbrellio_utils/click_house.rb index b42fa86..0fac368 100644 --- a/lib/umbrellio_utils/click_house.rb +++ b/lib/umbrellio_utils/click_house.rb @@ -6,7 +6,7 @@ module ClickHouse extend self - delegate :create_database, :drop_database, :tables, :config, to: :client + delegate :create_database, :drop_database, :tables, :list_table_columns, :config, to: :client def insert(table_name, db_name: self.db_name, rows: []) client.insert(full_table_name(table_name, db_name), rows, format: "JSONEachRow") From 840da0e881c275dffb716607ae31565a6204fdc7 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 15:45:13 +0600 Subject: [PATCH 46/58] fix --- lib/umbrellio_utils/click_house.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/umbrellio_utils/click_house.rb b/lib/umbrellio_utils/click_house.rb index 0fac368..b42fa86 100644 --- a/lib/umbrellio_utils/click_house.rb +++ b/lib/umbrellio_utils/click_house.rb @@ -6,7 +6,7 @@ module ClickHouse extend self - delegate :create_database, :drop_database, :tables, :list_table_columns, :config, to: :client + delegate :create_database, :drop_database, :tables, :config, to: :client def insert(table_name, db_name: self.db_name, rows: []) client.insert(full_table_name(table_name, db_name), rows, format: "JSONEachRow") From a293ac35932070b7d1606de59d3cd801ba79f651 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 15:48:22 +0600 Subject: [PATCH 47/58] fix --- spec/support/sequel_patches.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/sequel_patches.rb b/spec/support/sequel_patches.rb index 0558bee..82346ca 100644 --- a/spec/support/sequel_patches.rb +++ b/spec/support/sequel_patches.rb @@ -5,7 +5,7 @@ # Make Postgres return rows truly randomly in specs unless order is properly specified class Sequel::Postgres::Dataset # rubocop:disable Lint/ConstantDefinitionInBlock def select_sql - return super if @opts[:_skip_order_patch] + return super if @opts[:_skip_order_patch] || @opts[:append_sql] return super if @opts[:ch] && @opts[:order].present? order = @opts[:order].dup || [] fn = @opts.key?(:ch) ? :rand : :random From 181b2bdb873d49936cd005a5f2a842af98bfbf25 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 15:52:24 +0600 Subject: [PATCH 48/58] fixx --- spec/umbrellio_utils/clickhouse_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/umbrellio_utils/clickhouse_spec.rb b/spec/umbrellio_utils/clickhouse_spec.rb index 49bf402..837a089 100644 --- a/spec/umbrellio_utils/clickhouse_spec.rb +++ b/spec/umbrellio_utils/clickhouse_spec.rb @@ -27,7 +27,7 @@ context "with another source" do specify do expect(ch.from(ch.from(:test)).sql).to eq( - 'SELECT * FROM (SELECT * FROM "test" ORDER BY rand()) AS "t1" ORDER BY rand()'.b, + 'SELECT * FROM (SELECT * FROM "test") AS "t1" ORDER BY rand()'.b, ) end end From 0e0c873592b0ae773b89e84e336f985d9614eff0 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 15:54:18 +0600 Subject: [PATCH 49/58] fix --- lib/testing/sequel_patches.rb | 17 +++++++++++++++++ spec/support/sequel_patches.rb | 16 +--------------- 2 files changed, 18 insertions(+), 15 deletions(-) create mode 100644 lib/testing/sequel_patches.rb diff --git a/lib/testing/sequel_patches.rb b/lib/testing/sequel_patches.rb new file mode 100644 index 0000000..82346ca --- /dev/null +++ b/lib/testing/sequel_patches.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.before(:suite) do + # Make Postgres return rows truly randomly in specs unless order is properly specified + class Sequel::Postgres::Dataset # rubocop:disable Lint/ConstantDefinitionInBlock + def select_sql + return super if @opts[:_skip_order_patch] || @opts[:append_sql] + return super if @opts[:ch] && @opts[:order].present? + order = @opts[:order].dup || [] + fn = @opts.key?(:ch) ? :rand : :random + order << Sequel.function(fn) + clone(order:, _skip_order_patch: true).select_sql + end + end + end +end diff --git a/spec/support/sequel_patches.rb b/spec/support/sequel_patches.rb index 82346ca..b425708 100644 --- a/spec/support/sequel_patches.rb +++ b/spec/support/sequel_patches.rb @@ -1,17 +1,3 @@ # frozen_string_literal: true -RSpec.configure do |config| - config.before(:suite) do - # Make Postgres return rows truly randomly in specs unless order is properly specified - class Sequel::Postgres::Dataset # rubocop:disable Lint/ConstantDefinitionInBlock - def select_sql - return super if @opts[:_skip_order_patch] || @opts[:append_sql] - return super if @opts[:ch] && @opts[:order].present? - order = @opts[:order].dup || [] - fn = @opts.key?(:ch) ? :rand : :random - order << Sequel.function(fn) - clone(order:, _skip_order_patch: true).select_sql - end - end - end -end +require "testing/sequel_patches" From 907087b956e3f285189e0d55595a0315379dc4d7 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 15:57:36 +0600 Subject: [PATCH 50/58] fix --- lib/umbrellio-utils.rb | 4 ---- lib/{ => umbrellio_utils}/tasks/clickhouse_connect.rake | 0 lib/{ => umbrellio_utils}/testing/sequel_patches.rb | 0 3 files changed, 4 deletions(-) rename lib/{ => umbrellio_utils}/tasks/clickhouse_connect.rake (100%) rename lib/{ => umbrellio_utils}/testing/sequel_patches.rb (100%) diff --git a/lib/umbrellio-utils.rb b/lib/umbrellio-utils.rb index 91d9c47..37910e1 100644 --- a/lib/umbrellio-utils.rb +++ b/lib/umbrellio-utils.rb @@ -1,7 +1,3 @@ # frozen_string_literal: true require_relative "umbrellio_utils" - -if defined?(Rake) - Dir[File.join(__dir__, "tasks/**/*.rake")].each { |f| load f } -end diff --git a/lib/tasks/clickhouse_connect.rake b/lib/umbrellio_utils/tasks/clickhouse_connect.rake similarity index 100% rename from lib/tasks/clickhouse_connect.rake rename to lib/umbrellio_utils/tasks/clickhouse_connect.rake diff --git a/lib/testing/sequel_patches.rb b/lib/umbrellio_utils/testing/sequel_patches.rb similarity index 100% rename from lib/testing/sequel_patches.rb rename to lib/umbrellio_utils/testing/sequel_patches.rb From 3c364b54e7197cc49edb3fb66ed80c1d1248c47c Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 15:58:36 +0600 Subject: [PATCH 51/58] fix --- lib/umbrellio-utils.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/umbrellio-utils.rb b/lib/umbrellio-utils.rb index 37910e1..e56a39a 100644 --- a/lib/umbrellio-utils.rb +++ b/lib/umbrellio-utils.rb @@ -1,3 +1,7 @@ # frozen_string_literal: true require_relative "umbrellio_utils" + +if defined?(Rake) + Dir[File.join(__dir__, "umbrellio_utils/tasks/**/*.rake")].each { |f| load f } +end From 0cee95eb4b7b215d73ebf15fd0e3ad48799e8241 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 15:59:02 +0600 Subject: [PATCH 52/58] fix --- spec/support/sequel_patches.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/sequel_patches.rb b/spec/support/sequel_patches.rb index b425708..7df39a4 100644 --- a/spec/support/sequel_patches.rb +++ b/spec/support/sequel_patches.rb @@ -1,3 +1,3 @@ # frozen_string_literal: true -require "testing/sequel_patches" +require "umbrellio_utils/testing/sequel_patches" From 60a4ee87febf18cb4b33039f62780180ba82260c Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 16:00:24 +0600 Subject: [PATCH 53/58] fix --- lib/umbrellio-utils.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/umbrellio-utils.rb b/lib/umbrellio-utils.rb index e56a39a..2ce1e6f 100644 --- a/lib/umbrellio-utils.rb +++ b/lib/umbrellio-utils.rb @@ -5,3 +5,7 @@ if defined?(Rake) Dir[File.join(__dir__, "umbrellio_utils/tasks/**/*.rake")].each { |f| load f } end + +if defined?(RSpec) + Dir[File.join(__dir__, "umbrellio_utils/testing/**/*.rb")].each { |f| load f } +end From bf489c496845d62631e82f3550f0a7157b3396ff Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 16:05:48 +0600 Subject: [PATCH 54/58] fix --- lib/umbrellio-utils.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/umbrellio-utils.rb b/lib/umbrellio-utils.rb index 2ce1e6f..5fb4309 100644 --- a/lib/umbrellio-utils.rb +++ b/lib/umbrellio-utils.rb @@ -7,5 +7,5 @@ end if defined?(RSpec) - Dir[File.join(__dir__, "umbrellio_utils/testing/**/*.rb")].each { |f| load f } + Dir[File.join(__dir__, "umbrellio_utils/testing/**/*.rb")].each { |f| require f } end From ea2f33b13b546c06100dbb161608c39faad93f26 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 16:29:13 +0600 Subject: [PATCH 55/58] fix --- lib/umbrellio-utils.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/umbrellio-utils.rb b/lib/umbrellio-utils.rb index 5fb4309..2ce1e6f 100644 --- a/lib/umbrellio-utils.rb +++ b/lib/umbrellio-utils.rb @@ -7,5 +7,5 @@ end if defined?(RSpec) - Dir[File.join(__dir__, "umbrellio_utils/testing/**/*.rb")].each { |f| require f } + Dir[File.join(__dir__, "umbrellio_utils/testing/**/*.rb")].each { |f| load f } end From e4037d4f6d71fe2fcdcf19610770bede2bcc5dfb Mon Sep 17 00:00:00 2001 From: KirIgor Date: Tue, 2 Dec 2025 16:29:22 +0600 Subject: [PATCH 56/58] fix --- lib/umbrellio-utils.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/umbrellio-utils.rb b/lib/umbrellio-utils.rb index 2ce1e6f..e56a39a 100644 --- a/lib/umbrellio-utils.rb +++ b/lib/umbrellio-utils.rb @@ -5,7 +5,3 @@ if defined?(Rake) Dir[File.join(__dir__, "umbrellio_utils/tasks/**/*.rake")].each { |f| load f } end - -if defined?(RSpec) - Dir[File.join(__dir__, "umbrellio_utils/testing/**/*.rb")].each { |f| load f } -end From f6e009dd19c4c7c69b7e301b2dd13421d222fa69 Mon Sep 17 00:00:00 2001 From: KirIgor Date: Wed, 3 Dec 2025 18:48:31 +0600 Subject: [PATCH 57/58] fix --- lib/umbrellio_utils/click_house.rb | 10 +++++---- spec/support/database.rb | 4 ++-- spec/umbrellio_utils/clickhouse_spec.rb | 29 ++++++++++++++++++++----- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/lib/umbrellio_utils/click_house.rb b/lib/umbrellio_utils/click_house.rb index b42fa86..00692fc 100644 --- a/lib/umbrellio_utils/click_house.rb +++ b/lib/umbrellio_utils/click_house.rb @@ -104,10 +104,12 @@ def pg_table_connection(table) def with_temp_table( dataset, temp_table_name:, primary_key: [:id], primary_key_types: [:integer], **opts, & ) - UmbrellioUtils::Database.create_temp_table( - nil, primary_key:, primary_key_types:, temp_table_name:, & - ) - populate_temp_table!(temp_table_name, dataset) + unless DB.table_exists?(temp_table_name) + UmbrellioUtils::Database.create_temp_table( + nil, primary_key:, primary_key_types:, temp_table_name:, & + ) + populate_temp_table!(temp_table_name, dataset) + end UmbrellioUtils::Database.with_temp_table(nil, primary_key:, temp_table_name:, **opts, &) end diff --git a/spec/support/database.rb b/spec/support/database.rb index f673f11..4df0724 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -6,8 +6,8 @@ db_name = "umbrellio_utils_test" DB = Sequel.postgres( "umbrellio_utils_test", - user: "user", - password: "pass", + user: "igor", + password: "password", host: "localhost", port: 5432, ) diff --git a/spec/umbrellio_utils/clickhouse_spec.rb b/spec/umbrellio_utils/clickhouse_spec.rb index 837a089..6698aab 100644 --- a/spec/umbrellio_utils/clickhouse_spec.rb +++ b/spec/umbrellio_utils/clickhouse_spec.rb @@ -83,13 +83,30 @@ end describe "#with_temp_table" do - specify do - result = [] - dataset = ch.from(:test).order(:id) - ch.with_temp_table(dataset, temp_table_name: "some_test_table", page_size: 1) do |batch| - result << batch + let(:result) { [] } + let(:dataset) { ch.from(:test).order(:id) } + + context "when no temp table" do + before { DB.drop_table?(:some_test_table) } + + specify do + ch.with_temp_table(dataset, temp_table_name: "some_test_table", page_size: 1) do |batch| + result << batch + end + expect(result).to eq([[3], [2], [1]]) + end + end + + context "when table already exist" do + before { DB.create_table("some_test_table") { primary_key :id } } + before { DB[:some_test_table].multi_insert([{ id: 4 }, { id: 5 }, { id: 6 }]) } + + it "takes from existing" do + ch.with_temp_table(dataset, temp_table_name: "some_test_table", page_size: 1) do |batch| + result << batch + end + expect(result).to eq([[6], [5], [4]]) end - expect(result).to eq([[3], [2], [1]]) end end From 60513949f5f346d0c932dfc1e39b36408b8adeba Mon Sep 17 00:00:00 2001 From: KirIgor Date: Wed, 3 Dec 2025 18:51:04 +0600 Subject: [PATCH 58/58] fix --- .github/workflows/test.yml | 1 + spec/support/database.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c82ae3..a307874 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,7 @@ jobs: env: PGHOST: pg PGUSER: user + PGPASSWORD: pass steps: - uses: actions/checkout@v4 diff --git a/spec/support/database.rb b/spec/support/database.rb index 4df0724..fc4a558 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -6,8 +6,8 @@ db_name = "umbrellio_utils_test" DB = Sequel.postgres( "umbrellio_utils_test", - user: "igor", - password: "password", + user: ENV.fetch("PGUSER"), + password: ENV.fetch("PGPASSWORD"), host: "localhost", port: 5432, )