blazer 2.5.0 → 2.6.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +55 -9
  4. data/app/assets/stylesheets/blazer/application.css +1 -0
  5. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  6. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  7. data/app/assets/stylesheets/blazer/{bootstrap.css.erb → bootstrap.css} +0 -6
  8. data/app/controllers/blazer/base_controller.rb +45 -45
  9. data/app/controllers/blazer/dashboards_controller.rb +4 -11
  10. data/app/controllers/blazer/queries_controller.rb +28 -48
  11. data/app/models/blazer/query.rb +8 -2
  12. data/app/views/blazer/_variables.html.erb +5 -4
  13. data/app/views/blazer/dashboards/_form.html.erb +1 -1
  14. data/app/views/blazer/dashboards/show.html.erb +4 -4
  15. data/app/views/blazer/queries/_caching.html.erb +1 -1
  16. data/app/views/blazer/queries/_form.html.erb +3 -3
  17. data/app/views/blazer/queries/run.html.erb +1 -1
  18. data/app/views/blazer/queries/show.html.erb +12 -7
  19. data/app/views/layouts/blazer/application.html.erb +7 -2
  20. data/lib/blazer/adapters/athena_adapter.rb +51 -15
  21. data/lib/blazer/adapters/base_adapter.rb +16 -1
  22. data/lib/blazer/adapters/bigquery_adapter.rb +13 -2
  23. data/lib/blazer/adapters/cassandra_adapter.rb +15 -4
  24. data/lib/blazer/adapters/drill_adapter.rb +10 -0
  25. data/lib/blazer/adapters/druid_adapter.rb +36 -1
  26. data/lib/blazer/adapters/elasticsearch_adapter.rb +13 -2
  27. data/lib/blazer/adapters/hive_adapter.rb +10 -0
  28. data/lib/blazer/adapters/ignite_adapter.rb +12 -2
  29. data/lib/blazer/adapters/influxdb_adapter.rb +22 -10
  30. data/lib/blazer/adapters/mongodb_adapter.rb +4 -0
  31. data/lib/blazer/adapters/neo4j_adapter.rb +17 -2
  32. data/lib/blazer/adapters/opensearch_adapter.rb +4 -0
  33. data/lib/blazer/adapters/presto_adapter.rb +9 -0
  34. data/lib/blazer/adapters/salesforce_adapter.rb +5 -0
  35. data/lib/blazer/adapters/snowflake_adapter.rb +9 -0
  36. data/lib/blazer/adapters/soda_adapter.rb +9 -0
  37. data/lib/blazer/adapters/spark_adapter.rb +5 -0
  38. data/lib/blazer/adapters/sql_adapter.rb +30 -3
  39. data/lib/blazer/data_source.rb +85 -5
  40. data/lib/blazer/engine.rb +0 -4
  41. data/lib/blazer/run_statement.rb +7 -3
  42. data/lib/blazer/run_statement_job.rb +4 -2
  43. data/lib/blazer/slack_notifier.rb +5 -2
  44. data/lib/blazer/statement.rb +75 -0
  45. data/lib/blazer/version.rb +1 -1
  46. data/lib/blazer.rb +14 -6
  47. metadata +7 -4
@@ -35,6 +35,11 @@ module Blazer
35
35
  "SELECT Id FROM {table} LIMIT 10"
36
36
  end
37
37
 
38
+ # https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_quotedstringescapes.htm
39
+ def quoting
40
+ :backslash_escape
41
+ end
42
+
38
43
  protected
39
44
 
40
45
  def client
@@ -68,6 +68,15 @@ module Blazer
68
68
  def cancel(run_id)
69
69
  # todo
70
70
  end
71
+
72
+ # https://docs.snowflake.com/en/sql-reference/data-types-text.html#escape-sequences
73
+ def quoting
74
+ :backslash_escape
75
+ end
76
+
77
+ def parameter_binding
78
+ # TODO
79
+ end
71
80
  end
72
81
  end
73
82
  end
@@ -2,6 +2,10 @@ module Blazer
2
2
  module Adapters
3
3
  class SodaAdapter < BaseAdapter
