blazer 2.5.0 → 2.6.4

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +63 -15
  4. data/app/assets/javascripts/blazer/queries.js +12 -1
  5. data/app/assets/stylesheets/blazer/application.css +1 -0
  6. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  7. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  8. data/app/assets/stylesheets/blazer/{bootstrap.css.erb → bootstrap.css} +0 -6
  9. data/app/controllers/blazer/base_controller.rb +45 -45
  10. data/app/controllers/blazer/dashboards_controller.rb +4 -11
  11. data/app/controllers/blazer/queries_controller.rb +29 -48
  12. data/app/models/blazer/query.rb +8 -2
  13. data/app/views/blazer/_variables.html.erb +5 -4
  14. data/app/views/blazer/dashboards/_form.html.erb +1 -1
  15. data/app/views/blazer/dashboards/show.html.erb +6 -4
  16. data/app/views/blazer/queries/_caching.html.erb +1 -1
  17. data/app/views/blazer/queries/_form.html.erb +3 -3
  18. data/app/views/blazer/queries/run.html.erb +1 -1
  19. data/app/views/blazer/queries/show.html.erb +12 -7
  20. data/app/views/layouts/blazer/application.html.erb +7 -2
  21. data/lib/blazer/adapters/athena_adapter.rb +55 -18
  22. data/lib/blazer/adapters/base_adapter.rb +16 -1
  23. data/lib/blazer/adapters/bigquery_adapter.rb +13 -2
  24. data/lib/blazer/adapters/cassandra_adapter.rb +15 -4
  25. data/lib/blazer/adapters/drill_adapter.rb +10 -0
  26. data/lib/blazer/adapters/druid_adapter.rb +36 -1
  27. data/lib/blazer/adapters/elasticsearch_adapter.rb +13 -2
  28. data/lib/blazer/adapters/hive_adapter.rb +10 -0
  29. data/lib/blazer/adapters/ignite_adapter.rb +12 -2
  30. data/lib/blazer/adapters/influxdb_adapter.rb +22 -10
  31. data/lib/blazer/adapters/mongodb_adapter.rb +4 -0
  32. data/lib/blazer/adapters/neo4j_adapter.rb +17 -2
  33. data/lib/blazer/adapters/opensearch_adapter.rb +4 -0
  34. data/lib/blazer/adapters/presto_adapter.rb +9 -0
  35. data/lib/blazer/adapters/salesforce_adapter.rb +5 -0
  36. data/lib/blazer/adapters/snowflake_adapter.rb +9 -0
  37. data/lib/blazer/adapters/soda_adapter.rb +9 -0
  38. data/lib/blazer/adapters/spark_adapter.rb +5 -0
  39. data/lib/blazer/adapters/sql_adapter.rb +37 -3
  40. data/lib/blazer/data_source.rb +85 -5
  41. data/lib/blazer/engine.rb +0 -4
  42. data/lib/blazer/result.rb +2 -0
  43. data/lib/blazer/run_statement.rb +7 -3
  44. data/lib/blazer/run_statement_job.rb +4 -2
  45. data/lib/blazer/slack_notifier.rb +5 -2
  46. data/lib/blazer/statement.rb +75 -0
  47. data/lib/blazer/version.rb +1 -1
  48. data/lib/blazer.rb +14 -6
  49. metadata +7 -4
@@ -52,29 +52,26 @@ module Blazer
52
52
  @query.status = "active" if @query.respond_to?(:status)
53
53
 
54
54
  if @query.save
55
- redirect_to query_path(@query, variable_params(@query))
55
+ redirect_to query_path(@query, params: variable_params(@query))
56
56
  else
57
57
  render_errors @query
58
58
  end
59
59
  end
60
60
 
61
61
  def show
62
- @statement = @query.statement.dup
63
- process_vars(@statement, @query.data_source)
62
+ @statement = @query.statement_object
63
+ @success = process_vars(@statement)
64
64
 
65
65
  @smart_vars = {}
66
66
  @sql_errors = []
67
- data_source = Blazer.data_sources[@query.data_source]
68
67
  @bind_vars.each do |var|
