gitlab_quality-test_tooling 3.12.1 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 544d1dca9801992bdb2b7cbe9c34eb3dae7f7c47f84c3d51b5eb55dbfcaae6c6
4
- data.tar.gz: 8442c5eaa7f809c59cbad5e96ef46606ab7f1a881bc9040267929b66cf36f9d5
3
+ metadata.gz: 473fb18a44195986ea17fec58d277b67fd7589fca55a08250704687e2b70aa80
4
+ data.tar.gz: 4c18c0a11106a2a6103c1e5ba139dd68d1879ccb94360bddd77e1cf3beeafd34
5
5
  SHA512:
6
- metadata.gz: d1115577ba91a7bf0c1a8632a53502b58fad6b5c2cd62862230d70e26a31c7ef5eed202bed97f90cd2d8d98897bdc5a71f5006a4059a6b1bdac571144c5722c8
7
- data.tar.gz: 108b5be41cde544adee4439d56e61caeccab5a4fa5ebefcb785cea9b8dc70a4751cdce0659e7f4bf176165392a3e184dd944e8832bdc8c399661e87f36830c58
6
+ metadata.gz: ba2aac53d5ce9f33bba7791ddc5dfaae10dff68f118d0de6ab33ca9569b64e1a555f29ee9bb9d3d1b04d9f182e374dac27a3d75bd561531ceaa6c728f0a63545
7
+ data.tar.gz: 999efaaa3f6067aeb29b7c0cb00bf3ba8877f61fb108f8ef4213b1946e739a22249b77cf0a54feed33396ca6d5677920a34070d5b861a49535070f1cbc4bf464
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (3.12.1)
4
+ gitlab_quality-test_tooling (3.14.0)
5
5
  activesupport (>= 7.0)
6
6
  amatch (~> 0.4.1)
7
7
  fog-google (~> 1.24, >= 1.24.1)
@@ -7,6 +7,8 @@ module GitlabQuality
7
7
  SLOW_TEST_MESSAGE = '<!-- slow-test -->'
8
8
  SLOW_TEST_LABEL = '/label ~"rspec:slow test detected"'
9
9
  SLOW_TEST_NOTE_SOURCE_CODE = 'Generated by [`gitlab_quality-test_tooling`](https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling/-/blob/main/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb).'
10
+ SLOW_TEST_UNHEALTHY_PATTERNS = 'https://docs.gitlab.com/development/testing_guide/unhealthy_tests/#common-patterns-that-cause-slow-tests'
11
+ SLOW_TEST_BEST_PRACTICES = 'https://docs.gitlab.com/development/testing_guide/best_practices/#test-slowness'
10
12
 
11
13
  def initialize(token:, input_files:, merge_request_iid:, project: nil, dry_run: false, **_kwargs)
12
14
  @token = token
@@ -70,23 +72,36 @@ module GitlabQuality
70
72
  SLOW_TEST_LABEL,
71
73
  ":tools: #{SLOW_TEST_NOTE_SOURCE_CODE}\n",
72
74
  "---\n",
73
- ":snail: Slow tests detected in this merge request. These slow tests might be related to this merge request's changes.",
74
- "<details><summary>Click to expand</summary>\n",
75
- '| Job | File | Name | Duration | Expected duration |',
76
- '| --- | --- | --- | --- | --- |'
75
+ ":snail: Slow tests detected in this merge request. These slow tests might be related to this merge request's changes."
77
76
  ]
78
77
  end
79
78
 
79
+ def handbook_guidance
80
+ ":books: For guidance on improving test performance, see " \
81
+ "[Common Patterns That Cause Slow Tests](#{SLOW_TEST_UNHEALTHY_PATTERNS}) " \
82
+ "and [Best Practices – Test Slowness](#{SLOW_TEST_BEST_PRACTICES})."
83
+ end
84
+
85
+ def feature_spec?(slow_test)
86
+ slow_test.file.include?('spec/features/')
87
+ end
88
+
80
89
  def slow_test_rows(slow_test)
