blazer 2.4.2 → 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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/README.md +155 -57
  4. data/app/assets/javascripts/blazer/Chart.js +14000 -13979
  5. data/app/assets/javascripts/blazer/bootstrap.js +300 -97
  6. data/app/assets/javascripts/blazer/queries.js +12 -1
  7. data/app/assets/javascripts/blazer/vue.js +10754 -9687
  8. data/app/assets/stylesheets/blazer/application.css +5 -0
  9. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  10. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  11. data/app/assets/stylesheets/blazer/{bootstrap.css.erb → bootstrap.css} +527 -455
  12. data/app/controllers/blazer/base_controller.rb +45 -45
  13. data/app/controllers/blazer/dashboards_controller.rb +4 -11
  14. data/app/controllers/blazer/queries_controller.rb +31 -49
  15. data/app/models/blazer/query.rb +9 -3
  16. data/app/views/blazer/_variables.html.erb +5 -4
  17. data/app/views/blazer/dashboards/_form.html.erb +1 -1
  18. data/app/views/blazer/dashboards/show.html.erb +6 -4
  19. data/app/views/blazer/queries/_caching.html.erb +1 -1
  20. data/app/views/blazer/queries/_form.html.erb +3 -3
  21. data/app/views/blazer/queries/run.html.erb +5 -3
  22. data/app/views/blazer/queries/show.html.erb +12 -7
  23. data/app/views/layouts/blazer/application.html.erb +7 -2
  24. data/lib/blazer/adapters/athena_adapter.rb +73 -20
  25. data/lib/blazer/adapters/base_adapter.rb +16 -1
  26. data/lib/blazer/adapters/bigquery_adapter.rb +14 -3
  27. data/lib/blazer/adapters/cassandra_adapter.rb +15 -4
  28. data/lib/blazer/adapters/drill_adapter.rb +10 -0
  29. data/lib/blazer/adapters/druid_adapter.rb +36 -1
  30. data/lib/blazer/adapters/elasticsearch_adapter.rb +19 -4
  31. data/lib/blazer/adapters/hive_adapter.rb +10 -0
  32. data/lib/blazer/adapters/ignite_adapter.rb +12 -2
  33. data/lib/blazer/adapters/influxdb_adapter.rb +22 -10
  34. data/lib/blazer/adapters/mongodb_adapter.rb +4 -0
  35. data/lib/blazer/adapters/neo4j_adapter.rb +17 -2
  36. data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
  37. data/lib/blazer/adapters/presto_adapter.rb +9 -0
  38. data/lib/blazer/adapters/salesforce_adapter.rb +5 -0
  39. data/lib/blazer/adapters/snowflake_adapter.rb +9 -0
  40. data/lib/blazer/adapters/soda_adapter.rb +9 -0
  41. data/lib/blazer/adapters/spark_adapter.rb +5 -0
  42. data/lib/blazer/adapters/sql_adapter.rb +41 -4
  43. data/{app/mailers → lib}/blazer/check_mailer.rb +0 -0
  44. data/lib/blazer/data_source.rb +90 -8
  45. data/lib/blazer/engine.rb +1 -4
  46. data/lib/blazer/result.rb +19 -1
  47. data/lib/blazer/run_statement.rb +7 -3
  48. data/lib/blazer/run_statement_job.rb +4 -2
  49. data/{app/mailers → lib}/blazer/slack_notifier.rb +19 -4
  50. data/lib/blazer/statement.rb +75 -0
  51. data/lib/blazer/version.rb +1 -1
  52. data/lib/blazer.rb +32 -8
  53. data/lib/generators/blazer/templates/config.yml.tt +2 -2
  54. data/lib/tasks/blazer.rake +5 -5
  55. data/licenses/LICENSE-bootstrap.txt +1 -1
  56. metadata +10 -6
@@ -1,7 +1,7 @@
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 = []
@@ -9,18 +9,43 @@ module Blazer
9
9
  error = nil
10
10
 
11
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
- )
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
+ }
38
+ }
39
+
40
+ if settings["output_location"]
41
+ query_options[:result_configuration] = {output_location: settings["output_location"]}
42
+ end
43
+
44
+ if settings["workgroup"]
45
+ query_options[:work_group] = settings["workgroup"]
46
+ end
47
+
48
+ resp = client.start_query_execution(**query_options)
24
49
  query_execution_id = resp.query_execution_id
25
50
 
26
51
  timeout = data_source.timeout || 300
@@ -60,21 +85,21 @@ module Blazer
60
85
  column_types.each_with_index do |ct, i|
61
86
  # TODO more column_types
62
87
  case ct
63
- when "timestamp"
88
+ when "timestamp", "timestamp with time zone"
64
89
  rows.each do |row|
65
- row[i] = utc.parse(row[i])
90
+ row[i] &&= utc.parse(row[i])
66
91
  end
67
92
  when "date"
68
93
  rows.each do |row|
69
- row[i] = Date.parse(row[i])
94
+ row[i] &&= Date.parse(row[i])
70
95
  end
71
96
  when "bigint"
72
97
  rows.each do |row|
73
- row[i] = row[i].to_i
98
+ row[i] &&= row[i].to_i
74
99
  end
75
100
  when "double"
76
101
  rows.each do |row|