4
4
  def run_statement(statement, comment)
5
+ require "json"
6
+ require "net/http"
7
+ require "uri"
8
+
5
9
  columns = []
6
10
  rows = []
7
11
  error = nil
@@ -91,6 +95,11 @@ module Blazer
91
95
  def tables
92
96
  ["all"]
93
97
  end
98
+
99
+ # https://dev.socrata.com/docs/datatypes/text.html
100
+ def quoting
101
+ :single_quote_escape
102
+ end
94
103
  end
95
104
  end
96
105
  end
@@ -4,6 +4,11 @@ module Blazer
4
4
  def tables
5
5
  client.execute("SHOW TABLES").map { |r| r["tableName"] }
6
6
  end
7
+
8
+ # https://spark.apache.org/docs/latest/sql-ref-literals.html
9
+ def quoting
10
+ :backslash_escape
11
+ end
7
12
  end
8
13
  end
9
14
  end
@@ -15,7 +15,7 @@ module Blazer
15
15
  end
16
16
  end
17
17
 
18
- def run_statement(statement, comment)
18
+ def run_statement(statement, comment, bind_params = [])
19
19
  columns = []
20
20
  rows = []
21
21
  error = nil
@@ -24,7 +24,8 @@ module Blazer
24
24
  in_transaction do
25
25
  set_timeout(data_source.timeout) if data_source.timeout
26
26
 
27
- result = select_all("#{statement} /*#{comment}*/")
27
+ binds = bind_params.map { |v| ActiveRecord::Relation::QueryAttribute.new(nil, v, ActiveRecord::Type::Value.new) }
28
+ result = connection_model.connection.select_all("#{statement} /*#{comment}*/", nil, binds)
28
29
  columns = result.columns
29
30
  result.rows.each do |untyped_row|
30
31
  rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] && result.column_types[c] ? result.column_types[c].send(:cast_value, untyped_row[i]) : untyped_row[i] })
@@ -33,6 +34,7 @@ module Blazer
33
34
  rescue => e
34
35
  error = e.message.sub(/.+ERROR: /, "")
35
36
  error = Blazer::TIMEOUT_MESSAGE if Blazer::TIMEOUT_ERRORS.any? { |e| error.include?(e) }
37
+ error = Blazer::VARIABLE_MESSAGE if error.include?("syntax error at or near \"$") || error.include?("Incorrect syntax near '@") || error.include?("your MySQL server version for the right syntax to use near '?")
36
38
  reconnect if error.include?("PG::ConnectionBad")
37
39
  end
38
40
 
@@ -156,7 +158,7 @@ module Blazer
156
158
  # WITH not an optimization fence in Postgres 12+
157
159
  statement = <<~SQL
158
160
  WITH query AS (
159
- #{statement}
161
+ {placeholder}
160
162
  ),