81
90
  slow_test.map do |test|
82
91
  slow_test_table_row(test)
83
92
  end
84
93
  end
85
94
 
86
- def build_note(slow_test)
87
- rows = note_header + slow_test_rows(slow_test) + ["\n</details>"]
95
+ def build_note(slow_tests)
96
+ rows = note_header
97
+ rows << "\n#{handbook_guidance}" if slow_tests.any? { |t| feature_spec?(t) }
98
+ rows += [
99
+ "<details><summary>Click to expand</summary>\n",
100
+ '| Job | File | Name | Duration | Expected duration |',
101
+ '| --- | --- | --- | --- | --- |'
102
+ ]
88
103
 
89
- rows.join("\n")
104
+ (rows + slow_test_rows(slow_tests) + ["\n</details>"]).join("\n")
90
105
  end
91
106
 
92
107
  def note_comment_includes_slow_test?(gitlab_note, slow_test)
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+ require "json"
5
+
6
+ module GitlabQuality
7
+ module TestTooling
8
+ module TestMetricsExporter
9
+ class Client
10
+ ResponseError = Class.new(StandardError)
11
+
12
+ TESTS_PATH = "/api/v1/tests"
13
+ # Observer enforces a per-request cap; batch above this size to avoid silent failures.
14
+ MAX_BATCH_SIZE = 10_000
15
+
16
+ def initialize(url:, token:)
17
+ @url = url
18
+ @token = token
19
+ end
20
+
21
+ # POST array of test metric records to the observer service.
22
+ # Wraps each batch as { "tests" => [...] } and splits oversized payloads
23
+ # into chunks of at most MAX_BATCH_SIZE records.
24
+ #
25
+ # @param tests [Array<Hash>]
26
+ # @return [Boolean] true when every batch succeeds (or input is empty)
27
+ # @raise [ResponseError] on the first non-2xx batch response
28
+ def post_tests(tests)
29
+ tests.each_slice(MAX_BATCH_SIZE) do |batch|
30
+ response = HTTParty.post(
31
+ "#{url.to_s.chomp('/')}#{TESTS_PATH}",
32
+ body: { tests: batch }.to_json,
33
+ headers: {
34
+ "X-Gitlab-Token" => token,
35
+ "Content-Type" => "application/json"
36
+ }
37
+ )
38
+ raise ResponseError, "Observer request failed with status #{response.code}: #{response.body}" unless response.success?
39
+ end
40
+
41
+ true
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :url, :token
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "logger"
3
4
  require "singleton"
4
5
 
5
6
  module GitlabQuality
@@ -8,34 +9,6 @@ module GitlabQuality
8
9
  class Config
9
10
  include Singleton
10
11
 
11
- # GCS client configuration object used to push metrics as json file to gcs bucket
12
- #
13
- class GCS
14
- def initialize(project_id:, credentials:, bucket_name:, metrics_file_name:, dry_run: false)
15
- @project_id = project_id
16
- @credentials = credentials
17
- @bucket_name = bucket_name
18
- @metrics_file_name = metrics_file_name
19
- @dry_run = dry_run
20
- end
21
-
22
- attr_reader :project_id, :credentials, :bucket_name, :metrics_file_name, :dry_run
23
- end
24
-
25
- # ClickHouse client configuration object
26
- #
27
- class ClickHouse
28
- def initialize(url:, database:, table_name:, username:, password:)
29
- @url = url
30
- @database = database
31
- @table_name = table_name
32
- @username = username
33
- @password = password
34
- end
35
-
36
- attr_reader :url, :database, :table_name, :username, :password
37
- end
38
-
39
12
  class << self
40
13
  def configuration
41
14
  Config.instance
@@ -46,8 +19,9 @@ module GitlabQuality
46
19
  end
47
20
  end
48
21
 
