blazer 2.2.6

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of blazer might be problematic. Click here for more details.

Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +310 -0
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +1041 -0
  6. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  7. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
  8. data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
  9. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
  10. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
  11. data/app/assets/images/blazer/favicon.png +0 -0
  12. data/app/assets/javascripts/blazer/Chart.js +14456 -0
  13. data/app/assets/javascripts/blazer/Sortable.js +1540 -0
  14. data/app/assets/javascripts/blazer/ace.js +6 -0
  15. data/app/assets/javascripts/blazer/ace/ace.js +21301 -0
  16. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1993 -0
  17. data/app/assets/javascripts/blazer/ace/mode-sql.js +110 -0
  18. data/app/assets/javascripts/blazer/ace/snippets/sql.js +40 -0
  19. data/app/assets/javascripts/blazer/ace/snippets/text.js +14 -0
  20. data/app/assets/javascripts/blazer/ace/theme-twilight.js +116 -0
  21. data/app/assets/javascripts/blazer/application.js +81 -0
  22. data/app/assets/javascripts/blazer/bootstrap.js +2377 -0
  23. data/app/assets/javascripts/blazer/chartkick.js +2214 -0
  24. data/app/assets/javascripts/blazer/daterangepicker.js +1653 -0
  25. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  26. data/app/assets/javascripts/blazer/highlight.min.js +3 -0
  27. data/app/assets/javascripts/blazer/jquery-ujs.js +555 -0
  28. data/app/assets/javascripts/blazer/jquery.js +10364 -0
  29. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +325 -0
  30. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1212 -0
  31. data/app/assets/javascripts/blazer/moment.js +3043 -0
  32. data/app/assets/javascripts/blazer/queries.js +110 -0
  33. data/app/assets/javascripts/blazer/routes.js +26 -0
  34. data/app/assets/javascripts/blazer/selectize.js +3891 -0
  35. data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
  36. data/app/assets/javascripts/blazer/stupidtable.js +281 -0
  37. data/app/assets/javascripts/blazer/vue.js +10947 -0
  38. data/app/assets/stylesheets/blazer/application.css +234 -0
  39. data/app/assets/stylesheets/blazer/bootstrap.css.erb +6756 -0
  40. data/app/assets/stylesheets/blazer/daterangepicker.css +269 -0
  41. data/app/assets/stylesheets/blazer/github.css +125 -0
  42. data/app/assets/stylesheets/blazer/selectize.css +403 -0
  43. data/app/controllers/blazer/base_controller.rb +124 -0
  44. data/app/controllers/blazer/checks_controller.rb +56 -0
  45. data/app/controllers/blazer/dashboards_controller.rb +101 -0
  46. data/app/controllers/blazer/queries_controller.rb +347 -0
  47. data/app/helpers/blazer/base_helper.rb +43 -0
  48. data/app/mailers/blazer/check_mailer.rb +27 -0
  49. data/app/mailers/blazer/slack_notifier.rb +79 -0
  50. data/app/models/blazer/audit.rb +6 -0
  51. data/app/models/blazer/check.rb +104 -0
  52. data/app/models/blazer/connection.rb +5 -0
  53. data/app/models/blazer/dashboard.rb +17 -0
  54. data/app/models/blazer/dashboard_query.rb +9 -0
  55. data/app/models/blazer/query.rb +40 -0
  56. data/app/models/blazer/record.rb +5 -0
  57. data/app/views/blazer/_nav.html.erb +15 -0
  58. data/app/views/blazer/_variables.html.erb +124 -0
  59. data/app/views/blazer/check_mailer/failing_checks.html.erb +7 -0
  60. data/app/views/blazer/check_mailer/state_change.html.erb +48 -0
  61. data/app/views/blazer/checks/_form.html.erb +79 -0
  62. data/app/views/blazer/checks/edit.html.erb +3 -0
  63. data/app/views/blazer/checks/index.html.erb +69 -0
  64. data/app/views/blazer/checks/new.html.erb +3 -0
  65. data/app/views/blazer/dashboards/_form.html.erb +76 -0
  66. data/app/views/blazer/dashboards/edit.html.erb +3 -0
  67. data/app/views/blazer/dashboards/new.html.erb +3 -0
  68. data/app/views/blazer/dashboards/show.html.erb +51 -0
  69. data/app/views/blazer/queries/_form.html.erb +250 -0
  70. data/app/views/blazer/queries/docs.html.erb +131 -0
  71. data/app/views/blazer/queries/edit.html.erb +2 -0
  72. data/app/views/blazer/queries/home.html.erb +163 -0
  73. data/app/views/blazer/queries/new.html.erb +2 -0
  74. data/app/views/blazer/queries/run.html.erb +198 -0
  75. data/app/views/blazer/queries/schema.html.erb +55 -0
  76. data/app/views/blazer/queries/show.html.erb +75 -0
  77. data/app/views/layouts/blazer/application.html.erb +24 -0
  78. data/config/routes.rb +20 -0
  79. data/lib/blazer.rb +231 -0
  80. data/lib/blazer/adapters/athena_adapter.rb +129 -0
  81. data/lib/blazer/adapters/base_adapter.rb +53 -0
  82. data/lib/blazer/adapters/bigquery_adapter.rb +68 -0
  83. data/lib/blazer/adapters/cassandra_adapter.rb +59 -0
  84. data/lib/blazer/adapters/drill_adapter.rb +28 -0
  85. data/lib/blazer/adapters/druid_adapter.rb +67 -0
  86. data/lib/blazer/adapters/elasticsearch_adapter.rb +46 -0
  87. data/lib/blazer/adapters/influxdb_adapter.rb +45 -0
  88. data/lib/blazer/adapters/mongodb_adapter.rb +39 -0
  89. data/lib/blazer/adapters/neo4j_adapter.rb +47 -0
  90. data/lib/blazer/adapters/presto_adapter.rb +45 -0
  91. data/lib/blazer/adapters/salesforce_adapter.rb +45 -0
  92. data/lib/blazer/adapters/snowflake_adapter.rb +73 -0
  93. data/lib/blazer/adapters/soda_adapter.rb +96 -0
  94. data/lib/blazer/adapters/sql_adapter.rb +221 -0
  95. data/lib/blazer/data_source.rb +195 -0
  96. data/lib/blazer/detect_anomalies.R +19 -0
  97. data/lib/blazer/engine.rb +43 -0
  98. data/lib/blazer/result.rb +218 -0
  99. data/lib/blazer/run_statement.rb +40 -0
  100. data/lib/blazer/run_statement_job.rb +18 -0
  101. data/lib/blazer/version.rb +3 -0
  102. data/lib/generators/blazer/install_generator.rb +22 -0
  103. data/lib/generators/blazer/templates/config.yml.tt +73 -0
  104. data/lib/generators/blazer/templates/install.rb.tt +46 -0
  105. data/lib/tasks/blazer.rake +11 -0
  106. metadata +231 -0