161
163
  cohorts AS (
162
164
  SELECT user_id, MIN(#{cohort_column}) AS cohort_time FROM query
@@ -183,6 +185,27 @@ module Blazer
183
185
  connection_model.send(:sanitize_sql_array, params)
184
186
  end
185
187
 
188
+ def quoting
189
+ ->(value) { connection_model.connection.quote(value) }
190
+ end
191
+
192
+ # Redshift adapter silently ignores binds
193
+ def parameter_binding
194
+ if postgresql? || sqlite?
195
+ :numeric
196
+ elsif mysql? && connection_model.connection.prepared_statements?
197
+ # Active Record silently ignores binds with MySQL when prepared statements are disabled
198
+ :positional
199
+ elsif sqlserver?
200
+ proc do |statement, variables|
201
+ variables.each_with_index do |(k, _), i|
202
+ statement = statement.gsub("{#{k}}", "@#{i} ")
203
+ end
204
+ [statement, variables.values]
205
+ end
206
+ end
207
+ end
208
+
186
209
  protected
187
210
 
188
211
  def select_all(statement, params = [])
@@ -207,6 +230,10 @@ module Blazer
207
230
  ["MySQL", "Mysql2", "Mysql2Spatial"].include?(adapter_name)
208
231
  end
209
232
 
233
+ def sqlite?
234
+ ["SQLite"].include?(adapter_name)
235
+ end
236
+
210
237
  def sqlserver?
211
238
  ["SQLServer", "tinytds", "mssql"].include?(adapter_name)
212
239
  end
@@ -89,7 +89,19 @@ module Blazer
89
89
  Blazer.cache.delete(run_cache_key(run_id))
90
90
  end
91
91
 
92
+ def sub_variables(statement, vars)
93
+ statement = statement.dup
94
+ vars.each do |var, value|
95
+ # use block form to disable back-references
96
+ statement.gsub!("{#{var}}") { quote(value) }
97
+ end
98
+ statement
99
+ end
100
+
92
101
  def run_statement(statement, options = {})
102
+ statement = Statement.new(statement, self) if statement.is_a?(String)
103
+ statement.bind unless statement.bind_statement
104
+
93
105
  async = options[:async]
94
106
  result = nil
95
107
  if cache_mode != "off"
@@ -118,7 +130,7 @@ module Blazer
118
130
  if options[:run_id]
119
131
  comment << ",run_id:#{options[:run_id]}"
120
132
  end
121
- result = run_statement_helper(statement, comment, async ? options[:run_id] : nil)
133
+ result = run_statement_helper(statement, comment, async ? options[:run_id] : nil, options)
122
134
  end
123
135
 
124
136
  result
@@ -133,13 +145,68 @@ module Blazer
133
145
  end
134
146
 
135
147
  def statement_cache_key(statement)
136
- cache_key(["statement", id, Digest::MD5.hexdigest(statement.to_s.gsub("\r\n", "\n"))])
148
+ cache_key(["statement", id, Digest::MD5.hexdigest(statement.bind_statement.to_s.gsub("\r\n", "\n") + statement.bind_values.sort_by { |k, _| k }.to_json)])
137
149
  end
138
150
 
139
151
  def run_cache_key(run_id)
140
152
  cache_key(["run", run_id])
141
153
  end
142
154
 
155
+ def quote(value)
156
+ if quoting == :backslash_escape || quoting == :single_quote_escape
157
+ # only need to support types generated by process_vars
158
+ if value.is_a?(Integer) || value.is_a?(Float)
159
+ value.to_s
160
+ elsif value.nil?
161
+ "NULL"
162
+ else
163
+ value = value.to_formatted_s(:db) if value.is_a?(ActiveSupport::TimeWithZone)
164
+
165
+ if quoting == :backslash_escape
166
+ "'#{value.gsub("\\") { "\\\\" }.gsub("'") { "\\'" }}'"
167
+ else
168
+ "'#{value.gsub("'", "''")}'"
169
+ end
170
+ end
171
+ elsif quoting.respond_to?(:call)
172
+ quoting.call(value)
173
+ elsif quoting.nil?
174
+ raise Blazer::Error, "Quoting not specified"
175
+ else
176
+ raise Blazer::Error, "Unknown quoting"
177
+ end
178
+ end
179
+
180
+ def bind_params(statement, variables)
181
+ if parameter_binding == :positional
182
+ locations = []
183
+ variables.each do |k, v|
184
+ i = 0
185
+ while (idx = statement.index("{#{k}}", i))
186
+ locations << [v, idx]
187
+ i = idx + 1
188
+ end
189
+ end
190
+ variables.each do |k, v|
191
+ statement = statement.gsub("{#{k}}", "?")
192
+ end
193
+ [statement, locations.sort_by(&:last).map(&:first)]
194
+ elsif parameter_binding == :numeric
195
+ variables.each_with_index do |(k, v), i|
196
+ # add trailing space if followed by digit
197
+ # try to keep minimal to avoid fixing invalid queries like SELECT{var}
198
+ statement = statement.gsub(/#{Regexp.escape("{#{k}}")}(\d)/, "$#{i + 1} \\1").gsub("{#{k}}", "$#{i + 1}")
199
+ end
200
+ [statement, variables.values]
201
+ elsif parameter_binding.respond_to?(:call)
202
+ parameter_binding.call(statement, variables)
203
+ elsif parameter_binding.nil?
204
+ [sub_variables(statement, variables), []]
205
+ else
206
+ raise Blazer::Error, "Unknown bind parameters"
207
+ end
208
+ end
209
+
143
210
  protected
144
211
 
145
212
  def adapter_instance
@@ -157,9 +224,22 @@ module Blazer
157
224
  end
158
225
  end
159
226
 
160
- def run_statement_helper(statement, comment, run_id)
227
+ def quoting
228
+ @quoting ||= adapter_instance.quoting
229
+ end
230
+
231
+ def parameter_binding
232
+ @parameter_binding ||= adapter_instance.parameter_binding
233
+ end
234
+
235
+ def run_statement_helper(statement, comment, run_id, options)
161
236
  start_time = Time.now
162
- columns, rows, error = adapter_instance.run_statement(statement, comment)
237
+ columns, rows, error =
238
+ if adapter_instance.parameter_binding
239
+ adapter_instance.run_statement(statement.bind_statement, comment, statement.bind_values)
240
+ else
241
+ adapter_instance.run_statement(statement.bind_statement, comment)
242
+ end
163
243
  duration = Time.now - start_time
164
244
 
165
245
  cache_data = nil
@@ -168,7 +248,7 @@ module Blazer
168
248
  cache_data = Marshal.dump([columns, rows, error, cache ? Time.now : nil]) rescue nil
169
249
  end
170
250
 
171
- if cache && cache_data && adapter_instance.cachable?(statement)
251
+ if cache && cache_data && adapter_instance.cachable?(statement.bind_statement)
172
252
  Blazer.cache.write(statement_cache_key(statement), cache_data, expires_in: cache_expires_in.to_f * 60)
173
253
  end
174
254
 
data/lib/blazer/engine.rb CHANGED
@@ -30,10 +30,6 @@ module Blazer
30
30
  Blazer.anomaly_checks = Blazer.settings["anomaly_checks"] || false
31
31
  Blazer.forecasting = Blazer.settings["forecasting"] || false
32
32
  Blazer.async = Blazer.settings["async"] || false
33
- if Blazer.async
34
- require "blazer/run_statement_job"
35
- end
36
-
37
33
  Blazer.images = Blazer.settings["images"] || false
38
34
  Blazer.override_csp = Blazer.settings["override_csp"] || false
39
35
  Blazer.slack_oauth_token = Blazer.settings["slack_oauth_token"] || ENV["BLAZER_SLACK_OAUTH_TOKEN"]
@@ -1,12 +1,16 @@
1
1
  module Blazer
2
2
  class RunStatement
3
- def perform(data_source, statement, options = {})
3
+ def perform(statement, options = {})
4
4
  query = options[:query]
5
- Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
5
+
6
+ data_source = statement.data_source
7
+ statement.bind
6
8
 
7
9
  # audit
8
10
  if Blazer.audit
9
- audit = Blazer::Audit.new(statement: statement)
11
+ audit_statement = statement.bind_statement
12
+ audit_statement += "\n\n#{statement.bind_values.to_json}" if statement.bind_values.any?
13
+ audit = Blazer::Audit.new(statement: audit_statement)
10
14
  audit.query = query
11
15
  audit.data_source = data_source.id
12
16
  audit.user = options[:user]
@@ -3,10 +3,12 @@ module Blazer
3
3
  self.queue_adapter = :async
4
4
 
5
5
  def perform(data_source_id, statement, options)
6
- data_source = Blazer.data_sources[data_source_id]
6
+ statement = Blazer::Statement.new(statement, data_source_id)
7
+ statement.values = options.delete(:values)
8
+ data_source = statement.data_source
7
9
  begin
8
10
  ActiveRecord::Base.connection_pool.with_connection do
9
- Blazer::RunStatement.new.perform(data_source, statement, options)
11
+ Blazer::RunStatement.new.perform(statement, options)
10
12
  end
11
13
  rescue Exception => e
12
14
  Blazer::Result.new(data_source, [], [], "Unknown error", nil, false)
@@ -67,15 +67,18 @@ module Blazer
67
67
  Blazer::Engine.routes.url_helpers.query_url(id, ActionMailer::Base.default_url_options)
68
68
  end
69
69
 
70
+ # TODO use return value
70
71
  def self.post(payload)
71
72
  if Blazer.slack_webhook_url.present?
72
- post_api(Blazer.slack_webhook_url, payload, {})
73
+ response = post_api(Blazer.slack_webhook_url, payload, {})
74
+ response.is_a?(Net::HTTPSuccess) && response.body == "ok"
73
75
  else
74
76
  headers = {
75
77
  "Authorization" => "Bearer #{Blazer.slack_oauth_token}",
76
78
  "Content-type" => "application/json"
77
79
  }
78
- post_api("https://slack.com/api/chat.postMessage", payload, headers)
80
+ response = post_api("https://slack.com/api/chat.postMessage", payload, headers)
81
+ response.is_a?(Net::HTTPSuccess) && (JSON.parse(response.body)["ok"] rescue false)
79
82
  end
80
83
  end
81
84
 
@@ -0,0 +1,75 @@
1
+ module Blazer
2
+ class Statement
3
+ attr_reader :statement, :data_source, :bind_statement, :bind_values
4
+ attr_accessor :values
5
+
6
+ def initialize(statement, data_source = nil)
7
+ @statement = statement
8
+ @data_source = data_source.is_a?(String) ? Blazer.data_sources[data_source] : data_source
9
+ @values = {}
10
+ end
11
+
12
+ def variables
13
+ @variables ||= Blazer.extract_vars(statement)
14
+ end
15
+
16
+ def add_values(var_params)
17
+ variables.each do |var|
18
+ value = var_params[var].presence
19
+ value = nil unless value.is_a?(String) # ignore arrays and hashes
20
+ if value
21
+ if ["start_time", "end_time"].include?(var)
22
+ value = value.to_s.gsub(" ", "+") # fix for Quip bug
23
+ end
24
+
25
+ if var.end_with?("_at")
26
+ begin
27
+ value = Blazer.time_zone.parse(value)
28
+ rescue
29
+ # do nothing
30
+ end
31
+ end
32
+
33
+ unless value.is_a?(ActiveSupport::TimeWithZone)
34
+ if value =~ /\A\d+\z/
35
+ value = value.to_i
36
+ elsif value =~ /\A\d+\.\d+\z/
37
+ value = value.to_f
38
+ end
39
+ end
40
+ end
41
+ value = Blazer.transform_variable.call(var, value) if Blazer.transform_variable
42
+ @values[var] = value
43
+ end
44
+ end
45
+
46
+ def cohort_analysis?
47
+ /\/\*\s*cohort analysis\s*\*\//i.match?(statement)
48
+ end
49
+
50
+ def apply_cohort_analysis(period:, days:)
51
+ @statement = data_source.cohort_analysis_statement(statement, period: period, days: days).sub("{placeholder}") { statement }
52
+ end
53
+
54
+ # should probably transform before cohort analysis
55
+ # but keep previous order for now
56
+ def transformed_statement
57
+ statement = self.statement.dup
58
+ Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
59
+ statement
60
+ end
61
+
62
+ def bind
63
+ @bind_statement, @bind_values = data_source.bind_params(transformed_statement, values)
64
+ end
65
+
66
+ def display_statement
67
+ data_source.sub_variables(transformed_statement, values)
68
+ end
69
+
70
+ def clear_cache
71
+ bind if bind_statement.nil?
72
+ data_source.clear_cache(self)
73
+ end
74
+ end
75
+ end
@@ -1,3 +1,3 @@
1
1
  module Blazer
2
- VERSION = "2.5.0"
2
+ VERSION = "2.6.0"
3
3
  end
data/lib/blazer.rb CHANGED
@@ -1,14 +1,18 @@
1
1
  # dependencies
2
- require "csv"
3
- require "yaml"
4
2
  require "chartkick"
5
3
  require "safely/core"
6
4
 
5
+ # stdlib
6
+ require "csv"
7
+ require "json"
8
+ require "yaml"
9
+
7
10
  # modules
8
11
  require "blazer/version"
9
12
  require "blazer/data_source"
10
13
  require "blazer/result"
11
14
  require "blazer/run_statement"
15
+ require "blazer/statement"
12
16
 
13
17
  # adapters
14
18
  require "blazer/adapters/base_adapter"
@@ -43,6 +47,8 @@ module Blazer
43
47
  autoload :CheckMailer, "blazer/check_mailer"
44
48
  # net/http optional
45
49
  autoload :SlackNotifier, "blazer/slack_notifier"
50
+ # activejob optional
51
+ autoload :RunStatementJob, "blazer/run_statement_job"
46
52
 
47
53
  class << self
48
54
  attr_accessor :audit
@@ -76,6 +82,7 @@ module Blazer
76
82
  self.images = false
77
83
  self.override_csp = false
78
84
 
85
+ VARIABLE_MESSAGE = "Variable cannot be used in this position"
79
86
  TIMEOUT_MESSAGE = "Query timed out :("
80
87
  TIMEOUT_ERRORS = [
81
88
  "canceling statement due to statement timeout", # postgres
@@ -128,6 +135,7 @@ module Blazer
128
135
  end
129
136
  end
130
137
 
138
+ # TODO move to Statement and remove in 3.0.0
131
139
  def self.extract_vars(statement)
132
140
  # strip commented out lines
133
141
  # and regex {1} or {1,2}
@@ -148,9 +156,8 @@ module Blazer
148
156
 
149
157
  ActiveSupport::Notifications.instrument("run_check.blazer", check_id: check.id, query_id: check.query.id, state_was: check.state) do |instrument|
150
158
  # try 3 times on timeout errors
151
- data_source = data_sources[check.query.data_source]
152
- statement = check.query.statement
153
- Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
159
+ statement = check.query.statement_object
160
+ data_source = statement.data_source
154
161
 
155
162
  while tries <= 3
156
163
  result = data_source.run_statement(statement, refresh_cache: true, check: check, query: check.query)
@@ -178,7 +185,8 @@ module Blazer
178
185
  # TODO use proper logfmt
179
186
  Rails.logger.info "[blazer check] query=#{check.query.name} state=#{check.state} rows=#{result.rows.try(:size)} error=#{result.error}"
180
187
 
181
- instrument[:statement] = statement
188
+ # should be no variables
189
+ instrument[:statement] = statement.bind_statement
182
190
  instrument[:data_source] = data_source
183
191
  instrument[:state] = check.state
184
192
  instrument[:rows] = result.rows.try(:size)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blazer
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-05 00:00:00.000000000 Z
11
+ date: 2022-04-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -109,7 +109,9 @@ files:
109
109
  - app/assets/javascripts/blazer/stupidtable.js
110
110
  - app/assets/javascripts/blazer/vue.js
111
111
  - app/assets/stylesheets/blazer/application.css
112
- - app/assets/stylesheets/blazer/bootstrap.css.erb
112
+ - app/assets/stylesheets/blazer/bootstrap-propshaft.css
113
+ - app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb
114
+ - app/assets/stylesheets/blazer/bootstrap.css
113
115
  - app/assets/stylesheets/blazer/daterangepicker.css
114
116
  - app/assets/stylesheets/blazer/github.css
115
117
  - app/assets/stylesheets/blazer/selectize.css
@@ -184,6 +186,7 @@ files:
184
186
  - lib/blazer/run_statement.rb
185
187
  - lib/blazer/run_statement_job.rb
186
188
  - lib/blazer/slack_notifier.rb
189
+ - lib/blazer/statement.rb
187
190
  - lib/blazer/version.rb
188
191
  - lib/generators/blazer/install_generator.rb
189
192
  - lib/generators/blazer/templates/config.yml.tt
@@ -226,7 +229,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
226
229
  - !ruby/object:Gem::Version
227
230
  version: '0'
228
231
  requirements: []
229
- rubygems_version: 3.2.32
232
+ rubygems_version: 3.3.7
230
233
  signing_key:
231
234
  specification_version: 4
232
235
  summary: Explore your data with SQL. Easily create charts and dashboards, and share