49
- attr_reader :gcs_config, :clickhouse_config, :initial_run
50
- attr_accessor :run_type
22
+ attr_accessor :run_type,
23
+ :observer_url,
24
+ :observer_token
51
25
  attr_writer :extra_rspec_metadata_keys,
52
26
  :skip_record_proc,
53
27
  :test_retried_proc,
@@ -55,51 +29,13 @@ module GitlabQuality
55
29
  :spec_file_path_prefix,
56
30
  :logger
57
31
 
58
- # rubocop:disable Style/TrivialAccessors -- allows documenting that setting config enables the export as well as document input class type
59
-
60
- # Enable metrics export to gcs bucket by setting configuration object
32
+ # Whether observer export is configured
61
33
  #
62
- # @param config [Config::GCS]
63
- # @return [GCS]
64
- def gcs_config=(config)
65
- @gcs_config = config
66
- end
67
-
68
- # Enable metrics export to clickhouse by setting configuration object
69
- #
70
- # @param config [Config::ClickHouse]
71
- # @return [ClickHouse]
72
- def clickhouse_config=(config)
73
- @clickhouse_config = config
74
- end
75
-
76
- # Additional columns to be created in the table if initial_run setup is used
77
- # Columns should be defined in the format used for ALTER TABLE query, example;
78
- # [
79
- # "feature_category LowCardinality(String) DEFAULT ''",
80
- # "level LowCardinality(String) DEFAULT ''"
81
- # ]
82
- #
83
- # @param columns [Array]
84
- # @return [Array]
85
- def extra_metadata_columns=(columns)
86
- @extra_metadata_columns = columns
87
- end
88
-
89
- # rubocop:enable Style/TrivialAccessors
90
-
91
- # Marks execution as initial run and performs setup tasks before running tests, like creating database in ClickHouse
34
+ # Export is considered enabled when all required attributes are set
92
35
  #
93
36
  # @return [Boolean]
94
- def initial_run!
95
- @initial_run = true
96
- end
97
-
98
- # Additional metadata columns used during initial table creation
99
- #
100
- # @return [Array]
101
- def extra_metadata_columns
102
- @extra_metadata_columns ||= []
37
+ def observer_configured?
38
+ [observer_url, observer_token].none? { |value| value.nil? || value.to_s.empty? }
103
39
  end
104
40
 
