blazer 2.2.7 → 2.4.1
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.
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
| @@ -6,6 +6,8 @@ module Blazer | |
| 6 6 | 
             
                skip_after_action(*filters, raise: false)
         | 
| 7 7 | 
             
                skip_around_action(*filters, raise: false)
         | 
| 8 8 |  | 
| 9 | 
            +
                clear_helpers
         | 
| 10 | 
            +
             | 
| 9 11 | 
             
                protect_from_forgery with: :exception
         | 
| 10 12 |  | 
| 11 13 | 
             
                if ENV["BLAZER_PASSWORD"]
         | 
| @@ -65,6 +67,12 @@ module Blazer | |
| 65 67 | 
             
                    end
         | 
| 66 68 | 
             
                  end
         | 
| 67 69 |  | 
| 70 | 
            +
                  def add_cohort_analysis_vars
         | 
| 71 | 
            +
                    @bind_vars << "cohort_period" unless @bind_vars.include?("cohort_period")
         | 
| 72 | 
            +
                    @smart_vars["cohort_period"] = ["day", "week", "month"]
         | 
| 73 | 
            +
                    params[:cohort_period] ||= "week"
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
             | 
| 68 76 | 
             
                  def parse_smart_variables(var, data_source)
         | 
| 69 77 | 
             
                    smart_var_data_source =
         | 
| 70 78 | 
             
                      ([data_source] + Array(data_source.settings["inherit_smart_settings"]).map { |ds| Blazer.data_sources[ds] }).find { |ds| ds.smart_variables[var] }
         | 
| @@ -39,6 +39,8 @@ module Blazer | |
| 39 39 | 
             
                      @sql_errors << error if error
         | 
| 40 40 | 
             
                    end
         | 
| 41 41 | 
             
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  add_cohort_analysis_vars if @queries.any?(&:cohort_analysis?)
         | 
| 42 44 | 
             
                end
         | 
| 43 45 |  | 
| 44 46 | 
             
                def edit
         | 
| @@ -54,7 +56,7 @@ module Blazer | |
| 54 56 |  | 
| 55 57 | 
             
                def destroy
         | 
| 56 58 | 
             
                  @dashboard.destroy
         | 
| 57 | 
            -
                  redirect_to  | 
| 59 | 
            +
                  redirect_to root_path
         | 
| 58 60 | 
             
                end
         | 
| 59 61 |  | 
| 60 62 | 
             
                def refresh
         | 
| @@ -38,11 +38,18 @@ module Blazer | |
| 38 38 | 
             
                  if params[:fork_query_id]
         | 
| 39 39 | 
             
                    @query.statement ||= Blazer::Query.find(params[:fork_query_id]).try(:statement)
         | 
| 40 40 | 
             
                  end
         | 
| 41 | 
            +
                  if params[:upload_id]
         | 
| 42 | 
            +
                    upload = Blazer::Upload.find(params[:upload_id])
         | 
| 43 | 
            +
                    upload_settings = Blazer.settings["uploads"]
         | 
| 44 | 
            +
                    @query.data_source ||= upload_settings["data_source"]
         | 
| 45 | 
            +
                    @query.statement ||= "SELECT * FROM #{upload.table_name} LIMIT 10"
         | 
| 46 | 
            +
                  end
         | 
| 41 47 | 
             
                end
         | 
| 42 48 |  | 
| 43 49 | 
             
                def create
         | 
| 44 50 | 
             
                  @query = Blazer::Query.new(query_params)
         | 
| 45 51 | 
             
                  @query.creator = blazer_user if @query.respond_to?(:creator)
         | 
| 52 | 
            +
                  @query.status = "active" if @query.respond_to?(:status)
         | 
| 46 53 |  | 
| 47 54 | 
             
                  if @query.save
         | 
| 48 55 | 
             
                    redirect_to query_path(@query, variable_params(@query))
         | 
| @@ -64,7 +71,11 @@ module Blazer | |
| 64 71 | 
             
                    @sql_errors << error if error
         | 
| 65 72 | 
             
                  end
         | 
