database_recorder 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +46 -8
- data/lib/database_recorder/active_record/abstract_adapter_ext.rb +13 -0
- data/lib/database_recorder/{activerecord → active_record}/base_ext.rb +0 -0
- data/lib/database_recorder/{activerecord → active_record}/recorded_result.rb +0 -0
- data/lib/database_recorder/active_record/recorder.rb +48 -0
- data/lib/database_recorder/config.rb +29 -11
- data/lib/database_recorder/core.rb +34 -8
- data/lib/database_recorder/mysql2/client_ext.rb +19 -0
- data/lib/database_recorder/mysql2/recorded_result.rb +7 -5
- data/lib/database_recorder/mysql2/recorder.rb +56 -37
- data/lib/database_recorder/mysql2/statement_ext.rb +13 -0
- data/lib/database_recorder/pg/connection_ext.rb +49 -0
- data/lib/database_recorder/pg/recorded_result.rb +3 -3
- data/lib/database_recorder/pg/recorder.rb +42 -59
- data/lib/database_recorder/recording.rb +27 -10
- data/lib/database_recorder/rspec.rb +2 -2
- data/lib/database_recorder/storage/base.rb +13 -0
- data/lib/database_recorder/storage/file.rb +22 -21
- data/lib/database_recorder/storage/redis.rb +14 -17
- data/lib/database_recorder/version.rb +3 -1
- data/lib/database_recorder.rb +12 -12
- metadata +10 -6
- data/lib/database_recorder/activerecord/abstract_adapter_ext.rb +0 -33
- data/lib/database_recorder/activerecord/recorder.rb +0 -19
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: d0b2575a34a9e68ee59f68884a492a33c071a05b9d9999caf3baeca5c070c11d
         | 
| 4 | 
            +
              data.tar.gz: 29b78920c82393f3e9a23d334b7675d29dc96f3b77f13ff1bf33384cc3206a0d
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 0c7c56cc0c95e24b410ec5d63ac64b1944a20d7084d63ae64b0d80ea265b815435d2c3ce14eec3fb9d2f69261c567ddd59eaba24b4ca0c28f3993272fbb2e33b
         | 
| 7 | 
            +
              data.tar.gz: 78db7a6377ba564f5e039bd74f32ffe4c4f6d6c6ff5719340818738a7a4ea40caf4f2164da1cf72e1ebb3800863e1921d3c2bbf32952ff00dc1154487367fe4a
         | 
    
        data/README.md
    CHANGED
    
    | @@ -1,34 +1,66 @@ | |
| 1 1 | 
             
            # Database Recorder
         | 
