data_source.rb 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. require "digest/md5"
  2. module Blazer
  3. class DataSource
  4. attr_reader :id, :settings, :connection_model
  5. def initialize(id, settings)
  6. @id = id
  7. @settings = settings
  8. unless settings["url"] || Rails.env.development?
  9. raise Blazer::Error, "Empty url"
  10. end
  11. @connection_model =
  12. Class.new(Blazer::Connection) do
  13. def self.name
  14. "Blazer::Connection::#{object_id}"
  15. end
  16. establish_connection(settings["url"]) if settings["url"]
  17. end
  18. end
  19. def name
  20. settings["name"] || @id
  21. end
  22. def linked_columns
  23. settings["linked_columns"] || {}
  24. end
  25. def smart_columns
  26. settings["smart_columns"] || {}
  27. end
  28. def smart_variables
  29. settings["smart_variables"] || {}
  30. end
  31. def variable_defaults
  32. settings["variable_defaults"] || {}
  33. end
  34. def timeout
  35. settings["timeout"]
  36. end
  37. def cache
  38. @cache ||= begin
  39. if settings["cache"].is_a?(Hash)
  40. settings["cache"]
  41. elsif settings["cache"]
  42. {
  43. "mode" => "all",
  44. "expires_in" => settings["cache"]
  45. }
  46. end
  47. end
  48. end
  49. def cache_mode
  50. cache["mode"]
  51. end
  52. def cache_expires_in
  53. (cache["expires_in"] || 60).to_f
  54. end
  55. def cache_slow_threshold
  56. (cache["slow_threshold"] || 15).to_f
  57. end
  58. def local_time_suffix
  59. @local_time_suffix ||= Array(settings["local_time_suffix"])
  60. end
  61. def use_transaction?
  62. settings.key?("use_transaction") ? settings["use_transaction"] : true
  63. end
  64. def cost(statement)
  65. if postgresql? || redshift?
  66. begin
  67. result = connection_model.connection.select_all("EXPLAIN #{statement}")
  68. match = /cost=\d+\.\d+..(\d+\.\d+) /.match(result.rows.first.first)
  69. match[1] if match
  70. rescue ActiveRecord::StatementInvalid
  71. # do nothing
  72. end
  73. end
  74. end
  75. def run_statement(statement, options = {})
  76. columns = nil
  77. rows = nil
  78. error = nil
  79. cached_at = nil
  80. just_cached = false
  81. cache_key = self.cache_key(statement) if cache
  82. if cache && !options[:refresh_cache]
  83. value = Blazer.cache.read(cache_key)
  84. columns, rows, cached_at = Marshal.load(value) if value
  85. end
  86. unless rows
  87. comment = "blazer"
  88. if options[:user].respond_to?(:id)
  89. comment << ",user_id:#{options[:user].id}"
  90. end
  91. if options[:user].respond_to?(Blazer.user_name)
  92. # only include letters, numbers, and spaces to prevent injection
  93. comment << ",user_name:#{options[:user].send(Blazer.user_name).to_s.gsub(/[^a-zA-Z0-9 ]/, "")}"
  94. end
  95. if options[:query].respond_to?(:id)
  96. comment << ",query_id:#{options[:query].id}"
  97. end
  98. columns, rows, error, just_cached = run_statement_helper(statement, comment)
  99. end
  100. output = [columns, rows, error, cached_at]
  101. output << just_cached if options[:with_just_cached]
  102. output
  103. end
  104. def clear_cache(statement)
  105. Blazer.cache.delete(cache_key(statement))
  106. end
  107. def cache_key(statement)
  108. ["blazer", "v3", id, Digest::MD5.hexdigest(statement)].join("/")
  109. end
  110. def schemas
  111. default_schema = (postgresql? || redshift?) ? "public" : connection_model.connection_config[:database]
  112. settings["schemas"] || [connection_model.connection_config[:schema] || default_schema]
  113. end
  114. def tables
  115. columns, rows, error, cached_at = run_statement(connection_model.send(:sanitize_sql_array, ["SELECT table_name, column_name, ordinal_position, data_type FROM information_schema.columns WHERE table_schema IN (?)", schemas]))
  116. rows.map(&:first).uniq
  117. end
  118. def postgresql?
  119. ["PostgreSQL", "PostGIS"].include?(adapter_name)
  120. end
  121. def redshift?
  122. ["Redshift"].include?(adapter_name)
  123. end
  124. def mysql?
  125. ["MySQL", "Mysql2", "Mysql2Spatial"].include?(adapter_name)
  126. end
  127. def reconnect
  128. connection_model.establish_connection(settings["url"])
  129. end
  130. protected
  131. def run_statement_helper(statement, comment)
  132. columns = []
  133. rows = []
  134. error = nil
  135. start_time = Time.now
  136. in_transaction do
  137. begin
  138. if timeout
  139. if postgresql? || redshift?
  140. connection_model.connection.execute("SET statement_timeout = #{timeout.to_i * 1000}")
  141. elsif mysql?
  142. connection_model.connection.execute("SET max_execution_time = #{timeout.to_i * 1000}")
  143. else
  144. raise Blazer::TimeoutNotSupported, "Timeout not supported for #{adapter_name} adapter"
  145. end
  146. end
  147. result = connection_model.connection.select_all("#{statement} /*#{comment}*/")
  148. columns = result.columns
  149. cast_method = Rails::VERSION::MAJOR < 5 ? :type_cast : :cast_value
  150. result.rows.each do |untyped_row|
  151. rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| result.column_types[c].send(cast_method, untyped_row[i]) })
  152. end
  153. rescue ActiveRecord::StatementInvalid => e
  154. error = e.message.sub(/.+ERROR: /, "")
  155. error = Blazer::TIMEOUT_MESSAGE if Blazer::TIMEOUT_ERRORS.any? { |e| error.include?(e) }
  156. end
  157. end
  158. duration = Time.now - start_time
  159. just_cached = false
  160. if !error && (cache_mode == "all" || (cache_mode == "slow" && duration >= cache_slow_threshold))
  161. Blazer.cache.write(cache_key(statement), Marshal.dump([columns, rows, Time.now]), expires_in: cache_expires_in.to_f * 60)
  162. just_cached = true
  163. end
  164. [columns, rows, error, just_cached]
  165. end
  166. def adapter_name
  167. connection_model.connection.adapter_name
  168. end
  169. def in_transaction
  170. if use_transaction?
  171. connection_model.transaction do
  172. yield
  173. raise ActiveRecord::Rollback
  174. end
  175. else
  176. yield
  177. end
  178. end
  179. end
  180. end