| 66 73 |  | 
| 74 | 
            +
                  @query.update!(status: "active") if @query.try(:status) == "archived"
         | 
| 75 | 
            +
             | 
| 67 76 | 
             
                  Blazer.transform_statement.call(data_source, @statement) if Blazer.transform_statement
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  add_cohort_analysis_vars if @query.cohort_analysis?
         | 
| 68 79 | 
             
                end
         | 
| 69 80 |  | 
| 70 81 | 
             
                def edit
         | 
| @@ -72,6 +83,8 @@ module Blazer | |
| 72 83 |  | 
| 73 84 | 
             
                def run
         | 
| 74 85 | 
             
                  @statement = params[:statement]
         | 
| 86 | 
            +
                  # before process_vars
         | 
| 87 | 
            +
                  @cohort_analysis = Query.new(statement: @statement).cohort_analysis?
         | 
| 75 88 | 
             
                  data_source = params[:data_source]
         | 
| 76 89 | 
             
                  process_vars(@statement, data_source)
         | 
| 77 90 | 
             
                  @only_chart = params[:only_chart]
         | 
| @@ -80,6 +93,8 @@ module Blazer | |
| 80 93 | 
             
                  data_source = @query.data_source if @query && @query.data_source
         | 
| 81 94 | 
             
                  @data_source = Blazer.data_sources[data_source]
         | 
| 82 95 |  | 
| 96 | 
            +
                  run_cohort_analysis if @cohort_analysis
         | 
| 97 | 
            +
             | 
| 83 98 | 
             
                  # ensure viewable
         | 
| 84 99 | 
             
                  if !(@query || Query.new(data_source: @data_source.id)).viewable?(blazer_user)
         | 
| 85 100 | 
             
                    render_forbidden
         | 
| @@ -155,6 +170,7 @@ module Blazer | |
| 155 170 | 
             
                  @statement = @query.statement.dup
         | 
| 156 171 | 
             
                  process_vars(@statement, @query.data_source)
         | 
| 157 172 | 
             
                  Blazer.transform_statement.call(data_source, @statement) if Blazer.transform_statement
         | 
| 173 | 
            +
                  @statement = cohort_analysis_statement(data_source, @statement) if @query.cohort_analysis?
         | 
| 158 174 | 
             
                  data_source.clear_cache(@statement)
         | 
| 159 175 | 
             
                  redirect_to query_path(@query, variable_params(@query))
         | 
| 160 176 | 
             
                end
         | 
| @@ -232,7 +248,6 @@ module Blazer | |
| 232 248 | 
             
                      end
         | 
| 233 249 | 
             
                    end
         | 
| 234 250 |  | 
| 235 | 
            -
                    @filename = @query.name.parameterize if @query
         | 
| 236 251 | 
             
                    @min_width_types = @columns.each_with_index.select { |c, i| @first_row[i].is_a?(Time) || @first_row[i].is_a?(String) || @data_source.smart_columns[c] }.map(&:last)
         | 
| 237 252 |  | 
| 238 253 | 
             
                    @boom = @result.boom if @result
         | 
| @@ -259,6 +274,8 @@ module Blazer | |
| 259 274 | 
             
                      end
         | 
| 260 275 | 
             
                    end
         | 
| 261 276 |  | 
| 277 | 
            +
                    render_cohort_analysis if @cohort_analysis && !@error
         | 
| 278 | 
            +
             | 
| 262 279 | 
             
                    respond_to do |format|
         | 
| 263 280 | 
             
                      format.html do
         | 
| 264 281 | 
             
                        render layout: false
         | 
| @@ -279,7 +296,7 @@ module Blazer | |
| 279 296 | 
             
                      @queries = queries_by_ids(Blazer::Audit.where(user_id: blazer_user.id).order(created_at: :desc).limit(500).pluck(:query_id).uniq)
         | 
| 280 297 | 
             
                    else
         | 
| 281 298 | 
             
                      @queries = @queries.limit(limit) if limit
         | 
| 282 | 
            -
                      @queries = @queries.order(:name)
         | 