| 2 | 
            +
            [](https://badge.fury.io/rb/database_recorder)
         | 
| 3 | 
            +
            [](https://github.com/blocknotes/database_recorder/actions/workflows/linters.yml)
         | 
| 4 | 
            +
            [](https://github.com/blocknotes/database_recorder/actions/workflows/specs_active_record.yml)
         | 
| 5 | 
            +
            [](https://github.com/blocknotes/database_recorder/actions/workflows/specs_mysql.yml)
         | 
| 6 | 
            +
            [](https://github.com/blocknotes/database_recorder/actions/workflows/specs_postgres.yml)
         | 
| 2 7 |  | 
| 3 | 
            -
            Record database queries for testing and development purposes | 
| 4 | 
            -
             | 
| 8 | 
            +
            Record database queries for testing and development purposes.
         | 
| 9 | 
            +
            Supports only RSpec at the moment. Store queries information on files or Redis.
         | 
| 5 10 |  | 
| 6 11 | 
             
            Main features:
         | 
| 7 12 | 
             
            - store the history of the queries of a test when it run (for monitoring);
         | 
| 8 13 | 
             
            - eventually check if the current queries match the recorded ones (to prevent regressions);
         | 
| 9 14 | 
             
            - [EXPERIMENTAL] optionally replay the recorded queries replacing the original requests.
         | 
| 10 15 |  | 
| 11 | 
            -
             | 
| 16 | 
            +
            Sample output: [test.yml](extra/sample.yml)
         | 
| 12 17 |  | 
| 13 18 | 
             
            ## Install
         | 
| 14 19 |  | 
| 15 | 
            -
            - Add to your Gemfile: `gem 'database_recorder'` (:development, :test groups recommended)
         | 
| 16 | 
            -
            -  | 
| 17 | 
            -
              + Add to the `spec_helper.rb` (or rails_helper): `DatabaseRecorder::RSpec.setup`
         | 
| 18 | 
            -
              + In RSpec examples: add `:dbr` metadata
         | 
| 19 | 
            -
              + To verify the matching with the recorded query use: `dbr: { verify_queries: true }`
         | 
| 20 | 
            +
            - Add to your Gemfile: `gem 'database_recorder', require: false` (:development, :test groups recommended)
         | 
| 21 | 
            +
            - Using RSpec, add in **rails_helper.rb**:
         | 
| 20 22 |  | 
| 21 23 | 
             
            ```rb
         | 
| 24 | 
            +
            require 'database_recorder'
         | 
| 25 | 
            +
            DatabaseRecorder::RSpec.setup
         | 
| 26 | 
            +
            ```
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            - In the tests add `:dbr` metadata, examples:
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            ```rb
         | 
| 31 | 
            +
              # Activate DatabaseRecorder with the default options
         | 
| 22 32 | 
             
              it 'returns 3 posts', :dbr do
         | 
| 23 33 | 
             
                # ...
         | 
| 24 34 | 
             
              end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              # Verify queries comparing with the stored ones:
         | 
| 37 | 
            +
              it 'returns more posts', dbr: { verify_queries: true } do
         | 
| 38 | 
            +
                # ...
         | 
| 39 | 
            +
              end
         | 
| 25 40 | 
             
            ```
         | 
| 26 41 |  | 
| 42 | 
            +
            Or eventually apply the metadata per path:
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            ```rb
         | 
| 45 | 
            +
            RSpec.configure do |config|
         | 
| 46 | 
            +
              config.define_derived_metadata(file_path: %r{/spec/models/}) do |metadata|
         | 
| 47 | 
            +
                metadata[:dbr] = true
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
| 50 | 
            +
            ```
         | 
| 51 | 
            +
             | 
| 52 | 
            +
            Using an environment variable to enable it:
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            
         | 
| 55 | 
            +
             | 
| 27 56 | 
             
            ## Config
         | 
| 28 57 |  | 
| 29 58 | 
             
            Add to your _spec_helper.rb_:
         | 
| 30 59 |  | 
| 31 60 | 
             
            ```rb
         | 
| 61 | 
            +
            # Database driver to use: :active_record | :mysql2 | :pg
         | 
| 62 | 
            +
            DatabaseRecorder::Config.db_driver = :pg
         | 
| 63 | 
            +
             | 
| 32 64 | 
             
            # To print the queries while executing the specs: false | true | :color
         | 
| 33 65 | 
             
            DatabaseRecorder::Config.print_queries = true
         | 
| 34 66 |  | 
| @@ -38,6 +70,12 @@ DatabaseRecorder::Config.replay_recordings = true | |
| 38 70 | 
             
            # To store the queries: :file | :redis | nil
         | 
| 39 71 | 
             
            DatabaseRecorder::Config.storage = :redis
         | 
| 40 72 | 
             
            # nil to avoid storing the queries
         | 
| 73 | 
            +
             | 
| 74 | 
            +
            # File storage options
         | 
| 75 | 
            +
            DatabaseRecorder::Config.storage_options = { recordings_path: '/some/path' }
         | 
| 76 | 
            +
             | 
| 77 | 
            +
            # Redis storage options
         | 
| 78 | 
            +
            DatabaseRecorder::Config.storage_options = { connection: Redis.new }
         | 
| 41 79 | 
             
            ```
         | 
| 42 80 |  | 
| 43 81 | 
             
            ## History of the queries
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module DatabaseRecorder
         | 
| 4 | 
            +
              module ActiveRecord
         | 
| 5 | 
            +
                module Recorder
         | 
| 6 | 
            +
                  module_function
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def ignore_query?(sql, name)
         | 
| 9 | 
            +
                    !Recording.started? ||
         | 
| 10 | 
            +
                      %w[schema transaction].include?(name&.downcase) ||
         | 
| 11 | 
            +
                      sql.downcase.match?(/\A(begin|commit|release|rollback|savepoint)/i)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def record(adapter, sql, name = 'SQL', binds = [], type_casted_binds = [], *args)
         | 
| 15 | 
            +
                    return yield if ignore_query?(sql, name)
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    Core.log_query(sql, name)
         | 
| 18 | 
            +
                    if Config.replay_recordings && Recording.from_cache
         | 
| 19 | 
            +
                      Recording.push(sql: sql, binds: binds)
         | 
| 20 | 
            +
                      data = Recording.cached_query_for(sql)
         | 
| 21 | 
            +
                      return yield if !data || !data[:result] # cache miss
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                      RecordedResult.new(data[:result][:fields], data[:result][:values])
         | 
| 24 | 
            +
                    else
         | 
| 25 | 
            +
                      yield.tap do |result|
         | 
| 26 | 
            +
                        result_data =
         | 
| 27 | 
            +
                          if result && (result.respond_to?(:fields) || result.respond_to?(:columns))
         | 
| 28 | 
            +
                            fields = result.respond_to?(:fields) ? result.fields : result.columns
         | 
| 29 | 
            +
                            values = result.respond_to?(:values) ? result.values : result.to_a
         | 
| 30 | 
            +
                            { count: result.count, fields: fields, values: values }
         | 
| 31 | 
            +
                          end
         | 
| 32 | 
            +
                        Recording.push(sql: sql, name: name, binds: type_casted_binds, result: result_data)
         | 
| 33 | 
            +
                      end
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def setup
         | 
| 38 | 
            +
                    ::ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
         | 
| 39 | 
            +
                      prepend AbstractAdapterExt
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    # ::ActiveRecord::Base.class_eval do
         | 
| 43 | 
            +
                    #   prepend BaseExt
         | 
| 44 | 
            +
                    # end
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
            end
         | 
| @@ -7,28 +7,46 @@ module DatabaseRecorder | |
| 7 7 | 
             
              class Config
         | 
| 8 8 | 
             
                include Singleton
         | 
| 9 9 |  | 
| 10 | 
            -
                 | 
| 10 | 
            +
                DEFAULT_DB_DRIVER = :active_record
         | 
| 11 | 
            +
                DEFAULT_STORAGE = DatabaseRecorder::Storage::File
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                DB_DRIVER_VALUES = %i[active_record mysql2 pg].freeze
         | 
| 14 | 
            +
                PRINT_QUERIES_VALUES = [false, true, :color].freeze
         | 
| 15 | 
            +
                STORAGE_VALUES = {
         | 
| 16 | 
            +
                  file: DatabaseRecorder::Storage::File,
         | 
| 17 | 
            +
                  redis: DatabaseRecorder::Storage::Redis
         | 
| 18 | 
            +
                }.freeze
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                attr_accessor :db_driver, :print_queries, :replay_recordings, :storage, :storage_options
         | 
| 11 21 |  | 
| 12 22 | 
             
                class << self
         | 
| 13 23 | 
             
                  extend Forwardable
         | 
| 14 24 |  | 
| 15 | 
            -
                  def_delegators :instance, :db_driver, : | 
| 16 | 
            -
                                 : | 
| 25 | 
            +
                  def_delegators :instance, :db_driver, :print_queries, :replay_recordings, :replay_recordings=, :storage,
         | 
| 26 | 
            +
                                 :storage_options, :storage_options=
         | 
| 17 27 |  | 
| 18 28 | 
             
                  def load_defaults
         | 
| 19 | 
            -
                    instance.db_driver =  | 
| 20 | 
            -
                    instance.print_queries = false | 
| 29 | 
            +
                    instance.db_driver = DEFAULT_DB_DRIVER
         | 
| 30 | 
            +
                    instance.print_queries = false
         | 
| 21 31 | 
             
                    instance.replay_recordings = false
         | 
| 22 | 
            -
                     | 
| 32 | 
            +
                    instance.storage = DEFAULT_STORAGE
         | 
| 33 | 
            +
                    instance.storage_options = {}
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def db_driver=(value)
         | 
| 37 | 
            +
                    instance.db_driver = DB_DRIVER_VALUES.include?(value) ? value : DEFAULT_DB_DRIVER
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  def print_queries=(value)
         | 
| 41 | 
            +
                    instance.print_queries = PRINT_QUERIES_VALUES.include?(value) ? value : false
         | 
| 23 42 | 
             
                  end
         | 
| 24 43 |  | 
| 25 44 | 
             
                  def storage=(value)
         | 
| 26 45 | 
             
                    instance.storage =
         | 
| 27 | 
            -
                       | 
| 28 | 
            -
             | 
| 29 | 
            -
                       | 
| 30 | 
            -
             | 
| 31 | 
            -
                      else raise ArgumentError, "Unknown storage: #{value}"
         | 
| 46 | 
            +
                      if value.is_a?(Class) && value < Storage::Base
         | 
| 47 | 
            +
                        value
         | 
| 48 | 
            +
                      else
         | 
| 49 | 
            +
                        STORAGE_VALUES[value]
         | 
| 32 50 | 
             
                      end
         | 
| 33 51 | 
             
                  end
         | 
| 34 52 | 
             
                end
         | 
| @@ -5,19 +5,45 @@ module DatabaseRecorder | |
| 5 5 | 
             
                module_function
         | 
| 6 6 |  | 
| 7 7 | 
             
                def log_query(sql, source = nil)
         | 
| 8 | 
            -
                   | 
| 9 | 
            -
             | 
| 10 | 
            -
                     | 
| 11 | 
            -
             | 
| 12 | 
            -
                     | 
| 13 | 
            -
             | 
| 8 | 
            +
                  log =
         | 
| 9 | 
            +
                    case DatabaseRecorder::Config.print_queries
         | 
| 10 | 
            +
                    when true then "[DB] #{sql} [#{source}]"
         | 
| 11 | 
            +
                    when :color then "[DB] #{CodeRay.scan(sql, :sql).term} [#{source}]"
         | 
| 12 | 
            +
                    end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  puts log if log
         | 
| 15 | 
            +
                  log
         | 
| 14 16 | 
             
                end
         | 
| 15 17 |  | 
| 16 18 | 
             
                def setup
         | 
| 17 19 | 
             
                  case DatabaseRecorder::Config.db_driver
         | 
| 18 20 | 
             
                  when :active_record then ActiveRecord::Recorder.setup
         | 
| 19 | 
            -
                  when :mysql2 then Mysql2.setup
         | 
| 20 | 
            -
                  when :pg then PG.setup
         | 
| 21 | 
            +
                  when :mysql2 then Mysql2::Recorder.setup
         | 
| 22 | 
            +
                  when :pg then PG::Recorder.setup
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def string_keys_recursive(hash)
         | 
| 27 | 
            +
                  {}.tap do |h|
         | 
| 28 | 
            +
                    hash.each do |key, value|
         | 
| 29 | 
            +
                      h[key.to_s] = transform(value, :string_keys_recursive)
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def symbolize_recursive(hash)
         | 
| 35 | 
            +
                  {}.tap do |h|
         | 
| 36 | 
            +
                    hash.each do |key, value|
         | 
| 37 | 
            +
                      h[key.to_sym] = transform(value, :symbolize_recursive)
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def transform(value, source_method)
         | 
| 43 | 
            +
                  case value
         | 
| 44 | 
            +
                  when Hash then method(source_method).call(value)
         | 
| 45 | 
            +
                  when Array then value.map { |v| transform(v, source_method) }
         | 
| 46 | 
            +
                  else value
         | 
| 21 47 | 
             
                  end
         | 
| 22 48 | 
             
                end
         | 
| 23 49 | 
             
              end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module DatabaseRecorder
         | 
| 4 | 
            +
              module Mysql2
         | 
| 5 | 
            +
                module ClientExt
         | 
| 6 | 
            +
                  def query(sql, options = {})
         | 
| 7 | 
            +
                    Recorder.store_query(self, sql: sql, source: :query) do
         | 
| 8 | 
            +
                      super
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def prepare(*args)
         | 
| 13 | 
            +
                    Recorder.prepare_statement(self, sql: args[0], source: :prepare) do
         | 
| 14 | 
            +
                      super
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -4,7 +4,9 @@ | |
| 4 4 |  | 
| 5 5 | 
             
            module DatabaseRecorder
         | 
| 6 6 | 
             
              module Mysql2
         | 
| 7 | 
            -
                class RecordedResult | 
| 7 | 
            +
                class RecordedResult
         | 
| 8 | 
            +
                  # < ::Mysql2::Result
         | 
| 9 | 
            +
             | 
| 8 10 | 
             
                  # include Enumerable
         | 
| 9 11 | 
             
                  # extend Forwardable
         | 
| 10 12 |  | 
| @@ -15,10 +17,10 @@ module DatabaseRecorder | |
| 15 17 | 
             
                  alias :size :count
         | 
| 16 18 |  | 
| 17 19 | 
             
                  def prepare(data)
         | 
| 18 | 
            -
                    @count = data[ | 
| 19 | 
            -
                    @fields = data[ | 
| 20 | 
            -
                    @entries = data[ | 
| 21 | 
            -
                    # @values = data[ | 
| 20 | 
            +
                    @count = data[:count]
         | 
| 21 | 
            +
                    @fields = data[:fields]
         | 
| 22 | 
            +
                    @entries = data[:values]
         | 
| 23 | 
            +
                    # @values = data[:values]
         | 
| 22 24 | 
             
                  end
         | 
| 23 25 |  | 
| 24 26 | 
             
                  # def server_flags
         | 
| @@ -3,50 +3,69 @@ | |
| 3 3 | 
             
            module DatabaseRecorder
         | 
| 4 4 | 
             
              module Mysql2
         | 
| 5 5 | 
             
                module Recorder
         | 
| 6 | 
            -
                   | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
                     | 
| 6 | 
            +
                  module_function
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def ignore_query?(sql)
         | 
| 9 | 
            +
                    !Recording.started? ||
         | 
| 10 | 
            +
                      sql == 'SELECT LAST_INSERT_ID() AS _dbr_last_insert_id' ||
         | 
| 11 | 
            +
                      sql.downcase.match?(/\A(begin|commit|release|rollback|savepoint|show full fields from)/i) ||
         | 
| 12 | 
            +
                      sql.match?(/information_schema.statistics/)
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def format_result(result)
         | 
| 16 | 
            +
                    { count: result.count, fields: result.fields, values: result.to_a } if result.is_a?(::Mysql2::Result)
         | 
| 17 | 
            +
                    # else
         | 
| 18 | 
            +
                    #   last_insert_id = adapter.query('SELECT LAST_INSERT_ID() AS _dbr_last_insert_id').to_a
         | 
| 19 | 
            +
                    #   { 'count' => last_insert_id.count, 'fields' => ['id'], 'values' => last_insert_id }
         | 
| 10 20 | 
             
                  end
         | 
| 11 | 
            -
                end
         | 
| 12 21 |  | 
| 13 | 
            -
             | 
| 22 | 
            +
                  def prepare_statement(adapter, sql: nil, name: nil, binds: nil, source: nil)
         | 
| 23 | 
            +
                    @last_prepared = Recording.push_prepared(name: name, sql: sql, binds: binds, source: source)
         | 
| 24 | 
            +
                    yield if !Config.replay_recordings || Recording.cache.nil?
         | 
| 25 | 
            +
                  end
         | 
| 14 26 |  | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
                     | 
| 19 | 
            -
                    sql.match?(/information_schema.statistics/)
         | 
| 20 | 
            -
                end
         | 
| 27 | 
            +
                  def setup
         | 
| 28 | 
            +
                    ::Mysql2::Client.class_eval do
         | 
| 29 | 
            +
                      prepend ClientExt
         | 
| 30 | 
            +
                    end
         | 
| 21 31 |  | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
                  Core.log_query(sql)
         | 
| 26 | 
            -
                  if Config.replay_recordings && !Recording.cache.nil?
         | 
| 27 | 
            -
                    Recording.push(sql: sql)
         | 
| 28 | 
            -
                    data = Recording.cached_query_for(sql)
         | 
| 29 | 
            -
                    return yield unless data # cache miss
         | 
| 30 | 
            -
             | 
| 31 | 
            -
                    RecordedResult.new.prepare(data['result'].slice('count', 'fields', 'values')) if data['result']
         | 
| 32 | 
            -
                  else
         | 
| 33 | 
            -
                    yield.tap do |result|
         | 
| 34 | 
            -
                      result_data =
         | 
| 35 | 
            -
                        if result
         | 
| 36 | 
            -
                          { 'count' => result.count, 'fields' => result.fields, 'values' => result.to_a }
         | 
| 37 | 
            -
                        else
         | 
| 38 | 
            -
                          last_insert_id = recorder.query('SELECT LAST_INSERT_ID() AS _dbr_last_insert_id').to_a
         | 
| 39 | 
            -
                          { 'count' => last_insert_id.count, 'fields' => ['id'], 'values' => last_insert_id }
         | 
| 40 | 
            -
                        end
         | 
| 41 | 
            -
             | 
| 42 | 
            -
                      Recording.push(sql: sql, result: result_data)
         | 
| 32 | 
            +
                    ::Mysql2::Statement.class_eval do
         | 
| 33 | 
            +
                      prepend StatementExt
         | 
| 43 34 | 
             
                    end
         | 
| 44 35 | 
             
                  end
         | 
| 45 | 
            -
                end
         | 
| 46 36 |  | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
                     | 
| 37 | 
            +
                  def store_prepared_statement(adapter, source:, binds:)
         | 
| 38 | 
            +
                    # sql = @last_prepared&.send(:[], 'sql')
         | 
| 39 | 
            +
                    sql = @last_prepared[:sql]
         | 
| 40 | 
            +
                    Core.log_query(sql, source)
         | 
| 41 | 
            +
                    if Config.replay_recordings && !Recording.cache.nil?
         | 
| 42 | 
            +
                      data = Recording.cache.find { |query| query[:sql] == sql }
         | 
| 43 | 
            +
                      return yield unless data # cache miss
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                      Recording.push(sql: data[:sql], binds: data[:binds], source: source)
         | 
| 46 | 
            +
                      RecordedResult.new(data[:result].slice(:count, :fields, :values))
         | 
| 47 | 
            +
                    else
         | 
| 48 | 
            +
                      yield.tap do |result|
         | 
| 49 | 
            +
                        Recording.update_prepared(sql: sql, binds: binds, result: format_result(result), source: source)
         | 
| 50 | 
            +
                      end
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  def store_query(adapter, sql:, source:)
         | 
| 55 | 
            +
                    return yield if ignore_query?(sql)
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                    Core.log_query(sql, source)
         | 
| 58 | 
            +
                    if Config.replay_recordings && !Recording.cache.nil?
         | 
| 59 | 
            +
                      Recording.push(sql: sql, source: source)
         | 
| 60 | 
            +
                      data = Recording.cached_query_for(sql)
         | 
| 61 | 
            +
                      return yield unless data # cache miss
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                      RecordedResult.new.prepare(data[:result].slice(:count, :fields, :values)) if data[:result]
         | 
| 64 | 
            +
                    else
         | 
| 65 | 
            +
                      yield.tap do |result|
         | 
| 66 | 
            +
                        Recording.push(sql: sql, result: format_result(result), source: source)
         | 
| 67 | 
            +
                      end
         | 
| 68 | 
            +
                    end
         | 
| 50 69 | 
             
                  end
         | 
| 51 70 | 
             
                end
         | 
| 52 71 | 
             
              end
         | 
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module DatabaseRecorder
         | 
| 4 | 
            +
              module PG
         | 
| 5 | 
            +
                module ConnectionExt
         | 
| 6 | 
            +
                  def async_exec(sql)
         | 
| 7 | 
            +
                    Recorder.store_query(sql: sql, source: :async_exec) do
         | 
| 8 | 
            +
                      super
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def sync_exec(sql)
         | 
| 13 | 
            +
                    Recorder.store_query(sql: sql, source: :sync_exec) do
         | 
| 14 | 
            +
                      super
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def exec(*args)
         | 
| 19 | 
            +
                    Recorder.store_query(sql: args[0], source: :exec) do
         | 
| 20 | 
            +
                      super
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def exec_params(*args)
         | 
| 25 | 
            +
                    Recorder.store_query(sql: args[0], binds: args[1], source: :exec_params) do
         | 
| 26 | 
            +
                      super
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def exec_prepared(*args)
         | 
| 31 | 
            +
                    Recorder.store_prepared_statement(name: args[0], binds: args[1], source: :exec_prepared) do
         | 
| 32 | 
            +
                      super
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def prepare(*args)
         | 
| 37 | 
            +
                    Recorder.prepare_statement(name: args[0], sql: args[1], source: :prepare) do
         | 
| 38 | 
            +
                      super
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  def query(*args)
         | 
| 43 | 
            +
                    Recorder.store_query(sql: args[0], source: :query) do
         | 
| 44 | 
            +
                      super
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
| @@ -11,9 +11,9 @@ module DatabaseRecorder | |
| 11 11 | 
             
                  alias :rows :values
         | 
| 12 12 |  | 
| 13 13 | 
             
                  def initialize(data)
         | 
| 14 | 
            -
                    @count = data[ | 
| 15 | 
            -
                    @fields = data[ | 
| 16 | 
            -
                    @values = data[ | 
| 14 | 
            +
                    @count = data[:count]
         | 
| 15 | 
            +
                    @fields = data[:fields]
         | 
| 16 | 
            +
                    @values = data[:values]
         | 
| 17 17 | 
             
                  end
         | 
| 18 18 |  | 
| 19 19 | 
             
                  def clear; end
         | 
| @@ -3,81 +3,64 @@ | |
| 3 3 | 
             
            module DatabaseRecorder
         | 
| 4 4 | 
             
              module PG
         | 
| 5 5 | 
             
                module Recorder
         | 
| 6 | 
            -
                   | 
| 7 | 
            -
                    PG.record(sql: sql, source: :async_exec) do
         | 
| 8 | 
            -
                      super
         | 
| 9 | 
            -
                    end
         | 
| 10 | 
            -
                  end
         | 
| 6 | 
            +
                  module_function
         | 
| 11 7 |  | 
| 12 | 
            -
                  def  | 
| 13 | 
            -
                     | 
| 14 | 
            -
                       | 
| 15 | 
            -
             | 
| 8 | 
            +
                  def ignore_query?(sql)
         | 
| 9 | 
            +
                    !Recording.started? ||
         | 
| 10 | 
            +
                      sql.downcase.match?(/\A(begin|commit|release|rollback|savepoint)/i) ||
         | 
| 11 | 
            +
                      sql.match?(/ pg_attribute |SHOW max_identifier_length|SHOW search_path/)
         | 
| 16 12 | 
             
                  end
         | 
| 17 13 |  | 
| 18 | 
            -
                   | 
| 19 | 
            -
             | 
| 20 | 
            -
                   | 
| 21 | 
            -
                  # end
         | 
| 14 | 
            +
                  def format_result(result)
         | 
| 15 | 
            +
                    { count: result.count, fields: result.fields, values: result.values } if result
         | 
| 16 | 
            +
                  end
         | 
| 22 17 |  | 
| 23 | 
            -
                   | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
                   | 
| 18 | 
            +
                  def prepare_statement(sql: nil, name: nil, binds: nil, source: nil)
         | 
| 19 | 
            +
                    Recording.push_prepared(name: name, sql: sql, binds: binds, source: source)
         | 
| 20 | 
            +
                    yield if !Config.replay_recordings || Recording.cache.nil?
         | 
| 21 | 
            +
                  end
         | 
| 27 22 |  | 
| 28 | 
            -
                  def  | 
| 29 | 
            -
                    PG. | 
| 30 | 
            -
                       | 
| 23 | 
            +
                  def setup
         | 
| 24 | 
            +
                    ::PG::Connection.class_eval do
         | 
| 25 | 
            +
                      prepend ConnectionExt
         | 
| 31 26 | 
             
                    end
         | 
| 32 27 | 
             
                  end
         | 
| 33 28 |  | 
| 34 | 
            -
                   | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
                  # def sync_exec_params(*args)
         | 
| 40 | 
            -
                  #   puts ">>> #{args[0]}"
         | 
| 41 | 
            -
                  #   super
         | 
| 42 | 
            -
                  # end
         | 
| 29 | 
            +
                  def store_prepared_statement(name: nil, sql: nil, binds: nil, source: nil)
         | 
| 30 | 
            +
                    if Config.replay_recordings && !Recording.cache.nil?
         | 
| 31 | 
            +
                      data = Recording.cache.find { |query| query[:name] == name }
         | 
| 32 | 
            +
                      return yield unless data # cache miss
         | 
| 43 33 |  | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
                       | 
| 34 | 
            +
                      Core.log_query(data[:sql], source)
         | 
| 35 | 
            +
                      Recording.push(sql: data[:sql], binds: data[:binds], source: source)
         | 
| 36 | 
            +
                      RecordedResult.new(data[:result].slice(:count, :fields, :values))
         | 
| 37 | 
            +
                    else
         | 
| 38 | 
            +
                      Core.log_query(sql, source)
         | 
| 39 | 
            +
                      yield.tap do |query_result|
         | 
| 40 | 
            +
                        result = format_result(query_result)
         | 
| 41 | 
            +
                        query = Recording.update_prepared(name: name, sql: sql, binds: binds, result: result, source: source)
         | 
| 42 | 
            +
                        Core.log_query(query[:sql], source)
         | 
| 43 | 
            +
                      end
         | 
| 47 44 | 
             
                    end
         | 
| 48 45 | 
             
                  end
         | 
| 49 | 
            -
                end
         | 
| 50 46 |  | 
| 51 | 
            -
             | 
| 47 | 
            +
                  def store_query(name: nil, sql: nil, binds: nil, source: nil)
         | 
| 48 | 
            +
                    return yield if ignore_query?(sql)
         | 
| 52 49 |  | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
                     | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
                def record(sql:, binds: nil, source: nil)
         | 
| 60 | 
            -
                  return yield if ignore_query?(sql)
         | 
| 61 | 
            -
             | 
| 62 | 
            -
                  Core.log_query(sql, source)
         | 
| 63 | 
            -
                  if Config.replay_recordings && !Recording.cache.nil?
         | 
| 64 | 
            -
                    Recording.push(sql: sql, binds: binds)
         | 
| 65 | 
            -
                    data = Recording.cached_query_for(sql)
         | 
| 66 | 
            -
                    return yield unless data # cache miss
         | 
| 50 | 
            +
                    Core.log_query(sql, source)
         | 
| 51 | 
            +
                    @prepared_statement = nil
         | 
| 52 | 
            +
                    if Config.replay_recordings && !Recording.cache.nil?
         | 
| 53 | 
            +
                      Recording.push(sql: sql, binds: binds, source: source)
         | 
| 54 | 
            +
                      data = Recording.cached_query_for(sql)
         | 
| 55 | 
            +
                      return yield unless data # cache miss
         | 
| 67 56 |  | 
| 68 | 
            -
             | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
             | 
| 72 | 
            -
                       | 
| 57 | 
            +
                      RecordedResult.new(data[:result].slice(:count, :fields, :values))
         | 
| 58 | 
            +
                    else
         | 
| 59 | 
            +
                      yield.tap do |result|
         | 
| 60 | 
            +
                        Recording.push(name: name, sql: sql, binds: binds, result: format_result(result), source: source)
         | 
| 61 | 
            +
                      end
         | 
| 73 62 | 
             
                    end
         | 
| 74 63 | 
             
                  end
         | 
| 75 64 | 
             
                end
         | 
| 76 | 
            -
             | 
| 77 | 
            -
                def setup
         | 
| 78 | 
            -
                  ::PG::Connection.class_eval do
         | 
| 79 | 
            -
                    prepend Recorder
         | 
| 80 | 
            -
                  end
         | 
| 81 | 
            -
                end
         | 
| 82 65 | 
             
              end
         | 
| 83 66 | 
             
            end
         | 
| @@ -4,23 +4,25 @@ require 'forwardable' | |
| 4 4 |  | 
| 5 5 | 
             
            module DatabaseRecorder
         | 
| 6 6 | 
             
              class Recording
         | 
| 7 | 
            -
                attr_accessor :cache, :entities
         | 
| 8 | 
            -
                attr_reader :from_cache, :options, :queries, :started
         | 
| 7 | 
            +
                attr_accessor :cache, :entities, :metadata
         | 
| 8 | 
            +
                attr_reader :from_cache, :options, :prepared_queries, :queries, :started
         | 
| 9 9 |  | 
| 10 10 | 
             
                def initialize(options: {})
         | 
| 11 11 | 
             
                  (@@instances ||= {})[Process.pid] = self
         | 
| 12 12 | 
             
                  @cache = nil
         | 
| 13 13 | 
             
                  @entities = []
         | 
| 14 | 
            +
                  @metadata = {}
         | 
| 14 15 | 
             
                  @options = options
         | 
| 15 16 | 
             
                  @queries = []
         | 
| 16 17 | 
             
                  @search_index = 0
         | 
| 18 | 
            +
                  @@prepared_queries ||= {}
         | 
| 17 19 | 
             
                end
         | 
| 18 20 |  | 
| 19 21 | 
             
                def cached_query_for(sql)
         | 
| 20 22 | 
             
                  current = @search_index
         | 
| 21 23 | 
             
                  match = cache[@search_index..].find do |item|
         | 
| 22 24 | 
             
                    current += 1
         | 
| 23 | 
            -
                    item[ | 
| 25 | 
            +
                    item[:sql] == sql
         | 
| 24 26 | 
             
                  end
         | 
| 25 27 | 
             
                  return unless match
         | 
| 26 28 |  | 
| @@ -31,34 +33,49 @@ module DatabaseRecorder | |
| 31 33 | 
             
                end
         | 
| 32 34 |  | 
| 33 35 | 
             
                def new_entity(model:, id:)
         | 
| 34 | 
            -
                  @entities.push( | 
| 36 | 
            +
                  @entities.push(model: model, id: id)
         | 
| 35 37 | 
             
                end
         | 
| 36 38 |  | 
| 37 39 | 
             
                def pull_entity
         | 
| 38 40 | 
             
                  @entities.shift
         | 
| 39 41 | 
             
                end
         | 
| 40 42 |  | 
| 41 | 
            -
                def push(sql:, binds: nil, result: nil,  | 
| 42 | 
            -
                  query = {  | 
| 43 | 
            +
                def push(sql:, name: nil, binds: nil, result: nil, source: nil)
         | 
| 44 | 
            +
                  query = { name: name, sql: sql, binds: binds, result: result }.compact
         | 
| 43 45 | 
             
                  @queries.push(query)
         | 
| 44 46 | 
             
                end
         | 
| 45 47 |  | 
| 48 | 
            +
                def push_prepared(name: nil, sql: nil, binds: nil, result: nil, source: nil)
         | 
| 49 | 
            +
                  query = { name: name, sql: sql, binds: binds, result: result }.compact
         | 
| 50 | 
            +
                  @@prepared_queries[name || sql] = query
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 46 53 | 
             
                def start
         | 
| 47 54 | 
             
                  @started = true
         | 
| 48 | 
            -
                  storage = Config.storage&.new(self, name: options[:name])
         | 
| 55 | 
            +
                  storage = Config.storage&.new(self, name: options[:name], options: Config.storage_options)
         | 
| 49 56 | 
             
                  @from_cache = storage&.load
         | 
| 50 57 | 
             
                  yield
         | 
| 51 58 | 
             
                  storage&.save unless from_cache
         | 
| 52 59 | 
             
                  @started = false
         | 
| 53 | 
            -
                  result = { current_queries: queries.map {  | 
| 54 | 
            -
                  result[:stored_queries] = cache.map {  | 
| 60 | 
            +
                  result = { current_queries: queries.map { |query| query[:sql] } }
         | 
| 61 | 
            +
                  result[:stored_queries] = cache.map { |query| query[:sql] } if from_cache
         | 
| 55 62 | 
             
                  result
         | 
| 56 63 | 
             
                end
         | 
| 57 64 |  | 
| 65 | 
            +
                def update_prepared(name: nil, sql: nil, binds: nil, result: nil, source: nil)
         | 
| 66 | 
            +
                  query = @@prepared_queries[name || sql]
         | 
| 67 | 
            +
                  query[:sql] = sql if sql
         | 
| 68 | 
            +
                  query[:binds] = binds if binds
         | 
| 69 | 
            +
                  query[:result] = result if result
         | 
| 70 | 
            +
                  @queries.push(query)
         | 
| 71 | 
            +
                  query
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 58 74 | 
             
                class << self
         | 
| 59 75 | 
             
                  extend Forwardable
         | 
| 60 76 |  | 
| 61 | 
            -
                  def_delegators :current_instance, :cache, :cached_query_for, :from_cache, :new_entity, : | 
| 77 | 
            +
                  def_delegators :current_instance, :cache, :cached_query_for, :from_cache, :new_entity, :prepared_queries,
         | 
| 78 | 
            +
                                 :pull_entity, :push, :push_prepared, :queries, :update_prepared
         | 
| 62 79 |  | 
| 63 80 | 
             
                  def current_instance
         | 
| 64 81 | 
             
                    (@@instances ||= {})[Process.pid]
         | 
| @@ -14,9 +14,9 @@ module DatabaseRecorder | |
| 14 14 | 
             
                      ref = (example.metadata[:scoped_id] || '').split(':')[-1]
         | 
| 15 15 | 
             
                      options = {}
         | 
| 16 16 | 
             
                      options.merge!(example.metadata[:dbr]) if example.metadata[:dbr].is_a?(Hash)
         | 
| 17 | 
            -
                      options.merge!(example: example)
         | 
| 18 | 
            -
                      options.merge!(name: "#{example.full_description}__#{ref}") # TODO: if name is already set, append ref
         | 
| 17 | 
            +
                      options.merge!(example: example, name: "#{example.full_description}__#{ref}")
         | 
| 19 18 | 
             
                      Recording.new(options: options).tap do |recording|
         | 
| 19 | 
            +
                        recording.metadata = { example: example.id, started_at: Time.now }
         | 
| 20 20 | 
             
                        result = recording.start { example.run }
         | 
| 21 21 | 
             
                        if options[:verify_queries] && result[:stored_queries]
         | 
| 22 22 | 
             
                          expect(result[:stored_queries]).to match_array(result[:current_queries])
         | 
| @@ -2,18 +2,14 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module DatabaseRecorder
         | 
| 4 4 | 
             
              module Storage
         | 
| 5 | 
            -
                class File
         | 
| 6 | 
            -
                  def initialize(recording, name:)
         | 
| 7 | 
            -
                    @recording = recording
         | 
| 8 | 
            -
                    @name = name
         | 
| 9 | 
            -
                  end
         | 
| 10 | 
            -
             | 
| 5 | 
            +
                class File < Base
         | 
| 11 6 | 
             
                  def load
         | 
| 12 | 
            -
                    stored_data = ::File.exist?( | 
| 7 | 
            +
                    stored_data = ::File.exist?(storage_path) ? ::File.read(storage_path) : false
         | 
| 13 8 | 
             
                    if stored_data
         | 
| 14 | 
            -
                       | 
| 15 | 
            -
                       | 
| 16 | 
            -
                      @recording. | 
| 9 | 
            +
                      parsed_data = YAML.load(stored_data) # rubocop:disable Security/YAMLLoad
         | 
| 10 | 
            +
                      data = Core.symbolize_recursive(parsed_data)
         | 
| 11 | 
            +
                      @recording.cache = data[:queries] || []
         | 
| 12 | 
            +
                      @recording.entities = data[:entities]
         | 
| 17 13 | 
             
                      true
         | 
| 18 14 | 
             
                    else
         | 
| 19 15 | 
             
                      false
         | 
| @@ -21,10 +17,22 @@ module DatabaseRecorder | |
| 21 17 | 
             
                  end
         | 
| 22 18 |  | 
| 23 19 | 
             
                  def save
         | 
| 24 | 
            -
                    data = { | 
| 25 | 
            -
                    data[ | 
| 26 | 
            -
                     | 
| 27 | 
            -
                     | 
| 20 | 
            +
                    data = {}
         | 
| 21 | 
            +
                    data[:metadata] = @recording.metadata unless @recording.metadata.empty?
         | 
| 22 | 
            +
                    data[:queries] = @recording.queries if @recording.queries.any?
         | 
| 23 | 
            +
                    data[:entities] = @recording.entities if @recording.entities.any?
         | 
| 24 | 
            +
                    serialized_data = Core.string_keys_recursive(data).to_yaml
         | 
| 25 | 
            +
                    ::File.write(storage_path, serialized_data)
         | 
| 26 | 
            +
                    true
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def storage_path
         | 
| 30 | 
            +
                    @storage_path ||= begin
         | 
| 31 | 
            +
                      name = normalize_name(@name)
         | 
| 32 | 
            +
                      path = @options[:recordings_path] || 'spec/dbr'
         | 
| 33 | 
            +
                      FileUtils.mkdir_p(path)
         | 
| 34 | 
            +
                      "#{path}/#{name}.yml"
         | 
| 35 | 
            +
                    end
         | 
| 28 36 | 
             
                  end
         | 
| 29 37 |  | 
| 30 38 | 
             
                  private
         | 
| @@ -32,13 +40,6 @@ module DatabaseRecorder | |
| 32 40 | 
             
                  def normalize_name(string)
         | 
| 33 41 | 
             
                    string.gsub(%r{[:/]}, '-').gsub(/[^\w-]/, '_')
         | 
| 34 42 | 
             
                  end
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                  def record_file
         | 
| 37 | 
            -
                    name = normalize_name(@name)
         | 
| 38 | 
            -
                    path = 'spec/dbr'
         | 
| 39 | 
            -
                    FileUtils.mkdir_p(path)
         | 
| 40 | 
            -
                    "#{path}/#{name}.yml"
         | 
| 41 | 
            -
                  end
         | 
| 42 43 | 
             
                end
         | 
| 43 44 | 
             
              end
         | 
| 44 45 | 
             
            end
         | 
| @@ -2,18 +2,18 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module DatabaseRecorder
         | 
| 4 4 | 
             
              module Storage
         | 
| 5 | 
            -
                class Redis
         | 
| 6 | 
            -
                  def  | 
| 7 | 
            -
                    @ | 
| 8 | 
            -
                    @name = name
         | 
| 5 | 
            +
                class Redis < Base
         | 
| 6 | 
            +
                  def connection
         | 
| 7 | 
            +
                    @connection ||= @options[:connection] || ::Redis.new
         | 
| 9 8 | 
             
                  end
         | 
| 10 9 |  | 
| 11 10 | 
             
                  def load
         | 
| 12 | 
            -
                    stored_data =  | 
| 11 | 
            +
                    stored_data = connection.get(@name)
         | 
| 13 12 | 
             
                    if stored_data
         | 
| 14 | 
            -
                       | 
| 15 | 
            -
                       | 
| 16 | 
            -
                      @recording. | 
| 13 | 
            +
                      parsed_data = JSON.parse(stored_data)
         | 
| 14 | 
            +
                      data = Core.symbolize_recursive(parsed_data)
         | 
| 15 | 
            +
                      @recording.cache = data[:queries] || []
         | 
| 16 | 
            +
                      @recording.entities = data[:entities]
         | 
| 17 17 | 
             
                      true
         | 
| 18 18 | 
             
                    else
         | 
| 19 19 | 
             
                      false
         | 
| @@ -21,16 +21,13 @@ module DatabaseRecorder | |
| 21 21 | 
             
                  end
         | 
| 22 22 |  | 
| 23 23 | 
             
                  def save
         | 
| 24 | 
            -
                    data = { | 
| 25 | 
            -
                    data[ | 
| 24 | 
            +
                    data = {}
         | 
| 25 | 
            +
                    data[:metadata] = @recording.metadata unless @recording.metadata.empty?
         | 
| 26 | 
            +
                    data[:queries] = @recording.queries if @recording.queries.any?
         | 
| 27 | 
            +
                    data[:entities] = @recording.entities if @recording.entities.any?
         | 
| 26 28 | 
             
                    serialized_data = data.to_json
         | 
| 27 | 
            -
                     | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
                  class << self
         | 
| 31 | 
            -
                    def connection
         | 
| 32 | 
            -
                      @connection ||= ::Redis.new
         | 
| 33 | 
            -
                    end
         | 
| 29 | 
            +
                    connection.set(@name, serialized_data)
         | 
| 30 | 
            +
                    true
         | 
| 34 31 | 
             
                  end
         | 
| 35 32 | 
             
                end
         | 
| 36 33 | 
             
              end
         | 
    
        data/lib/database_recorder.rb
    CHANGED
    
    | @@ -3,22 +3,22 @@ | |
| 3 3 | 
             
            require_relative 'database_recorder/core'
         | 
| 4 4 |  | 
| 5 5 | 
             
            if defined? ::ActiveRecord
         | 
| 6 | 
            -
              require_relative 'database_recorder/ | 
| 7 | 
            -
              require_relative 'database_recorder/ | 
| 8 | 
            -
              require_relative 'database_recorder/ | 
| 9 | 
            -
              require_relative 'database_recorder/ | 
| 6 | 
            +
              require_relative 'database_recorder/active_record/abstract_adapter_ext'
         | 
| 7 | 
            +
              require_relative 'database_recorder/active_record/base_ext'
         | 
| 8 | 
            +
              require_relative 'database_recorder/active_record/recorded_result'
         | 
| 9 | 
            +
              require_relative 'database_recorder/active_record/recorder'
         | 
| 10 10 | 
             
            end
         | 
| 11 11 |  | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 12 | 
            +
            require_relative 'database_recorder/mysql2/client_ext'
         | 
| 13 | 
            +
            require_relative 'database_recorder/mysql2/recorded_result'
         | 
| 14 | 
            +
            require_relative 'database_recorder/mysql2/recorder'
         | 
| 15 | 
            +
            require_relative 'database_recorder/mysql2/statement_ext'
         | 
| 16 16 |  | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
            end
         | 
| 17 | 
            +
            require_relative 'database_recorder/pg/connection_ext'
         | 
| 18 | 
            +
            require_relative 'database_recorder/pg/recorded_result'
         | 
| 19 | 
            +
            require_relative 'database_recorder/pg/recorder'
         | 
| 21 20 |  | 
| 21 | 
            +
            require_relative 'database_recorder/storage/base'
         | 
| 22 22 | 
             
            require_relative 'database_recorder/storage/file'
         | 
| 23 23 | 
             
            require_relative 'database_recorder/storage/redis'
         | 
| 24 24 |  | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: database_recorder
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.1 | 
| 4 | 
            +
              version: 0.2.1
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Mattia Roccoberton
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2022-04- | 
| 11 | 
            +
            date: 2022-04-16 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: coderay
         | 
| @@ -35,18 +35,22 @@ files: | |
| 35 35 | 
             
            - MIT-LICENSE
         | 
| 36 36 | 
             
            - README.md
         | 
| 37 37 | 
             
            - lib/database_recorder.rb
         | 
| 38 | 
            -
            - lib/database_recorder/ | 
| 39 | 
            -
            - lib/database_recorder/ | 
| 40 | 
            -
            - lib/database_recorder/ | 
| 41 | 
            -
            - lib/database_recorder/ | 
| 38 | 
            +
            - lib/database_recorder/active_record/abstract_adapter_ext.rb
         | 
| 39 | 
            +
            - lib/database_recorder/active_record/base_ext.rb
         | 
| 40 | 
            +
            - lib/database_recorder/active_record/recorded_result.rb
         | 
| 41 | 
            +
            - lib/database_recorder/active_record/recorder.rb
         | 
| 42 42 | 
             
            - lib/database_recorder/config.rb
         | 
| 43 43 | 
             
            - lib/database_recorder/core.rb
         | 
| 44 | 
            +
            - lib/database_recorder/mysql2/client_ext.rb
         | 
| 44 45 | 
             
            - lib/database_recorder/mysql2/recorded_result.rb
         | 
| 45 46 | 
             
            - lib/database_recorder/mysql2/recorder.rb
         | 
| 47 | 
            +
            - lib/database_recorder/mysql2/statement_ext.rb
         | 
| 48 | 
            +
            - lib/database_recorder/pg/connection_ext.rb
         | 
| 46 49 | 
             
            - lib/database_recorder/pg/recorded_result.rb
         | 
| 47 50 | 
             
            - lib/database_recorder/pg/recorder.rb
         | 
| 48 51 | 
             
            - lib/database_recorder/recording.rb
         | 
| 49 52 | 
             
            - lib/database_recorder/rspec.rb
         | 
| 53 | 
            +
            - lib/database_recorder/storage/base.rb
         | 
| 50 54 | 
             
            - lib/database_recorder/storage/file.rb
         | 
| 51 55 | 
             
            - lib/database_recorder/storage/redis.rb
         | 
| 52 56 | 
             
            - lib/database_recorder/version.rb
         | 
| @@ -1,33 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            module DatabaseRecorder
         | 
| 4 | 
            -
              module ActiveRecord
         | 
| 5 | 
            -
                module AbstractAdapterExt
         | 
| 6 | 
            -
                  def log(sql, name = 'SQL', binds = [], type_casted_binds = [], *args)
         | 
| 7 | 
            -
                    # puts "--- #{sql} | #{type_casted_binds}", "  > #{name}"
         | 
| 8 | 
            -
                    return super unless Recording.started?
         | 
| 9 | 
            -
                    return super if %w[schema transaction].include?(name&.downcase)
         | 
| 10 | 
            -
                    return super if sql.downcase.match(/\A(begin|commit|release|rollback|savepoint)/i)
         | 
| 11 | 
            -
             | 
| 12 | 
            -
                    Core.log_query(sql, name)
         | 
| 13 | 
            -
                    if Config.replay_recordings && Recording.from_cache
         | 
| 14 | 
            -
                      Recording.push(sql: sql, binds: binds)
         | 
| 15 | 
            -
                      data = Recording.cached_query_for(sql)
         | 
| 16 | 
            -
                      return yield if !data || !data['result'] # cache miss
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                      RecordedResult.new(data['result']['fields'], data['result']['values'])
         | 
| 19 | 
            -
                    else
         | 
| 20 | 
            -
                      super.tap do |result|
         | 
| 21 | 
            -
                        result_data =
         | 
| 22 | 
            -
                          if result.is_a?(::ActiveRecord::Result)
         | 
| 23 | 
            -
                            fields = result.respond_to?(:fields) ? result.fields : result.columns
         | 
| 24 | 
            -
                            values = result.respond_to?(:values) ? result.values : result.to_a
         | 
| 25 | 
            -
                            { 'count' => result.count, 'fields' => fields, 'values' => values }
         | 
| 26 | 
            -
                          end
         | 
| 27 | 
            -
                        Recording.push(sql: sql, name: name, binds: type_casted_binds, result: result_data)
         | 
| 28 | 
            -
                      end
         | 
| 29 | 
            -
                    end
         | 
| 30 | 
            -
                  end
         | 
| 31 | 
            -
                end
         | 
| 32 | 
            -
              end
         | 
| 33 | 
            -
            end
         | 
| @@ -1,19 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            module DatabaseRecorder
         | 
| 4 | 
            -
              module ActiveRecord
         | 
| 5 | 
            -
                module Recorder
         | 
| 6 | 
            -
                  module_function
         | 
| 7 | 
            -
             | 
| 8 | 
            -
                  def setup
         | 
| 9 | 
            -
                    ::ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
         | 
| 10 | 
            -
                      prepend AbstractAdapterExt
         | 
| 11 | 
            -
                    end
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                    # ::ActiveRecord::Base.class_eval do
         | 
| 14 | 
            -
                    #   prepend BaseExt
         | 
| 15 | 
            -
                    # end
         | 
| 16 | 
            -
                  end
         | 
| 17 | 
            -
                end
         | 
| 18 | 
            -
              end
         | 
| 19 | 
            -
            end
         |