105
41
  # Extra rspec metadata keys to include in exported metrics
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'active_support/core_ext/object/blank'
5
+
6
+ require_relative 'config'
7
+ require_relative 'formatter'
8
+
9
+ module GitlabQuality
10
+ module TestTooling
11
+ module TestMetricsExporter
12
+ class ConfigHelper
13
+ REQUIRED_OBSERVER_ENV_VARS = %w[
14
+ GLCI_OBSERVER_URL
15
+ GLCI_OBSERVER_AUTH_TOKEN
16
+ ].freeze
17
+
18
+ class << self
19
+ def configure!(run_type = test_run_type)
20
+ return unless ENV.fetch("CI", nil) && ENV.fetch("GLCI_EXPORT_TEST_METRICS", "true") == "true" && run_type
21
+
22
+ RSpec.configure do |rspec_config|
23
+ next if rspec_config.dry_run?
24
+
25
+ Config.configure do |exporter_config|
26
+ self.logger = exporter_config.logger
27
+ next warn_missing_observer_variables unless observer_env_vars_present?
28
+
29
+ yield(exporter_config) if block_given?
30
+ configure_exporter!(exporter_config, run_type)
31
+
32
+ rspec_config.add_formatter Formatter
33
+
34
+ logger.info("Test metrics export is enabled for run type: #{run_type}")
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ attr_writer :logger
42
+
43
+ def logger
44
+ @logger ||= Logger.new($stdout)
45
+ end
46
+
47
+ def observer_env_vars_present?
48
+ REQUIRED_OBSERVER_ENV_VARS.all? { |var| ENV.fetch(var, nil) && !ENV[var].empty? }
49
+ end
50
+
51
+ def configure_exporter!(config, run_type)
52
+ config.run_type = run_type
53
+ config.custom_metrics_proc = custom_metrics_proc
54
+
55
+ configure_observer!(config)
56
+ end
57
+
58
+ def configure_observer!(config)
59
+ config.observer_url = observer_url
60
+ config.observer_token = observer_token
61
+ end
62
+
63
+ def warn_missing_observer_variables
64
+ missing = REQUIRED_OBSERVER_ENV_VARS.reject { |var| ENV.fetch(var, nil) && !ENV[var].empty? }
65
+ logger.warn("Test metrics export is enabled but missing environment variables: #{missing.join(', ')}")
66
+ end
67
+
68
+ def custom_metrics_proc
69
+ proc do |_example|
70
+ { pipeline_type: pipeline_type, ci_pipeline_id: ci_pipeline_id }
71
+ end
72
+ end
73
+
74
+ def default_branch?
75
+ ENV["CI_COMMIT_REF_NAME"] == ENV["CI_DEFAULT_BRANCH"]
76
+ end
77
+
78
+ def pipeline_type
79
+ @pipeline_type ||= if default_branch? && ENV["SCHEDULE_TYPE"].present?
80
+ "default_branch_scheduled_pipeline"
81
+ elsif default_branch?
82
+ "default_branch_pipeline"
83
+ elsif ENV["CI_COMMIT_REF_NAME"]&.match?(/^[\d-]+-stable-ee$/)
84
+ "stable_branch_pipeline"
85
+ elsif ENV["CI_MERGE_REQUEST_TARGET_BRANCH_NAME"]&.match?(/^[\d-]+-stable-ee$/)
86
+ "backport_merge_request_pipeline"
87
+ elsif ENV["CI_MERGE_REQUEST_IID"].present?
88
+ "merge_request_pipeline"
89
+ elsif ENV["CI_PIPELINE_SOURCE"] == "pipeline"
90
+ "downstream_pipeline"
91
+ else
92
+ "unknown"
93
+ end
94
+ end
95
+
96
+ def test_run_type
97
+ @run_type ||= ENV.fetch("GLCI_TEST_METRICS_RUN_TYPE", nil)
98
+ end
99
+
100
+ def observer_url
101
+ ENV.fetch("GLCI_OBSERVER_URL", nil)
102
+ end
103
+
104
+ def observer_token
105
+ ENV.fetch("GLCI_OBSERVER_AUTH_TOKEN", nil)
106
+ end
107
+
108
+ def ci_pipeline_id
109
+ (ENV["PARENT_PIPELINE_ID"] || ENV.fetch("CI_PIPELINE_ID", nil)).to_i
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -2,27 +2,17 @@
2
2
 
3
3
  require "rspec/core/formatters/base_formatter"
4
4
 
5
+ require_relative "test_metrics"
6
+ require_relative "client"
7
+
5
8
  module GitlabQuality
6
9
  module TestTooling
7
10
  module TestMetricsExporter
8
11
  class Formatter < RSpec::Core::Formatters::BaseFormatter
9
- include Utils
10
-
11
- RSpec::Core::Formatters.register(self, :start, :stop)
12
+ RSpec::Core::Formatters.register(self, :stop)
12
13
 
13
14
  LOG_PREFIX = "[MetricsExporter]"
14
15
 
15
- def start(_notification)
16
- return unless config.initial_run
17
-
18
- logger.info("#{LOG_PREFIX} Running initial setup for metrics export")
19
- raise "Initial setup is enabled, but clickhouse configuration is missing!" unless clickhouse_config
20
-
21
- create_clickhouse_metrics_table
22
- rescue StandardError => e
23
- logger.error("#{LOG_PREFIX} Error occurred during initial setup: #{e.message}")
24
- end
25
-
26
16
  def stop(notification)
27
17
  logger.debug("#{LOG_PREFIX} Starting test metrics export")
