railsblazer 2.0.0
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 +7 -0
- data/.gitattributes +1 -0
- data/.github/ISSUE_TEMPLATE.md +0 -0
- data/.gitignore +14 -0
- data/CHANGELOG.md +247 -0
- data/CONTRIBUTING.md +42 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +855 -0
- data/Rakefile +1 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
- data/app/assets/javascripts/blazer/Chart.js +14145 -0
- data/app/assets/javascripts/blazer/Sortable.js +1144 -0
- data/app/assets/javascripts/blazer/ace.js +6 -0
- data/app/assets/javascripts/blazer/ace/ace.js +11 -0
- data/app/assets/javascripts/blazer/ace/ext-language_tools.js +5 -0
- data/app/assets/javascripts/blazer/ace/mode-sql.js +1 -0
- data/app/assets/javascripts/blazer/ace/snippets/sql.js +1 -0
- data/app/assets/javascripts/blazer/ace/snippets/text.js +1 -0
- data/app/assets/javascripts/blazer/ace/theme-twilight.js +1 -0
- data/app/assets/javascripts/blazer/application.js +79 -0
- data/app/assets/javascripts/blazer/bootstrap.js +2366 -0
- data/app/assets/javascripts/blazer/chartkick.js +1693 -0
- data/app/assets/javascripts/blazer/daterangepicker.js +1505 -0
- data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
- data/app/assets/javascripts/blazer/highlight.pack.js +1 -0
- data/app/assets/javascripts/blazer/jquery.js +10308 -0
- data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +263 -0
- data/app/assets/javascripts/blazer/jquery_ujs.js +469 -0
- data/app/assets/javascripts/blazer/moment-timezone.js +1007 -0
- data/app/assets/javascripts/blazer/moment.js +3043 -0
- data/app/assets/javascripts/blazer/queries.js +110 -0
- data/app/assets/javascripts/blazer/routes.js +23 -0
- data/app/assets/javascripts/blazer/selectize.js +3667 -0
- data/app/assets/javascripts/blazer/stupidtable.js +114 -0
- data/app/assets/javascripts/blazer/vue.js +7515 -0
- data/app/assets/stylesheets/blazer/application.css +198 -0
- data/app/assets/stylesheets/blazer/bootstrap.css.erb +6202 -0
- data/app/assets/stylesheets/blazer/daterangepicker-bs3.css +375 -0
- data/app/assets/stylesheets/blazer/github.css +125 -0
- data/app/assets/stylesheets/blazer/selectize.default.css +387 -0
- data/app/controllers/blazer/base_controller.rb +113 -0
- data/app/controllers/blazer/checks_controller.rb +56 -0
- data/app/controllers/blazer/dashboards_controller.rb +105 -0
- data/app/controllers/blazer/queries_controller.rb +337 -0
- data/app/helpers/blazer/base_helper.rb +57 -0
- data/app/mailers/blazer/check_mailer.rb +27 -0
- data/app/mailers/blazer/slack_notifier.rb +76 -0
- data/app/models/blazer/audit.rb +6 -0
- data/app/models/blazer/check.rb +104 -0
- data/app/models/blazer/connection.rb +5 -0
- data/app/models/blazer/dashboard.rb +13 -0
- data/app/models/blazer/dashboard_query.rb +9 -0
- data/app/models/blazer/query.rb +40 -0
- data/app/models/blazer/record.rb +5 -0
- data/app/views/blazer/_nav.html.erb +16 -0
- data/app/views/blazer/_variables.html.erb +102 -0
- data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
- data/app/views/blazer/check_mailer/state_change.html.erb +47 -0
- data/app/views/blazer/checks/_form.html.erb +79 -0
- data/app/views/blazer/checks/edit.html.erb +1 -0
- data/app/views/blazer/checks/index.html.erb +43 -0
- data/app/views/blazer/checks/new.html.erb +1 -0
- data/app/views/blazer/dashboards/_form.html.erb +76 -0
- data/app/views/blazer/dashboards/edit.html.erb +1 -0
- data/app/views/blazer/dashboards/new.html.erb +1 -0
- data/app/views/blazer/dashboards/show.html.erb +47 -0
- data/app/views/blazer/queries/_form.html.erb +240 -0
- data/app/views/blazer/queries/edit.html.erb +2 -0
- data/app/views/blazer/queries/home.html.erb +152 -0
- data/app/views/blazer/queries/new.html.erb +2 -0
- data/app/views/blazer/queries/run.html.erb +165 -0
- data/app/views/blazer/queries/schema.html.erb +20 -0
- data/app/views/blazer/queries/show.html.erb +73 -0
- data/app/views/layouts/blazer/application.html.erb +24 -0
- data/blazer-0.0.1.gem +0 -0
- data/blazer.gemspec +27 -0
- data/config/routes.rb +16 -0
- data/lib/blazer.rb +223 -0
- data/lib/blazer/adapters/athena_adapter.rb +128 -0
- data/lib/blazer/adapters/base_adapter.rb +53 -0
- data/lib/blazer/adapters/bigquery_adapter.rb +68 -0
- data/lib/blazer/adapters/cassandra_adapter.rb +59 -0
- data/lib/blazer/adapters/drill_adapter.rb +28 -0
- data/lib/blazer/adapters/druid_adapter.rb +67 -0
- data/lib/blazer/adapters/elasticsearch_adapter.rb +46 -0
- data/lib/blazer/adapters/mongodb_adapter.rb +39 -0
- data/lib/blazer/adapters/presto_adapter.rb +45 -0
- data/lib/blazer/adapters/snowflake_adapter.rb +73 -0
- data/lib/blazer/adapters/sql_adapter.rb +182 -0
- data/lib/blazer/data_source.rb +195 -0
- data/lib/blazer/detect_anomalies.R +19 -0
- data/lib/blazer/engine.rb +30 -0
- data/lib/blazer/result.rb +170 -0
- data/lib/blazer/run_statement.rb +40 -0
- data/lib/blazer/run_statement_job.rb +21 -0
- data/lib/blazer/version.rb +3 -0
- data/lib/generators/blazer/install_generator.rb +39 -0
- data/lib/generators/blazer/templates/config.yml.tt +62 -0
- data/lib/generators/blazer/templates/install.rb.tt +46 -0
- data/lib/tasks/blazer.rake +11 -0
- data/railsblazer-0.0.1.gem +0 -0
- metadata +234 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class BigQueryAdapter < BaseAdapter
|
|
4
|
+
def run_statement(statement, comment)
|
|
5
|
+
columns = []
|
|
6
|
+
rows = []
|
|
7
|
+
error = nil
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
results = bigquery.query(statement)
|
|
11
|
+
|
|
12
|
+
# complete? was removed in google-cloud-bigquery 0.29.0
|
|
13
|
+
# code is for backward compatibility
|
|
14
|
+
if !results.respond_to?(:complete?) || results.complete?
|
|
15
|
+
columns = results.first.keys.map(&:to_s) if results.size > 0
|
|
16
|
+
rows = results.map(&:values)
|
|
17
|
+
else
|
|
18
|
+
error = Blazer::TIMEOUT_MESSAGE
|
|
19
|
+
end
|
|
20
|
+
rescue => e
|
|
21
|
+
error = e.message
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
[columns, rows, error]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def tables
|
|
28
|
+
table_refs.map { |t| "#{t.project_id}.#{t.dataset_id}.#{t.table_id}" }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def schema
|
|
32
|
+
table_refs.map do |table_ref|
|
|
33
|
+
{
|
|
34
|
+
schema: table_ref.dataset_id,
|
|
35
|
+
table: table_ref.table_id,
|
|
36
|
+
columns: table_columns(table_ref)
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def preview_statement
|
|
42
|
+
"SELECT * FROM `{table}` LIMIT 10"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def bigquery
|
|
48
|
+
@bigquery ||= begin
|
|
49
|
+
require "google/cloud/bigquery"
|
|
50
|
+
Google::Cloud::Bigquery.new(
|
|
51
|
+
project: settings["project"],
|
|
52
|
+
keyfile: settings["keyfile"]
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def table_refs
|
|
58
|
+
bigquery.datasets.map(&:tables).flat_map { |table_list| table_list.map(&:table_ref) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def table_columns(table_ref)
|
|
62
|
+
schema = bigquery.service.get_table(table_ref.dataset_id, table_ref.table_id).schema
|
|
63
|
+
return [] if schema.nil?
|
|
64
|
+
schema.fields.map { |field| {name: field.name, data_type: field.type} }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class CassandraAdapter < BaseAdapter
|
|
4
|
+
def run_statement(statement, comment)
|
|
5
|
+
columns = []
|
|
6
|
+
rows = []
|
|
7
|
+
error = nil
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
response = session.execute("#{statement} /*#{comment}*/")
|
|
11
|
+
rows = response.map { |r| r.values }
|
|
12
|
+
columns = rows.any? ? response.first.keys : []
|
|
13
|
+
rescue => e
|
|
14
|
+
error = e.message
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
[columns, rows, error]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def tables
|
|
21
|
+
session.execute("SELECT table_name FROM system_schema.tables WHERE keyspace_name = '#{keyspace}'").map { |r| r["table_name"] }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
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.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
|
+
end
|
|
28
|
+
|
|
29
|
+
def preview_statement
|
|
30
|
+
"SELECT * FROM {table} LIMIT 10"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def cluster
|
|
36
|
+
@cluster ||= begin
|
|
37
|
+
require "cassandra"
|
|
38
|
+
options = {hosts: [uri.host]}
|
|
39
|
+
options[:port] = uri.port if uri.port
|
|
40
|
+
options[:username] = uri.user if uri.user
|
|
41
|
+
options[:password] = uri.password if uri.password
|
|
42
|
+
::Cassandra.cluster(options)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def session
|
|
47
|
+
@session ||= cluster.connect(keyspace)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def uri
|
|
51
|
+
@uri ||= URI.parse(data_source.settings["url"])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def keyspace
|
|
55
|
+
@keyspace ||= uri.path[1..-1]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class DrillAdapter < BaseAdapter
|
|
4
|
+
def run_statement(statement, comment)
|
|
5
|
+
columns = []
|
|
6
|
+
rows = []
|
|
7
|
+
error = nil
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
# remove trailing semicolon
|
|
11
|
+
response = drill.query(statement.sub(/;\s*\z/, ""))
|
|
12
|
+
rows = response.map { |r| r.values }
|
|
13
|
+
columns = rows.any? ? response.first.keys : []
|
|
14
|
+
rescue => e
|
|
15
|
+
error = e.message
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
[columns, rows, error]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def drill
|
|
24
|
+
@drill ||= ::Drill.new(url: settings["url"])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class DruidAdapter < BaseAdapter
|
|
4
|
+
TIMESTAMP_REGEX = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\z/
|
|
5
|
+
|
|
6
|
+
def run_statement(statement, comment)
|
|
7
|
+
columns = []
|
|
8
|
+
rows = []
|
|
9
|
+
error = nil
|
|
10
|
+
|
|
11
|
+
header = {"Content-Type" => "application/json", "Accept" => "application/json"}
|
|
12
|
+
timeout = data_source.timeout ? data_source.timeout.to_i : 300
|
|
13
|
+
data = {
|
|
14
|
+
query: statement,
|
|
15
|
+
context: {
|
|
16
|
+
timeout: timeout * 1000
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
uri = URI.parse("#{settings["url"]}/druid/v2/sql/")
|
|
21
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
22
|
+
http.read_timeout = timeout
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
response = JSON.parse(http.post(uri.request_uri, data.to_json, header).body)
|
|
26
|
+
if response.is_a?(Hash)
|
|
27
|
+
error = response["errorMessage"] || "Unknown error: #{response.inspect}"
|
|
28
|
+
if error.include?("timed out")
|
|
29
|
+
error = Blazer::TIMEOUT_MESSAGE
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
columns = (response.first || {}).keys
|
|
33
|
+
rows = response.map { |r| r.values }
|
|
34
|
+
|
|
35
|
+
# Druid doesn't return column types
|
|
36
|
+
# and no timestamp type in JSON
|
|
37
|
+
rows.each do |row|
|
|
38
|
+
row.each_with_index do |v, i|
|
|
39
|
+
if v.is_a?(String) && TIMESTAMP_REGEX.match(v)
|
|
40
|
+
row[i] = Time.parse(v)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
rescue => e
|
|
46
|
+
error = e.message
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
[columns, rows, error]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def tables
|
|
53
|
+
result = data_source.run_statement("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA NOT IN ('INFORMATION_SCHEMA') ORDER BY TABLE_NAME")
|
|
54
|
+
result.rows.map(&:first)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def schema
|
|
58
|
+
result = data_source.run_statement("SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, ORDINAL_POSITION FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA NOT IN ('INFORMATION_SCHEMA') ORDER BY 1, 2")
|
|
59
|
+
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]} }} }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def preview_statement
|
|
63
|
+
"SELECT * FROM {table} LIMIT 10"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class ElasticsearchAdapter < BaseAdapter
|
|
4
|
+
def run_statement(statement, comment)
|
|
5
|
+
columns = []
|
|
6
|
+
rows = []
|
|
7
|
+
error = nil
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
response = client.xpack.sql.query(body: {query: "#{statement} /*#{comment}*/"})
|
|
11
|
+
columns = response["columns"].map { |v| v["name"] }
|
|
12
|
+
# Elasticsearch does not differentiate between dates and times
|
|
13
|
+
date_indexes = response["columns"].each_index.select { |i| response["columns"][i]["type"] == "date" }
|
|
14
|
+
if columns.any?
|
|
15
|
+
rows = response["rows"]
|
|
16
|
+
date_indexes.each do |i|
|
|
17
|
+
rows.each do |row|
|
|
18
|
+
row[i] = Time.parse(row[i])
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
rescue => e
|
|
23
|
+
error = e.message
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
[columns, rows, error]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def tables
|
|
30
|
+
indices = client.cat.indices(format: "json").map { |v| v["index"] }
|
|
31
|
+
aliases = client.cat.aliases(format: "json").map { |v| v["alias"] }
|
|
32
|
+
(indices + aliases).uniq.sort
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def preview_statement
|
|
36
|
+
"SELECT * FROM \"{table}\" LIMIT 10"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
def client
|
|
42
|
+
@client ||= Elasticsearch::Client.new(url: settings["url"])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class MongodbAdapter < BaseAdapter
|
|
4
|
+
def run_statement(statement, comment)
|
|
5
|
+
columns = []
|
|
6
|
+
rows = []
|
|
7
|
+
error = nil
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
documents = db.command({:$eval => "#{statement.strip}.toArray()", nolock: true}).documents.first["retval"]
|
|
11
|
+
columns = documents.flat_map { |r| r.keys }.uniq
|
|
12
|
+
rows = documents.map { |r| columns.map { |c| r[c] } }
|
|
13
|
+
rescue => e
|
|
14
|
+
error = e.message
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
[columns, rows, error]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def tables
|
|
21
|
+
db.collection_names
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def preview_statement
|
|
25
|
+
"db.{table}.find().limit(10)"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
protected
|
|
29
|
+
|
|
30
|
+
def client
|
|
31
|
+
@client ||= Mongo::Client.new(settings["url"], connect_timeout: 1, socket_timeout: 1, server_selection_timeout: 1)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def db
|
|
35
|
+
@db ||= client.database
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class PrestoAdapter < BaseAdapter
|
|
4
|
+
def run_statement(statement, comment)
|
|
5
|
+
columns = []
|
|
6
|
+
rows = []
|
|
7
|
+
error = nil
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
columns, rows = client.run("#{statement} /*#{comment}*/")
|
|
11
|
+
columns = columns.map(&:name)
|
|
12
|
+
rescue => e
|
|
13
|
+
error = e.message
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
[columns, rows, error]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def tables
|
|
20
|
+
_, rows = client.run("SHOW TABLES")
|
|
21
|
+
rows.map(&:first)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def preview_statement
|
|
25
|
+
"SELECT * FROM {table} LIMIT 10"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
protected
|
|
29
|
+
|
|
30
|
+
def client
|
|
31
|
+
@client ||= begin
|
|
32
|
+
uri = URI.parse(settings["url"])
|
|
33
|
+
query = uri.query ? CGI::parse(uri.query) : {}
|
|
34
|
+
Presto::Client.new(
|
|
35
|
+
server: "#{uri.host}:#{uri.port}",
|
|
36
|
+
catalog: uri.path.to_s.sub(/\A\//, ""),
|
|
37
|
+
schema: query["schema"] || "public",
|
|
38
|
+
user: uri.user,
|
|
39
|
+
http_debug: false
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class SnowflakeAdapter < SqlAdapter
|
|
4
|
+
def initialize(data_source)
|
|
5
|
+
@data_source = data_source
|
|
6
|
+
|
|
7
|
+
@@registered ||= begin
|
|
8
|
+
require "active_record/connection_adapters/odbc_adapter"
|
|
9
|
+
require "odbc_adapter/adapters/postgresql_odbc_adapter"
|
|
10
|
+
|
|
11
|
+
ODBCAdapter.register(/snowflake/, ODBCAdapter::Adapters::PostgreSQLODBCAdapter) do
|
|
12
|
+
# Explicitly turning off prepared statements as they are not yet working with
|
|
13
|
+
# snowflake + the ODBC ActiveRecord adapter
|
|
14
|
+
def prepared_statements
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Quoting needs to be changed for snowflake
|
|
19
|
+
def quote_column_name(name)
|
|
20
|
+
name.to_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Override dbms_type_cast to get the values encoded in UTF-8
|
|
26
|
+
def dbms_type_cast(columns, values)
|
|
27
|
+
int_column = {}
|
|
28
|
+
columns.each_with_index do |c, i|
|
|
29
|
+
int_column[i] = c.type == 3 && c.scale == 0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
float_column = {}
|
|
33
|
+
columns.each_with_index do |c, i|
|
|
34
|
+
float_column[i] = c.type == 3 && c.scale != 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
values.each do |row|
|
|
38
|
+
row.each_index do |idx|
|
|
39
|
+
val = row[idx]
|
|
40
|
+
if val
|
|
41
|
+
if int_column[idx]
|
|
42
|
+
row[idx] = val.to_i
|
|
43
|
+
elsif float_column[idx]
|
|
44
|
+
row[idx] = val.to_f
|
|
45
|
+
elsif val.is_a?(String)
|
|
46
|
+
row[idx] = val.force_encoding('UTF-8')
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@connection_model =
|
|
56
|
+
Class.new(Blazer::Connection) do
|
|
57
|
+
def self.name
|
|
58
|
+
"Blazer::Connection::SnowflakeAdapter#{object_id}"
|
|
59
|
+
end
|
|
60
|
+
if data_source.settings["conn_str"]
|
|
61
|
+
establish_connection(adapter: "odbc", conn_str: data_source.settings["conn_str"])
|
|
62
|
+
elsif data_source.settings["dsn"]
|
|
63
|
+
establish_connection(adapter: "odbc", dsn: data_source.settings["dsn"])
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def cancel(run_id)
|
|
69
|
+
# todo
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
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)
|
|
19
|
+
columns = []
|
|
20
|
+
rows = []
|
|
21
|
+
error = nil
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
in_transaction do
|
|
25
|
+
set_timeout(data_source.timeout) if data_source.timeout
|
|
26
|
+
|
|
27
|
+
result = select_all("#{statement} /*#{comment}*/")
|
|
28
|
+
columns = result.columns
|
|
29
|
+
cast_method = Rails::VERSION::MAJOR < 5 ? :type_cast : :cast_value
|
|
30
|
+
result.rows.each do |untyped_row|
|
|
31
|
+
rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] ? result.column_types[c].send(cast_method, untyped_row[i]) : untyped_row[i] })
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
rescue => e
|
|
35
|
+
error = e.message.sub(/.+ERROR: /, "")
|
|
36
|
+
error = Blazer::TIMEOUT_MESSAGE if Blazer::TIMEOUT_ERRORS.any? { |e| error.include?(e) }
|
|
37
|
+
reconnect if error.include?("PG::ConnectionBad")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
[columns, rows, error]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def tables
|
|
44
|
+
result = data_source.run_statement(connection_model.send(:sanitize_sql_array, ["SELECT table_name FROM information_schema.tables WHERE table_schema IN (?) ORDER BY table_name", schemas]), refresh_cache: true)
|
|
45
|
+
result.rows.map(&:first)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def schema
|
|
49
|
+
result = data_source.run_statement(connection_model.send(:sanitize_sql_array, ["SELECT table_schema, table_name, column_name, data_type, ordinal_position FROM information_schema.columns WHERE table_schema IN (?) ORDER BY 1, 2", schemas]))
|
|
50
|
+
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]} }} }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def preview_statement
|
|
54
|
+
if postgresql?
|
|
55
|
+
"SELECT * FROM \"{table}\" LIMIT 10"
|
|
56
|
+
elsif sqlserver?
|
|
57
|
+
"SELECT TOP (10) * FROM {table}"
|
|
58
|
+
else
|
|
59
|
+
"SELECT * FROM {table} LIMIT 10"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reconnect
|
|
64
|
+
connection_model.establish_connection(settings["url"])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def cost(statement)
|
|
68
|
+
result = explain(statement)
|
|
69
|
+
if sqlserver?
|
|
70
|
+
result["TotalSubtreeCost"]
|
|
71
|
+
else
|
|
72
|
+
match = /cost=\d+\.\d+..(\d+\.\d+) /.match(result)
|
|
73
|
+
match[1] if match
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def explain(statement)
|
|
78
|
+
if postgresql? || redshift?
|
|
79
|
+
select_all("EXPLAIN #{statement}").rows.first.first
|
|
80
|
+
elsif sqlserver?
|
|
81
|
+
begin
|
|
82
|
+
execute("SET SHOWPLAN_ALL ON")
|
|
83
|
+
result = select_all(statement).each.first
|
|
84
|
+
ensure
|
|
85
|
+
execute("SET SHOWPLAN_ALL OFF")
|
|
86
|
+
end
|
|
87
|
+
result
|
|
88
|
+
end
|
|
89
|
+
rescue
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def cancel(run_id)
|
|
94
|
+
if postgresql?
|
|
95
|
+
select_all("SELECT pg_cancel_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND query LIKE '%,run_id:#{run_id}%'")
|
|
96
|
+
elsif redshift?
|
|
97
|
+
first_row = select_all("SELECT pid FROM stv_recents WHERE status = 'Running' AND query LIKE '%,run_id:#{run_id}%'").first
|
|
98
|
+
if first_row
|
|
99
|
+
select_all("CANCEL #{first_row["pid"].to_i}")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def cachable?(statement)
|
|
105
|
+
!%w[CREATE ALTER UPDATE INSERT DELETE].include?(statement.split.first.to_s.upcase)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
protected
|
|
109
|
+
|
|
110
|
+
def select_all(statement)
|
|
111
|
+
connection_model.connection.select_all(statement)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# seperate from select_all to prevent mysql error
|
|
115
|
+
def execute(statement)
|
|
116
|
+
connection_model.connection.execute(statement)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def postgresql?
|
|
120
|
+
["PostgreSQL", "PostGIS"].include?(adapter_name)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def redshift?
|
|
124
|
+
["Redshift"].include?(adapter_name)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def mysql?
|
|
128
|
+
["MySQL", "Mysql2", "Mysql2Spatial"].include?(adapter_name)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def sqlserver?
|
|
132
|
+
["SQLServer", "tinytds", "mssql"].include?(adapter_name)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def adapter_name
|
|
136
|
+
# prevent bad data source from taking down queries/new
|
|
137
|
+
connection_model.connection.adapter_name rescue nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def schemas
|
|
141
|
+
settings["schemas"] || [connection_model.connection_config[:schema] || default_schema]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def default_schema
|
|
145
|
+
if postgresql? || redshift?
|
|
146
|
+
"public"
|
|
147
|
+
elsif sqlserver?
|
|
148
|
+
"dbo"
|
|
149
|
+
else
|
|
150
|
+
connection_model.connection_config[:database]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def set_timeout(timeout)
|
|
155
|
+
if postgresql? || redshift?
|
|
156
|
+
execute("SET #{use_transaction? ? "LOCAL " : ""}statement_timeout = #{timeout.to_i * 1000}")
|
|
157
|
+
elsif mysql?
|
|
158
|
+
execute("SET max_execution_time = #{timeout.to_i * 1000}")
|
|
159
|
+
else
|
|
160
|
+
raise Blazer::TimeoutNotSupported, "Timeout not supported for #{adapter_name} adapter"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def use_transaction?
|
|
165
|
+
settings.key?("use_transaction") ? settings["use_transaction"] : true
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def in_transaction
|
|
169
|
+
connection_model.connection_pool.with_connection do
|
|
170
|
+
if use_transaction?
|
|
171
|
+
connection_model.transaction do
|
|
172
|
+
yield
|
|
173
|
+
raise ActiveRecord::Rollback
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
yield
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|