77
- row[i] = row[i].to_f
102
+ row[i] &&= row[i].to_f
78
103
  end
79
104
  end
80
105
  end
@@ -105,12 +130,29 @@ module Blazer
105
130
  "SELECT * FROM {table} LIMIT 10"
106
131
  end
107
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
+
108
143
  private
109
144
 
110
145
  def database
111
146
  @database ||= settings["database"] || "default"
112
147
  end
113
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
+
114
156
  def fetch_error(query_execution_id)
115
157
  client.get_query_execution(
116
158
  query_execution_id: query_execution_id
@@ -118,11 +160,22 @@ module Blazer
118
160
  end
119
161
 
120
162
  def client
121
- @client ||= Aws::Athena::Client.new
163
+ @client ||= Aws::Athena::Client.new(**client_options)
122
164
  end
123
165
 
124
166
  def glue
125
- @glue ||= Aws::Glue::Client.new
167
+ @glue ||= Aws::Glue::Client.new(**client_options)
168
+ end
169
+
170
+ def client_options
171
+ @client_options ||= begin
172
+ options = {}
173
+ if settings["access_key_id"] || settings["secret_access_key"]
174
+ options[:credentials] = Aws::Credentials.new(settings["access_key_id"], settings["secret_access_key"])
175
+ end
176
+ options[:region] = settings["region"] if settings["region"]
177
+ options
178
+ end
126
179
  end
127
180
  end
128
181
  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,24 +1,25 @@
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
14
14
  if !results.respond_to?(:complete?) || results.complete?
15
15
  columns = results.first.keys.map(&:to_s) if results.size > 0
16
- rows = results.map(&:values)
16
+ rows = results.all.map(&:values)
17
17
  else
18
18
  error = Blazer::TIMEOUT_MESSAGE
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
@@ -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,26 +1,27 @@
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.xpack.sql.query(body: {query: "#{statement} /*#{comment}*/"})
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
- date_indexes = response["columns"].each_index.select { |i| response["columns"][i]["type"] == "date" }
13
+ date_indexes = response["columns"].each_index.select { |i| ["date", "datetime"].include?(response["columns"][i]["type"]) }
14
14
  if columns.any?
15
15
  rows = response["rows"]
16
16
  date_indexes.each do |i|
17
17
  rows.each do |row|
18
- row[i] = Time.parse(row[i])
18
+ row[i] &&= Time.parse(row[i])
19
19
  end
20
20
  end
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,8 +37,22 @@ 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
 
52
+ def endpoint
53
+ @endpoint ||= client.info["version"]["number"].to_i >= 7 ? "_sql" : "_xpack/sql"
54
+ end
55
+
41
56
  def client
42
57
  @client ||= Elasticsearch::Client.new(url: settings["url"])
43
58
  end
@@ -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
- columns = result["columns"]
12
- rows = result["values"]
13
-
14
- # parse time columns
15
- # current approach isn't ideal, but result doesn't include types
16
- # another approach would be to check the format
17
- time_index = columns.index("time")
18
- if time_index
19
- rows.each do |row|
20
- row[time_index] = Time.parse(row[time_index]) if row[time_index]
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
@@ -25,6 +25,10 @@ module Blazer
25
25
  "db.{table}.find().limit(10)"
26
26
  end
27
27
 
28
+ def quoting
29
+ :backslash_escape
30
+ end
31
+
28
32
  protected
29
33
 
30
34
  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
@@ -0,0 +1,52 @@
1
+ module Blazer
2
+ module Adapters
3
+ class OpensearchAdapter < BaseAdapter
4
+ def run_statement(statement, comment)
5
+ columns = []
6
+ rows = []
7
+ error = nil
8
+
9
+ begin
10
+ response = client.transport.perform_request("POST", "_plugins/_sql", {}, {query: "#{statement} /*#{comment}*/"}).body
11
+ columns = response["schema"].map { |v| v["name"] }
12
+ # TODO typecast more types
13
+ # https://github.com/opensearch-project/sql/blob/main/docs/user/general/datatypes.rst
14
+ date_indexes = response["schema"].each_index.select { |i| response["schema"][i]["type"] == "timestamp" }
15
+ if columns.any?
16
+ rows = response["datarows"]
17
+ utc = ActiveSupport::TimeZone["Etc/UTC"]
18
+ date_indexes.each do |i|
19
+ rows.each do |row|
20
+ row[i] &&= utc.parse(row[i])
21
+ end
22
+ end
23
+ end
24
+ rescue => e
25
+ error = e.message
26
+ end
27
+
28
+ [columns, rows, error]
29
+ end
30
+
31
+ def tables
32
+ indices = client.cat.indices(format: "json").map { |v| v["index"] }
33
+ aliases = client.cat.aliases(format: "json").map { |v| v["alias"] }
34
+ (indices + aliases).uniq.sort
35
+ end
36
+
37
+ def preview_statement
38
+ "SELECT * FROM `{table}` LIMIT 10"
39
+ end
40
+
41
+ def quoting
42
+ # unknown
43
+ end
44
+
45
+ protected
46
+
47
+ def client
48
+ @client ||= OpenSearch::Client.new(url: settings["url"])
49
+ end
50
+ end
51
+ end
52
+ end
@@ -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