28
18
  data = notification.examples.filter_map do |example|
@@ -32,13 +22,18 @@ module GitlabQuality
32
22
  end
33
23
  return logger.warn("#{LOG_PREFIX} No test execution records found, metrics will not be exported!") if data.empty?
34
24
 
35
- push_to_gcs(data)
36
- push_to_clickhouse(data)
25
+ push_to_observer(data)
37
26
  end
38
27
 
39
28
  private
40
29
 
41
- delegate :gcs_config, :clickhouse_config, to: :config
30
+ def config
31
+ Config.configuration
32
+ end
33
+
34
+ def logger
35
+ config.logger
36
+ end
42
37
 
43
38
  # Single common timestamp for all exported example metrics to keep data points consistently grouped
44
39
  #
@@ -46,34 +41,25 @@ module GitlabQuality
46
41
  def time
47
42
  return @time if @time
48
43
 
49
- ci_created_at = Runtime::Env.ci_pipeline_created_at
44
+ ci_created_at = ENV.fetch("CI_PIPELINE_CREATED_AT", nil)
50
45
  @time = (ci_created_at ? Time.strptime(ci_created_at, '%Y-%m-%dT%H:%M:%S%z') : Time.now.utc).strftime('%Y-%m-%dT%H:%M:%S.%6N')
51
46
  end
52
47
 
53
- # Push data to gcs
48
+ # Push data to observer service
54
49
  #
55
- # @param data [Array]
50
+ # @param data [Array<Hash>]
56
51
  # @return [void]
57
- def push_to_gcs(data)
58
- return logger.debug("#{LOG_PREFIX} GCS configuration missing, skipping gcs export!") unless gcs_config
52
+ def push_to_observer(data)
53
+ return logger.debug("#{LOG_PREFIX} Observer configuration missing, skipping export!") unless config.observer_configured?
59
54
 
60
- gcs_client.put_object(gcs_config.bucket_name, gcs_config.metrics_file_name, data.to_json)
61
- logger.info("#{LOG_PREFIX} Successfully pushed #{data.size} entries to GCS bucket!")
55
+ observer_client.post_tests(data)
56
+ logger.info("#{LOG_PREFIX} Successfully pushed #{data.size} entries to Observer!")
62
57
  rescue StandardError => e
63
- logger.error("#{LOG_PREFIX} Error occurred while pushing metrics to GCS: #{e.message}")
58
+ logger.error("#{LOG_PREFIX} Error occurred while pushing metrics to Observer: #{e.message}")
64
59
  end
65
60
 
66
- # Push data to clickhouse
67
- #
68
- # @param data [Array<Hash>]
69
- # @return [void]
70
- def push_to_clickhouse(data)
71
- return logger.debug("ClickHouse configuration missing, skipping ClickHouse export!") unless clickhouse_config
72
-
73
- clickhouse_client.insert_json_data(clickhouse_config.table_name, data)
74
- logger.info("#{LOG_PREFIX} Successfully pushed #{data.size} entries to ClickHouse!")
75
- rescue StandardError => e
76
- logger.error("#{LOG_PREFIX} Error occurred while pushing metrics to ClickHouse: #{e.message}")
61
+ def observer_client
62
+ Client.new(url: config.observer_url, token: config.observer_token)
77
63
  end
78
64
  end
79
65
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "3.12.1"
5
+ VERSION = "3.14.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab_quality-test_tooling
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.12.1
4
+ version: 3.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab Quality
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-20 00:00:00.000000000 Z
11
+ date: 2026-05-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -588,10 +588,11 @@ files:
588
588
  - lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb
589
589
  - lib/gitlab_quality/test_tooling/test_metric/json_test_metric.rb
590
590
  - lib/gitlab_quality/test_tooling/test_metrics/json_test_metric_collection.rb
591
+ - lib/gitlab_quality/test_tooling/test_metrics_exporter/client.rb
591
592
  - lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb
593
+ - lib/gitlab_quality/test_tooling/test_metrics_exporter/config_helper.rb
592
594
  - lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb
593
595
  - lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb
594
- - lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb
595
596
  - lib/gitlab_quality/test_tooling/test_quarantine/quarantine_formatter.rb
596
597
  - lib/gitlab_quality/test_tooling/test_quarantine/quarantine_helper.rb
597
598
  - lib/gitlab_quality/test_tooling/test_result/base_test_result.rb
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GitlabQuality
4
- module TestTooling
5
- module TestMetricsExporter
6
- module Utils
7
- # Instance of metrics exporter configuration
8
- #
9
- # @return [Config]
10
- def config
11
- Config.configuration
12
- end
13
-
14
- # Configured logger instance
15
- #
16
- # @return [Logger]
17
- def logger
18
- config.logger
19
- end
20
-
21
- # Configured clickhouse client
22
- #
23
- # @return [ClickHouse::Client]
24
- def clickhouse_client
25
- ClickHouse::Client.new(
26
- url: config.clickhouse_config.url,
27
- database: config.clickhouse_config.database,
28
- username: config.clickhouse_config.username,
29
- password: config.clickhouse_config.password,
30
- logger: logger
31
- )
32
- end
33
-
34
- # GCS client instance
35
- #
36
- # @return [Fog::Storage::Google, GcsTools::GCSMockClient]
37
- def gcs_client
38
- GcsTools.gcs_client(
39
- project_id: config.gcs_config.project_id,
40
- credentials: config.gcs_config.credentials,
41
- dry_run: config.gcs_config.dry_run
42
- )
43
- end
44
-
45
- # Create table for metrics export using current ClickHouse configuration
46
- #
47
- # This method is mostly for schema documentation but it can be used together with initial_run! method in
48
- #
49
- # @return [void]
50
- def create_clickhouse_metrics_table
51
- table_name = config.clickhouse_config.table_name
52
-
53
- clickhouse_client.query(<<~SQL)
54
- CREATE TABLE IF NOT EXISTS #{table_name}
55
- (
56
- timestamp DateTime64(6, 'UTC'),
57
- id String,
58
- name String,
59
- hash String,
60
- file_path String,
61
- status LowCardinality(String),
62
- run_time UInt32,
63
- location String,
64
- quarantined Bool,
65
- test_retried Bool,
66
- feature_category LowCardinality(String) DEFAULT 'unknown',
67
- run_type LowCardinality(String) DEFAULT 'unknown',
68
- spec_file_path_prefix LowCardinality(String) DEFAULT '',
69
- ci_project_id UInt32,
70
- ci_job_name LowCardinality(String),
71
- ci_job_id UInt64,
72
- ci_pipeline_id UInt64,
73
- ci_merge_request_iid UInt32 DEFAULT 0,
74
- ci_project_path LowCardinality(String),
75
- ci_branch String,
76
- ci_target_branch LowCardinality(String),
77
- ci_server_url LowCardinality(String) DEFAULT 'https://gitlab.com',
78
- exception_class String DEFAULT '',
79
- exception_classes Array(String) DEFAULT [],
80
- failure_exception String DEFAULT ''
81
- )
82
- ENGINE = MergeTree()
83
- PARTITION BY toYYYYMM(timestamp)
84
- ORDER BY (ci_project_path, status, run_type, feature_category, file_path, timestamp, ci_pipeline_id)
85
- SETTINGS index_granularity = 8192;
86
- SQL
87
- return if config.extra_metadata_columns.empty?
88
-
89
- clickhouse_client.query(
90
- "ALTER TABLE #{table_name} #{config.extra_metadata_columns.map { |column| "ADD COLUMN IF NOT EXISTS #{column}" }.join(', ')};"
91
- )
92
- end
93
-
94
- module_function :config, :logger, :clickhouse_client
95
- end
96
- end
97
- end
98
- end