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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +63 -15
- data/app/assets/javascripts/blazer/queries.js +12 -1
- data/app/assets/stylesheets/blazer/application.css +1 -0
- data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
- data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
- data/app/assets/stylesheets/blazer/{bootstrap.css.erb → bootstrap.css} +0 -6
- data/app/controllers/blazer/base_controller.rb +45 -45
- data/app/controllers/blazer/dashboards_controller.rb +4 -11
- data/app/controllers/blazer/queries_controller.rb +29 -48
- data/app/models/blazer/query.rb +8 -2
- data/app/views/blazer/_variables.html.erb +5 -4
- data/app/views/blazer/dashboards/_form.html.erb +1 -1
- data/app/views/blazer/dashboards/show.html.erb +6 -4
- data/app/views/blazer/queries/_caching.html.erb +1 -1
- data/app/views/blazer/queries/_form.html.erb +3 -3
- data/app/views/blazer/queries/run.html.erb +1 -1
- data/app/views/blazer/queries/show.html.erb +12 -7
- data/app/views/layouts/blazer/application.html.erb +7 -2
- data/lib/blazer/adapters/athena_adapter.rb +55 -18
- data/lib/blazer/adapters/base_adapter.rb +16 -1
- data/lib/blazer/adapters/bigquery_adapter.rb +13 -2
- data/lib/blazer/adapters/cassandra_adapter.rb +15 -4
- data/lib/blazer/adapters/drill_adapter.rb +10 -0
- data/lib/blazer/adapters/druid_adapter.rb +36 -1
- data/lib/blazer/adapters/elasticsearch_adapter.rb +13 -2
- data/lib/blazer/adapters/hive_adapter.rb +10 -0
- data/lib/blazer/adapters/ignite_adapter.rb +12 -2
- data/lib/blazer/adapters/influxdb_adapter.rb +22 -10
- data/lib/blazer/adapters/mongodb_adapter.rb +4 -0
- data/lib/blazer/adapters/neo4j_adapter.rb +17 -2
- data/lib/blazer/adapters/opensearch_adapter.rb +4 -0
- data/lib/blazer/adapters/presto_adapter.rb +9 -0
- data/lib/blazer/adapters/salesforce_adapter.rb +5 -0
- data/lib/blazer/adapters/snowflake_adapter.rb +9 -0
- data/lib/blazer/adapters/soda_adapter.rb +9 -0
- data/lib/blazer/adapters/spark_adapter.rb +5 -0
- data/lib/blazer/adapters/sql_adapter.rb +37 -3
- data/lib/blazer/data_source.rb +85 -5
- data/lib/blazer/engine.rb +0 -4
- data/lib/blazer/result.rb +2 -0
- data/lib/blazer/run_statement.rb +7 -3
- data/lib/blazer/run_statement_job.rb +4 -2
- data/lib/blazer/slack_notifier.rb +5 -2
- data/lib/blazer/statement.rb +75 -0
- data/lib/blazer/version.rb +1 -1
- data/lib/blazer.rb +14 -6
- metadata +7 -4
@@ -3,15 +3,37 @@ module Blazer
|
|
3
3
|
class DruidAdapter < BaseAdapter
|
4
4
|
TIMESTAMP_REGEX = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\z/
|
5
5
|
|
6
|
-
def run_statement(statement, comment)
|
6
|
+
def run_statement(statement, comment, bind_params)
|
7
|
+
require "json"
|
8
|
+
require "net/http"
|
9
|
+
require "uri"
|
10
|
+
|
7
11
|
columns = []
|
8
12
|
rows = []
|
9
13
|
error = nil
|
10
14
|
|
15
|
+
params =
|
16
|
+
bind_params.map do |v|
|
17
|
+
type =
|
18
|
+
case v
|
19
|
+
when Integer
|
20
|
+
"BIGINT"
|
21
|
+
when Float
|
22
|
+
"DOUBLE"
|
23
|
+
when ActiveSupport::TimeWithZone
|
24
|
+
v = (v.to_f * 1000).round
|
25
|
+
"TIMESTAMP"
|
26
|
+
else
|
27
|
+
"VARCHAR"
|
28
|
+
end
|
29
|
+
{type: type, value: v}
|
30
|
+
end
|
31
|
+
|
11
32
|
header = {"Content-Type" => "application/json", "Accept" => "application/json"}
|
12
33
|
timeout = data_source.timeout ? data_source.timeout.to_i : 300
|
13
34
|
data = {
|
14
35
|
query: statement,
|
36
|
+
parameters: params,
|
15
37
|
context: {
|
16
38
|
timeout: timeout * 1000
|
17
39
|
}
|
@@ -27,6 +49,8 @@ module Blazer
|
|
27
49
|
error = response["errorMessage"] || "Unknown error: #{response.inspect}"
|
28
50
|
if error.include?("timed out")
|
29
51
|
error = Blazer::TIMEOUT_MESSAGE
|
52
|
+
elsif error.include?("Encountered \"?\" at")
|
53
|
+
error = Blazer::VARIABLE_MESSAGE
|
30
54
|
end
|
31
55
|
else
|
32
56
|
columns = (response.first || {}).keys
|
@@ -62,6 +86,17 @@ module Blazer
|
|
62
86
|
def preview_statement
|
63
87
|
"SELECT * FROM {table} LIMIT 10"
|
64
88
|
end
|
89
|
+
|
90
|
+
# https://druid.apache.org/docs/latest/querying/sql.html#identifiers-and-literals
|
91
|
+
# docs only mention double quotes
|
92
|
+
def quoting
|
93
|
+
:single_quote_escape
|
94
|
+
end
|
95
|
+
|
96
|
+
# https://druid.apache.org/docs/latest/querying/sql.html#dynamic-parameters
|
97
|
+
def parameter_binding
|
98
|
+
:positional
|
99
|
+
end
|
65
100
|
end
|
66
101
|
end
|
67
102
|
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module Blazer
|
2
2
|
module Adapters
|
3
3
|
class ElasticsearchAdapter < 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 = client.transport.perform_request("POST", endpoint, {}, {query: "#{statement} /*#{comment}*/"}).body
|
10
|
+
response = client.transport.perform_request("POST", endpoint, {}, {query: "#{statement} /*#{comment}*/", params: bind_params}).body
|
11
11
|
columns = response["columns"].map { |v| v["name"] }
|
12
12
|
# Elasticsearch does not differentiate between dates and times
|
13
13
|
date_indexes = response["columns"].each_index.select { |i| ["date", "datetime"].include?(response["columns"][i]["type"]) }
|
@@ -21,6 +21,7 @@ module Blazer
|
|
21
21
|
end
|
22
22
|
rescue => e
|
23
23
|
error = e.message
|
24
|
+
error = Blazer::VARIABLE_MESSAGE if error.include?("mismatched input '?'")
|
24
25
|
end
|
25
26
|
|
26
27
|
[columns, rows, error]
|
@@ -36,6 +37,16 @@ module Blazer
|
|
36
37
|
"SELECT * FROM \"{table}\" LIMIT 10"
|
37
38
|
end
|
38
39
|
|
40
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-lexical-structure.html#sql-syntax-string-literals
|
41
|
+
def quoting
|
42
|
+
:single_quote_escape
|
43
|
+
end
|
44
|
+
|
45
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-rest-params.html
|
46
|
+
def parameter_binding
|
47
|
+
:positional
|
48
|
+
end
|
49
|
+
|
39
50
|
protected
|
40
51
|
|
41
52
|
def endpoint
|
@@ -25,6 +25,16 @@ module Blazer
|
|
25
25
|
"SELECT * FROM {table} LIMIT 10"
|
26
26
|
end
|
27
27
|
|
28
|
+
# https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Types#LanguageManualTypes-StringsstringStrings
|
29
|
+
def quoting
|
30
|
+
:backslash_escape
|
31
|
+
end
|
32
|
+
|
33
|
+
# has variable substitution, but sets for session
|
34
|
+
# https://cwiki.apache.org/confluence/display/Hive/LanguageManual+VariableSubstitution
|
35
|
+
def parameter_binding
|
36
|
+
end
|
37
|
+
|
28
38
|
protected
|
29
39
|
|
30
40
|
def client
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module Blazer
|
2
2
|
module Adapters
|
3
3
|
class IgniteAdapter < 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
|
-
result = client.query("#{statement} /*#{comment}*/", schema: default_schema, statement_type: :select, timeout: data_source.timeout)
|
10
|
+
result = client.query("#{statement} /*#{comment}*/", bind_params, schema: default_schema, statement_type: :select, timeout: data_source.timeout)
|
11
11
|
columns = result.any? ? result.first.keys : []
|
12
12
|
rows = result.map(&:values)
|
13
13
|
rescue => e
|
@@ -37,6 +37,16 @@ module Blazer
|
|
37
37
|
# 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]] }
|
38
38
|
# end
|
39
39
|
|
40
|
+
def quoting
|
41
|
+
:single_quote_escape
|
42
|
+
end
|
43
|
+
|
44
|
+
# query arguments
|
45
|
+
# https://ignite.apache.org/docs/latest/binary-client-protocol/sql-and-scan-queries#op_query_sql
|
46
|
+
def parameter_binding
|
47
|
+
:positional
|
48
|
+
end
|
49
|
+
|
40
50
|
private
|
41
51
|
|
42
52
|
def default_schema
|
@@ -8,16 +8,19 @@ module Blazer
|
|
8
8
|
|
9
9
|
begin
|
10
10
|
result = client.query(statement, denormalize: false).first
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
11
|
+
|
12
|
+
if result
|
13
|
+
columns = result["columns"]
|
14
|
+
rows = result["values"]
|
15
|
+
|
16
|
+
# parse time columns
|
17
|
+
# current approach isn't ideal, but result doesn't include types
|
18
|
+
# another approach would be to check the format
|
19
|
+
time_index = columns.index("time")
|
20
|
+
if time_index
|
21
|
+
rows.each do |row|
|
22
|
+
row[time_index] = Time.parse(row[time_index]) if row[time_index]
|
23
|
+
end
|
21
24
|
end
|
22
25
|
end
|
23
26
|
rescue => e
|
@@ -35,6 +38,15 @@ module Blazer
|
|
35
38
|
"SELECT * FROM {table} LIMIT 10"
|
36
39
|
end
|
37
40
|
|
41
|
+
# https://docs.influxdata.com/influxdb/v1.8/query_language/spec/#strings
|
42
|
+
def quoting
|
43
|
+
:backslash_escape
|
44
|
+
end
|
45
|
+
|
46
|
+
def parameter_binding
|
47
|
+
# not supported
|
48
|
+
end
|
49
|
+
|
38
50
|
protected
|
39
51
|
|
40
52
|
def client
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module Blazer
|
2
2
|
module Adapters
|
3
3
|
class Neo4jAdapter < 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
|
-
result = session.query("#{statement} /*#{comment}*/")
|
10
|
+
result = session.query("#{statement} /*#{comment}*/", bind_params)
|
11
11
|
columns = result.columns.map(&:to_s)
|
12
12
|
rows = []
|
13
13
|
result.each do |row|
|
@@ -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?("Invalid input '$'")
|
22
23
|
end
|
23
24
|
|
24
25
|
[columns, rows, error]
|
@@ -33,6 +34,20 @@ module Blazer
|
|
33
34
|
"MATCH (n:{table}) RETURN n LIMIT 10"
|
34
35
|
end
|
35
36
|
|
37
|
+
# https://neo4j.com/docs/cypher-manual/current/syntax/expressions/#cypher-expressions-string-literals
|
38
|
+
def quoting
|
39
|
+
:backslash_escape
|
40
|
+
end
|
41
|
+
|
42
|
+
def parameter_binding
|
43
|
+
proc do |statement, variables|
|
44
|
+
variables.each_key do |k|
|
45
|
+
statement = statement.gsub("{#{k}}") { "$#{k} " }
|
46
|
+
end
|
47
|
+
[statement, variables]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
36
51
|
protected
|
37
52
|
|
38
53
|
def session
|
@@ -25,6 +25,15 @@ module Blazer
|
|
25
25
|
"SELECT * FROM {table} LIMIT 10"
|
26
26
|
end
|
27
27
|
|
28
|
+
def quoting
|
29
|
+
:single_quote_escape
|
30
|
+
end
|
31
|
+
|
32
|
+
# TODO support prepared statements - https://prestodb.io/docs/current/sql/prepare.html
|
33
|
+
# feature request for variables - https://github.com/prestodb/presto/issues/5918
|
34
|
+
def parameter_binding
|
35
|
+
end
|
36
|
+
|
28
37
|
protected
|
29
38
|
|
30
39
|
def client
|
@@ -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
|
@@ -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
|
-
|
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
|
-
|
161
|
+
{placeholder}
|
160
162
|
),
|
161
163
|
cohorts AS (
|
162
164
|
SELECT user_id, MIN(#{cohort_column}) AS cohort_time FROM query
|
@@ -183,6 +185,30 @@ 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? && (ActiveRecord::VERSION::STRING.to_f >= 6.1 || prepared_statements?)
|
195
|
+
# Active Record < 6.1 silently ignores binds with Postgres when prepared statements are disabled
|
196
|
+
:numeric
|
197
|
+
elsif sqlite?
|
198
|
+
:numeric
|
199
|
+
elsif mysql? && prepared_statements?
|
200
|
+
# Active Record silently ignores binds with MySQL when prepared statements are disabled
|
201
|
+
:positional
|
202
|
+
elsif sqlserver?
|
203
|
+
proc do |statement, variables|
|
204
|
+
variables.each_with_index do |(k, _), i|
|
205
|
+
statement = statement.gsub("{#{k}}", "@#{i} ")
|
206
|
+
end
|
207
|
+
[statement, variables.values]
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
186
212
|
protected
|
187
213
|
|
188
214
|
def select_all(statement, params = [])
|
@@ -207,6 +233,10 @@ module Blazer
|
|
207
233
|
["MySQL", "Mysql2", "Mysql2Spatial"].include?(adapter_name)
|
208
234
|
end
|
209
235
|
|
236
|
+
def sqlite?
|
237
|
+
["SQLite"].include?(adapter_name)
|
238
|
+
end
|
239
|
+
|
210
240
|
def sqlserver?
|
211
241
|
["SQLServer", "tinytds", "mssql"].include?(adapter_name)
|
212
242
|
end
|
@@ -282,6 +312,10 @@ module Blazer
|
|
282
312
|
end
|
283
313
|
end
|
284
314
|
end
|
315
|
+
|
316
|
+
def prepared_statements?
|
317
|
+
connection_model.connection.prepared_statements
|
318
|
+
end
|
285
319
|
end
|
286
320
|
end
|
287
321
|
end
|
data/lib/blazer/data_source.rb
CHANGED
@@ -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.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
|
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 =
|
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"]
|
data/lib/blazer/result.rb
CHANGED
data/lib/blazer/run_statement.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
module Blazer
|
2
2
|
class RunStatement
|
3
|
-
def perform(
|
3
|
+
def perform(statement, options = {})
|
4
4
|
query = options[:query]
|
5
|
-
|
5
|
+
|
6
|
+
data_source = statement.data_source
|
7
|
+
statement.bind
|
6
8
|
|
7
9
|
# audit
|
8
10
|
if Blazer.audit
|
9
|
-
|
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
|
-
|
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(
|
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
|
|