| 299 | 
            +
                      @queries = @queries.active.order(:name)
         | 
| 283 300 | 
             
                    end
         | 
| 284 301 | 
             
                    @queries = @queries.to_a
         | 
| 285 302 |  | 
| @@ -300,7 +317,7 @@ module Blazer | |
| 300 317 | 
             
                  end
         | 
| 301 318 |  | 
| 302 319 | 
             
                  def queries_by_ids(favorite_query_ids)
         | 
| 303 | 
            -
                    queries = Blazer::Query.named.where(id: favorite_query_ids)
         | 
| 320 | 
            +
                    queries = Blazer::Query.active.named.where(id: favorite_query_ids)
         | 
| 304 321 | 
             
                    queries = queries.includes(:creator) if Blazer.user_class
         | 
| 305 322 | 
             
                    queries = queries.index_by(&:id)
         | 
| 306 323 | 
             
                    favorite_query_ids.map { |query_id| queries[query_id] }.compact
         | 
| @@ -343,5 +360,104 @@ module Blazer | |
| 343 360 | 
             
                  def blazer_run_id
         | 
| 344 361 | 
             
                    params[:run_id].to_s.gsub(/[^a-z0-9\-]/i, "")
         | 
| 345 362 | 
             
                  end
         | 
| 363 | 
            +
             | 
| 364 | 
            +
                  def run_cohort_analysis
         | 
| 365 | 
            +
                    unless @data_source.supports_cohort_analysis?
         | 
| 366 | 
            +
                      @cohort_error = "This data source does not support cohort analysis"
         | 
| 367 | 
            +
                    end
         | 
| 368 | 
            +
             | 
| 369 | 
            +
                    @show_cohort_rows = !params[:query_id] || @cohort_error
         | 
| 370 | 
            +
             | 
| 371 | 
            +
                    unless @show_cohort_rows
         | 
| 372 | 
            +
                      @statement = cohort_analysis_statement(@data_source, @statement)
         | 
| 373 | 
            +
                    end
         | 
| 374 | 
            +
                  end
         | 
| 375 | 
            +
             | 
| 376 | 
            +
                  def cohort_analysis_statement(data_source, statement)
         | 
| 377 | 
            +
                    @cohort_period = params["cohort_period"] || "week"
         | 
| 378 | 
            +
                    @cohort_period = "week" unless ["day", "week", "month"].include?(@cohort_period)
         | 
| 379 | 
            +
             | 
| 380 | 
            +
                    # for now
         | 
| 381 | 
            +
                    @conversion_period = @cohort_period
         | 
| 382 | 
            +
                    @cohort_days =
         | 
| 383 | 
            +
                      case @cohort_period
         | 
| 384 | 
            +
                      when "day"
         | 
| 385 | 
            +
                        1
         | 
| 386 | 
            +
                      when "week"
         | 
| 387 | 
            +
                        7
         | 
| 388 | 
            +
                      when "month"
         | 
| 389 | 
            +
                        30
         | 
| 390 | 
            +
                      end
         | 
| 391 | 
            +
             | 
| 392 | 
            +
                    data_source.cohort_analysis_statement(statement, period: @cohort_period, days: @cohort_days)
         | 
| 393 | 
            +
                  end
         | 
| 394 | 
            +
             | 
| 395 | 
            +
                  def render_cohort_analysis
         | 
| 396 | 
            +
                    if @show_cohort_rows
         | 
| 397 | 
            +
                      @cohort_analysis = false
         | 
| 398 | 
            +
             | 
| 399 | 
            +
                      @row_limit = 1000
         | 
| 400 | 
            +
             | 
| 401 | 
            +
                      # check results
         | 
| 402 | 
            +
                      unless @cohort_error
         | 
| 403 | 
            +
                        # check names
         | 
| 404 | 
            +
                        expected_columns = ["user_id", "conversion_time"]
         | 
| 405 | 
            +
                        missing_columns = expected_columns - @result.columns
         | 
| 406 | 
            +
                        if missing_columns.any?
         | 
