blazer_xlsx 3.0.5

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 (148) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +442 -0
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +1093 -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/Sortable.js +3709 -0
  13. data/app/assets/javascripts/blazer/ace/ace.js +19630 -0
  14. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1981 -0
  15. data/app/assets/javascripts/blazer/ace/mode-sql.js +215 -0
  16. data/app/assets/javascripts/blazer/ace/snippets/sql.js +16 -0
  17. data/app/assets/javascripts/blazer/ace/snippets/text.js +9 -0
  18. data/app/assets/javascripts/blazer/ace/theme-twilight.js +18 -0
  19. data/app/assets/javascripts/blazer/ace.js +6 -0
  20. data/app/assets/javascripts/blazer/application.js +84 -0
  21. data/app/assets/javascripts/blazer/bootstrap.js +2580 -0
  22. data/app/assets/javascripts/blazer/chart.umd.js +13 -0
  23. data/app/assets/javascripts/blazer/chartjs-adapter-date-fns.bundle.js +6322 -0
  24. data/app/assets/javascripts/blazer/chartkick.js +2570 -0
  25. data/app/assets/javascripts/blazer/daterangepicker.js +1578 -0
  26. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  27. data/app/assets/javascripts/blazer/highlight.min.js +466 -0
  28. data/app/assets/javascripts/blazer/jquery.js +10872 -0
  29. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +325 -0
  30. data/app/assets/javascripts/blazer/mapkick.bundle.js +1029 -0
  31. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1548 -0
  32. data/app/assets/javascripts/blazer/moment.js +5685 -0
  33. data/app/assets/javascripts/blazer/queries.js +130 -0
  34. data/app/assets/javascripts/blazer/rails-ujs.js +746 -0
  35. data/app/assets/javascripts/blazer/routes.js +26 -0
  36. data/app/assets/javascripts/blazer/selectize.js +3891 -0
  37. data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
  38. data/app/assets/javascripts/blazer/stupidtable.js +281 -0
  39. data/app/assets/javascripts/blazer/vue.global.prod.js +1 -0
  40. data/app/assets/stylesheets/blazer/application.css +243 -0
  41. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  42. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  43. data/app/assets/stylesheets/blazer/bootstrap.css +6828 -0
  44. data/app/assets/stylesheets/blazer/daterangepicker.css +410 -0
  45. data/app/assets/stylesheets/blazer/github.css +125 -0
  46. data/app/assets/stylesheets/blazer/selectize.css +403 -0
  47. data/app/controllers/blazer/base_controller.rb +135 -0
  48. data/app/controllers/blazer/checks_controller.rb +56 -0
  49. data/app/controllers/blazer/dashboards_controller.rb +99 -0
  50. data/app/controllers/blazer/queries_controller.rb +472 -0
  51. data/app/controllers/blazer/uploads_controller.rb +147 -0
  52. data/app/helpers/blazer/base_helper.rb +39 -0
  53. data/app/models/blazer/audit.rb +6 -0
  54. data/app/models/blazer/check.rb +104 -0
  55. data/app/models/blazer/connection.rb +5 -0
  56. data/app/models/blazer/dashboard.rb +17 -0
  57. data/app/models/blazer/dashboard_query.rb +9 -0
  58. data/app/models/blazer/query.rb +42 -0
  59. data/app/models/blazer/record.rb +5 -0
  60. data/app/models/blazer/upload.rb +11 -0
  61. data/app/models/blazer/uploads_connection.rb +7 -0
  62. data/app/views/blazer/_nav.html.erb +18 -0
  63. data/app/views/blazer/_variables.html.erb +127 -0
  64. data/app/views/blazer/check_mailer/failing_checks.html.erb +7 -0
  65. data/app/views/blazer/check_mailer/state_change.html.erb +48 -0
  66. data/app/views/blazer/checks/_form.html.erb +79 -0
  67. data/app/views/blazer/checks/edit.html.erb +3 -0
  68. data/app/views/blazer/checks/index.html.erb +72 -0
  69. data/app/views/blazer/checks/new.html.erb +3 -0
  70. data/app/views/blazer/dashboards/_form.html.erb +82 -0
  71. data/app/views/blazer/dashboards/edit.html.erb +3 -0
  72. data/app/views/blazer/dashboards/new.html.erb +3 -0
  73. data/app/views/blazer/dashboards/show.html.erb +53 -0
  74. data/app/views/blazer/queries/_caching.html.erb +16 -0
  75. data/app/views/blazer/queries/_cohorts.html.erb +48 -0
  76. data/app/views/blazer/queries/_form.html.erb +255 -0
  77. data/app/views/blazer/queries/docs.html.erb +147 -0
  78. data/app/views/blazer/queries/edit.html.erb +2 -0
  79. data/app/views/blazer/queries/home.html.erb +169 -0
  80. data/app/views/blazer/queries/new.html.erb +2 -0
  81. data/app/views/blazer/queries/run.html.erb +183 -0
  82. data/app/views/blazer/queries/schema.html.erb +55 -0
  83. data/app/views/blazer/queries/show.html.erb +72 -0
  84. data/app/views/blazer/uploads/_form.html.erb +27 -0
  85. data/app/views/blazer/uploads/edit.html.erb +3 -0
  86. data/app/views/blazer/uploads/index.html.erb +55 -0
  87. data/app/views/blazer/uploads/new.html.erb +3 -0
  88. data/app/views/layouts/blazer/application.html.erb +25 -0
  89. data/config/routes.rb +25 -0
  90. data/lib/blazer/adapters/athena_adapter.rb +182 -0
  91. data/lib/blazer/adapters/base_adapter.rb +76 -0
  92. data/lib/blazer/adapters/bigquery_adapter.rb +79 -0
  93. data/lib/blazer/adapters/cassandra_adapter.rb +70 -0
  94. data/lib/blazer/adapters/drill_adapter.rb +38 -0
  95. data/lib/blazer/adapters/druid_adapter.rb +102 -0
  96. data/lib/blazer/adapters/elasticsearch_adapter.rb +61 -0
  97. data/lib/blazer/adapters/hive_adapter.rb +55 -0
  98. data/lib/blazer/adapters/ignite_adapter.rb +64 -0
  99. data/lib/blazer/adapters/influxdb_adapter.rb +57 -0
  100. data/lib/blazer/adapters/neo4j_adapter.rb +62 -0
  101. data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
  102. data/lib/blazer/adapters/presto_adapter.rb +54 -0
  103. data/lib/blazer/adapters/salesforce_adapter.rb +50 -0
  104. data/lib/blazer/adapters/snowflake_adapter.rb +82 -0
  105. data/lib/blazer/adapters/soda_adapter.rb +105 -0
  106. data/lib/blazer/adapters/spark_adapter.rb +14 -0
  107. data/lib/blazer/adapters/sql_adapter.rb +353 -0
  108. data/lib/blazer/adapters.rb +17 -0
  109. data/lib/blazer/anomaly_detectors.rb +22 -0
  110. data/lib/blazer/check_mailer.rb +27 -0
  111. data/lib/blazer/data_source.rb +266 -0
  112. data/lib/blazer/engine.rb +42 -0
  113. data/lib/blazer/forecasters.rb +7 -0
  114. data/lib/blazer/result.rb +178 -0
  115. data/lib/blazer/result_cache.rb +71 -0
  116. data/lib/blazer/run_statement.rb +45 -0
  117. data/lib/blazer/run_statement_job.rb +20 -0
  118. data/lib/blazer/slack_notifier.rb +94 -0
  119. data/lib/blazer/statement.rb +77 -0
  120. data/lib/blazer/version.rb +3 -0
  121. data/lib/blazer.rb +282 -0
  122. data/lib/generators/blazer/install_generator.rb +22 -0
  123. data/lib/generators/blazer/templates/config.yml.tt +79 -0
  124. data/lib/generators/blazer/templates/install.rb.tt +47 -0
  125. data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
  126. data/lib/generators/blazer/uploads_generator.rb +18 -0
  127. data/lib/tasks/blazer.rake +20 -0
  128. data/licenses/LICENSE-ace.txt +24 -0
  129. data/licenses/LICENSE-bootstrap.txt +21 -0
  130. data/licenses/LICENSE-chart.js.txt +9 -0
  131. data/licenses/LICENSE-chartjs-adapter-date-fns.txt +9 -0
  132. data/licenses/LICENSE-chartkick.js.txt +22 -0
  133. data/licenses/LICENSE-date-fns.txt +21 -0
  134. data/licenses/LICENSE-daterangepicker.txt +21 -0
  135. data/licenses/LICENSE-fuzzysearch.txt +20 -0
  136. data/licenses/LICENSE-highlight.js.txt +29 -0
  137. data/licenses/LICENSE-jquery.txt +20 -0
  138. data/licenses/LICENSE-kurkle-color.txt +9 -0
  139. data/licenses/LICENSE-mapkick-bundle.txt +1029 -0
  140. data/licenses/LICENSE-moment-timezone.txt +20 -0
  141. data/licenses/LICENSE-moment.txt +22 -0
  142. data/licenses/LICENSE-rails-ujs.txt +20 -0
  143. data/licenses/LICENSE-selectize.txt +202 -0
  144. data/licenses/LICENSE-sortable.txt +21 -0
  145. data/licenses/LICENSE-stickytableheaders.txt +20 -0
  146. data/licenses/LICENSE-stupidtable.txt +19 -0
  147. data/licenses/LICENSE-vue.txt +21 -0
  148. metadata +271 -0