69
- smart_var, error = parse_smart_variables(var, data_source)
68
+ smart_var, error = parse_smart_variables(var, @statement.data_source)
70
69
  @smart_vars[var] = smart_var if smart_var
71
70
  @sql_errors << error if error
72
71
  end
73
72
 
74
73
  @query.update!(status: "active") if @query.respond_to?(:status) && @query.status.in?(["archived", nil])
75
74
 
76
- Blazer.transform_statement.call(data_source, @statement) if Blazer.transform_statement
77
-
78
75
  add_cohort_analysis_vars if @query.cohort_analysis?
79
76
  end
80
77
 
@@ -82,17 +79,25 @@ module Blazer
82
79
  end
83
80
 
84
81
  def run
85
- @statement = params[:statement]
86
- # before process_vars
87
- @cohort_analysis = Query.new(statement: @statement).cohort_analysis?
88
- data_source = params[:data_source]
89
- process_vars(@statement, data_source)
90
- @only_chart = params[:only_chart]
91
- @run_id = blazer_params[:run_id]
92
82
  @query = Query.find_by(id: params[:query_id]) if params[:query_id]
83
+
84
+ # use query data source when present
85
+ # need to update viewable? logic below if this changes
93
86
  data_source = @query.data_source if @query && @query.data_source
87
+ data_source ||= params[:data_source]
94
88
  @data_source = Blazer.data_sources[data_source]
95
89
 
90
+ @statement = Blazer::Statement.new(params[:statement], @data_source)
91
+ # before process_vars
92
+ @cohort_analysis = @statement.cohort_analysis?
93
+
94
+ # fallback for now for users with open tabs
95
+ # TODO remove fallback in future version
96
+ @var_params = request.request_parameters["variables"] || request.request_parameters
97
+ @success = process_vars(@statement, @var_params)
98
+ @only_chart = params[:only_chart]
99
+ @run_id = blazer_params[:run_id]
100
+
96
101
  run_cohort_analysis if @cohort_analysis
97
102
 
98
103
  # ensure viewable
@@ -128,7 +133,7 @@ module Blazer
128
133
 
129
134
  options = {user: blazer_user, query: @query, refresh_cache: params[:check], run_id: @run_id, async: Blazer.async}
130
135
  if Blazer.async && request.format.symbol != :csv
131
- Blazer::RunStatementJob.perform_later(@data_source.id, @statement, options)
136
+ Blazer::RunStatementJob.perform_later(@data_source.id, @statement.statement, options.merge(values: @statement.values))
132
137
  wait_start = Time.now
133
138
  loop do
134
139
  sleep(0.1)
@@ -136,7 +141,7 @@ module Blazer
136
141
  break if @result || Time.now - wait_start > 3
137
142
  end
138
143
  else
139
- @result = Blazer::RunStatement.new.perform(@data_source, @statement, options)
144
+ @result = Blazer::RunStatement.new.perform(@statement, options)
140
145
  end
141
146
 
142
147
  if @result
@@ -166,13 +171,8 @@ module Blazer
166
171
  end
167
172
 
168
173
  def refresh
169
- data_source = Blazer.data_sources[@query.data_source]
170
- @statement = @query.statement.dup
171
- process_vars(@statement, @query.data_source)
172
- Blazer.transform_statement.call(data_source, @statement) if Blazer.transform_statement
173
- @statement = cohort_analysis_statement(data_source, @statement) if @query.cohort_analysis?
174
- data_source.clear_cache(@statement)
175
- redirect_to query_path(@query, variable_params(@query))
174
+ refresh_query(@query)
175
+ redirect_to query_path(@query, params: variable_params(@query))
176
176
  end
177
177
 
178
178
  def update
@@ -185,7 +185,7 @@ module Blazer
185
185
  @query.errors.add(:base, "Sorry, permission denied")
186
186
  end
187
187
  if @query.errors.empty? && @query.update(query_params)
188
- redirect_to query_path(@query, variable_params(@query))
188
+ redirect_to query_path(@query, params: variable_params(@query))
189
189
  else