| 407 | 
            +
                          @cohort_error = "Expected user_id and conversion_time columns"
         | 
| 408 | 
            +
                        end
         | 
| 409 | 
            +
             | 
| 410 | 
            +
                        # check types (user_id can be any type)
         | 
| 411 | 
            +
                        unless @cohort_error
         | 
| 412 | 
            +
                          column_types = @result.columns.zip(@result.column_types).to_h
         | 
| 413 | 
            +
             | 
| 414 | 
            +
                          if !column_types["cohort_time"].in?(["time", nil])
         | 
| 415 | 
            +
                            @cohort_error = "cohort_time must be time column"
         | 
| 416 | 
            +
                          elsif !column_types["conversion_time"].in?(["time", nil])
         | 
| 417 | 
            +
                            @cohort_error = "conversion_time must be time column"
         | 
| 418 | 
            +
                          end
         | 
| 419 | 
            +
                        end
         | 
| 420 | 
            +
                      end
         | 
| 421 | 
            +
                    else
         | 
| 422 | 
            +
                      @today = Blazer.time_zone.today
         | 
| 423 | 
            +
                      @min_cohort_date, @max_cohort_date = @result.rows.map { |r| r[0] }.minmax
         | 
| 424 | 
            +
                      @buckets = {}
         | 
| 425 | 
            +
                      @rows.each do |r|
         | 
| 426 | 
            +
                        @buckets[[r[0], r[1]]] = r[2]
         | 
| 427 | 
            +
                      end
         | 
| 428 | 
            +
             | 
| 429 | 
            +
                      @cohort_dates = []
         | 
| 430 | 
            +
                      current_date = @max_cohort_date
         | 
| 431 | 
            +
                      while current_date && current_date >= @min_cohort_date
         | 
| 432 | 
            +
                        @cohort_dates << current_date
         | 
| 433 | 
            +
                        current_date =
         | 
| 434 | 
            +
                          case @cohort_period
         | 
| 435 | 
            +
                          when "day"
         | 
| 436 | 
            +
                            current_date - 1
         | 
| 437 | 
            +
                          when "week"
         | 
| 438 | 
            +
                            current_date - 7
         | 
| 439 | 
            +
                          else
         | 
| 440 | 
            +
                            current_date.prev_month
         | 
| 441 | 
            +
                          end
         | 
| 442 | 
            +
                      end
         | 
| 443 | 
            +
             | 
| 444 | 
            +
                      num_cols = @cohort_dates.size
         | 
| 445 | 
            +
                      @columns = ["Cohort", "Users"] + num_cols.times.map { |i| "#{@conversion_period.titleize} #{i + 1}" }
         | 
| 446 | 
            +
                      rows = []
         | 
| 447 | 
            +
                      date_format = @cohort_period == "month" ? "%b %Y" : "%b %-e, %Y"
         | 
| 448 | 
            +
                      @cohort_dates.each do |date|
         | 
| 449 | 
            +
                        row = [date.strftime(date_format), @buckets[[date, 0]] || 0]
         | 
| 450 | 
            +
             | 
| 451 | 
            +
                        num_cols.times do |i|
         | 
| 452 | 
            +
                          if @today >= date + (@cohort_days * i)
         | 
| 453 | 
            +
                            row << (@buckets[[date, i + 1]] || 0)
         | 
| 454 | 
            +
                          end
         | 
| 455 | 
            +
                        end
         | 
| 456 | 
            +
             | 
| 457 | 
            +
                        rows << row
         | 
| 458 | 
            +
                      end
         | 
| 459 | 
            +
                      @rows = rows
         | 
| 460 | 
            +
                    end
         | 
| 461 | 
            +
                  end
         | 
| 346 462 | 
             
              end
         | 
| 347 463 | 
             
            end
         | 
| @@ -0,0 +1,147 @@ | |
| 1 | 
            +
            module Blazer
         | 
| 2 | 
            +
              class UploadsController < BaseController
         | 
| 3 | 
            +
                before_action :ensure_uploads
         | 
