queries_controller.rb 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. module Blazer
  2. class QueriesController < ApplicationController
  3. # skip all filters
  4. skip_filter *_process_action_callbacks.map(&:filter)
  5. protect_from_forgery with: :exception
  6. if ENV["BLAZER_PASSWORD"]
  7. http_basic_authenticate_with name: ENV["BLAZER_USERNAME"], password: ENV["BLAZER_PASSWORD"]
  8. end
  9. layout "blazer/application"
  10. before_action :ensure_database_url
  11. before_action :set_query, only: [:show, :edit, :update, :destroy]
  12. def index
  13. @queries = Blazer::Query.order(:name)
  14. @queries = @queries.includes(:creator) if Blazer.user_class
  15. @trending_queries = Blazer::Audit.group(:query_id).where("created_at > ?", 2.days.ago).having("COUNT(DISTINCT user_id) >= 3").uniq.count(:user_id)
  16. end
  17. def new
  18. @query = Blazer::Query.new(statement: params[:statement])
  19. end
  20. def create
  21. @query = Blazer::Query.new(query_params)
  22. @query.creator = current_user if respond_to?(:current_user) && Blazer.user_class
  23. if @query.save
  24. redirect_to @query
  25. else
  26. render :new
  27. end
  28. end
  29. def show
  30. @statement = @query.statement
  31. process_vars(@statement)
  32. @smart_vars = {}
  33. @sql_errors = []
  34. @bind_vars.each do |var|
  35. query = smart_variables[var]
  36. if query
  37. rows, error = run_statement(query)
  38. @smart_vars[var] = rows.map { |v| v.values.reverse }
  39. @sql_errors << error if error
  40. end
  41. end
  42. end
  43. def edit
  44. end
  45. def run
  46. @statement = params[:statement]
  47. process_vars(@statement)
  48. if @success
  49. @query = Query.find_by(id: params[:query_id]) if params[:query_id]
  50. # audit
  51. if Blazer.audit
  52. audit = Blazer::Audit.new(statement: @statement)
  53. audit.query = @query
  54. audit.user = current_user if respond_to?(:current_user) && Blazer.user_class
  55. audit.save!
  56. end
  57. @rows, @error = run_statement(@statement)
  58. @columns = {}
  59. if @rows.any?
  60. @rows.first.each do |key, value|
  61. @columns[key] =
  62. case value
  63. when Integer
  64. "int"
  65. when Float
  66. "float"
  67. else
  68. "string-ins"
  69. end
  70. end
  71. end
  72. @filename = @query.name.parameterize if @query
  73. @min_width_types = (@rows.first || {}).select { |k, v| v.is_a?(Time) || v.is_a?(String) || smart_columns[k] }.keys
  74. @boom = {}
  75. @columns.keys.each do |key|
  76. query = smart_columns[key]
  77. if query
  78. values = @rows.map { |r| r[key] }.compact.uniq
  79. rows, error = run_statement(ActiveRecord::Base.send(:sanitize_sql_array, [query.sub("{value}", "(?)"), values]))
  80. @boom[key] = Hash[rows.map(&:values)]
  81. end
  82. end
  83. @linked_columns = linked_columns
  84. end
  85. respond_to do |format|
  86. format.html do
  87. render layout: false
  88. end
  89. format.csv do
  90. send_data csv_data(@rows), type: "text/csv; charset=utf-8; header=present", disposition: "attachment; filename=\"#{@query ? @query.name.parameterize : 'query'}.csv\""
  91. end
  92. end
  93. end
  94. def update
  95. if @query.update(query_params)
  96. redirect_to query_path(@query)
  97. else
  98. render :edit
  99. end
  100. end
  101. def destroy
  102. @query.destroy
  103. redirect_to root_url
  104. end
  105. private
  106. def ensure_database_url
  107. render text: "BLAZER_DATABASE_URL required" if !ENV["BLAZER_DATABASE_URL"] && !Rails.env.development?
  108. end
  109. def set_query
  110. @query = Blazer::Query.find(params[:id].to_s.split("-").first)
  111. end
  112. def query_params
  113. params.require(:query).permit(:name, :description, :statement)
  114. end
  115. def csv_data(rows)
  116. CSV.generate do |csv|
  117. if rows.any?
  118. csv << rows.first.keys
  119. end
  120. rows.each do |row|
  121. csv << row.values
  122. end
  123. end
  124. end
  125. def run_statement(statement)
  126. rows = []
  127. error = nil
  128. begin
  129. Blazer::Connection.transaction do
  130. Blazer::Connection.connection.execute("SET statement_timeout = #{Blazer.timeout * 1000}") if Blazer.timeout && postgresql?
  131. result = Blazer::Connection.connection.select_all(statement)
  132. result.each do |untyped_row|
  133. row = {}
  134. untyped_row.each do |k, v|
  135. row[k] = result.column_types.empty? ? v : result.column_types[k].send(:type_cast, v)
  136. end
  137. rows << row
  138. end
  139. raise ActiveRecord::Rollback
  140. end
  141. rescue ActiveRecord::StatementInvalid => e
  142. error = e.message.sub(/.+ERROR: /, "")
  143. end
  144. [rows, error]
  145. end
  146. def extract_vars(statement)
  147. statement.scan(/\{.*?\}/).map { |v| v[1...-1] }.uniq
  148. end
  149. def process_vars(statement)
  150. @bind_vars = extract_vars(statement)
  151. @success = @bind_vars.all? { |v| params[v] }
  152. if @success
  153. @bind_vars.each do |var|
  154. value = params[var].presence
  155. value = value.to_i if value.to_i.to_s == value
  156. if var.end_with?("_at")
  157. value = Blazer.time_zone.parse(value) rescue nil
  158. end
  159. statement.gsub!("{#{var}}", ActiveRecord::Base.connection.quote(value))
  160. end
  161. end
  162. end
  163. def settings
  164. YAML.load(File.read(Rails.root.join("config", "blazer.yml")))
  165. end
  166. def linked_columns
  167. settings["linked_columns"] || {}
  168. end
  169. def smart_columns
  170. settings["smart_columns"] || {}
  171. end
  172. def smart_variables
  173. settings["smart_variables"] || {}
  174. end
  175. def tables
  176. default_schema = postgresql? ? "public" : Blazer::Connection.connection_config[:database]
  177. schema = Blazer::Connection.connection_config[:schema] || default_schema
  178. rows, error = run_statement(Blazer::Connection.send(:sanitize_sql_array, ["SELECT table_name, column_name, ordinal_position, data_type FROM information_schema.columns WHERE table_schema = ?", schema]))
  179. Hash[rows.group_by { |r| r["table_name"] }.map { |t, f| [t, f.sort_by { |f| f["ordinal_position"] }.map { |f| f.slice("column_name", "data_type") }] }.sort_by { |t, _f| t }]
  180. end
  181. helper_method :tables
  182. def postgresql?
  183. Blazer::Connection.connection.adapter_name == "PostgreSQL"
  184. end
  185. end
  186. end