190
190
  render_errors @query
191
191
  end
@@ -282,6 +282,9 @@ module Blazer
282
282
  render layout: false
283
283
  end
284
284
  format.csv do
285
+ # not ideal, but useful for testing
286
+ raise Error, @error if @error && Rails.env.test?
287
+
285
288
  send_data csv_data(@columns, @rows, @data_source), type: "text/csv; charset=utf-8; header=present", disposition: "attachment; filename=\"#{@query.try(:name).try(:parameterize).presence || 'query'}.csv\""
286
289
  end
287
290
  end
@@ -363,34 +366,12 @@ module Blazer
363
366
  end
364
367
 
365
368
  def run_cohort_analysis
366
- unless @data_source.supports_cohort_analysis?
369
+ unless @statement.data_source.supports_cohort_analysis?
367
370
  @cohort_error = "This data source does not support cohort analysis"
368
371
  end
369
372
 
370
373
  @show_cohort_rows = !params[:query_id] || @cohort_error
371
-
372
- unless @show_cohort_rows
373
- @statement = cohort_analysis_statement(@data_source, @statement)
374
- end
375
- end
376
-
377
- def cohort_analysis_statement(data_source, statement)
378
- @cohort_period = params["cohort_period"] || "week"
379
- @cohort_period = "week" unless ["day", "week", "month"].include?(@cohort_period)
380
-
381
- # for now
382
- @conversion_period = @cohort_period
383
- @cohort_days =
384
- case @cohort_period
385
- when "day"
386
- 1
387
- when "week"
388
- 7
389
- when "month"
390
- 30
391
- end
392
-
393
- data_source.cohort_analysis_statement(statement, period: @cohort_period, days: @cohort_days)
374
+ cohort_analysis_statement(@statement) unless @show_cohort_rows
394
375
  end
395
376
 
396
377
  def render_cohort_analysis
@@ -35,13 +35,19 @@ module Blazer
35
35
  end
36
36
 
37
37
  def variables
38
- variables = Blazer.extract_vars(statement)
38
+ # don't require data_source to be loaded
39
+ variables = Statement.new(statement).variables
39
40
  variables += ["cohort_period"] if cohort_analysis?
40
41
  variables
41
42
  end
42
43
 
43
44
  def cohort_analysis?
44
- /\/\*\s*cohort analysis\s*\*\//i.match?(statement)
45
+ # don't require data_source to be loaded
46
+ Statement.new(statement).cohort_analysis?
47
+ end
48
+
49
+ def statement_object
50
+ Statement.new(statement, data_source)
45
51
  end
46
52
  end
47
53
  end
@@ -1,4 +1,5 @@
1
1
  <% if @bind_vars.any? %>
2
+ <% var_params = request.query_parameters %>
2
3
  <script>
3
4
  <%= blazer_js_var "timeZone", Blazer.time_zone.tzinfo.name %>
4
5
  var now = moment.tz(timeZone)
@@ -19,14 +20,14 @@
19
20
  <% @bind_vars.each_with_index do |var, i| %>
20
21
  <%= label_tag var, var %>
21
22
  <% if (data = @smart_vars[var]) %>
22
- <%= select_tag var, options_for_select([[nil, nil]] + data, selected: params[var]), style: "margin-right: 20px; width: 200px; display: none;" %>
23
+ <%= select_tag var, options_for_select([[nil, nil]] + data, selected: var_params[var]), style: "margin-right: 20px; width: 200px; display: none;" %>
23
24
  <script>
24
25
  $("#<%= var %>").selectize({
25
26
  create: true
26
27
  });
27
28
  </script>
28
29
  <% elsif var.end_with?("_at") || var == "start_time" || var == "end_time" %>
29
- <%= hidden_field_tag var, params[var] %>
30
+ <%= hidden_field_tag var, var_params[var] %>
30
31
 
31
32
  <div class="selectize-control single" style="width: 200px;">
32
33
  <div id="<%= var %>-select" class="selectize-input" style="display: inline-block;">
@@ -58,13 +59,13 @@
58
59
  })()