| 4 | 
            +
                before_action :set_upload, only: [:show, :edit, :update, :destroy]
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def index
         | 
| 7 | 
            +
                  @uploads = Blazer::Upload.order(:table)
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def new
         | 
| 11 | 
            +
                  @upload = Blazer::Upload.new
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def create
         | 
| 15 | 
            +
                  @upload = Blazer::Upload.new(upload_params)
         | 
| 16 | 
            +
                  # use creator_id instead of creator
         | 
| 17 | 
            +
                  # since we setup association without checking if column exists
         | 
| 18 | 
            +
                  @upload.creator = blazer_user if @upload.respond_to?(:creator_id=) && blazer_user
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  success = params.require(:upload).key?(:file)
         | 
| 21 | 
            +
                  if success
         | 
| 22 | 
            +
                    Blazer::Upload.transaction do
         | 
| 23 | 
            +
                      success = @upload.save
         | 
| 24 | 
            +
                      if success
         | 
| 25 | 
            +
                        begin
         | 
| 26 | 
            +
                          update_file(@upload)
         | 
| 27 | 
            +
                        rescue CSV::MalformedCSVError, Blazer::UploadError => e
         | 
| 28 | 
            +
                          @upload.errors.add(:base, e.message)
         | 
| 29 | 
            +
                          success = false
         | 
| 30 | 
            +
                          raise ActiveRecord::Rollback
         | 
| 31 | 
            +
                        end
         | 
| 32 | 
            +
                      end
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
                  else
         | 
| 35 | 
            +
                    @upload.errors.add(:base, "File can't be blank")
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  if success
         | 
| 39 | 
            +
                    redirect_to upload_path(@upload)
         | 
| 40 | 
            +
                  else
         | 
| 41 | 
            +
                    render_errors @upload
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def show
         | 
| 46 | 
            +
                  redirect_to new_query_path(upload_id: @upload.id)
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def edit
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def update
         | 
| 53 | 
            +
                  original_table = @upload.table
         | 
| 54 | 
            +
                  @upload.assign_attributes(upload_params)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  success = nil
         | 
| 57 | 
            +
                  Blazer::Upload.transaction do
         | 
| 58 | 
            +
                    success = @upload.save
         | 
| 59 | 
            +
                    if success
         | 
| 60 | 
            +
                      if params.require(:upload).key?(:file)
         | 
| 61 | 
            +
                        begin
         | 
| 62 | 
            +
                          update_file(@upload, drop: original_table)
         | 
| 63 | 
            +
                        rescue CSV::MalformedCSVError, Blazer::UploadError => e
         | 
| 64 | 
            +
                          @upload.errors.add(:base, e.message)
         | 
| 65 | 
            +
                          success = false
         | 
| 66 | 
            +
                          raise ActiveRecord::Rollback
         | 
| 67 | 
            +
                        end
         | 
| 68 | 
            +
                      elsif @upload.table != original_table
         | 
| 69 | 
            +
                        Blazer.uploads_connection.execute("ALTER TABLE #{Blazer.uploads_table_name(original_table)} RENAME TO #{Blazer.uploads_connection.quote_table_name(@upload.table)}")
         | 
| 70 | 
            +
                      end
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  if success
         | 
| 75 | 
            +
                    redirect_to upload_path(@upload)
         | 
| 76 | 
            +
                  else
         | 
| 77 | 
            +
                    render_errors @upload
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                def destroy
         | 
| 82 | 
            +
                  Blazer.uploads_connection.execute("DROP TABLE IF EXISTS #{@upload.table_name}")
         | 
| 83 | 
            +
                  @upload.destroy
         | 
| 84 | 
            +
                  redirect_to uploads_path
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                private
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  def update_file(upload, drop: nil)
         | 
| 90 | 
            +
                    file = params.require(:upload).fetch(:file)
         | 
| 91 | 
            +
                    raise Blazer::UploadError, "File is not a CSV" if file.content_type != "text/csv"
         | 
| 92 | 
            +
                    raise Blazer::UploadError, "File is too large (maximum is 100MB)" if file.size > 100.megabytes
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                    contents = file.read
         | 
