blazer 2.2.7 → 2.4.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of blazer might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +119 -167
- data/app/assets/stylesheets/blazer/application.css +4 -0
- data/app/controllers/blazer/base_controller.rb +8 -0
- data/app/controllers/blazer/dashboards_controller.rb +3 -1
- data/app/controllers/blazer/queries_controller.rb +119 -3
- data/app/controllers/blazer/uploads_controller.rb +147 -0
- data/app/models/blazer/query.rb +9 -2
- data/app/models/blazer/upload.rb +11 -0
- data/app/models/blazer/uploads_connection.rb +7 -0
- data/app/views/blazer/_nav.html.erb +3 -0
- data/app/views/blazer/checks/_form.html.erb +1 -1
- data/app/views/blazer/checks/index.html.erb +3 -0
- data/app/views/blazer/dashboards/_form.html.erb +1 -1
- data/app/views/blazer/dashboards/show.html.erb +1 -1
- 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/docs.html.erb +6 -0
- data/app/views/blazer/queries/home.html.erb +3 -0
- data/app/views/blazer/queries/run.html.erb +15 -17
- data/app/views/blazer/queries/show.html.erb +1 -1
- 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/config/routes.rb +5 -0
- data/lib/blazer.rb +22 -0
- data/lib/blazer/adapters/base_adapter.rb +8 -0
- data/lib/blazer/adapters/hive_adapter.rb +45 -0
- data/lib/blazer/adapters/spark_adapter.rb +9 -0
- data/lib/blazer/adapters/sql_adapter.rb +64 -1
- data/lib/blazer/data_source.rb +1 -1
- data/lib/blazer/version.rb +1 -1
- data/lib/generators/blazer/templates/config.yml.tt +6 -0
- data/lib/generators/blazer/templates/install.rb.tt +1 -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 +9 -0
- metadata +20 -7
@@ -31,7 +31,7 @@
|
|
31
31
|
<% end %>
|
32
32
|
|
33
33
|
<script>
|
34
|
-
<%= blazer_js_var "queries", Blazer::Query.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
|
34
|
+
<%= blazer_js_var "queries", Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
|
35
35
|
<%= blazer_js_var "dashboardQueries", @queries || @dashboard.dashboard_queries.order(:position).map(&:query) %>
|
36
36
|
|
37
37
|
var app = new Vue({
|
@@ -39,7 +39,7 @@
|
|
39
39
|
</div>
|
40
40
|
</div>
|
41
41
|
<script>
|
42
|
-
<%= blazer_js_var "data", {statement: @statements[i], query_id: query.id, data_source: query.data_source, only_chart: true} %>
|
42
|
+
<%= blazer_js_var "data", {statement: @statements[i], query_id: query.id, data_source: query.data_source, only_chart: true, cohort_period: params[:cohort_period]} %>
|
43
43
|
|
44
44
|
runQuery(data, function (data) {
|
45
45
|
$("#chart-<%= i %>").html(data)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<% if @cached_at || @just_cached %>
|
2
|
+
<p class="text-muted" style="float: right;">
|
3
|
+
<% if @cached_at %>
|
4
|
+
Cached <%= time_ago_in_words(@cached_at, include_seconds: true) %> ago
|
5
|
+
<% elsif params[:query_id] %>
|
6
|
+
Cached just now
|
7
|
+
<% if @data_source.cache_mode == "slow" %>
|
8
|
+
(over <%= "%g" % @data_source.cache_slow_threshold %>s)
|
9
|
+
<% end %>
|
10
|
+
<% end %>
|
11
|
+
|
12
|
+
<% if @query && params[:query_id] %>
|
13
|
+
<%= link_to "Refresh", refresh_query_path(@query, variable_params(@query)), method: :post %>
|
14
|
+
<% end %>
|
15
|
+
</p>
|
16
|
+
<% end %>
|
@@ -0,0 +1,48 @@
|
|
1
|
+
<% unless @only_chart %>
|
2
|
+
<%= render partial: "caching" %>
|
3
|
+
<p class="text-muted" style="margin-bottom: 10px;">
|
4
|
+
<%= pluralize(@rows.size, "cohort") %>
|
5
|
+
</p>
|
6
|
+
<% end %>
|
7
|
+
<% if @rows.any? %>
|
8
|
+
<div class="results-container">
|
9
|
+
<table class="table results-table">
|
10
|
+
<thead>
|
11
|
+
<tr>
|
12
|
+
<th style="min-width: 100px;">Cohort</th>
|
13
|
+
<% 12.times do |i| %>
|
14
|
+
<th style="width: 7.5%; text-align: right;"><%= @conversion_period.titleize %> <%= i + 1 %></th>
|
15
|
+
<% end %>
|
16
|
+
</tr>
|
17
|
+
</thead>
|
18
|
+
<tbody>
|
19
|
+
<% @rows.each do |row| %>
|
20
|
+
<tr>
|
21
|
+
<td>
|
22
|
+
<%= row[0] %>
|
23
|
+
<div style="font-size: 12px; color: #999;"><%= row[1] == 1 ? "1 user" : "#{number_with_delimiter(row[1])} users" %></div>
|
24
|
+
</td>
|
25
|
+
<% 12.times do |i| %>
|
26
|
+
<td style="text-align: right;">
|
27
|
+
<% num = row[i + 2] %>
|
28
|
+
<% if num %>
|
29
|
+
<% denom = row[1] %>
|
30
|
+
<% if denom > 0 %>
|
31
|
+
<%= (100.0 * num / denom).round %>%
|
32
|
+
<% else %>
|
33
|
+
-
|
34
|
+
<% end %>
|
35
|
+
<div style="font-size: 12px; color: #999;"><%= number_with_delimiter(num) %></div>
|
36
|
+
<% else %>
|
37
|
+
-
|
38
|
+
<% end %>
|
39
|
+
</td>
|
40
|
+
<% end %>
|
41
|
+
</tr>
|
42
|
+
<% end %>
|
43
|
+
</tbody>
|
44
|
+
</table>
|
45
|
+
</div>
|
46
|
+
<% elsif @only_chart %>
|
47
|
+
<p class="text-muted">No cohorts</p>
|
48
|
+
<% end %>
|
@@ -129,3 +129,9 @@
|
|
129
129
|
</table>
|
130
130
|
|
131
131
|
<p>Use the column name <code>target</code> to draw a line for goals.</p>
|
132
|
+
|
133
|
+
<% if @data_source.supports_cohort_analysis? %>
|
134
|
+
<h2>Cohort Analysis</h2>
|
135
|
+
|
136
|
+
<p>Create a query with the comment <code>/* cohort analysis */</code>. The result should have columns named <code>user_id</code> and <code>conversion_time</code> and optionally <code>cohort_time</code>.</p>
|
137
|
+
<% end %>
|
@@ -19,6 +19,9 @@
|
|
19
19
|
</button>
|
20
20
|
<ul class="dropdown-menu">
|
21
21
|
<li><%= link_to "Checks", checks_path %></li>
|
22
|
+
<% if Blazer.uploads? %>
|
23
|
+
<li><%= link_to "Uploads", uploads_path %></li>
|
24
|
+
<% end %>
|
22
25
|
<li role="separator" class="divider"></li>
|
23
26
|
<li><%= link_to "New Dashboard", new_dashboard_path %></li>
|
24
27
|
<li><%= link_to "New Check", new_check_path %></li>
|
@@ -6,25 +6,20 @@
|
|
6
6
|
<% else %>
|
7
7
|
<div class="alert alert-info">Can’t preview queries with variables...yet!</div>
|
8
8
|
<% end %>
|
9
|
+
<% elsif @cohort_analysis %>
|
10
|
+
<% if @cohort_error %>
|
11
|
+
<div class="alert alert-info"><%= @cohort_error %></div>
|
12
|
+
<% else %>
|
13
|
+
<%= render partial: "cohorts" %>
|
14
|
+
<% end %>
|
9
15
|
<% else %>
|
10
16
|
<% unless @only_chart %>
|
11
|
-
|
12
|
-
<p class="text-muted" style="float: right;">
|
13
|
-
<% if @cached_at %>
|
14
|
-
Cached <%= time_ago_in_words(@cached_at, include_seconds: true) %> ago
|
15
|
-
<% elsif params[:query_id] %>
|
16
|
-
Cached just now
|
17
|
-
<% if @data_source.cache_mode == "slow" %>
|
18
|
-
(over <%= "%g" % @data_source.cache_slow_threshold %>s)
|
19
|
-
<% end %>
|
20
|
-
<% end %>
|
21
|
-
|
22
|
-
<% if @query && params[:query_id] %>
|
23
|
-
<%= link_to "Refresh", refresh_query_path(@query, variable_params(@query)), method: :post %>
|
24
|
-
<% end %>
|
25
|
-
</p>
|
26
|
-
<% end %>
|
17
|
+
<%= render partial: "caching" %>
|
27
18
|
<p class="text-muted" style="margin-bottom: 10px;">
|
19
|
+
<% if @row_limit && @rows.size > @row_limit %>
|
20
|
+
First
|
21
|
+
<% @rows = @rows.first(@row_limit) %>
|
22
|
+
<% end %>
|
28
23
|
<%= pluralize(@rows.size, "row") %>
|
29
24
|
|
30
25
|
<% @checks.select(&:state).each do |check| %>
|
@@ -43,6 +38,9 @@
|
|
43
38
|
<% if @forecast_error %>
|
44
39
|
<div class="alert alert-danger"><%= @forecast_error %></div>
|
45
40
|
<% end %>
|
41
|
+
<% if @cohort_error %>
|
42
|
+
<div class="alert alert-info"><%= @cohort_error %></div>
|
43
|
+
<% end %>
|
46
44
|
<% if @rows.any? %>
|
47
45
|
<% values = @rows.first %>
|
48
46
|
<% chart_id = SecureRandom.hex %>
|
@@ -147,7 +145,7 @@
|
|
147
145
|
<% elsif @columns == ["PLAN"] && @data_source.adapter == "druid" %>
|
148
146
|
<pre><code><%= @rows[0][0] %></code></pre>
|
149
147
|
<% else %>
|
150
|
-
<table class="table results-table"
|
148
|
+
<table class="table results-table">
|
151
149
|
<thead>
|
152
150
|
<tr>
|
153
151
|
<% @columns.each_with_index do |key, i| %>
|
@@ -14,7 +14,7 @@
|
|
14
14
|
<%= link_to "Fork", new_query_path(variable_params(@query).merge(fork_query_id: @query.id, data_source: @query.data_source, name: @query.name)), class: "btn btn-info" %>
|
15
15
|
|
16
16
|
<% if !@error && @success %>
|
17
|
-
<%= button_to "Download", run_queries_path(query_id: @query.id, format: "csv", forecast: params[:forecast]), params: {statement: @statement}, class: "btn btn-primary" %>
|
17
|
+
<%= button_to "Download", run_queries_path(query_id: @query.id, format: "csv", forecast: params[:forecast], cohort_period: params[:cohort_period]), params: {statement: @statement}, class: "btn btn-primary" %>
|
18
18
|
<% end %>
|
19
19
|
</div>
|
20
20
|
</div>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<%= form_for @upload, html: {class: "small-form"} do |f| %>
|
2
|
+
<% if @upload.errors.any? %>
|
3
|
+
<div class="alert alert-danger"><%= @upload.errors.full_messages.first %></div>
|
4
|
+
<% elsif !@upload.persisted? %>
|
5
|
+
<p>Create a database table from a CSV file. The table will be created in the <code><%= Blazer.settings["uploads"]["schema"] %></code> schema.</p>
|
6
|
+
<% end %>
|
7
|
+
|
8
|
+
<div class="form-group">
|
9
|
+
<%= f.label :table %>
|
10
|
+
<%= f.text_field :table, class: "form-control" %>
|
11
|
+
</div>
|
12
|
+
<div class="form-group">
|
13
|
+
<%= f.label :description %>
|
14
|
+
<%= f.text_area :description, placeholder: "Optional", style: "height: 60px;", class: "form-control" %>
|
15
|
+
</div>
|
16
|
+
<div class="form-group">
|
17
|
+
<%= f.label :file %>
|
18
|
+
<%= f.file_field :file, accept: "text/csv", style: "margin-top: 6px; margin-bottom: 21px;" %>
|
19
|
+
</div>
|
20
|
+
<p>
|
21
|
+
<% if @upload.persisted? %>
|
22
|
+
<%= link_to "Delete", upload_path(@upload), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
|
23
|
+
<% end %>
|
24
|
+
<%= f.submit "Save", class: "btn btn-success" %>
|
25
|
+
<%= link_to "Back", :back, class: "btn btn-link" %>
|
26
|
+
</p>
|
27
|
+
<% end %>
|
@@ -0,0 +1,55 @@
|
|
1
|
+
<% blazer_title "Uploads" %>
|
2
|
+
|
3
|
+
<div id="header">
|
4
|
+
<div class="pull-right" style="line-height: 34px;">
|
5
|
+
<div class="btn-group">
|
6
|
+
<%= link_to "New Upload", new_upload_path, class: "btn btn-info" %>
|
7
|
+
<button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
8
|
+
<span class="caret"></span>
|
9
|
+
<span class="sr-only">Toggle Dropdown</span>
|
10
|
+
</button>
|
11
|
+
<ul class="dropdown-menu">
|
12
|
+
<li><%= link_to "Home", root_path %></li>
|
13
|
+
<li><%= link_to "Checks", checks_path %></li>
|
14
|
+
<li role="separator" class="divider"></li>
|
15
|
+
<li><%= link_to "New Query", new_query_path %></li>
|
16
|
+
<li><%= link_to "New Dashboard", new_dashboard_path %></li>
|
17
|
+
<li><%= link_to "New Check", new_check_path %></li>
|
18
|
+
</ul>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
|
22
|
+
<input id="search" type="text" placeholder="Start typing a table or person" style="width: 300px; display: inline-block;" class="search form-control" />
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<table id="uploads" class="table">
|
26
|
+
<thead>
|
27
|
+
<tr>
|
28
|
+
<th>Table</th>
|
29
|
+
<th style="width: 60%;"></th>
|
30
|
+
<% if Blazer.user_class %>
|
31
|
+
<th style="width: 20%; text-align: right;">Mastermind</th>
|
32
|
+
<% end%>
|
33
|
+
</tr>
|
34
|
+
</thead>
|
35
|
+
<tbody>
|
36
|
+
<% @uploads.each do |upload| %>
|
37
|
+
<tr>
|
38
|
+
<td><%= link_to upload.table, edit_upload_path(upload) %></td>
|
39
|
+
<td><%= truncate(upload.description, length: 100, separator: " ") %></td>
|
40
|
+
<% if Blazer.user_class %>
|
41
|
+
<td class="creator"><%= blazer_user && upload.creator == blazer_user ? "You" : upload.creator.try(Blazer.user_name) %></td>
|
42
|
+
<% end %>
|
43
|
+
</tr>
|
44
|
+
<% end %>
|
45
|
+
</tbody>
|
46
|
+
</table>
|
47
|
+
|
48
|
+
<script>
|
49
|
+
$("#search").on("keyup", function() {
|
50
|
+
var value = $(this).val().toLowerCase()
|
51
|
+
$("#uploads tbody tr").filter( function() {
|
52
|
+
$(this).toggle($(this).text().toLowerCase().indexOf(value) > -1)
|
53
|
+
})
|
54
|
+
}).focus()
|
55
|
+
</script>
|
data/config/routes.rb
CHANGED
data/lib/blazer.rb
CHANGED
@@ -18,12 +18,14 @@ require "blazer/adapters/cassandra_adapter"
|
|
18
18
|
require "blazer/adapters/drill_adapter"
|
19
19
|
require "blazer/adapters/druid_adapter"
|
20
20
|
require "blazer/adapters/elasticsearch_adapter"
|
21
|
+
require "blazer/adapters/hive_adapter"
|
21
22
|
require "blazer/adapters/influxdb_adapter"
|
22
23
|
require "blazer/adapters/mongodb_adapter"
|
23
24
|
require "blazer/adapters/neo4j_adapter"
|
24
25
|
require "blazer/adapters/presto_adapter"
|
25
26
|
require "blazer/adapters/salesforce_adapter"
|
26
27
|
require "blazer/adapters/soda_adapter"
|
28
|
+
require "blazer/adapters/spark_adapter"
|
27
29
|
require "blazer/adapters/sql_adapter"
|
28
30
|
require "blazer/adapters/snowflake_adapter"
|
29
31
|
|
@@ -32,6 +34,7 @@ require "blazer/engine"
|
|
32
34
|
|
33
35
|
module Blazer
|
34
36
|
class Error < StandardError; end
|
37
|
+
class UploadError < Error; end
|
35
38
|
class TimeoutNotSupported < Error; end
|
36
39
|
|
37
40
|
class << self
|
@@ -206,6 +209,23 @@ module Blazer
|
|
206
209
|
slack_webhook_url.present?
|
207
210
|
end
|
208
211
|
|
212
|
+
def self.uploads?
|
213
|
+
settings.key?("uploads")
|
214
|
+
end
|
215
|
+
|
216
|
+
def self.uploads_connection
|
217
|
+
raise "Empty url for uploads" unless settings.dig("uploads", "url")
|
218
|
+
Blazer::UploadsConnection.connection
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.uploads_schema
|
222
|
+
settings.dig("uploads", "schema") || "uploads"
|
223
|
+
end
|
224
|
+
|
225
|
+
def self.uploads_table_name(name)
|
226
|
+
uploads_connection.quote_table_name("#{uploads_schema}.#{name}")
|
227
|
+
end
|
228
|
+
|
209
229
|
def self.adapters
|
210
230
|
@adapters ||= {}
|
211
231
|
end
|
@@ -221,11 +241,13 @@ Blazer.register_adapter "cassandra", Blazer::Adapters::CassandraAdapter
|
|
221
241
|
Blazer.register_adapter "drill", Blazer::Adapters::DrillAdapter
|
222
242
|
Blazer.register_adapter "druid", Blazer::Adapters::DruidAdapter
|
223
243
|
Blazer.register_adapter "elasticsearch", Blazer::Adapters::ElasticsearchAdapter
|
244
|
+
Blazer.register_adapter "hive", Blazer::Adapters::HiveAdapter
|
224
245
|
Blazer.register_adapter "influxdb", Blazer::Adapters::InfluxdbAdapter
|
225
246
|
Blazer.register_adapter "neo4j", Blazer::Adapters::Neo4jAdapter
|
226
247
|
Blazer.register_adapter "presto", Blazer::Adapters::PrestoAdapter
|
227
248
|
Blazer.register_adapter "mongodb", Blazer::Adapters::MongodbAdapter
|
228
249
|
Blazer.register_adapter "salesforce", Blazer::Adapters::SalesforceAdapter
|
229
250
|
Blazer.register_adapter "soda", Blazer::Adapters::SodaAdapter
|
251
|
+
Blazer.register_adapter "spark", Blazer::Adapters::SparkAdapter
|
230
252
|
Blazer.register_adapter "sql", Blazer::Adapters::SqlAdapter
|
231
253
|
Blazer.register_adapter "snowflake", Blazer::Adapters::SnowflakeAdapter
|
@@ -0,0 +1,45 @@
|
|
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
|
+
protected
|
29
|
+
|
30
|
+
def client
|
31
|
+
@client ||= begin
|
32
|
+
uri = URI.parse(settings["url"])
|
33
|
+
Hexspace::Client.new(
|
34
|
+
host: uri.host,
|
35
|
+
port: uri.port,
|
36
|
+
username: uri.user,
|
37
|
+
password: uri.password,
|
38
|
+
database: uri.path.sub(/\A\//, ""),
|
39
|
+
mode: uri.scheme.to_sym
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -27,7 +27,7 @@ module Blazer
|
|
27
27
|
result = select_all("#{statement} /*#{comment}*/")
|
28
28
|
columns = result.columns
|
29
29
|
result.rows.each do |untyped_row|
|
30
|
-
rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] ? result.column_types[c].send(:cast_value, untyped_row[i]) : untyped_row[i] })
|
30
|
+
rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] && result.column_types[c] ? result.column_types[c].send(:cast_value, untyped_row[i]) : untyped_row[i] })
|
31
31
|
end
|
32
32
|
end
|
33
33
|
rescue => e
|
@@ -122,6 +122,67 @@ module Blazer
|
|
122
122
|
!%w[CREATE ALTER UPDATE INSERT DELETE].include?(statement.split.first.to_s.upcase)
|
123
123
|
end
|
124
124
|
|
125
|
+
def supports_cohort_analysis?
|
126
|
+
postgresql? || mysql?
|
127
|
+
end
|
128
|
+
|
129
|
+
# TODO treat date columns as already in time zone
|
130
|
+
def cohort_analysis_statement(statement, period:, days:)
|
131
|
+
raise "Cohort analysis not supported" unless supports_cohort_analysis?
|
132
|
+
|
133
|
+
cohort_column = statement =~ /\bcohort_time\b/ ? "cohort_time" : "conversion_time"
|
134
|
+
tzname = Blazer.time_zone.tzinfo.name
|
135
|
+
|
136
|
+
if mysql?
|
137
|
+
time_sql = "CONVERT_TZ(cohorts.cohort_time, '+00:00', ?)"
|
138
|
+
case period
|
139
|
+
when "day"
|
140
|
+
date_sql = "CAST(DATE_FORMAT(#{time_sql}, '%Y-%m-%d') AS DATE)"
|
141
|
+
date_params = [tzname]
|
142
|
+
when "week"
|
143
|
+
date_sql = "CAST(DATE_FORMAT(#{time_sql} - INTERVAL ((5 + DAYOFWEEK(#{time_sql})) % 7) DAY, '%Y-%m-%d') AS DATE)"
|
144
|
+
date_params = [tzname, tzname]
|
145
|
+
else
|
146
|
+
date_sql = "CAST(DATE_FORMAT(#{time_sql}, '%Y-%m-01') AS DATE)"
|
147
|
+
date_params = [tzname]
|
148
|
+
end
|
149
|
+
bucket_sql = "CAST(CEIL(TIMESTAMPDIFF(SECOND, cohorts.cohort_time, query.conversion_time) / ?) AS INTEGER)"
|
150
|
+
else
|
151
|
+
date_sql = "date_trunc(?, cohorts.cohort_time::timestamptz AT TIME ZONE ?)::date"
|
152
|
+
date_params = [period, tzname]
|
153
|
+
bucket_sql = "CEIL(EXTRACT(EPOCH FROM query.conversion_time - cohorts.cohort_time) / ?)::int"
|
154
|
+
end
|
155
|
+
|
156
|
+
# WITH not an optimization fence in Postgres 12+
|
157
|
+
statement = <<~SQL
|
158
|
+
WITH query AS (
|
159
|
+
#{statement}
|
160
|
+
),
|
161
|
+
cohorts AS (
|
162
|
+
SELECT user_id, MIN(#{cohort_column}) AS cohort_time FROM query
|
163
|
+
WHERE user_id IS NOT NULL AND #{cohort_column} IS NOT NULL
|
164
|
+
GROUP BY 1
|
165
|
+
)
|
166
|
+
SELECT
|
167
|
+
#{date_sql} AS period,
|
168
|
+
0 AS bucket,
|
169
|
+
COUNT(DISTINCT cohorts.user_id)
|
170
|
+
FROM cohorts GROUP BY 1
|
171
|
+
UNION ALL
|
172
|
+
SELECT
|
173
|
+
#{date_sql} AS period,
|
174
|
+
#{bucket_sql} AS bucket,
|
175
|
+
COUNT(DISTINCT query.user_id)
|
176
|
+
FROM cohorts INNER JOIN query ON query.user_id = cohorts.user_id
|
177
|
+
WHERE query.conversion_time IS NOT NULL
|
178
|
+
AND query.conversion_time >= cohorts.cohort_time
|
179
|
+
#{cohort_column == "conversion_time" ? "AND query.conversion_time != cohorts.cohort_time" : ""}
|
180
|
+
GROUP BY 1, 2
|
181
|
+
SQL
|
182
|
+
params = [statement] + date_params + date_params + [days.to_i * 86400]
|
183
|
+
connection_model.send(:sanitize_sql_array, params)
|
184
|
+
end
|
185
|
+
|
125
186
|
protected
|
126
187
|
|
127
188
|
def select_all(statement, params = [])
|
@@ -165,6 +226,8 @@ module Blazer
|
|
165
226
|
"public"
|
166
227
|
elsif sqlserver?
|
167
228
|
"dbo"
|
229
|
+
elsif connection_model.respond_to?(:connection_db_config)
|
230
|
+
connection_model.connection_db_config.database
|
168
231
|
else
|
169
232
|
connection_model.connection_config[:database]
|
170
233
|
end
|