59
60
  </script>
60
61
  <% else %>
61
- <%= text_field_tag var, params[var], style: "width: 120px; margin-right: 20px;", autofocus: i == 0 && !var.end_with?("_at") && !params[var], class: "form-control" %>
62
+ <%= text_field_tag var, var_params[var], style: "width: 120px; margin-right: 20px;", autofocus: i == 0 && !var.end_with?("_at") && !var_params[var], class: "form-control" %>
62
63
  <% end %>
63
64
  <% end %>
64
65
 
65
66
  <% if date_vars %>
66
67
  <% date_vars.each do |var| %>
67
- <%= hidden_field_tag var, params[var] %>
68
+ <%= hidden_field_tag var, var_params[var] %>
68
69
  <% end %>
69
70
 
70
71
  <%= label_tag nil, date_vars.join(" & ") %>
@@ -1,4 +1,4 @@
1
- <%= form_for @dashboard, url: (@dashboard.persisted? ? dashboard_path(@dashboard, variable_params(@dashboard)) : dashboards_path(variable_params(@dashboard))), html: {id: "app", class: "small-form"} do |f| %>
1
+ <%= form_for @dashboard, url: (@dashboard.persisted? ? dashboard_path(@dashboard, params: variable_params(@dashboard)) : dashboards_path(params: variable_params(@dashboard))), html: {id: "app", class: "small-form"} do |f| %>
2
2
  <% if @dashboard.errors.any? %>
3
3
  <div class="alert alert-danger"><%= @dashboard.errors.full_messages.first %></div>
4
4
  <% end %>
@@ -10,7 +10,7 @@
10
10
  </h3>
11
11
  </div>
12
12
  <div class="col-sm-3 text-right">
13
- <%= link_to "Edit", edit_dashboard_path(@dashboard, variable_params(@dashboard)), class: "btn btn-info" %>
13
+ <%= link_to "Edit", edit_dashboard_path(@dashboard, params: variable_params(@dashboard)), class: "btn btn-info" %>
14
14
  </div>
15
15
  </div>
16
16
  </div>
@@ -21,7 +21,7 @@
21
21
  <% if @data_sources.any? { |ds| ds.cache_mode != "off" } %>
22
22
  <p class="text-muted" style="float: right;">
23
23
  Some queries may be cached
24
- <%= link_to "Refresh", refresh_dashboard_path(@dashboard, variable_params(@dashboard)), method: :post %>
24
+ <%= link_to "Refresh", refresh_dashboard_path(@dashboard, params: variable_params(@dashboard)), method: :post %>
25
25
  </p>
26
26
  <% end %>
27
27
 
@@ -33,13 +33,15 @@
33
33
 
34
34
  <% @queries.each_with_index do |query, i| %>
35
35
  <div class="chart-container">
36
- <h4><%= link_to query.friendly_name, query_path(query, variable_params(query)), target: "_blank" %></h4>
36
+ <h4><%= link_to query.friendly_name, query_path(query, params: variable_params(query)), target: "_blank" %></h4>
37
37
  <div id="chart-<%= i %>" class="chart">
38
38
  <p class="text-muted">Loading...</p>
39
39
  </div>
40
40
  </div>
41
41
  <script>
42
- <%= blazer_js_var "data", {statement: @statements[i], query_id: query.id, data_source: query.data_source, only_chart: true, cohort_period: params[:cohort_period]} %>
42
+ <% data = {statement: query.statement, query_id: query.id, data_source: query.data_source, variables: variable_params(query), only_chart: true} %>
43
+ <% data.merge!(cohort_period: params[:cohort_period]) if params[:cohort_period] %>
44
+ <%= blazer_js_var "data", data %>
43
45
 