@@ -0,0 +1,55 @@
1
+ <% blazer_title "Schema: #{@data_source.name}" %>
2
+
3
+ <h1>Schema: <%= @data_source.name %></h1>
4
+
5
+ <hr />
6
+
7
+ <div id="header">
8
+ <input id="search" type="text" placeholder="Start typing a table or column" style="width: 300px; display: inline-block;" class="search form-control" />
9
+ </div>
10
+
11
+ <% @schema.each do |table| %>
12
+ <table class="table schema-table">
13
+ <thead>
14
+ <tr>
15
+ <th colspan="2">
16
+ <% if table[:schema] != "public" %><%= table[:schema] %>.<% end %><%= table[:table] %>
17
+ </th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <% table[:columns].each do |column| %>
22
+ <tr>
23
+ <td style="width: 60%;"><%= column[:name] %></td>
24
+ <td class="text-muted"><%= column[:data_type] %></td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
29
+ <% end %>
30
+
31
+ <script>
32
+ $("#search").on("keyup", function() {
33
+ var value = $(this).val().toLowerCase()
34
+ $(".schema-table").filter(function() {
35
+ // if found in table name, show entire table
36
+ // if just found in rows, show row
37
+
38
+ var found = $(this).find("thead").text().toLowerCase().indexOf(value) > -1
39
+
40
+ if (found) {
41
+ $(this).find("tbody tr").toggle(true)
42
+ } else {
43
+ $(this).find("tbody tr").filter(function() {
44
+ var found2 = $(this).text().toLowerCase().indexOf(value) > -1
45
+ $(this).toggle(found2)
46
+ if (found2) {
47
+ found = true
48
+ }
49
+ })
50
+ }
51
+
52
+ $(this).toggle(found)
53
+ })
54
+ }).focus()
55
+ </script>
@@ -0,0 +1,75 @@
1
+ <% blazer_title @query.name %>
2
+
3
+ <div class="topbar">
4
+ <div class="container">
5
+ <div class="row" style="padding-top: 13px;">
6
+ <div class="col-sm-9">
7
+ <%= render partial: "blazer/nav" %>
8
+ <h3 style="line-height: 34px; display: inline; margin-left: 5px;">
9
+ <%= @query.name %>
10
+ </h3>
11
+ </div>
12
+ <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" %>
15
+
16
+ <% if !@error && @success %>
17
+ <%= button_to "Download", run_queries_path(query_id: @query.id, format: "csv", forecast: params[:forecast]), params: {statement: @statement}, class: "btn btn-primary" %>
18
+ <% end %>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <div style="margin-bottom: 60px;"></div>
25
+
26
+ <% if @sql_errors.any? %>
27
+ <div class="alert alert-danger">
28
+ <ul>
29
+ <% @sql_errors.each do |message| %>
30
+ <li><%= message %></li>
31
+ <% end %>
32
+ </ul>
33
+ </div>
34
+ <% end %>
35
+
36
+ <% if @query.description.present? %>
37
+ <p><%= @query.description %></p>
38
+ <% end %>
39
+
40
+ <%= render partial: "blazer/variables", locals: {action: query_path(@query)} %>
41
+
42
+ <pre id="code"><code><%= @statement %></code></pre>
43
+
44
+ <% if @success %>
45
+ <div id="results">
46
+ <p class="text-muted">Loading...</p>
47
+ </div>
48
+
49
+ <script>
50
+ function showRun(data) {
51
+ $("#results").html(data)
52
+ $("#results table").stupidtable(stupidtableCustomSettings).stickyTableHeaders({fixedOffset: 60})
53
+ }
54
+
55
+ function showError(message) {
56
+ $("#results").addClass("query-error").html(message)
57
+ }
58
+
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 %>
62
+
63
+ runQuery(data, showRun, showError)
64
+ </script>
65
+ <% end %>
66
+
67
+ <% unless %w(mongodb).include?(Blazer.data_sources[@query.data_source].adapter) %>
68
+ <script>
69
+ // do not highlight really long queries
70
+ // this can lead to performance issues
71
+ if ($("code").text().length < 10000) {
72
+ hljs.highlightBlock(document.getElementById("code"));
73
+ }
74
+ </script>
75
+ <% end %>
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= blazer_title ? blazer_title : "Blazer" %></title>
5
+
6
+ <meta charset="utf-8" />
7
+ <%= favicon_link_tag "blazer/favicon.png" %>
8
+ <%= stylesheet_link_tag "blazer/application" %>
9
+ <%= javascript_include_tag "blazer/application" %>
10
+ <script>
11
+ <%= blazer_js_var "rootPath", root_path %>
12
+ </script>
13
+ <% if blazer_maps? %>
14
+ <%= stylesheet_link_tag "https://api.mapbox.com/mapbox.js/v3.3.1/mapbox.css", integrity: "sha384-vxzdEt+wZRPNQbhChjmiaFMLWg86IGuq1NGDehJHsD2mphYkxXll/eSs16WWi6Dq", crossorigin: "anonymous" %>
15
+ <%= javascript_include_tag "https://api.mapbox.com/mapbox.js/v3.3.1/mapbox.js", integrity: "sha384-CTBEiDLiZJ8gkAQ3fYGoeiRp81/ecNiBkGz11jXFALOZ6++rbnqmdo6OImkmr1MO", crossorigin: "anonymous" %>
16
+ <% end %>
17
+ <%= csrf_meta_tags %>
18
+ </head>
19
+ <body>
20
+ <div class="container">
21
+ <%= yield %>
22
+ </div>
23
+ </body>
24
+ </html>
@@ -0,0 +1,20 @@
1
+ Blazer::Engine.routes.draw do
2
+ resources :queries do
3
+ post :run, on: :collection # err on the side of caution
4
+ post :cancel, on: :collection
5
+ post :refresh, on: :member
6
+ get :tables, on: :collection
7
+ get :schema, on: :collection
8
+ get :docs, on: :collection
9
+ end
10
+
11
+ resources :checks, except: [:show] do
12
+ get :run, on: :member
13
+ end
14
+
15
+ resources :dashboards, except: [:index] do
16
+ post :refresh, on: :member
17
+ end
18
+
19
+ root to: "queries#home"
20
+ end
@@ -0,0 +1,231 @@
1
+ # dependencies
2
+ require "csv"
3
+ require "yaml"
4
+ require "chartkick"
5
+ require "safely/core"
6
+
7
+ # modules
8
+ require "blazer/version"
9
+ require "blazer/data_source"
10
+ require "blazer/result"
11
+ require "blazer/run_statement"
12
+
13
+ # adapters
14
+ require "blazer/adapters/base_adapter"
15
+ require "blazer/adapters/athena_adapter"
16
+ require "blazer/adapters/bigquery_adapter"
17
+ require "blazer/adapters/cassandra_adapter"
18
+ require "blazer/adapters/drill_adapter"
19
+ require "blazer/adapters/druid_adapter"
20
+ require "blazer/adapters/elasticsearch_adapter"
21
+ require "blazer/adapters/influxdb_adapter"
22
+ require "blazer/adapters/mongodb_adapter"
23
+ require "blazer/adapters/neo4j_adapter"
24
+ require "blazer/adapters/presto_adapter"
25
+ require "blazer/adapters/salesforce_adapter"
26
+ require "blazer/adapters/soda_adapter"
27
+ require "blazer/adapters/sql_adapter"
28
+ require "blazer/adapters/snowflake_adapter"
29
+
30
+ # engine
31
+ require "blazer/engine"
32
+
33
+ module Blazer
34
+ class Error < StandardError; end
35
+ class TimeoutNotSupported < Error; end
36
+
37
+ class << self
38
+ attr_accessor :audit
39
+ attr_reader :time_zone
40
+ attr_accessor :user_name
41
+ attr_writer :user_class
42
+ attr_writer :user_method
43
+ attr_accessor :before_action
44
+ attr_accessor :from_email
45
+ attr_accessor :cache
46
+ attr_accessor :transform_statement
47
+ attr_accessor :transform_variable
48
+ attr_accessor :check_schedules
49
+ attr_accessor :anomaly_checks
50
+ attr_accessor :forecasting
51
+ attr_accessor :async
52
+ attr_accessor :images
53
+ attr_accessor :query_viewable
54
+ attr_accessor :query_editable
55
+ attr_accessor :override_csp
56
+ attr_accessor :slack_webhook_url
57
+ attr_accessor :mapbox_access_token
58
+ end
59
+ self.audit = true
60
+ self.user_name = :name
61
+ self.check_schedules = ["5 minutes", "1 hour", "1 day"]
62
+ self.anomaly_checks = false
63
+ self.forecasting = false
64
+ self.async = false
65
+ self.images = false
66
+ self.override_csp = false
67
+
68
+ TIMEOUT_MESSAGE = "Query timed out :("
69
+ TIMEOUT_ERRORS = [
70
+ "canceling statement due to statement timeout", # postgres
71
+ "canceling statement due to conflict with recovery", # postgres
72
+ "cancelled on user's request", # redshift
73
+ "canceled on user's request", # redshift
74
+ "system requested abort", # redshift
75
+ "maximum statement execution time exceeded" # mysql
76
+ ]
77
+
78
+ def self.time_zone=(time_zone)
79
+ @time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
80
+ end
81
+
82
+ def self.user_class
83
+ if !defined?(@user_class)
84
+ @user_class = settings.key?("user_class") ? settings["user_class"] : (User.name rescue nil)
85
+ end
86
+ @user_class
87
+ end
88
+
89
+ def self.user_method
90
+ if !defined?(@user_method)
91
+ @user_method = settings["user_method"]
92
+ if user_class
93
+ @user_method ||= "current_#{user_class.to_s.downcase.singularize}"
94
+ end
95
+ end
96
+ @user_method
97
+ end
98
+
99
+ def self.settings
100
+ @settings ||= begin
101
+ path = Rails.root.join("config", "blazer.yml").to_s
102
+ if File.exist?(path)
103
+ YAML.load(ERB.new(File.read(path)).result)
104
+ else
105
+ {}
106
+ end
107
+ end
108
+ end
109
+
110
+ def self.data_sources
111
+ @data_sources ||= begin
112
+ ds = Hash.new { |hash, key| raise Blazer::Error, "Unknown data source: #{key}" }
113
+ settings["data_sources"].each do |id, s|
114
+ ds[id] = Blazer::DataSource.new(id, s)
115
+ end
116
+ ds
117
+ end
118
+ end
119
+
120
+ def self.extract_vars(statement)
121
+ # strip commented out lines
122
+ # and regex {1} or {1,2}
123
+ statement.to_s.gsub(/\-\-.+/, "").gsub(/\/\*.+\*\//m, "").scan(/\{\w*?\}/i).map { |v| v[1...-1] }.reject { |v| /\A\d+(\,\d+)?\z/.match(v) || v.empty? }.uniq
124
+ end
125
+
126
+ def self.run_checks(schedule: nil)
127
+ checks = Blazer::Check.includes(:query)
128
+ checks = checks.where(schedule: schedule) if schedule
129
+ checks.find_each do |check|
130
+ next if check.state == "disabled"
131
+ Safely.safely { run_check(check) }
132
+ end
133
+ end
134
+
135
+ def self.run_check(check)
136
+ tries = 1
137
+
138
+ ActiveSupport::Notifications.instrument("run_check.blazer", check_id: check.id, query_id: check.query.id, state_was: check.state) do |instrument|
139
+ # try 3 times on timeout errors
140
+ data_source = data_sources[check.query.data_source]
141
+ statement = check.query.statement
142
+ Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
143
+
144
+ while tries <= 3
145
+ result = data_source.run_statement(statement, refresh_cache: true, check: check, query: check.query)
146
+ if result.timed_out?
147
+ Rails.logger.info "[blazer timeout] query=#{check.query.name}"
148
+ tries += 1
149
+ sleep(10)
150
+ elsif result.error.to_s.start_with?("PG::ConnectionBad")
151
+ data_source.reconnect
152
+ Rails.logger.info "[blazer reconnect] query=#{check.query.name}"
153
+ tries += 1
154
+ sleep(10)
155
+ else
156
+ break
157
+ end
158
+ end
159
+
160
+ begin
161
+ check.reload # in case state has changed since job started
162
+ check.update_state(result)
163
+ rescue ActiveRecord::RecordNotFound
164
+ # check deleted
165
+ end
166
+
167
+ # TODO use proper logfmt
168
+ Rails.logger.info "[blazer check] query=#{check.query.name} state=#{check.state} rows=#{result.rows.try(:size)} error=#{result.error}"
169
+
170
+ instrument[:statement] = statement
171
+ instrument[:data_source] = data_source
172
+ instrument[:state] = check.state
173
+ instrument[:rows] = result.rows.try(:size)
174
+ instrument[:error] = result.error
175
+ instrument[:tries] = tries
176
+ end
177
+ end
178
+
179
+ def self.send_failing_checks
180
+ emails = {}
181
+ slack_channels = {}
182
+
183
+ Blazer::Check.includes(:query).where(state: ["failing", "error", "timed out", "disabled"]).find_each do |check|
184
+ check.split_emails.each do |email|
185
+ (emails[email] ||= []) << check
186
+ end
187
+ check.split_slack_channels.each do |channel|
188
+ (slack_channels[channel] ||= []) << check
189
+ end
190
+ end
191
+
192
+ emails.each do |email, checks|
193
+ Safely.safely do
194
+ Blazer::CheckMailer.failing_checks(email, checks).deliver_now
195
+ end
196
+ end
197
+
198
+ slack_channels.each do |channel, checks|
199
+ Safely.safely do
200
+ Blazer::SlackNotifier.failing_checks(channel, checks)
201
+ end
202
+ end
203
+ end
204
+
205
+ def self.slack?
206
+ slack_webhook_url.present?
207
+ end
208
+
209
+ def self.adapters
210
+ @adapters ||= {}
211
+ end
212
+
213
+ def self.register_adapter(name, adapter)
214
+ adapters[name] = adapter
215
+ end
216
+ end
217
+
218
+ Blazer.register_adapter "athena", Blazer::Adapters::AthenaAdapter
219
+ Blazer.register_adapter "bigquery", Blazer::Adapters::BigQueryAdapter
220
+ Blazer.register_adapter "cassandra", Blazer::Adapters::CassandraAdapter
221
+ Blazer.register_adapter "drill", Blazer::Adapters::DrillAdapter
222
+ Blazer.register_adapter "druid", Blazer::Adapters::DruidAdapter
223
+ Blazer.register_adapter "elasticsearch", Blazer::Adapters::ElasticsearchAdapter
224
+ Blazer.register_adapter "influxdb", Blazer::Adapters::InfluxdbAdapter
225
+ Blazer.register_adapter "neo4j", Blazer::Adapters::Neo4jAdapter
226
+ Blazer.register_adapter "presto", Blazer::Adapters::PrestoAdapter
227
+ Blazer.register_adapter "mongodb", Blazer::Adapters::MongodbAdapter
228
+ Blazer.register_adapter "salesforce", Blazer::Adapters::SalesforceAdapter
229
+ Blazer.register_adapter "soda", Blazer::Adapters::SodaAdapter
230
+ Blazer.register_adapter "sql", Blazer::Adapters::SqlAdapter
231
+ Blazer.register_adapter "snowflake", Blazer::Adapters::SnowflakeAdapter
@@ -0,0 +1,129 @@
1
+ module Blazer
2
+ module Adapters
3
+ class AthenaAdapter < BaseAdapter
4
+ def run_statement(statement, comment)
5
+ require "digest/md5"
6
+
7
+ columns = []
8
+ rows = []
9
+ error = nil
10
+
11
+ begin
12
+ resp =
13
+ client.start_query_execution(
14
+ query_string: statement,
15
+ # use token so we fetch cached results after query is run
16
+ client_request_token: Digest::MD5.hexdigest([statement,data_source.id].join("/")),
17
+ query_execution_context: {
18
+ database: database,
19
+ },
20
+ result_configuration: {
21
+ output_location: settings["output_location"]
22
+ }
23
+ )
24
+ query_execution_id = resp.query_execution_id
25
+
26
+ timeout = data_source.timeout || 300
27
+ stop_at = Time.now + timeout
28
+ resp = nil
29
+
30
+ begin
31
+ resp = client.get_query_results(
32
+ query_execution_id: query_execution_id
33
+ )
34
+ rescue Aws::Athena::Errors::InvalidRequestException => e
35
+ unless e.message.start_with?("Query has not yet finished.")
36
+ raise e
37
+ end
38
+ if Time.now < stop_at
39
+ sleep(3)
40
+ retry
41
+ end
42
+ end
43
+
44
+ if resp && resp.result_set
45
+ column_info = resp.result_set.result_set_metadata.column_info
46
+ columns = column_info.map(&:name)
47
+ column_types = column_info.map(&:type)
48
+
49
+ untyped_rows = []
50
+
51
+ # paginated
52
+ resp.each do |page|
53
+ untyped_rows.concat page.result_set.rows.map { |r| r.data.map(&:var_char_value) }
54
+ end
55
+
56
+ utc = ActiveSupport::TimeZone['Etc/UTC']
57
+
58
+ rows = untyped_rows[1..-1] || []
59
+ rows = untyped_rows[0..-1] unless column_info.present?
60
+ column_types.each_with_index do |ct, i|
61
+ # TODO more column_types
62
+ case ct
63
+ when "timestamp"
64
+ rows.each do |row|
65
+ row[i] = utc.parse(row[i])
66
+ end
67
+ when "date"
68
+ rows.each do |row|
69
+ row[i] = Date.parse(row[i])
70
+ end
71
+ when "bigint"
72
+ rows.each do |row|
73
+ row[i] = row[i].to_i
74
+ end
75
+ when "double"
76
+ rows.each do |row|
77
+ row[i] = row[i].to_f
78
+ end
79
+ end
80
+ end
81
+ elsif resp
82
+ error = fetch_error(query_execution_id)
83
+ else
84
+ error = Blazer::TIMEOUT_MESSAGE
85
+ end
86
+ rescue Aws::Athena::Errors::InvalidRequestException => e
87
+ error = e.message
88
+ if error == "Query did not finish successfully. Final query state: FAILED"
89
+ error = fetch_error(query_execution_id)
90
+ end
91
+ end
92
+
93
+ [columns, rows, error]
94
+ end
95
+
96
+ def tables
97
+ glue.get_tables(database_name: database).table_list.map(&:name).sort
98
+ end
99
+
100
+ def schema
101
+ glue.get_tables(database_name: database).table_list.map { |t| {table: t.name, columns: t.storage_descriptor.columns.map { |c| {name: c.name, data_type: c.type} }} }
102
+ end
103
+
104
+ def preview_statement
105
+ "SELECT * FROM {table} LIMIT 10"
106
+ end
107
+
108
+ private
109
+
110
+ def database
111
+ @database ||= settings["database"] || "default"
112
+ end
113
+
114
+ def fetch_error(query_execution_id)
115
+ client.get_query_execution(
116
+ query_execution_id: query_execution_id
117
+ ).query_execution.status.state_change_reason
118
+ end
119
+
120
+ def client
121
+ @client ||= Aws::Athena::Client.new
122
+ end
123
+
124
+ def glue
125
+ @glue ||= Aws::Glue::Client.new
126
+ end
127
+ end
128
+ end
129
+ end