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.
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