@@ -0,0 +1,353 @@
1
+ module Blazer
2
+ module Adapters
3
+ class SqlAdapter < BaseAdapter
4
+ attr_reader :connection_model
5
+
6
+ def initialize(data_source)
7
+ super
8
+
9
+ @connection_model =
10
+ Class.new(Blazer::Connection) do
11
+ def self.name
12
+ "Blazer::Connection::Adapter#{object_id}"
13
+ end
14
+ establish_connection(data_source.settings["url"]) if data_source.settings["url"]
15
+ end
16
+ end
17
+
18
+ def run_statement(statement, comment, bind_params = [])
19
+ columns = []
20
+ rows = []
21
+ error = nil
22
+
23
+ begin
24
+ result = nil
25
+ in_transaction do
26
+ set_timeout(data_source.timeout) if data_source.timeout
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)
29
+ end
30
+
31
+ columns = result.columns
32
+ rows = result.rows
33
+
34
+ # cast values
35
+ if result.column_types.any?
36
+ types = columns.map { |c| result.column_types[c] }
37
+ rows =
38
+ rows.map do |row|
39
+ row.map.with_index do |v, i|
40
+ v && (t = types[i]) ? t.send(:cast_value, v) : v
41
+ end
42
+ end
43
+ end
44
+
45
+ # fix for non-ASCII column names and charts
46
+ if adapter_name == "Trilogy"
47
+ columns.map! { |k| k.dup.force_encoding(Encoding::UTF_8) }
48
+ end
49
+
50
+ # fix for binary data
51
+ if mysql?
52
+ rows =
53
+ rows.map do |row|
54
+ row.map do |v|
55
+ if v.is_a?(String) && v.encoding == Encoding::BINARY
56
+ "0x#{v.unpack1("H*").upcase}"
57
+ else
58
+ v
59
+ end
60
+ end
61
+ end
62
+ end
63
+ rescue => e
64
+ error = e.message.sub(/.+ERROR: /, "")
65
+ error = Blazer::TIMEOUT_MESSAGE if Blazer::TIMEOUT_ERRORS.any? { |e| error.include?(e) }
66
+ 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 '?")
67
+ if error.include?("could not determine data type of parameter")
68
+ error += " - try adding casting to variables and make sure none are inside a string literal"
69
+ end
70
+ reconnect if error.include?("PG::ConnectionBad")
71
+ end
72
+
73
+ [columns, rows, error]
74
+ end
75
+
76
+ def tables
77
+ sql = add_schemas("SELECT table_schema, table_name FROM information_schema.tables")
78
+ result = data_source.run_statement(sql, refresh_cache: true)
79
+ if postgresql? || redshift? || snowflake?
80
+ result.rows.sort_by { |r| [r[0] == default_schema ? "" : r[0], r[1]] }.map do |row|
81
+ table =
82
+ if row[0] == default_schema
83
+ row[1]
84
+ else
85
+ "#{row[0]}.#{row[1]}"
86
+ end
87
+
88
+ table = table.downcase if snowflake?
89
+
90
+ {
91
+ table: table,
92
+ value: connection_model.connection.quote_table_name(table)
93
+ }
94
+ end
95
+ else
96
+ result.rows.map(&:second).sort
97
+ end
98
+ end
99
+
100
+ def schema
101
+ sql = add_schemas("SELECT table_schema, table_name, column_name, data_type, ordinal_position FROM information_schema.columns")
102
+ result = data_source.run_statement(sql)
103
+ result.rows.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]} }} }.sort_by { |t| [t[:schema] == default_schema ? "" : t[:schema], t[:table]] }
104
+ end
105
+
106
+ def preview_statement
107
+ if sqlserver?
108
+ "SELECT TOP (10) * FROM {table}"
109
+ else
110
+ "SELECT * FROM {table} LIMIT 10"
111
+ end
112
+ end
113
+
114
+ def reconnect
115
+ connection_model.establish_connection(settings["url"])
116
+ end
117
+
118
+ def cost(statement)
119
+ result = explain(statement)
120
+ if sqlserver?
121
+ result["TotalSubtreeCost"]
122
+ else
123
+ match = /cost=\d+\.\d+..(\d+\.\d+) /.match(result)
124
+ match[1] if match
125
+ end
126
+ end
127
+
128
+ def explain(statement)
129
+ if postgresql? || redshift?
130
+ select_all("EXPLAIN #{statement}").rows.first.first
131
+ elsif sqlserver?
132
+ begin
133
+ execute("SET SHOWPLAN_ALL ON")
134
+ result = select_all(statement).each.first
135
+ ensure
136
+ execute("SET SHOWPLAN_ALL OFF")
137
+ end
138
+ result
139
+ end
140
+ rescue
141
+ nil
142
+ end
143
+
144
+ def cancel(run_id)
145
+ if postgresql?
146
+ select_all("SELECT pg_cancel_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND query LIKE ?", ["%,run_id:#{run_id}%"])
147
+ elsif redshift?
148
+ first_row = select_all("SELECT pid FROM stv_recents WHERE status = 'Running' AND query LIKE ?", ["%,run_id:#{run_id}%"]).first
149
+ if first_row
150
+ select_all("CANCEL #{first_row["pid"].to_i}")
151
+ end
152
+ end
153
+ end
154
+
155
+ def cachable?(statement)
156
+ !%w[CREATE ALTER UPDATE INSERT DELETE].include?(statement.split.first.to_s.upcase)
157
+ end
158
+
159
+ def supports_cohort_analysis?
160
+ postgresql? || mysql?
161
+ end
162
+
163
+ # TODO treat date columns as already in time zone
164
+ def cohort_analysis_statement(statement, period:, days:)
165
+ raise "Cohort analysis not supported" unless supports_cohort_analysis?
166
+
167
+ cohort_column = statement =~ /\bcohort_time\b/ ? "cohort_time" : "conversion_time"
168
+ tzname = Blazer.time_zone.tzinfo.name
169
+
170
+ if mysql?
171
+ time_sql = "CONVERT_TZ(cohorts.cohort_time, '+00:00', ?)"
172
+ case period
173
+ when "day"
174
+ date_sql = "CAST(DATE_FORMAT(#{time_sql}, '%Y-%m-%d') AS DATE)"
175
+ date_params = [tzname]
176
+ when "week"
177
+ date_sql = "CAST(DATE_FORMAT(#{time_sql} - INTERVAL ((5 + DAYOFWEEK(#{time_sql})) % 7) DAY, '%Y-%m-%d') AS DATE)"
178
+ date_params = [tzname, tzname]
179
+ else
180
+ date_sql = "CAST(DATE_FORMAT(#{time_sql}, '%Y-%m-01') AS DATE)"
181
+ date_params = [tzname]
182
+ end
183
+ bucket_sql = "CAST(CEIL(TIMESTAMPDIFF(SECOND, cohorts.cohort_time, query.conversion_time) / ?) AS SIGNED)"
184
+ else
185
+ date_sql = "date_trunc(?, cohorts.cohort_time::timestamptz AT TIME ZONE ?)::date"
186
+ date_params = [period, tzname]
187
+ bucket_sql = "CEIL(EXTRACT(EPOCH FROM query.conversion_time - cohorts.cohort_time) / ?)::int"
188
+ end
189
+
190
+ # WITH not an optimization fence in Postgres 12+
191
+ statement = <<~SQL
192
+ WITH query AS (
193
+ {placeholder}
194
+ ),
195
+ cohorts AS (
196
+ SELECT user_id, MIN(#{cohort_column}) AS cohort_time FROM query
197
+ WHERE user_id IS NOT NULL AND #{cohort_column} IS NOT NULL
198
+ GROUP BY 1
199
+ )
200
+ SELECT
201
+ #{date_sql} AS period,
202
+ 0 AS bucket,
203
+ COUNT(DISTINCT cohorts.user_id)
204
+ FROM cohorts GROUP BY 1
205
+ UNION ALL
206
+ SELECT
207
+ #{date_sql} AS period,
208
+ #{bucket_sql} AS bucket,
209
+ COUNT(DISTINCT query.user_id)
210
+ FROM cohorts INNER JOIN query ON query.user_id = cohorts.user_id
211
+ WHERE query.conversion_time IS NOT NULL
212
+ AND query.conversion_time >= cohorts.cohort_time
213
+ #{cohort_column == "conversion_time" ? "AND query.conversion_time != cohorts.cohort_time" : ""}
214
+ GROUP BY 1, 2
215
+ SQL
216
+ params = [statement] + date_params + date_params + [days.to_i * 86400]
217
+ connection_model.send(:sanitize_sql_array, params)
218
+ end
219
+
220
+ def quoting
221
+ ->(value) { connection_model.connection.quote(value) }
222
+ end
223
+
224
+ # Redshift adapter silently ignores binds
225
+ def parameter_binding
226
+ if postgresql?
227
+ :numeric
228
+ elsif sqlite? && prepared_statements?
229
+ # Active Record silently ignores binds with SQLite when prepared statements are disabled
230
+ :numeric
231
+ elsif mysql? && prepared_statements?
232
+ # Active Record silently ignores binds with MySQL when prepared statements are disabled
233
+ :positional
234
+ elsif sqlserver?
235
+ proc do |statement, variables|
236
+ variables.each_with_index do |(k, _), i|
237
+ statement = statement.gsub("{#{k}}", "@#{i} ")
238
+ end
239
+ [statement, variables.values]
240
+ end
241
+ end
242
+ end
243
+
244
+ protected
245
+
246
+ def select_all(statement, params = [])
247
+ statement = connection_model.send(:sanitize_sql_array, [statement] + params) if params.any?
248
+ connection_model.connection.select_all(statement)
249
+ end
250
+
251
+ # seperate from select_all to prevent mysql error
252
+ def execute(statement)
253
+ connection_model.connection.execute(statement)
254
+ end
255
+
256
+ def postgresql?
257
+ ["PostgreSQL", "PostGIS"].include?(adapter_name)
258
+ end
259
+
260
+ def redshift?
261
+ ["Redshift"].include?(adapter_name)
262
+ end
263
+
264
+ def mysql?
265
+ ["MySQL", "Mysql2", "Mysql2Spatial", "Trilogy"].include?(adapter_name)
266
+ end
267
+
268
+ def sqlite?
269
+ ["SQLite"].include?(adapter_name)
270
+ end
271
+
272
+ def sqlserver?
273
+ ["SQLServer", "tinytds", "mssql"].include?(adapter_name)
274
+ end
275
+
276
+ def snowflake?
277
+ data_source.adapter == "snowflake"
278
+ end
279
+
280
+ def adapter_name
281
+ # prevent bad data source from taking down queries/new
282
+ connection_model.connection.adapter_name rescue nil
283
+ end
284
+
285
+ def default_schema
286
+ @default_schema ||= begin
287
+ if postgresql? || redshift?
288
+ "public"
289
+ elsif sqlserver?
290
+ "dbo"
291
+ elsif connection_model.respond_to?(:connection_db_config)
292
+ connection_model.connection_db_config.database
293
+ else
294
+ connection_model.connection_config[:database]
295
+ end
296
+ end
297
+ end
298
+
299
+ def add_schemas(query)
300
+ if settings["schemas"]
301
+ where = "table_schema IN (?)"
302
+ schemas = settings["schemas"]
303
+ elsif mysql?
304
+ where = "table_schema IN (?)"
305
+ schemas = [default_schema]
306
+ else
307
+ where = "table_schema NOT IN (?)"
308
+ schemas = ["information_schema"]
309
+ schemas.map!(&:upcase) if snowflake?
310
+ schemas << "pg_catalog" if postgresql? || redshift?
311
+ end
312
+ connection_model.send(:sanitize_sql_array, ["#{query} WHERE #{where}", schemas])
313
+ end
314
+
315
+ def set_timeout(timeout)
316
+ if postgresql? || redshift?
317
+ execute("SET #{use_transaction? ? "LOCAL " : ""}statement_timeout = #{timeout.to_i * 1000}")
318
+ elsif mysql?
319
+ # use send as this method is private in Rails 4.2
320
+ mariadb = connection_model.connection.send(:mariadb?) rescue false
321
+ if mariadb
322
+ execute("SET max_statement_time = #{timeout.to_i * 1000}")
323
+ else
324
+ execute("SET max_execution_time = #{timeout.to_i * 1000}")
325
+ end
326
+ else
327
+ raise Blazer::TimeoutNotSupported, "Timeout not supported for #{adapter_name} adapter"
328
+ end
329
+ end
330
+
331
+ def use_transaction?
332
+ settings.key?("use_transaction") ? settings["use_transaction"] : true
333
+ end
334
+
335
+ def in_transaction
336
+ connection_model.connection_pool.with_connection do
337
+ if use_transaction?
338
+ connection_model.transaction do
339
+ yield
340
+ raise ActiveRecord::Rollback
341
+ end
342
+ else
343
+ yield
344
+ end
345
+ end
346
+ end
347
+
348
+ def prepared_statements?
349
+ connection_model.connection.prepared_statements
350
+ end
351
+ end
352
+ end
353
+ end
@@ -0,0 +1,17 @@
1
+ Blazer.register_adapter "athena", Blazer::Adapters::AthenaAdapter
2
+ Blazer.register_adapter "bigquery", Blazer::Adapters::BigQueryAdapter
3
+ Blazer.register_adapter "cassandra", Blazer::Adapters::CassandraAdapter
4
+ Blazer.register_adapter "drill", Blazer::Adapters::DrillAdapter
5
+ Blazer.register_adapter "druid", Blazer::Adapters::DruidAdapter
6
+ Blazer.register_adapter "elasticsearch", Blazer::Adapters::ElasticsearchAdapter
7
+ Blazer.register_adapter "hive", Blazer::Adapters::HiveAdapter
8
+ Blazer.register_adapter "ignite", Blazer::Adapters::IgniteAdapter
9
+ Blazer.register_adapter "influxdb", Blazer::Adapters::InfluxdbAdapter
10
+ Blazer.register_adapter "neo4j", Blazer::Adapters::Neo4jAdapter
11
+ Blazer.register_adapter "opensearch", Blazer::Adapters::OpensearchAdapter
12
+ Blazer.register_adapter "presto", Blazer::Adapters::PrestoAdapter
13
+ Blazer.register_adapter "salesforce", Blazer::Adapters::SalesforceAdapter
14
+ Blazer.register_adapter "soda", Blazer::Adapters::SodaAdapter
15
+ Blazer.register_adapter "spark", Blazer::Adapters::SparkAdapter
16
+ Blazer.register_adapter "sql", Blazer::Adapters::SqlAdapter
17
+ Blazer.register_adapter "snowflake", Blazer::Adapters::SnowflakeAdapter
@@ -0,0 +1,22 @@
1
+ Blazer.register_anomaly_detector "anomaly_detection" do |series|
2
+ anomalies = AnomalyDetection.detect(series.to_h, period: :auto)
3
+ anomalies.include?(series.last[0])
4
+ end
5
+
6
+ Blazer.register_anomaly_detector "prophet" do |series|
7
+ df = Rover::DataFrame.new(series[0..-2].map { |v| {"ds" => v[0], "y" => v[1]} })
8
+ m = Prophet.new(interval_width: 0.99)
9
+ m.logger.level = ::Logger::FATAL # no logging
10
+ m.fit(df)
11
+ future = Rover::DataFrame.new(series[-1..-1].map { |v| {"ds" => v[0]} })
12
+ forecast = m.predict(future).to_a[0]
13
+ lower = forecast["yhat_lower"]
14
+ upper = forecast["yhat_upper"]
15
+ value = series.last[1]
16
+ value < lower || value > upper
17
+ end
18
+
19
+ Blazer.register_anomaly_detector "trend" do |series|
20
+ anomalies = Trend.anomalies(series.to_h)
21
+ anomalies.include?(series.last[0])
22
+ end
@@ -0,0 +1,27 @@
1
+ module Blazer
2
+ class CheckMailer < ActionMailer::Base
3
+ include ActionView::Helpers::TextHelper
4
+
5
+ default from: Blazer.from_email if Blazer.from_email
6
+ layout false
7
+
8
+ def state_change(check, state, state_was, rows_count, error, columns, rows, column_types, check_type)
9
+ @check = check
10
+ @state = state
11
+ @state_was = state_was
12
+ @rows_count = rows_count
13
+ @error = error
14
+ @columns = columns
15
+ @rows = rows
16
+ @column_types = column_types
17
+ @check_type = check_type
18
+ mail to: check.emails, reply_to: check.emails, subject: "Check #{state.titleize}: #{check.query.name}"
19
+ end
20
+
21
+ def failing_checks(email, checks)
22
+ @checks = checks
23
+ # add reply_to for mailing lists
24
+ mail to: email, reply_to: email, subject: "#{pluralize(checks.size, "Check")} Failing"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,266 @@
1
+ module Blazer
2
+ class DataSource
3
+ extend Forwardable
4
+
5
+ attr_reader :id, :settings
6
+
7
+ def_delegators :adapter_instance, :schema, :tables, :preview_statement, :reconnect, :cost, :explain, :cancel, :supports_cohort_analysis?, :cohort_analysis_statement
8
+
9
+ def initialize(id, settings)
10
+ @id = id
11
+ @settings = settings
12
+ end
13
+
14
+ def adapter
15
+ settings["adapter"] || detect_adapter
16
+ end
17
+
18
+ def name
19
+ settings["name"] || @id
20
+ end
21
+
22
+ def linked_columns
23
+ settings["linked_columns"] || {}
24
+ end
25
+
26
+ def smart_columns
27
+ settings["smart_columns"] || {}
28
+ end
29
+
30
+ def smart_variables
31
+ settings["smart_variables"] || {}
32
+ end
33
+
34
+ def variable_defaults
35
+ settings["variable_defaults"] || {}
36
+ end
37
+
38
+ def timeout
39
+ settings["timeout"]
40
+ end
41
+
42
+ def cache
43
+ @cache ||= begin
44
+ if settings["cache"].is_a?(Hash)
45
+ settings["cache"]
46
+ elsif settings["cache"]
47
+ {
48
+ "mode" => "all",
49
+ "expires_in" => settings["cache"]
50
+ }
51
+ else
52
+ {
53
+ "mode" => "off"
54
+ }
55
+ end
56
+ end
57
+ end
58
+
59
+ def cache_mode
60
+ cache["mode"]
61
+ end
62
+
63
+ def cache_expires_in
64
+ (cache["expires_in"] || 60).to_f
65
+ end
66
+
67
+ def cache_slow_threshold
68
+ (cache["slow_threshold"] || 15).to_f
69
+ end
70
+
71
+ def local_time_suffix
72
+ @local_time_suffix ||= Array(settings["local_time_suffix"])
73
+ end
74
+
75
+ def result_cache
76
+ @result_cache ||= Blazer::ResultCache.new(self)
77
+ end
78
+
79
+ def run_results(run_id)
80
+ result_cache.read_run(run_id)
81
+ end
82
+
83
+ def delete_results(run_id)
84
+ result_cache.delete_run(run_id)
85
+ end
86
+
87
+ def sub_variables(statement, vars)
88
+ statement = statement.dup
89
+ vars.each do |var, value|
90
+ # use block form to disable back-references
91
+ statement.gsub!("{#{var}}") { quote(value) }
92
+ end
93
+ statement
94
+ end
95
+
96
+ def run_statement(statement, options = {})
97
+ statement = Statement.new(statement, self) if statement.is_a?(String)
98
+ statement.bind unless statement.bind_statement
99
+
100
+ result = nil
101
+ if cache_mode != "off"
102
+ if options[:refresh_cache]
103
+ clear_cache(statement) # for checks
104
+ else
105
+ result = result_cache.read_statement(statement)
106
+ end
107
+ end
108
+
109
+ unless result
110
+ comment = "blazer"
111
+ if options[:user].respond_to?(:id)
112
+ comment << ",user_id:#{options[:user].id}"
113
+ end
114
+ if options[:user].respond_to?(Blazer.user_name)
115
+ # only include letters, numbers, and spaces to prevent injection
116
+ comment << ",user_name:#{options[:user].send(Blazer.user_name).to_s.gsub(/[^a-zA-Z0-9 ]/, "")}"
117
+ end
118
+ if options[:query].respond_to?(:id)
119
+ comment << ",query_id:#{options[:query].id}"
120
+ end
121
+ if options[:check]
122
+ comment << ",check_id:#{options[:check].id},check_emails:#{options[:check].emails}"
123
+ end
124
+ if options[:run_id]
125
+ comment << ",run_id:#{options[:run_id]}"
126
+ end
127
+ result = run_statement_helper(statement, comment, options)
128
+ end
129
+
130
+ if options[:async] && options[:run_id]
131
+ run_id = options[:run_id]
132
+ begin
133
+ result_cache.write_run(run_id, result)
134
+ rescue
135
+ result = Blazer::Result.new(self, [], [], "Error storing the results of this query :(", nil, false)
136
+ result_cache.write_run(run_id, result)
137
+ end
138
+ end
139
+
140
+ result
141
+ end
142
+
143
+ def clear_cache(statement)
144
+ result_cache.delete_statement(statement)
145
+ end
146
+
147
+ def quote(value)
148
+ if quoting == :backslash_escape || quoting == :single_quote_escape
149
+ # only need to support types generated by process_vars
150
+ if value.is_a?(Integer) || value.is_a?(Float)
151
+ value.to_s
152
+ elsif value.nil?
153
+ "NULL"
154
+ else
155
+ value = value.to_formatted_s(:db) if value.is_a?(ActiveSupport::TimeWithZone)
156
+
157
+ if quoting == :backslash_escape
158
+ "'#{value.gsub("\\") { "\\\\" }.gsub("'") { "\\'" }}'"
159
+ else
160
+ "'#{value.gsub("'", "''")}'"
161
+ end
162
+ end
163
+ elsif quoting.respond_to?(:call)
164
+ quoting.call(value)
165
+ elsif quoting.nil?
166
+ raise Blazer::Error, "Quoting not specified"
167
+ else
168
+ raise Blazer::Error, "Unknown quoting"
169
+ end
170
+ end
171
+
172
+ def bind_params(statement, variables)
173
+ if parameter_binding == :positional
174
+ locations = []
175
+ variables.each do |k, v|
176
+ i = 0
177
+ while (idx = statement.index("{#{k}}", i))
178
+ locations << [v, idx]
179
+ i = idx + 1
180
+ end
181
+ end
182
+ variables.each do |k, v|
183
+ statement = statement.gsub("{#{k}}", "?")
184
+ end
185
+ [statement, locations.sort_by(&:last).map(&:first)]
186
+ elsif parameter_binding == :numeric
187
+ variables.each_with_index do |(k, v), i|
188
+ # add trailing space if followed by digit
189
+ # try to keep minimal to avoid fixing invalid queries like SELECT{var}
190
+ statement = statement.gsub(/#{Regexp.escape("{#{k}}")}(\d)/, "$#{i + 1} \\1").gsub("{#{k}}", "$#{i + 1}")
191
+ end
192
+ [statement, variables.values]
193
+ elsif parameter_binding.respond_to?(:call)
194
+ parameter_binding.call(statement, variables)
195
+ elsif parameter_binding.nil?
196
+ [sub_variables(statement, variables), []]
197
+ else
198
+ raise Blazer::Error, "Unknown bind parameters"
199
+ end
200
+ end
201
+
202
+ protected
203
+
204
+ def adapter_instance
205
+ @adapter_instance ||= begin
206
+ # TODO add required settings to adapters
207
+ unless settings["url"] || Rails.env.development? || ["bigquery", "athena", "snowflake", "salesforce"].include?(settings["adapter"])
208
+ raise Blazer::Error, "Empty url for data source: #{id}"
209
+ end
210
+
211
+ unless Blazer.adapters[adapter]
212
+ raise Blazer::Error, "Unknown adapter"
213
+ end
214
+
215
+ Blazer.adapters[adapter].new(self)
216
+ end
217
+ end
218
+
219
+ def quoting
220
+ @quoting ||= adapter_instance.quoting
221
+ end
222
+
223
+ def parameter_binding
224
+ @parameter_binding ||= adapter_instance.parameter_binding
225
+ end
226
+
227
+ def run_statement_helper(statement, comment, options)
228
+ start_time = Blazer.monotonic_time
229
+ columns, rows, error =
230
+ if adapter_instance.parameter_binding
231
+ adapter_instance.run_statement(statement.bind_statement, comment, statement.bind_values)
232
+ else
233
+ adapter_instance.run_statement(statement.bind_statement, comment)
234
+ end
235
+ duration = Blazer.monotonic_time - start_time
236
+
237
+ cache = !error && (cache_mode == "all" || (cache_mode == "slow" && duration >= cache_slow_threshold))
238
+
239
+ result = Blazer::Result.new(self, columns, rows, error, cache ? Time.now : nil, false)
240
+
241
+ if cache && adapter_instance.cachable?(statement.bind_statement)
242
+ begin
243
+ result_cache.write_statement(statement, result, expires_in: cache_expires_in.to_f * 60)
244
+ # set just_cached after caching
245
+ result.just_cached = true
246
+ rescue
247
+ # do nothing
248
+ end
249
+ end
250
+
251
+ result.cached_at = nil
252
+ result
253
+ end
254
+
255
+ # TODO check for adapter with same name, default to sql
256
+ def detect_adapter
257
+ scheme = settings["url"].to_s.split("://").first
258
+ case scheme
259
+ when "presto", "cassandra", "ignite"
260
+ scheme
261
+ else
262
+ "sql"
263
+ end
264
+ end
265
+ end
266
+ end