| 95 | 
            +
                    rows = CSV.parse(contents, converters: %i[numeric date date_time])
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                    # friendly column names
         | 
| 98 | 
            +
                    columns = rows.shift.map { |v| v.to_s.encode("UTF-8").gsub("%", " pct ").parameterize.gsub("-", "_") }
         | 
| 99 | 
            +
                    duplicate_column = columns.find { |c| columns.count(c) > 1 }
         | 
| 100 | 
            +
                    raise Blazer::UploadError, "Duplicate column name: #{duplicate_column}" if duplicate_column
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    column_types =
         | 
| 103 | 
            +
                      columns.size.times.map do |i|
         | 
| 104 | 
            +
                        values = rows.map { |r| r[i] }.uniq.compact
         | 
| 105 | 
            +
                        if values.all? { |v| v.is_a?(Integer) && v >= -9223372036854775808 && v <= 9223372036854775807 }
         | 
| 106 | 
            +
                          "bigint"
         | 
| 107 | 
            +
                        elsif values.all? { |v| v.is_a?(Numeric) }
         | 
| 108 | 
            +
                          "decimal"
         | 
| 109 | 
            +
                        elsif values.all? { |v| v.is_a?(DateTime) }
         | 
| 110 | 
            +
                          "timestamptz"
         | 
| 111 | 
            +
                        elsif values.all? { |v| v.is_a?(Date) }
         | 
| 112 | 
            +
                          "date"
         | 
| 113 | 
            +
                        else
         | 
| 114 | 
            +
                          "text"
         | 
| 115 | 
            +
                        end
         | 
| 116 | 
            +
                      end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                    begin
         | 
| 119 | 
            +
                      # maybe SET LOCAL statement_timeout = '30s'
         | 
| 120 | 
            +
                      # maybe regenerate CSV in Ruby to ensure consistent parsing
         | 
| 121 | 
            +
                      Blazer.uploads_connection.transaction do
         | 
| 122 | 
            +
                        Blazer.uploads_connection.execute("DROP TABLE IF EXISTS #{Blazer.uploads_table_name(drop)}") if drop
         | 
