diff --git a/app/services/ci/collect_aggregate_pipeline_analytics_service.rb b/app/services/ci/collect_aggregate_pipeline_analytics_service.rb index 39e2f2d110c73d48b14d26bb5339dbee02bcf5b8..32db944382087cbc80b8be3c87e0ce3e23f838b9 100644 --- a/app/services/ci/collect_aggregate_pipeline_analytics_service.rb +++ b/app/services/ci/collect_aggregate_pipeline_analytics_service.rb @@ -28,7 +28,7 @@ def calculate_aggregate_count(query, result) return if status_groups.exclude?(:any) all_query = query.select(query.count_pipelines_function.as('all')) - result[:any] = ::ClickHouse::Client.select(all_query.to_sql, :main).first['all'] + result[:any] = ::ClickHouse::Client.select(all_query, :main).first['all'] end def calculate_aggregate_status_group_counts(query, result) @@ -39,7 +39,7 @@ def calculate_aggregate_status_group_counts(query, result) .by_status(selected_statuses) .group_by_status - result_by_status = ::ClickHouse::Client.select(query.to_sql, :main).map(&:values).to_h + result_by_status = ::ClickHouse::Client.select(query, :main).map(&:values).to_h result_by_status.each_pair { |status, count| result[STATUS_TO_STATUS_GROUP[status]] += count } end @@ -47,7 +47,7 @@ def calculate_aggregate_duration_percentiles(query, result) return if duration_percentiles.empty? duration_query = query.select(*duration_percentiles.map { |p| query.duration_quantile_function(p) }) - duration_result = ::ClickHouse::Client.select(duration_query.to_sql, :main) + duration_result = ::ClickHouse::Client.select(duration_query, :main) result[:duration_statistics] = duration_result.first.symbolize_keys.transform_values do |interval| interval.to_f.round(3).seconds end diff --git a/app/services/ci/collect_time_series_pipeline_analytics_service.rb b/app/services/ci/collect_time_series_pipeline_analytics_service.rb index 3ee25619be1d91679e16d31f689a07a0a6c2aae2..206d56e0ad44792c3d08854a0591790d455e03b8 100644 --- a/app/services/ci/collect_time_series_pipeline_analytics_service.rb +++ b/app/services/ci/collect_time_series_pipeline_analytics_service.rb @@ -173,7 +173,7 @@ def group_and_sum_counts(counts_by_status_and_time) end def execute_select_query(query) - ::ClickHouse::Client.select(query.to_sql, :main).map(&:symbolize_keys) + ::ClickHouse::Client.select(query, :main).map(&:symbolize_keys) end def collect_metrics diff --git a/gems/click_house-client/lib/click_house/client/query_like.rb b/gems/click_house-client/lib/click_house/client/query_like.rb index 9e9ee46a3388cb23126630e4201619b84a3d21de..b33c45ee0f80d5a6cb0df643fba99f66be4525c6 100644 --- a/gems/click_house-client/lib/click_house/client/query_like.rb +++ b/gems/click_house-client/lib/click_house/client/query_like.rb @@ -14,6 +14,11 @@ def to_sql def to_redacted_sql(bind_index_manager = BindIndexManager.new) raise NotImplementedError end + + # Override when placeholders should be supported + def placeholders + {} + end end end end diff --git a/gems/click_house-client/spec/click_house/client/query_like_spec.rb b/gems/click_house-client/spec/click_house/client/query_like_spec.rb index 8b8426bd5fd8f30054f3ba4cbec30cdf17be84cc..b2c99fc81890de3f8b60143ee00afa6d2d165978 100644 --- a/gems/click_house-client/spec/click_house/client/query_like_spec.rb +++ b/gems/click_house-client/spec/click_house/client/query_like_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ClickHouse::Client::QueryLike do +RSpec.describe ClickHouse::Client::QueryLike, feature_category: :database do subject(:query) { described_class.new } describe '#to_sql' do @@ -12,4 +12,8 @@ describe '#to_redacted_sql' do it { expect { query.to_redacted_sql }.to raise_error(NotImplementedError) } end + + describe '#placeholders' do + it { expect(query.placeholders).to eq({}) } + end end diff --git a/lib/click_house/models/base_model.rb b/lib/click_house/models/base_model.rb index ec453bb638bd99d64180ffdf8fc62eab60ed6493..044c434701d51147b36129652c2d60758fe42b64 100644 --- a/lib/click_house/models/base_model.rb +++ b/lib/click_house/models/base_model.rb @@ -3,10 +3,10 @@ # rubocop: disable CodeReuse/ActiveRecord module ClickHouse module Models - class BaseModel + class BaseModel < ClickHouse::Client::QueryLike extend Forwardable - def_delegators :@query_builder, :to_sql + def_delegators :@query_builder, :to_sql, :to_redacted_sql def initialize(query_builder = ClickHouse::QueryBuilder.new(self.class.table_name)) @query_builder = query_builder diff --git a/lib/click_house/redactor.rb b/lib/click_house/redactor.rb index 6ca7c46e747a93b7aa22ce0c683378fc78705b4e..32e442d4b6f718cfcb6a95ec6abc05cff039cb25 100644 --- a/lib/click_house/redactor.rb +++ b/lib/click_house/redactor.rb @@ -43,10 +43,26 @@ def self.redact_condition(condition, bind_manager) condition.left.gt(Arel.sql(bind_manager.next_bind_str)) when Arel::Nodes::GreaterThanOrEqual condition.left.gteq(Arel.sql(bind_manager.next_bind_str)) + when Arel::Nodes::NamedFunction + redact_named_function(condition, bind_manager) else raise ArgumentError, "Unsupported Arel node type for Redactor: #{condition.class}" end end + + def self.redact_named_function(condition, bind_manager) + redacted_condition = + Arel::Nodes::NamedFunction.new(condition.name, condition.expressions.dup) + + case redacted_condition.name + when 'startsWith' + redacted_condition.expressions[1] = Arel.sql(bind_manager.next_bind_str) + else + redacted_condition.expressions = redacted_condition.expressions.map { Arel.sql(bind_manager.next_bind_str) } + end + + redacted_condition + end end end # rubocop:enable CodeReuse/ActiveRecord diff --git a/spec/lib/click_house/models/base_model_spec.rb b/spec/lib/click_house/models/base_model_spec.rb index fcee40d48e3fa655c7e1efd8a49fba1d44af4b77..e6e22522f80b64cf3076cbf889fff5691b438ddc 100644 --- a/spec/lib/click_house/models/base_model_spec.rb +++ b/spec/lib/click_house/models/base_model_spec.rb @@ -15,16 +15,26 @@ def self.table_name end end - describe '#to_sql' do - it 'delegates to the query builder' do - expect(query_builder).to receive(:to_sql).and_return("SELECT * FROM dummy_table") + it { expect(described_class).to be < ClickHouse::Client::QueryLike } + + shared_examples 'method delegated to query builder' do |method_name| + it "delegates ##{method_name} to @query_builder" do + expect(query_builder).to receive(method_name).and_return("SELECT * FROM dummy_table") dummy_instance = dummy_class.new(query_builder) - expect(dummy_instance.to_sql).to eq("SELECT * FROM dummy_table") + expect(dummy_instance.public_send(method_name)).to eq("SELECT * FROM dummy_table") end end + describe '#to_sql' do + it_behaves_like 'method delegated to query builder', :to_sql + end + + describe '#to_redacted_sql' do + it_behaves_like 'method delegated to query builder', :to_redacted_sql + end + describe '#where' do it 'returns a new instance with refined query' do dummy_instance = dummy_class.new(query_builder) diff --git a/spec/lib/click_house/redactor_spec.rb b/spec/lib/click_house/redactor_spec.rb index d8354b6cbb9fad7b905053c94e53b4f022ddf75d..3b16908f24f40caaa0ce3a468b6d4eacbea49bb5 100644 --- a/spec/lib/click_house/redactor_spec.rb +++ b/spec/lib/click_house/redactor_spec.rb @@ -107,6 +107,42 @@ expect(redacted_query).to eq(expected_redacted_sql) end + + context 'when condition is a named function' do + let(:function_name) { 'randomFunction' } + let(:expressions) { [Arel.sql("'traversal_path'"), Arel.sql('42')] } + let(:function_node) do + Arel::Nodes::NamedFunction.new( + function_name, + expressions + ) + end + + subject(:redacted_query) do + new_builder = builder.where(function_node) + described_class.redact(new_builder) + end + + it 'redacts all condition expressions' do + expected_redacted_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" WHERE randomFunction($1, $2) + SQL + + expect(redacted_query).to eq(expected_redacted_sql) + end + + context 'when using startsWith function' do + let(:function_name) { 'startsWith' } + + it 'redacts only value argument' do + expected_redacted_sql = <<~SQL.lines(chomp: true).join(' ') + SELECT * FROM "test_table" WHERE startsWith('traversal_path', $1) + SQL + + expect(redacted_query).to eq(expected_redacted_sql) + end + end + end end context 'with unsupported arel nodes' do diff --git a/spec/support/shared_examples/click_house/ci/collect_pipeline_analytics_shared_examples.rb b/spec/support/shared_examples/click_house/ci/collect_pipeline_analytics_shared_examples.rb index 31c6036768e98da3e0de83784401c70e526925a4..580423710d75ff350bddb2692e222a112d4d245f 100644 --- a/spec/support/shared_examples/click_house/ci/collect_pipeline_analytics_shared_examples.rb +++ b/spec/support/shared_examples/click_house/ci/collect_pipeline_analytics_shared_examples.rb @@ -1,6 +1,20 @@ # frozen_string_literal: true RSpec.shared_examples_for 'a pipeline analytics service' do + it 'does not execute raw sql queries' do + allow(::ClickHouse::Client).to receive(:select).and_call_original + + expect(::ClickHouse::Client) + .not_to receive(:select) + .with(instance_of(String), instance_of(Symbol)) + + expect(::ClickHouse::Client) + .to receive(:select) + .with(kind_of(ClickHouse::Client::QueryLike), instance_of(Symbol)) + + result + end + context 'when ClickHouse database is not configured' do before do allow(::Gitlab::ClickHouse).to receive(:configured?).and_return(false)