railsblazer 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +1 -0
  3. data/.github/ISSUE_TEMPLATE.md +0 -0
  4. data/.gitignore +14 -0
  5. data/CHANGELOG.md +247 -0
  6. data/CONTRIBUTING.md +42 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +855 -0
  10. data/Rakefile +1 -0
  11. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  12. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
  13. data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
  14. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
  15. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
  16. data/app/assets/javascripts/blazer/Chart.js +14145 -0
  17. data/app/assets/javascripts/blazer/Sortable.js +1144 -0
  18. data/app/assets/javascripts/blazer/ace.js +6 -0
  19. data/app/assets/javascripts/blazer/ace/ace.js +11 -0
  20. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +5 -0
  21. data/app/assets/javascripts/blazer/ace/mode-sql.js +1 -0
  22. data/app/assets/javascripts/blazer/ace/snippets/sql.js +1 -0
  23. data/app/assets/javascripts/blazer/ace/snippets/text.js +1 -0
  24. data/app/assets/javascripts/blazer/ace/theme-twilight.js +1 -0
  25. data/app/assets/javascripts/blazer/application.js +79 -0
  26. data/app/assets/javascripts/blazer/bootstrap.js +2366 -0
  27. data/app/assets/javascripts/blazer/chartkick.js +1693 -0
  28. data/app/assets/javascripts/blazer/daterangepicker.js +1505 -0
  29. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  30. data/app/assets/javascripts/blazer/highlight.pack.js +1 -0
  31. data/app/assets/javascripts/blazer/jquery.js +10308 -0
  32. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +263 -0
  33. data/app/assets/javascripts/blazer/jquery_ujs.js +469 -0
  34. data/app/assets/javascripts/blazer/moment-timezone.js +1007 -0
  35. data/app/assets/javascripts/blazer/moment.js +3043 -0
  36. data/app/assets/javascripts/blazer/queries.js +110 -0
  37. data/app/assets/javascripts/blazer/routes.js +23 -0
  38. data/app/assets/javascripts/blazer/selectize.js +3667 -0
  39. data/app/assets/javascripts/blazer/stupidtable.js +114 -0
  40. data/app/assets/javascripts/blazer/vue.js +7515 -0
  41. data/app/assets/stylesheets/blazer/application.css +198 -0
  42. data/app/assets/stylesheets/blazer/bootstrap.css.erb +6202 -0
  43. data/app/assets/stylesheets/blazer/daterangepicker-bs3.css +375 -0
  44. data/app/assets/stylesheets/blazer/github.css +125 -0
  45. data/app/assets/stylesheets/blazer/selectize.default.css +387 -0
  46. data/app/controllers/blazer/base_controller.rb +113 -0
  47. data/app/controllers/blazer/checks_controller.rb +56 -0
  48. data/app/controllers/blazer/dashboards_controller.rb +105 -0
  49. data/app/controllers/blazer/queries_controller.rb +337 -0
  50. data/app/helpers/blazer/base_helper.rb +57 -0
  51. data/app/mailers/blazer/check_mailer.rb +27 -0
  52. data/app/mailers/blazer/slack_notifier.rb +76 -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 +13 -0
  57. data/app/models/blazer/dashboard_query.rb +9 -0
  58. data/app/models/blazer/query.rb +40 -0
  59. data/app/models/blazer/record.rb +5 -0
  60. data/app/views/blazer/_nav.html.erb +16 -0
  61. data/app/views/blazer/_variables.html.erb +102 -0
  62. data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
  63. data/app/views/blazer/check_mailer/state_change.html.erb +47 -0
  64. data/app/views/blazer/checks/_form.html.erb +79 -0
  65. data/app/views/blazer/checks/edit.html.erb +1 -0
  66. data/app/views/blazer/checks/index.html.erb +43 -0
  67. data/app/views/blazer/checks/new.html.erb +1 -0
  68. data/app/views/blazer/dashboards/_form.html.erb +76 -0
  69. data/app/views/blazer/dashboards/edit.html.erb +1 -0
  70. data/app/views/blazer/dashboards/new.html.erb +1 -0
  71. data/app/views/blazer/dashboards/show.html.erb +47 -0
  72. data/app/views/blazer/queries/_form.html.erb +240 -0
  73. data/app/views/blazer/queries/edit.html.erb +2 -0
  74. data/app/views/blazer/queries/home.html.erb +152 -0
  75. data/app/views/blazer/queries/new.html.erb +2 -0
  76. data/app/views/blazer/queries/run.html.erb +165 -0
  77. data/app/views/blazer/queries/schema.html.erb +20 -0
  78. data/app/views/blazer/queries/show.html.erb +73 -0
  79. data/app/views/layouts/blazer/application.html.erb +24 -0
  80. data/blazer-0.0.1.gem +0 -0
  81. data/blazer.gemspec +27 -0
  82. data/config/routes.rb +16 -0
  83. data/lib/blazer.rb +223 -0
  84. data/lib/blazer/adapters/athena_adapter.rb +128 -0
  85. data/lib/blazer/adapters/base_adapter.rb +53 -0
  86. data/lib/blazer/adapters/bigquery_adapter.rb +68 -0
  87. data/lib/blazer/adapters/cassandra_adapter.rb +59 -0
  88. data/lib/blazer/adapters/drill_adapter.rb +28 -0
  89. data/lib/blazer/adapters/druid_adapter.rb +67 -0
  90. data/lib/blazer/adapters/elasticsearch_adapter.rb +46 -0
  91. data/lib/blazer/adapters/mongodb_adapter.rb +39 -0
  92. data/lib/blazer/adapters/presto_adapter.rb +45 -0
  93. data/lib/blazer/adapters/snowflake_adapter.rb +73 -0
  94. data/lib/blazer/adapters/sql_adapter.rb +182 -0
  95. data/lib/blazer/data_source.rb +195 -0
  96. data/lib/blazer/detect_anomalies.R +19 -0
  97. data/lib/blazer/engine.rb +30 -0
  98. data/lib/blazer/result.rb +170 -0
  99. data/lib/blazer/run_statement.rb +40 -0
  100. data/lib/blazer/run_statement_job.rb +21 -0
  101. data/lib/blazer/version.rb +3 -0
  102. data/lib/generators/blazer/install_generator.rb +39 -0
  103. data/lib/generators/blazer/templates/config.yml.tt +62 -0
  104. data/lib/generators/blazer/templates/install.rb.tt +46 -0
  105. data/lib/tasks/blazer.rake +11 -0
  106. data/railsblazer-0.0.1.gem +0 -0
  107. 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