| 123 | 
            +
                        Blazer.uploads_connection.execute("CREATE TABLE #{upload.table_name} (#{columns.map.with_index { |c, i| "#{Blazer.uploads_connection.quote_column_name(c)} #{column_types[i]}" }.join(", ")})")
         | 
| 124 | 
            +
                        Blazer.uploads_connection.raw_connection.copy_data("COPY #{upload.table_name} FROM STDIN CSV HEADER") do
         | 
| 125 | 
            +
                          Blazer.uploads_connection.raw_connection.put_copy_data(contents)
         | 
| 126 | 
            +
                        end
         | 
| 127 | 
            +
                      end
         | 
| 128 | 
            +
                    rescue ActiveRecord::StatementInvalid => e
         | 
| 129 | 
            +
                      raise Blazer::UploadError, "Table already exists" if e.message.include?("PG::DuplicateTable")
         | 
| 130 | 
            +
                      raise e
         | 
| 131 | 
            +
                    end
         | 
| 132 | 
            +
                  end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                  def upload_params
         | 
| 135 | 
            +
                    params.require(:upload).except(:file).permit(:table, :description)
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  def set_upload
         | 
| 139 | 
            +
                    @upload = Blazer::Upload.find(params[:id])
         | 
| 140 | 
            +
                  end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                  # routes aren't added, but also check here
         | 
| 143 | 
            +
                  def ensure_uploads
         | 
| 144 | 
            +
                    render plain: "Uploads not enabled" unless Blazer.uploads?
         | 
| 145 | 
            +
                  end
         | 
| 146 | 
            +
              end
         | 
| 147 | 
            +
            end
         | 
    
        data/app/models/blazer/query.rb
    CHANGED
    
    | @@ -8,7 +8,8 @@ module Blazer | |
| 8 8 |  | 
| 9 9 | 
             
                validates :statement, presence: true
         | 
| 10 10 |  | 
| 11 | 
            -
                scope : | 
| 11 | 
            +
                scope :active, -> { column_names.include?("status") ? where(status: "active") : all }
         | 
| 12 | 
            +
                scope :named, -> { where.not(name: "") }
         | 
| 12 13 |  | 
| 13 14 | 
             
                def to_param
         | 
| 14 15 | 
             
                  [id, name].compact.join("-").gsub("'", "").parameterize
         | 
| @@ -34,7 +35,13 @@ module Blazer | |
| 34 35 | 
             
                end
         | 
| 35 36 |  | 
| 36 37 | 
             
                def variables
         | 
| 37 | 
            -
                  Blazer.extract_vars(statement)
         | 
| 38 | 
            +
                  variables = Blazer.extract_vars(statement)
         | 
| 39 | 
            +
                  variables += ["cohort_period"] if cohort_analysis?
         | 
| 40 | 
            +
                  variables
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def cohort_analysis?
         | 
| 44 | 
            +
                  /\/\*\s*cohort analysis\s*\*\//i.match?(statement)
         | 
| 38 45 | 
             
                end
         | 
| 39 46 | 
             
              end
         | 
| 40 47 | 
             
            end
         | 
| @@ -0,0 +1,11 @@ | |
| 1 | 
            +
            module Blazer
         | 
| 2 | 
            +
              class Upload < Record
         | 
| 3 | 
            +
                belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                validates :table, presence: true, uniqueness: true, format: {with: /\A[a-z0-9_]+\z/, message: "can only contain lowercase letters, numbers, and underscores"}, length: {maximum: 63}
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def table_name
         | 
| 8 | 
            +
                  Blazer.uploads_table_name(table)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
            end
         | 
| @@ -6,6 +6,9 @@ | |
| 6 6 | 
             
              </button>
         | 
| 7 7 | 
             
              <ul class="dropdown-menu">
         | 
| 8 8 | 
             
                <li><%= link_to "Checks", checks_path %></li>
         | 
| 9 | 
            +
                <% if Blazer.uploads? %>
         | 
| 10 | 
            +
                  <li><%= link_to "Uploads", uploads_path %></li>
         | 
| 11 | 
            +
                <% end %>
         | 
| 9 12 | 
             
                <li role="separator" class="divider"></li>
         | 
| 10 13 | 
             
                <li><%= link_to "New Query", new_query_path %></li>
         | 
| 11 14 | 
             
                <li><%= link_to "New Dashboard", new_dashboard_path %></li>
         | 
| @@ -13,7 +13,7 @@ | |
| 13 13 | 
             
                  <%= f.select :query_id, [], {include_blank: true} %>
         | 
| 14 14 | 
             
                </div>
         | 
| 15 15 | 
             
                <script>
         | 
| 16 | 
            -
                  <%= blazer_js_var "queries", Blazer::Query.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
         | 
| 16 | 
            +
                  <%= blazer_js_var "queries", Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} } %>
         | 
| 17 17 | 
             
                  <%= blazer_js_var "items", [@check.query_id].compact %>
         | 
| 18 18 |  | 
| 19 19 | 
             
                  $("#check_query_id").selectize({options: queries, items: items, highlight: false, maxOptions: 100}).parents(".hide").removeClass("hide");
         | 
| @@ -10,6 +10,9 @@ | |
| 10 10 | 
             
                  </button>
         | 
| 11 11 | 
             
                  <ul class="dropdown-menu">
         | 
| 12 12 | 
             
                    <li><%= link_to "Home", root_path %></li>
         | 
| 13 | 
            +
                    <% if Blazer.uploads? %>
         | 
| 14 | 
            +
                      <li><%= link_to "Uploads", uploads_path %></li>
         | 
| 15 | 
            +
                    <% end %>
         | 
| 13 16 | 
             
                    <li role="separator" class="divider"></li>
         | 
| 14 17 | 
             
                    <li><%= link_to "New Query", new_query_path %></li>
         | 
| 15 18 | 
             
                    <li><%= link_to "New Dashboard", new_dashboard_path %></li>
         |