44
46
  runQuery(data, function (data) {
45
47
  $("#chart-<%= i %>").html(data)
@@ -10,7 +10,7 @@
10
10
  <% end %>
11
11
 
12
12
  <% if @query && params[:query_id] %>
13
- <%= link_to "Refresh", refresh_query_path(@query, variable_params(@query)), method: :post %>
13
+ <%= link_to "Refresh", refresh_query_path(@query, params: variable_params(@query, @var_params)), method: :post %>
14
14
  <% end %>
15
15
  </p>
16
16
  <% end %>
@@ -3,7 +3,7 @@
3
3
  <% end %>
4
4
 
5
5
  <div id="app" v-cloak>
6
- <%= form_for @query, url: (@query.persisted? ? query_path(@query, variable_params(@query)) : queries_path(variable_params(@query))), html: {autocomplete: "off"} do |f| %>
6
+ <%= form_for @query, url: (@query.persisted? ? query_path(@query, params: variable_params(@query)) : queries_path(params: variable_params(@query))), html: {autocomplete: "off"} do |f| %>
7
7
  <div class="row">
8
8
  <div id="statement-box" class="col-xs-8">
9
9
  <div class= "form-group">
@@ -67,7 +67,7 @@
67
67
  </div>
68
68
 
69
69
  <script>
70
- <%= blazer_js_var "params", variable_params(@query) %>
70
+ <%= blazer_js_var "variableParams", variable_params(@query) %>
71
71
  <%= blazer_js_var "previewStatement", Hash[Blazer.data_sources.map { |k, v| [k, (v.preview_statement rescue "")] }] %>
72
72
 
73
73
  var app = new Vue({
@@ -95,7 +95,7 @@
95
95
  this.error = false
96
96
  cancelAllQueries()
97
97
 
98
- var data = $.extend({}, params, {statement: this.getSQL(), data_source: $("#query_data_source").val()})
98
+ var data = {statement: this.getSQL(), data_source: $("#query_data_source").val(), variables: variableParams}
99
99
 
100
100
  var _this = this
101
101
 
@@ -31,7 +31,7 @@
31
31
 
32
32
  <% if @query && @result.forecastable? && !params[:forecast] %>
33
33
  &middot;
34
- <%= link_to "Forecast", query_path(@query, {forecast: "t"}.merge(variable_params(@query))) %>
34
+ <%= link_to "Forecast", query_path(@query, params: {forecast: "t"}.merge(variable_params(@query))) %>
35
35
  <% end %>
36
36
  </p>
37
37
  <% end %>
@@ -1,5 +1,12 @@
1
1
  <% blazer_title @query.name %>
2
2
 
3
+ <% if @success %>
4
+ <% run_data = {statement: @query.statement, query_id: @query.id, data_source: @query.data_source, variables: variable_params(@query)} %>
5
+ <% run_data.merge!(forecast: "t") if params[:forecast] %>
6
+ <% run_data.merge!(cohort_period: params[:cohort_period]) if params[:cohort_period] %>
7
+ <% run_data.transform_keys!(&:to_s) if Rails::VERSION::STRING.to_f == 5.0 %>
8
+ <% end %>
9
+
3
10
  <div class="topbar">
4
11
  <div class="container">
5
12
  <div class="row" style="padding-top: 13px;">
@@ -10,11 +17,11 @@
10
17
  </h3>
11
18
  </div>
12
19
  <div class="col-sm-3 text-right">
13
- <%= link_to "Edit", edit_query_path(@query, variable_params(@query)), class: "btn btn-default", disabled: !@query.editable?(blazer_user) %>
14
- <%= link_to "Fork", new_query_path(variable_params(@query).merge(fork_query_id: @query.id, data_source: @query.data_source, name: @query.name)), class: "btn btn-info" %>
20
+ <%= link_to "Edit", edit_query_path(@query, params: variable_params(@query)), class: "btn btn-default", disabled: !@query.editable?(blazer_user) %>
21
+ <%= link_to "Fork", new_query_path(params: variable_params(@query).merge(fork_query_id: @query.id, data_source: @query.data_source, name: @query.name)), class: "btn btn-info" %>
15
22
 
16
23
  <% if !@error && @success %>
17
- <%= button_to "Download", run_queries_path(query_id: @query.id, format: "csv", forecast: params[:forecast], cohort_period: params[:cohort_period]), params: {statement: @statement}, class: "btn btn-primary" %>
24
+ <%= button_to "Download", run_queries_path(format: "csv"), params: run_data, class: "btn btn-primary" %>
18
25
  <% end %>
19
26
  </div>
20
27
  </div>
@@ -39,7 +46,7 @@
39
46
 
40
47
  <%= render partial: "blazer/variables", locals: {action: query_path(@query)} %>
41
48
 
42
- <pre id="code"><code><%= @statement %></code></pre>
49
+ <pre id="code"><code><%= @statement.display_statement %></code></pre>
43
50
 
44
51
  <% if @success %>
45
52
  <div id="results">
@@ -56,9 +63,7 @@
56
63
  $("#results").addClass("query-error").html(message)
57
64
  }
58
65
 
59
- <% data = variable_params(@query).merge(statement: @statement, query_id: @query.id, data_source: @query.data_source) %>
60
- <% data.merge!(forecast: "t") if params[:forecast] %>
61
- <%= blazer_js_var "data", data %>
66
+ <%= blazer_js_var "data", run_data %>
62
67
 
63
68
  runQuery(data, showRun, showError)
64
69
  </script>
@@ -5,8 +5,13 @@
5
5
 
6
6
  <meta charset="utf-8" />
7
7
  <%= favicon_link_tag "blazer/favicon.png" %>
8
- <%= stylesheet_link_tag "blazer/application" %>
9
- <%= javascript_include_tag "blazer/application" %>
8
+ <% if defined?(Propshaft::Railtie) %>
9
+ <%= stylesheet_link_tag "blazer/bootstrap-propshaft", "blazer/bootstrap", "blazer/selectize", "blazer/github", "blazer/daterangepicker", "blazer/application" %>
10
+ <%= javascript_include_tag "blazer/jquery", "blazer/jquery-ujs", "blazer/stupidtable", "blazer/stupidtable-custom-settings", "blazer/jquery.stickytableheaders", "blazer/selectize", "blazer/highlight.min", "blazer/moment", "blazer/moment-timezone-with-data", "blazer/daterangepicker", "blazer/Chart.js", "blazer/chartkick", "blazer/ace/ace", "blazer/ace/ext-language_tools", "blazer/ace/theme-twilight", "blazer/ace/mode-sql", "blazer/ace/snippets/text", "blazer/ace/snippets/sql", "blazer/Sortable", "blazer/bootstrap", "blazer/vue", "blazer/routes", "blazer/queries", "blazer/fuzzysearch", "blazer/application" %>
11
+ <% else %>
12
+ <%= stylesheet_link_tag "blazer/application" %>
13
+ <%= javascript_include_tag "blazer/application" %>
14
+ <% end %>
10
15
  <script>
11
16
  <%= blazer_js_var "rootPath", root_path %>
12
17
  </script>
@@ -1,31 +1,50 @@
1
1
  module Blazer
2
2
  module Adapters
3
3
  class AthenaAdapter < BaseAdapter
4
- def run_statement(statement, comment)
4
+ def run_statement(statement, comment, bind_params = [])
5
5
  require "digest/md5"
6
6
 
7
7
  columns = []
8
8
  rows = []
9
9
  error = nil
10
10
 
11
- query_options = {
12
- query_string: statement,
13
- # use token so we fetch cached results after query is run
14
- client_request_token: Digest::MD5.hexdigest([statement, data_source.id, settings["workgroup"]].compact.join("/")),
15
- query_execution_context: {
16
- database: database,
11
+ begin
12
+ # use empty? since any? doesn't work for [nil]
13
+ if !bind_params.empty?
14
+ request_token = Digest::MD5.hexdigest([statement, bind_params.to_json, data_source.id, settings["workgroup"]].compact.join("/"))
15
+ statement_name = "blazer_#{request_token}"
16
+ begin
17
+ client.create_prepared_statement({
18
+ statement_name: statement_name,
19
+ work_group: settings["workgroup"],
20
+ query_statement: statement
21
+ })
22
+ rescue Aws::Athena::Errors::InvalidRequestException => e
23
+ raise e unless e.message.include?("already exists in WorkGroup")
24
+ end
25
+ using_statement = bind_params.map { |v| data_source.quote(v) }.join(", ")
26
+ statement = "EXECUTE #{statement_name} USING #{using_statement}"
27
+ else
28
+ request_token = Digest::MD5.hexdigest([statement, data_source.id, settings["workgroup"]].compact.join("/"))
29
+ end
30
+
31
+ query_options = {
32
+ query_string: statement,
33
+ # use token so we fetch cached results after query is run
34
+ client_request_token: request_token,
35
+ query_execution_context: {
36
+ database: database,
37
+ }
17
38
  }
18
- }
19
39
 
20
- if settings["output_location"]
21
- query_options[:result_configuration] = {output_location: settings["output_location"]}
22
- end
40
+ if settings["output_location"]
41
+ query_options[:result_configuration] = {output_location: settings["output_location"]}
42
+ end
23
43
 
24
- if settings["workgroup"]
25
- query_options[:work_group] = settings["workgroup"]
26
- end
44
+ if settings["workgroup"]
45
+ query_options[:work_group] = settings["workgroup"]
46
+ end
27
47
 
28
- begin
29
48
  resp = client.start_query_execution(**query_options)
30
49
  query_execution_id = resp.query_execution_id
31
50
 
@@ -111,12 +130,29 @@ module Blazer
111
130
  "SELECT * FROM {table} LIMIT 10"
112
131
  end
113
132
 
133
+ # https://docs.aws.amazon.com/athena/latest/ug/select.html#select-escaping
134
+ def quoting
135
+ :single_quote_escape
136
+ end
137
+
138
+ # https://docs.aws.amazon.com/athena/latest/ug/querying-with-prepared-statements.html
139
+ def parameter_binding
140
+ engine_version > 1 ? :positional : nil
141
+ end
142
+
114
143
  private
115
144
 
116
145
  def database
117
146
  @database ||= settings["database"] || "default"
118
147
  end
119
148
 
149
+ # note: this setting is experimental
150
+ # it does *not* need to be set to use engine version 2
151
+ # prepared statements must be manually deleted if enabled
152
+ def engine_version
153
+ @engine_version ||= (settings["engine_version"] || 1).to_i
154
+ end
155
+
120
156
  def fetch_error(query_execution_id)
121
157
  client.get_query_execution(
122
158
  query_execution_id: query_execution_id
@@ -133,11 +169,12 @@ module Blazer
133
169
 
134
170
  def client_options
135
171
  @client_options ||= begin
172
+ options = {}
136
173
  if settings["access_key_id"] || settings["secret_access_key"]
137
- {credentials: Aws::Credentials.new(settings["access_key_id"], settings["secret_access_key"])}
138
- else
139
- {}
174
+ options[:credentials] = Aws::Credentials.new(settings["access_key_id"], settings["secret_access_key"])
140
175
  end
176
+ options[:region] = settings["region"] if settings["region"]
177
+ options
141
178
  end
142
179
  end
143
180
  end
@@ -8,7 +8,22 @@ module Blazer
8
8
  end
9
9
 
10
10
  def run_statement(statement, comment)
11
- # the one required method
11
+ # required
12
+ end
13
+
14
+ def quoting
15
+ # required, how to quote variables
16
+ # :backslash_escape - single quote strings and convert ' to \' and \ to \\
17
+ # :single_quote_escape - single quote strings and convert ' to ''
18
+ # ->(value) { ... } - custom method
19
+ end
20
+
21
+ def parameter_binding
22
+ # optional, but recommended when possible for security
23
+ # if specified, quoting is only used for display
24
+ # :positional - ?
25
+ # :numeric - $1
26
+ # ->(statement, values) { ... } - custom method
12
27
  end
13
28
 
14
29
  def tables
@@ -1,13 +1,13 @@
1
1
  module Blazer
2
2
  module Adapters
3
3
  class BigQueryAdapter < BaseAdapter
4
- def run_statement(statement, comment)
4
+ def run_statement(statement, comment, bind_params)
5
5
  columns = []
6
6
  rows = []
7
7
  error = nil
8
8
 
9
9
  begin
10
- results = bigquery.query(statement)
10
+ results = bigquery.query(statement, params: bind_params)
11
11
 
12
12
  # complete? was removed in google-cloud-bigquery 0.29.0
13
13
  # code is for backward compatibility
@@ -19,6 +19,7 @@ module Blazer
19
19
  end
20
20
  rescue => e
21
21
  error = e.message
22
+ error = Blazer::VARIABLE_MESSAGE if error.include?("Syntax error: Unexpected \"?\"")
22
23
  end
23
24
 
24
25
  [columns, rows, error]
@@ -42,6 +43,16 @@ module Blazer
42
43
  "SELECT * FROM `{table}` LIMIT 10"
43
44
  end
44
45
 
46
+ # https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#string_and_bytes_literals
47
+ def quoting
48
+ :backslash_escape
49
+ end
50
+
51
+ # https://cloud.google.com/bigquery/docs/parameterized-queries
52
+ def parameter_binding
53
+ :positional
54
+ end
55
+
45
56
  private
46
57
 
47
58
  def bigquery
@@ -1,28 +1,29 @@
1
1
  module Blazer
2
2
  module Adapters
3
3
  class CassandraAdapter < BaseAdapter
4
- def run_statement(statement, comment)
4
+ def run_statement(statement, comment, bind_params)
5
5
  columns = []
6
6
  rows = []
7
7
  error = nil
8
8
 
9
9
  begin
10
- response = session.execute("#{statement} /*#{comment}*/")
10
+ response = session.execute("#{statement} /*#{comment}*/", arguments: bind_params)
11
11
  rows = response.map { |r| r.values }
12
12
  columns = rows.any? ? response.first.keys : []
13
13
  rescue => e
14
14
  error = e.message
15
+ error = Blazer::VARIABLE_MESSAGE if error.include?("no viable alternative at input '?'")
15
16
  end
16
17
 
17
18
  [columns, rows, error]
18
19
  end
19
20
 
20
21
  def tables
21
- session.execute("SELECT table_name FROM system_schema.tables WHERE keyspace_name = '#{keyspace}'").map { |r| r["table_name"] }
22
+ session.execute("SELECT table_name FROM system_schema.tables WHERE keyspace_name = #{data_source.quote(keyspace)}").map { |r| r["table_name"] }
22
23
  end
23
24
 
24
25
  def schema
25
- result = session.execute("SELECT keyspace_name, table_name, column_name, type, position FROM system_schema.columns WHERE keyspace_name = '#{keyspace}'")
26
+ result = session.execute("SELECT keyspace_name, table_name, column_name, type, position FROM system_schema.columns WHERE keyspace_name = #{data_source.quote(keyspace)}")
26
27
  result.map(&:values).group_by { |r| [r[0], r[1]] }.map { |k, vs| {schema: k[0], table: k[1], columns: vs.sort_by { |v| v[2] }.map { |v| {name: v[2], data_type: v[3]} }} }
27
28
  end
28
29
 
@@ -30,6 +31,16 @@ module Blazer
30
31
  "SELECT * FROM {table} LIMIT 10"
31
32
  end
32
33
 
34
+ # https://docs.datastax.com/en/cql-oss/3.3/cql/cql_reference/escape_char_r.html
35
+ def quoting
36
+ :single_quote_escape
37
+ end
38
+
39
+ # https://docs.datastax.com/en/developer/nodejs-driver/3.0/features/parameterized-queries/
40
+ def parameter_binding
41
+ :positional
42
+ end
43
+
33
44
  private
34
45
 
35
46
  def cluster
@@ -18,6 +18,16 @@ module Blazer
18
18
  [columns, rows, error]
19
19
  end
20
20
 
21
+ # https://drill.apache.org/docs/lexical-structure/#string
22
+ def quoting
23
+ :single_quote_escape
24
+ end
25
+
26
+ # https://issues.apache.org/jira/browse/DRILL-5079
27
+ def parameter_binding
28
+ # not supported
29
+ end
30
+
21
31
  private
22
32
 
23
33
  def drill