finery 3.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/CHANGELOG.md +426 -0
- data/CONTRIBUTING.md +49 -0
- data/LICENSE.txt +25 -0
- data/README.md +1144 -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/images/blazer/favicon.png +0 -0
- data/app/assets/javascripts/blazer/Sortable.js +3709 -0
- data/app/assets/javascripts/blazer/ace/ace.js +19630 -0
- data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1981 -0
- data/app/assets/javascripts/blazer/ace/mode-sql.js +215 -0
- data/app/assets/javascripts/blazer/ace/snippets/sql.js +16 -0
- data/app/assets/javascripts/blazer/ace/snippets/text.js +9 -0
- data/app/assets/javascripts/blazer/ace/theme-twilight.js +18 -0
- data/app/assets/javascripts/blazer/ace.js +6 -0
- data/app/assets/javascripts/blazer/application.js +87 -0
- data/app/assets/javascripts/blazer/bootstrap.js +2580 -0
- data/app/assets/javascripts/blazer/chart.umd.js +13 -0
- data/app/assets/javascripts/blazer/chartjs-adapter-date-fns.bundle.js +6322 -0
- data/app/assets/javascripts/blazer/chartjs-plugin-annotation.min.js +7 -0
- data/app/assets/javascripts/blazer/chartkick.js +2570 -0
- data/app/assets/javascripts/blazer/daterangepicker.js +1578 -0
- data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
- data/app/assets/javascripts/blazer/highlight.min.js +466 -0
- data/app/assets/javascripts/blazer/jquery.js +10872 -0
- data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +325 -0
- data/app/assets/javascripts/blazer/mapkick.bundle.js +1029 -0
- data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1548 -0
- data/app/assets/javascripts/blazer/moment.js +5685 -0
- data/app/assets/javascripts/blazer/queries.js +130 -0
- data/app/assets/javascripts/blazer/rails-ujs.js +746 -0
- data/app/assets/javascripts/blazer/routes.js +26 -0
- data/app/assets/javascripts/blazer/selectize.js +3891 -0
- data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
- data/app/assets/javascripts/blazer/stupidtable.js +281 -0
- data/app/assets/javascripts/blazer/vue.global.prod.js +1 -0
- data/app/assets/stylesheets/blazer/application.css +243 -0
- data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
- data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
- data/app/assets/stylesheets/blazer/bootstrap.css +6828 -0
- data/app/assets/stylesheets/blazer/daterangepicker.css +410 -0
- data/app/assets/stylesheets/blazer/github.css +125 -0
- data/app/assets/stylesheets/blazer/selectize.css +403 -0
- data/app/controllers/blazer/base_controller.rb +133 -0
- data/app/controllers/blazer/checks_controller.rb +56 -0
- data/app/controllers/blazer/dashboards_controller.rb +99 -0
- data/app/controllers/blazer/queries_controller.rb +468 -0
- data/app/controllers/blazer/uploads_controller.rb +147 -0
- data/app/helpers/blazer/base_helper.rb +83 -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 +17 -0
- data/app/models/blazer/dashboard_query.rb +9 -0
- data/app/models/blazer/query.rb +42 -0
- data/app/models/blazer/record.rb +5 -0
- data/app/models/blazer/upload.rb +11 -0
- data/app/models/blazer/uploads_connection.rb +7 -0
- data/app/views/blazer/_nav.html.erb +18 -0
- data/app/views/blazer/_variables.html.erb +127 -0
- data/app/views/blazer/check_mailer/failing_checks.html.erb +7 -0
- data/app/views/blazer/check_mailer/state_change.html.erb +48 -0
- data/app/views/blazer/checks/_form.html.erb +79 -0
- data/app/views/blazer/checks/edit.html.erb +3 -0
- data/app/views/blazer/checks/index.html.erb +72 -0
- data/app/views/blazer/checks/new.html.erb +3 -0
- data/app/views/blazer/dashboards/_form.html.erb +82 -0
- data/app/views/blazer/dashboards/edit.html.erb +3 -0
- data/app/views/blazer/dashboards/new.html.erb +3 -0
- data/app/views/blazer/dashboards/show.html.erb +53 -0
- data/app/views/blazer/queries/_caching.html.erb +16 -0
- data/app/views/blazer/queries/_cohorts.html.erb +48 -0
- data/app/views/blazer/queries/_form.html.erb +255 -0
- data/app/views/blazer/queries/docs.html.erb +147 -0
- data/app/views/blazer/queries/edit.html.erb +2 -0
- data/app/views/blazer/queries/home.html.erb +169 -0
- data/app/views/blazer/queries/new.html.erb +2 -0
- data/app/views/blazer/queries/run.html.erb +188 -0
- data/app/views/blazer/queries/schema.html.erb +55 -0
- data/app/views/blazer/queries/show.html.erb +72 -0
- data/app/views/blazer/uploads/_form.html.erb +27 -0
- data/app/views/blazer/uploads/edit.html.erb +3 -0
- data/app/views/blazer/uploads/index.html.erb +55 -0
- data/app/views/blazer/uploads/new.html.erb +3 -0
- data/app/views/layouts/blazer/application.html.erb +25 -0
- data/config/routes.rb +25 -0
- data/lib/blazer/adapters/athena_adapter.rb +182 -0
- data/lib/blazer/adapters/base_adapter.rb +76 -0
- data/lib/blazer/adapters/bigquery_adapter.rb +79 -0
- data/lib/blazer/adapters/cassandra_adapter.rb +70 -0
- data/lib/blazer/adapters/clickhouse_adapter.rb +84 -0
- data/lib/blazer/adapters/drill_adapter.rb +38 -0
- data/lib/blazer/adapters/druid_adapter.rb +102 -0
- data/lib/blazer/adapters/elasticsearch_adapter.rb +61 -0
- data/lib/blazer/adapters/hive_adapter.rb +55 -0
- data/lib/blazer/adapters/ignite_adapter.rb +64 -0
- data/lib/blazer/adapters/influxdb_adapter.rb +57 -0
- data/lib/blazer/adapters/neo4j_adapter.rb +62 -0
- data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
- data/lib/blazer/adapters/presto_adapter.rb +54 -0
- data/lib/blazer/adapters/salesforce_adapter.rb +50 -0
- data/lib/blazer/adapters/snowflake_adapter.rb +82 -0
- data/lib/blazer/adapters/soda_adapter.rb +105 -0
- data/lib/blazer/adapters/spark_adapter.rb +14 -0
- data/lib/blazer/adapters/sql_adapter.rb +324 -0
- data/lib/blazer/adapters.rb +18 -0
- data/lib/blazer/annotations.rb +47 -0
- data/lib/blazer/anomaly_detectors.rb +22 -0
- data/lib/blazer/check_mailer.rb +27 -0
- data/lib/blazer/data_source.rb +270 -0
- data/lib/blazer/engine.rb +42 -0
- data/lib/blazer/forecasters.rb +7 -0
- data/lib/blazer/result.rb +178 -0
- data/lib/blazer/result_cache.rb +71 -0
- data/lib/blazer/run_statement.rb +44 -0
- data/lib/blazer/run_statement_job.rb +20 -0
- data/lib/blazer/slack_notifier.rb +94 -0
- data/lib/blazer/statement.rb +77 -0
- data/lib/blazer/version.rb +3 -0
- data/lib/blazer.rb +286 -0
- data/lib/finery.rb +3 -0
- data/lib/generators/blazer/install_generator.rb +22 -0
- data/lib/generators/blazer/templates/config.yml.tt +83 -0
- data/lib/generators/blazer/templates/install.rb.tt +47 -0
- data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
- data/lib/generators/blazer/uploads_generator.rb +18 -0
- data/lib/tasks/blazer.rake +20 -0
- data/lib/tasks/finery.rake +20 -0
- data/licenses/LICENSE-ace.txt +24 -0
- data/licenses/LICENSE-bootstrap.txt +21 -0
- data/licenses/LICENSE-chart.js.txt +9 -0
- data/licenses/LICENSE-chartjs-adapter-date-fns.txt +9 -0
- data/licenses/LICENSE-chartkick.js.txt +22 -0
- data/licenses/LICENSE-date-fns.txt +21 -0
- data/licenses/LICENSE-daterangepicker.txt +21 -0
- data/licenses/LICENSE-fuzzysearch.txt +20 -0
- data/licenses/LICENSE-highlight.js.txt +29 -0
- data/licenses/LICENSE-jquery.txt +20 -0
- data/licenses/LICENSE-kurkle-color.txt +9 -0
- data/licenses/LICENSE-mapkick-bundle.txt +1029 -0
- data/licenses/LICENSE-moment-timezone.txt +20 -0
- data/licenses/LICENSE-moment.txt +22 -0
- data/licenses/LICENSE-rails-ujs.txt +20 -0
- data/licenses/LICENSE-selectize.txt +202 -0
- data/licenses/LICENSE-sortable.txt +21 -0
- data/licenses/LICENSE-stickytableheaders.txt +20 -0
- data/licenses/LICENSE-stupidtable.txt +19 -0
- data/licenses/LICENSE-vue.txt +21 -0
- metadata +250 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class BaseAdapter
|
|
4
|
+
attr_reader :data_source
|
|
5
|
+
|
|
6
|
+
def initialize(data_source)
|
|
7
|
+
@data_source = data_source
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def run_statement(statement, comment)
|
|
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
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def tables
|
|
30
|
+
[] # optional, but nice to have
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def schema
|
|
34
|
+
[] # optional, but nice to have
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def preview_statement
|
|
38
|
+
"" # also optional, but nice to have
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reconnect
|
|
42
|
+
# optional
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def cost(statement)
|
|
46
|
+
# optional
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def explain(statement)
|
|
50
|
+
# optional
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def cancel(run_id)
|
|
54
|
+
# optional
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def cachable?(statement)
|
|
58
|
+
true # optional
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def supports_cohort_analysis?
|
|
62
|
+
false # optional
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cohort_analysis_statement(statement, period:, days:)
|
|
66
|
+
# optional
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
protected
|
|
70
|
+
|
|
71
|
+
def settings
|
|
72
|
+
@data_source.settings
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class BigQueryAdapter < BaseAdapter
|
|
4
|
+
def run_statement(statement, comment, bind_params)
|
|
5
|
+
columns = []
|
|
6
|
+
rows = []
|
|
7
|
+
error = nil
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
results = bigquery.query(statement, params: bind_params)
|
|
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.all.map(&:values)
|
|
17
|
+
else
|
|
18
|
+
error = Blazer::TIMEOUT_MESSAGE
|
|
19
|
+
end
|
|
20
|
+
rescue => e
|
|
21
|
+
error = e.message
|
|
22
|
+
error = Blazer::VARIABLE_MESSAGE if error.include?("Syntax error: Unexpected \"?\"")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
[columns, rows, error]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def tables
|
|
29
|
+
table_refs.map { |t| "#{t.project_id}.#{t.dataset_id}.#{t.table_id}" }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def schema
|
|
33
|
+
table_refs.map do |table_ref|
|
|
34
|
+
{
|
|
35
|
+
schema: table_ref.dataset_id,
|
|
36
|
+
table: table_ref.table_id,
|
|
37
|
+
columns: table_columns(table_ref)
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def preview_statement
|
|
43
|
+
"SELECT * FROM `{table}` LIMIT 10"
|
|
44
|
+
end
|
|
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
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def bigquery
|
|
59
|
+
@bigquery ||= begin
|
|
60
|
+
require "google/cloud/bigquery"
|
|
61
|
+
Google::Cloud::Bigquery.new(
|
|
62
|
+
project: settings["project"],
|
|
63
|
+
keyfile: settings["keyfile"]
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def table_refs
|
|
69
|
+
bigquery.datasets.map(&:tables).flat_map { |table_list| table_list.map(&:table_ref) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def table_columns(table_ref)
|
|
73
|
+
schema = bigquery.service.get_table(table_ref.dataset_id, table_ref.table_id).schema
|
|
74
|
+
return [] if schema.nil?
|
|
75
|
+
schema.fields.map { |field| {name: field.name, data_type: field.type} }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class CassandraAdapter < BaseAdapter
|
|
4
|
+
def run_statement(statement, comment, bind_params)
|
|
5
|
+
columns = []
|
|
6
|
+
rows = []
|
|
7
|
+
error = nil
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
response = session.execute("#{statement} /*#{comment}*/", arguments: bind_params)
|
|
11
|
+
rows = response.map { |r| r.values }
|
|
12
|
+
columns = rows.any? ? response.first.keys : []
|
|
13
|
+
rescue => e
|
|
14
|
+
error = e.message
|
|
15
|
+
error = Blazer::VARIABLE_MESSAGE if error.include?("no viable alternative at input '?'")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
[columns, rows, error]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def tables
|
|
22
|
+
session.execute("SELECT table_name FROM system_schema.tables WHERE keyspace_name = #{data_source.quote(keyspace)}").map { |r| r["table_name"] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def schema
|
|
26
|
+
result = session.execute("SELECT keyspace_name, table_name, column_name, type, position FROM system_schema.columns WHERE keyspace_name = #{data_source.quote(keyspace)}")
|
|
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]} }} }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def preview_statement
|
|
31
|
+
"SELECT * FROM {table} LIMIT 10"
|
|
32
|
+
end
|
|
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
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def cluster
|
|
47
|
+
@cluster ||= begin
|
|
48
|
+
require "cassandra"
|
|
49
|
+
options = {hosts: [uri.host]}
|
|
50
|
+
options[:port] = uri.port if uri.port
|
|
51
|
+
options[:username] = uri.user if uri.user
|
|
52
|
+
options[:password] = uri.password if uri.password
|
|
53
|
+
::Cassandra.cluster(options)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def session
|
|
58
|
+
@session ||= cluster.connect(keyspace)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def uri
|
|
62
|
+
@uri ||= URI.parse(data_source.settings["url"])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def keyspace
|
|
66
|
+
@keyspace ||= uri.path[1..-1]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class ClickhouseAdapter < BaseAdapter
|
|
4
|
+
DATE_TIME_TYPES = ["DateTime", "DateTime(%s)", "DateTime64(%d, %s)"].freeze
|
|
5
|
+
|
|
6
|
+
def run_statement(statement, _comment)
|
|
7
|
+
columns = []
|
|
8
|
+
rows = []
|
|
9
|
+
error = nil
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
result = connection.select_all(statement)
|
|
13
|
+
unless result.data.blank?
|
|
14
|
+
date_time_columns = result.meta
|
|
15
|
+
.select { |column| column["type"].in?(DATE_TIME_TYPES) }
|
|
16
|
+
.map { |column| column["name"] }
|
|
17
|
+
columns = result.first.keys
|
|
18
|
+
rows = result.map { |row| convert_time_columns(row, date_time_columns).values }
|
|
19
|
+
end
|
|
20
|
+
rescue => e
|
|
21
|
+
error = e.message
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
[columns, rows, error]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def tables
|
|
28
|
+
connection.tables
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def schema
|
|
32
|
+
statement = <<-SQL
|
|
33
|
+
SELECT table, name, type
|
|
34
|
+
FROM system.columns
|
|
35
|
+
WHERE database = currentDatabase()
|
|
36
|
+
ORDER BY table, position
|
|
37
|
+
SQL
|
|
38
|
+
|
|
39
|
+
response = connection.post(query: { query: statement, default_format: "CSV" })
|
|
40
|
+
response.body
|
|
41
|
+
.group_by { |row| row[0] }
|
|
42
|
+
.transform_values { |columns| columns.map { |c| { name: c[1], data_type: c[2] } } }
|
|
43
|
+
.map { |table, columns| { schema: "public", table: table, columns: columns } }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def preview_statement
|
|
47
|
+
"SELECT * FROM {table} LIMIT 10"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def explain(statement)
|
|
51
|
+
connection.explain(statement)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
protected
|
|
55
|
+
|
|
56
|
+
def connection
|
|
57
|
+
@connection ||= ClickHouse::Connection.new(config)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def config
|
|
61
|
+
@config ||= begin
|
|
62
|
+
uri = URI.parse(settings["url"])
|
|
63
|
+
options = {
|
|
64
|
+
scheme: uri.scheme,
|
|
65
|
+
host: uri.host,
|
|
66
|
+
port: uri.port,
|
|
67
|
+
username: uri.user,
|
|
68
|
+
password: uri.password,
|
|
69
|
+
database: uri.path.sub(/\A\//, ""),
|
|
70
|
+
ssl_verify: settings.fetch("ssl_verify", false)
|
|
71
|
+
}.compact
|
|
72
|
+
ClickHouse::Config.new(**options)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def convert_time_columns(row, date_time_columns)
|
|
77
|
+
time_values = row.slice(*date_time_columns).transform_values!(&:to_time)
|
|
78
|
+
row.merge(time_values)
|
|
79
|
+
rescue NoMethodError
|
|
80
|
+
row
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
# 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
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def drill
|
|
34
|
+
@drill ||= ::Drill.new(url: settings["url"])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
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, bind_params)
|
|
7
|
+
require "json"
|
|
8
|
+
require "net/http"
|
|
9
|
+
require "uri"
|
|
10
|
+
|
|
11
|
+
columns = []
|
|
12
|
+
rows = []
|
|
13
|
+
error = nil
|
|
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
|
+
|
|
32
|
+
header = {"Content-Type" => "application/json", "Accept" => "application/json"}
|
|
33
|
+
timeout = data_source.timeout ? data_source.timeout.to_i : 300
|
|
34
|
+
data = {
|
|
35
|
+
query: statement,
|
|
36
|
+
parameters: params,
|
|
37
|
+
context: {
|
|
38
|
+
timeout: timeout * 1000
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
uri = URI.parse("#{settings["url"]}/druid/v2/sql/")
|
|
43
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
44
|
+
http.read_timeout = timeout
|
|
45
|
+
|
|
46
|
+
begin
|
|
47
|
+
response = JSON.parse(http.post(uri.request_uri, data.to_json, header).body)
|
|
48
|
+
if response.is_a?(Hash)
|
|
49
|
+
error = response["errorMessage"] || "Unknown error: #{response.inspect}"
|
|
50
|
+
if error.include?("timed out")
|
|
51
|
+
error = Blazer::TIMEOUT_MESSAGE
|
|
52
|
+
elsif error.include?("Encountered \"?\" at")
|
|
53
|
+
error = Blazer::VARIABLE_MESSAGE
|
|
54
|
+
end
|
|
55
|
+
else
|
|
56
|
+
columns = (response.first || {}).keys
|
|
57
|
+
rows = response.map { |r| r.values }
|
|
58
|
+
|
|
59
|
+
# Druid doesn't return column types
|
|
60
|
+
# and no timestamp type in JSON
|
|
61
|
+
rows.each do |row|
|
|
62
|
+
row.each_with_index do |v, i|
|
|
63
|
+
if v.is_a?(String) && TIMESTAMP_REGEX.match(v)
|
|
64
|
+
row[i] = Time.parse(v)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
rescue => e
|
|
70
|
+
error = e.message
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
[columns, rows, error]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def tables
|
|
77
|
+
result = data_source.run_statement("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA NOT IN ('INFORMATION_SCHEMA') ORDER BY TABLE_NAME")
|
|
78
|
+
result.rows.map(&:first)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def schema
|
|
82
|
+
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")
|
|
83
|
+
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]} }} }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def preview_statement
|
|
87
|
+
"SELECT * FROM {table} LIMIT 10"
|
|
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
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class ElasticsearchAdapter < BaseAdapter
|
|
4
|
+
def run_statement(statement, comment, bind_params)
|
|
5
|
+
columns = []
|
|
6
|
+
rows = []
|
|
7
|
+
error = nil
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
response = client.transport.perform_request("POST", endpoint, {}, {query: "#{statement} /*#{comment}*/", params: bind_params}).body
|
|
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| ["date", "datetime"].include?(response["columns"][i]["type"]) }
|
|
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
|
+
error = Blazer::VARIABLE_MESSAGE if error.include?("mismatched input '?'")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
[columns, rows, error]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def tables
|
|
31
|
+
indices = client.cat.indices(format: "json").map { |v| v["index"] }
|
|
32
|
+
aliases = client.cat.aliases(format: "json").map { |v| v["alias"] }
|
|
33
|
+
(indices + aliases).uniq.sort
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def preview_statement
|
|
37
|
+
"SELECT * FROM \"{table}\" LIMIT 10"
|
|
38
|
+
end
|
|
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
|
+
|
|
50
|
+
protected
|
|
51
|
+
|
|
52
|
+
def endpoint
|
|
53
|
+
@endpoint ||= client.info["version"]["number"].to_i >= 7 ? "_sql" : "_xpack/sql"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def client
|
|
57
|
+
@client ||= Elasticsearch::Client.new(url: settings["url"])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class HiveAdapter < BaseAdapter
|
|
4
|
+
def run_statement(statement, comment)
|
|
5
|
+
columns = []
|
|
6
|
+
rows = []
|
|
7
|
+
error = nil
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
result = client.execute("#{statement} /*#{comment}*/")
|
|
11
|
+
columns = result.any? ? result.first.keys : []
|
|
12
|
+
rows = result.map(&:values)
|
|
13
|
+
rescue => e
|
|
14
|
+
error = e.message
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
[columns, rows, error]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def tables
|
|
21
|
+
client.execute("SHOW TABLES").map { |r| r["tab_name"] }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def preview_statement
|
|
25
|
+
"SELECT * FROM {table} LIMIT 10"
|
|
26
|
+
end
|
|
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
|
+
|
|
38
|
+
protected
|
|
39
|
+
|
|
40
|
+
def client
|
|
41
|
+
@client ||= begin
|
|
42
|
+
uri = URI.parse(settings["url"])
|
|
43
|
+
Hexspace::Client.new(
|
|
44
|
+
host: uri.host,
|
|
45
|
+
port: uri.port,
|
|
46
|
+
username: uri.user,
|
|
47
|
+
password: uri.password,
|
|
48
|
+
database: uri.path.sub(/\A\//, ""),
|
|
49
|
+
mode: uri.scheme.to_sym
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class IgniteAdapter < BaseAdapter
|
|
4
|
+
def run_statement(statement, comment, bind_params)
|
|
5
|
+
columns = []
|
|
6
|
+
rows = []
|
|
7
|
+
error = nil
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
result = client.query("#{statement} /*#{comment}*/", bind_params, schema: default_schema, statement_type: :select, timeout: data_source.timeout)
|
|
11
|
+
columns = result.any? ? result.first.keys : []
|
|
12
|
+
rows = result.map(&:values)
|
|
13
|
+
rescue => e
|
|
14
|
+
error = e.message
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
[columns, rows, error]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def preview_statement
|
|
21
|
+
"SELECT * FROM {table} LIMIT 10"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def tables
|
|
25
|
+
sql = "SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema NOT IN ('INFORMATION_SCHEMA', 'SYS')"
|
|
26
|
+
result = data_source.run_statement(sql)
|
|
27
|
+
result.rows.reject { |row| row[1].start_with?("__") }.map do |row|
|
|
28
|
+
(row[0] == default_schema ? row[1] : "#{row[0]}.#{row[1]}").downcase
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# TODO figure out error
|
|
33
|
+
# Table `__T0` can be accessed only within Ignite query context.
|
|
34
|
+
# def schema
|
|
35
|
+
# sql = "SELECT table_schema, table_name, column_name, data_type, ordinal_position FROM information_schema.columns WHERE table_schema NOT IN ('INFORMATION_SCHEMA', 'SYS')"
|
|
36
|
+
# result = data_source.run_statement(sql)
|
|
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
|
+
# end
|
|
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
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def default_schema
|
|
53
|
+
"PUBLIC"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def client
|
|
57
|
+
@client ||= begin
|
|
58
|
+
uri = URI(settings["url"])
|
|
59
|
+
Ignite::Client.new(host: uri.host, port: uri.port, username: uri.user, password: uri.password)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class InfluxdbAdapter < BaseAdapter
|
|
4
|
+
def run_statement(statement, comment)
|
|
5
|
+
columns = []
|
|
6
|
+
rows = []
|
|
7
|
+
error = nil
|
|
8
|
+
|
|
9
|
+
begin
|
|
10
|
+
result = client.query(statement, denormalize: false).first
|
|
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
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
rescue => e
|
|
27
|
+
error = e.message
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
[columns, rows, error]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def tables
|
|
34
|
+
client.list_series
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def preview_statement
|
|
38
|
+
"SELECT * FROM {table} LIMIT 10"
|
|
39
|
+
end
|
|
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
|
+
|
|
50
|
+
protected
|
|
51
|
+
|
|
52
|
+
def client
|
|
53
|
+
@client ||= InfluxDB::Client.new(url: settings["url"])
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|