database_recorder 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5d51e080ba82dada7af93fbb37380af72e25fef001ecc3f691080048b951f67f
4
+ data.tar.gz: 5a902b17cab7f30b5cedae2b1e00e374fdc58b3e992cf3bc08d1be8421f37421
5
+ SHA512:
6
+ metadata.gz: 936d740c398c162be09d7f9631c64ad3c516a55ab787991104df7e32bd53299ebf1d7b50d0f6b605f7118e495a7c7e1d551f2029085d123451291a6c054547f5
7
+ data.tar.gz: 260f2d6c7bd1d3b65c67a25db41fce5efbd65dc19f294af7e852003c08e78126d58d0f6679b48299ff2ad0646eb96577f8ffc2383e137278e44f222d13326f70
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Mattia Roccoberton
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # Database Recorder
2
+
3
+ Record database queries for testing and development purposes only.
4
+ Support only RSpec at the moment, storing logs data on files or Redis.
5
+
6
+ Main features:
7
+ - store the history of the queries of a test when it run (for monitoring);
8
+ - eventually check if the current queries match the recorded ones (to prevent regressions);
9
+ - [EXPERIMENTAL] optionally replay the recorded queries replacing the original requests.
10
+
11
+ See below for more details.
12
+
13
+ ## Install
14
+
15
+ - Add to your Gemfile: `gem 'database_recorder'` (:development, :test groups recommended)
16
+ - With RSpec:
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
+
21
+ ```rb
22
+ it 'returns 3 posts', :dbr do
23
+ # ...
24
+ end
25
+ ```
26
+
27
+ ## Config
28
+
29
+ Add to your _spec_helper.rb_:
30
+
31
+ ```rb
32
+ # To print the queries while executing the specs: false | true | :color
33
+ DatabaseRecorder::Config.print_queries = true
34
+
35
+ # Replay the recordings intercepting the queries
36
+ DatabaseRecorder::Config.replay_recordings = true
37
+
38
+ # To store the queries: :file | :redis | nil
39
+ DatabaseRecorder::Config.storage = :redis
40
+ # nil to avoid storing the queries
41
+ ```
42
+
43
+ ## History of the queries
44
+
45
+ Using the `print_queries` config option is possible to see the executed queries while running the specs. It can be used to identify easily what is going on in a specific example without having to analyze the log files.
46
+
47
+ Using the `:file` storage, the history is also recorded to files (default path: **spec/dbr**) in YAML format. This is useful for checking what's happening with more details, it includes the query results and some extra data.
48
+
49
+ ## Test queries' changes
50
+
51
+ This feature can be used to prevent queries regressions.
52
+ It requires to have previously stored the history of the queries (which could be versioned if using file storage).
53
+ It can be activated using `dbr: { verify_queries: true }` metadata.
54
+
55
+ To work correctly in requires `prepared_statements: true` option in the **database.yml** config file, in the connection block options (available for both Postgres and MySQL).
56
+
57
+ ## Replay the recorded queries
58
+
59
+ This feature is not stable (at this stage), so use it carefully and supports only deterministic tests (so it doesn't work with Faker, random data or random order specs) and only Postgres is supported for now.
60
+ It requires to have previously stored the history of the queries.
61
+ Using this feature can improve the test suite performances (especially using redis storage).
62
+ It can be activated using `replay_recordings` config option.
63
+
64
+ Some workarounds to make it works:
65
+ - Run specs with `bin/rspec --order defined`
66
+ - Set a specific seed for Faker (optionally with an ENV var): `Faker::Config.random = Random.new(42)`
67
+ - Set a specific Ruby seed (optionally with an ENV var): `srand(42)`
68
+
69
+ ## Do you like it? Star it!
70
+
71
+ If you use this component just star it. A developer is more motivated to improve a project when there is some interest.
72
+
73
+ Or consider offering me a coffee, it's a small thing but it is greatly appreciated: [about me](https://www.blocknot.es/about-me).
74
+
75
+ ## Contributors
76
+
77
+ - [Mattia Roccoberton](https://blocknot.es): author
78
+
79
+ ## License
80
+
81
+ The gem is available as open-source under the terms of the [MIT](MIT-LICENSE).
@@ -0,0 +1,33 @@
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
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseRecorder
4
+ module ActiveRecord
5
+ module BaseExt
6
+ def create_or_update(**, &block)
7
+ return super if !new_record? || !Recording.started?
8
+
9
+ cached = Config.replay_recordings && Recording.from_cache ? Recording.pull_entity : false
10
+ if cached
11
+ # self.id = cached['id']
12
+ super
13
+ else
14
+ super.tap do |_result|
15
+ Recording.new_entity(model: self.class.to_s, id: id)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require 'forwardable'
4
+
5
+ module DatabaseRecorder
6
+ module ActiveRecord
7
+ class RecordedResult < ::ActiveRecord::Result
8
+ alias :cmd_tuples :count
9
+ alias :fields :columns
10
+ alias :values :rows
11
+
12
+ def clear; end
13
+
14
+ def fmod(_index)
15
+ -1
16
+ end
17
+
18
+ def ftype(_index)
19
+ 20
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'singleton'
5
+
6
+ module DatabaseRecorder
7
+ class Config
8
+ include Singleton
9
+
10
+ attr_accessor :db_driver, :print_queries, :replay_recordings, :storage
11
+
12
+ class << self
13
+ extend Forwardable
14
+
15
+ def_delegators :instance, :db_driver, :db_driver=, :print_queries, :print_queries=, :replay_recordings,
16
+ :replay_recordings=, :storage
17
+
18
+ def load_defaults
19
+ instance.db_driver = :active_record
20
+ instance.print_queries = false # false | true | :color
21
+ instance.replay_recordings = false
22
+ self.storage = :file # :file | :redis
23
+ end
24
+
25
+ def storage=(value)
26
+ instance.storage =
27
+ case value
28
+ when :file then DatabaseRecorder::Storage::File
29
+ when :redis then DatabaseRecorder::Storage::Redis
30
+ when nil then nil
31
+ else raise ArgumentError, "Unknown storage: #{value}"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ DatabaseRecorder::Config.load_defaults
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseRecorder
4
+ module Core
5
+ module_function
6
+
7
+ def log_query(sql, source = nil)
8
+ case DatabaseRecorder::Config.print_queries
9
+ when true
10
+ puts "[DB] #{sql} [#{source}]"
11
+ when :color
12
+ puts "[DB] #{CodeRay.scan(sql, :sql).term} [#{source}]"
13
+ end
14
+ end
15
+
16
+ def setup
17
+ case DatabaseRecorder::Config.db_driver
18
+ when :active_record then ActiveRecord::Recorder.setup
19
+ when :mysql2 then Mysql2.setup
20
+ when :pg then PG.setup
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require 'forwardable'
4
+
5
+ module DatabaseRecorder
6
+ module Mysql2
7
+ class RecordedResult # < ::Mysql2::Result
8
+ # include Enumerable
9
+ # extend Forwardable
10
+
11
+ # def_delegators :to_a, :each
12
+
13
+ attr_reader :count, :entries, :fields # , :values
14
+
15
+ alias :size :count
16
+
17
+ def prepare(data)
18
+ @count = data['count']
19
+ @fields = data['fields']
20
+ @entries = data['values']
21
+ # @values = data['values']
22
+ end
23
+
24
+ # def server_flags
25
+ # { no_good_index_used: false, no_index_used: true, query_was_slow: false }
26
+ # end
27
+
28
+ # def to_a
29
+ # @to_a ||= values.each_with_object([]) do |value, list|
30
+ # list << fields.zip(value).to_h
31
+ # end
32
+ # end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseRecorder
4
+ module Mysql2
5
+ module Recorder
6
+ def query(sql, options = {})
7
+ Mysql2.record(self, sql: sql) do
8
+ super
9
+ end
10
+ end
11
+ end
12
+
13
+ module_function
14
+
15
+ def ignore_query?(sql)
16
+ !Recording.started? ||
17
+ sql == 'SELECT LAST_INSERT_ID() AS _dbr_last_insert_id' ||
18
+ sql.downcase.match?(/\A(begin|commit|release|rollback|savepoint|show full fields from)/i) ||
19
+ sql.match?(/information_schema.statistics/)
20
+ end
21
+
22
+ def record(recorder, sql:)
23
+ return yield if ignore_query?(sql)
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)
43
+ end
44
+ end
45
+ end
46
+
47
+ def setup
48
+ ::Mysql2::Client.class_eval do
49
+ prepend Recorder
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseRecorder
4
+ module PG
5
+ class RecordedResult
6
+ attr_reader :count, :fields, :values, :type_map
7
+
8
+ alias :cmd_tuples :count
9
+ alias :columns :fields
10
+ alias :ntuples :count
11
+ alias :rows :values
12
+
13
+ def initialize(data)
14
+ @count = data['count']
15
+ @fields = data['fields']
16
+ @values = data['values']
17
+ end
18
+
19
+ def clear; end
20
+
21
+ def first
22
+ to_a.first
23
+ end
24
+
25
+ def fmod(_index)
26
+ -1
27
+ end
28
+
29
+ def ftype(_index)
30
+ 20
31
+ end
32
+
33
+ def map_types!(type_map)
34
+ @type_map = type_map
35
+ self
36
+ end
37
+
38
+ def nfields
39
+ fields.size
40
+ end
41
+
42
+ def to_a
43
+ @to_a ||= values.each_with_object([]) do |value, list|
44
+ list << fields.zip(value).to_h
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseRecorder
4
+ module PG
5
+ module Recorder
6
+ def async_exec(sql)
7
+ PG.record(sql: sql, source: :async_exec) do
8
+ super
9
+ end
10
+ end
11
+
12
+ def sync_exec(sql)
13
+ PG.record(sql: sql, source: :sync_exec) do
14
+ super
15
+ end
16
+ end
17
+
18
+ # def exec(*args)
19
+ # puts "-E- #{args[0]}"
20
+ # super
21
+ # end
22
+
23
+ # def query(*args)
24
+ # puts "-Q- #{args[0]}"
25
+ # super
26
+ # end
27
+
28
+ def exec_params(*args)
29
+ PG.record(sql: args[0], binds: args[1], source: :exec_params) do
30
+ super
31
+ end
32
+ end
33
+
34
+ # def async_exec_params(*args)
35
+ # puts ">>> #{args[0]}"
36
+ # super
37
+ # end
38
+
39
+ # def sync_exec_params(*args)
40
+ # puts ">>> #{args[0]}"
41
+ # super
42
+ # end
43
+
44
+ def exec_prepared(*args)
45
+ PG.record(sql: args[0], binds: args[1], source: :exec_prepared) do
46
+ super
47
+ end
48
+ end
49
+ end
50
+
51
+ module_function
52
+
53
+ def ignore_query?(sql)
54
+ !Recording.started? ||
55
+ sql.downcase.match?(/\A(begin|commit|release|rollback|savepoint)/i) ||
56
+ sql.match?(/ pg_attribute |SHOW max_identifier_length|SHOW search_path/)
57
+ end
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
67
+
68
+ RecordedResult.new(data['result'].slice('count', 'fields', 'values'))
69
+ else
70
+ yield.tap do |result|
71
+ result_data = result ? { 'count' => result.count, 'fields' => result.fields, 'values' => result.values } : nil
72
+ Recording.push(sql: sql, binds: binds, result: result_data)
73
+ end
74
+ end
75
+ end
76
+
77
+ def setup
78
+ ::PG::Connection.class_eval do
79
+ prepend Recorder
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module DatabaseRecorder
6
+ class Recording
7
+ attr_accessor :cache, :entities
8
+ attr_reader :from_cache, :options, :queries, :started
9
+
10
+ def initialize(options: {})
11
+ (@@instances ||= {})[Process.pid] = self
12
+ @cache = nil
13
+ @entities = []
14
+ @options = options
15
+ @queries = []
16
+ @search_index = 0
17
+ end
18
+
19
+ def cached_query_for(sql)
20
+ current = @search_index
21
+ match = cache[@search_index..].find do |item|
22
+ current += 1
23
+ item['sql'] == sql # TODo: try matching by normalized query (no values)
24
+ end
25
+ return unless match
26
+
27
+ @search_index = current
28
+ match
29
+
30
+ # cache.shift # TMP
31
+ end
32
+
33
+ def new_entity(model:, id:)
34
+ @entities.push('model' => model, 'id' => id)
35
+ end
36
+
37
+ def pull_entity
38
+ @entities.shift
39
+ end
40
+
41
+ def push(sql:, binds: nil, result: nil, name: nil)
42
+ query = { 'name' => name, 'sql' => sql, 'binds' => binds, 'result' => result }.compact
43
+ @queries.push(query)
44
+ end
45
+
46
+ def start
47
+ @started = true
48
+ storage = Config.storage&.new(self, name: options[:name])
49
+ @from_cache = storage&.load
50
+ yield
51
+ storage&.save unless from_cache
52
+ @started = false
53
+ result = { current_queries: queries.map { _1['sql'] } }
54
+ result[:stored_queries] = cache.map { _1['sql'] } if from_cache
55
+ result
56
+ end
57
+
58
+ class << self
59
+ extend Forwardable
60
+
61
+ def_delegators :current_instance, :cache, :cached_query_for, :from_cache, :new_entity, :pull_entity, :push
62
+
63
+ def current_instance
64
+ (@@instances ||= {})[Process.pid]
65
+ end
66
+
67
+ def started?
68
+ current_instance&.started
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseRecorder
4
+ module RSpec
5
+ module_function
6
+
7
+ def setup
8
+ ::RSpec.configure do |config|
9
+ config.before(:suite) do
10
+ Core.setup
11
+ end
12
+
13
+ config.around(:each, :dbr) do |example|
14
+ ref = (example.metadata[:scoped_id] || '').split(':')[-1]
15
+ options = {}
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
19
+ Recording.new(options: options).tap do |recording|
20
+ result = recording.start { example.run }
21
+ if options[:verify_queries] && result[:stored_queries]
22
+ expect(result[:stored_queries]).to match_array(result[:current_queries])
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseRecorder
4
+ module Storage
5
+ class File
6
+ def initialize(recording, name:)
7
+ @recording = recording
8
+ @name = name
9
+ end
10
+
11
+ def load
12
+ stored_data = ::File.exist?(record_file) ? ::File.read(record_file) : false
13
+ if stored_data
14
+ data = YAML.load(stored_data)
15
+ @recording.cache = data['queries']
16
+ @recording.entities = data['entities']
17
+ true
18
+ else
19
+ false
20
+ end
21
+ end
22
+
23
+ def save
24
+ data = { 'queries' => @recording.queries }
25
+ data['entities'] = @recording.entities if @recording.entities.any?
26
+ serialized_data = data.to_yaml
27
+ ::File.write(record_file, serialized_data)
28
+ end
29
+
30
+ private
31
+
32
+ def normalize_name(string)
33
+ string.gsub(%r{[:/]}, '-').gsub(/[^\w-]/, '_')
34
+ 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
+ end
43
+ end
44
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseRecorder
4
+ module Storage
5
+ class Redis
6
+ def initialize(recording, name:)
7
+ @recording = recording
8
+ @name = name
9
+ end
10
+
11
+ def load
12
+ stored_data = self.class.connection.get(@name)
13
+ if stored_data
14
+ data = JSON.parse(stored_data)
15
+ @recording.cache = data['queries']
16
+ @recording.entities = data['entities']
17
+ true
18
+ else
19
+ false
20
+ end
21
+ end
22
+
23
+ def save
24
+ data = { 'queries' => @recording.queries }
25
+ data['entities'] = @recording.entities if @recording.entities.any?
26
+ serialized_data = data.to_json
27
+ self.class.connection.set(@name, serialized_data)
28
+ end
29
+
30
+ class << self
31
+ def connection
32
+ @connection ||= ::Redis.new
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseRecorder
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'database_recorder/core'
4
+
5
+ if defined? ::ActiveRecord
6
+ require_relative 'database_recorder/activerecord/abstract_adapter_ext'
7
+ require_relative 'database_recorder/activerecord/base_ext'
8
+ require_relative 'database_recorder/activerecord/recorded_result'
9
+ require_relative 'database_recorder/activerecord/recorder'
10
+ end
11
+
12
+ if defined? ::Mysql2
13
+ require_relative 'database_recorder/mysql2/recorded_result'
14
+ require_relative 'database_recorder/mysql2/recorder'
15
+ end
16
+
17
+ if defined? ::PG
18
+ require_relative 'database_recorder/pg/recorded_result'
19
+ require_relative 'database_recorder/pg/recorder'
20
+ end
21
+
22
+ require_relative 'database_recorder/storage/file'
23
+ require_relative 'database_recorder/storage/redis'
24
+
25
+ require_relative 'database_recorder/rspec'
26
+ require_relative 'database_recorder/recording'
27
+ require_relative 'database_recorder/config'
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: database_recorder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mattia Roccoberton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-04-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: coderay
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ description: Record application queries, verify them against stored queries, and replay
28
+ them.
29
+ email:
30
+ - mat@blocknot.es
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - MIT-LICENSE
36
+ - README.md
37
+ - lib/database_recorder.rb
38
+ - lib/database_recorder/activerecord/abstract_adapter_ext.rb
39
+ - lib/database_recorder/activerecord/base_ext.rb
40
+ - lib/database_recorder/activerecord/recorded_result.rb
41
+ - lib/database_recorder/activerecord/recorder.rb
42
+ - lib/database_recorder/config.rb
43
+ - lib/database_recorder/core.rb
44
+ - lib/database_recorder/mysql2/recorded_result.rb
45
+ - lib/database_recorder/mysql2/recorder.rb
46
+ - lib/database_recorder/pg/recorded_result.rb
47
+ - lib/database_recorder/pg/recorder.rb
48
+ - lib/database_recorder/recording.rb
49
+ - lib/database_recorder/rspec.rb
50
+ - lib/database_recorder/storage/file.rb
51
+ - lib/database_recorder/storage/redis.rb
52
+ - lib/database_recorder/version.rb
53
+ homepage: https://github.com/blocknotes/database_recorder
54
+ licenses:
55
+ - MIT
56
+ metadata:
57
+ homepage_uri: https://github.com/blocknotes/database_recorder
58
+ source_code_uri: https://github.com/blocknotes/database_recorder
59
+ rubygems_mfa_required: 'true'
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.6.0
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.1.6
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: A SQL database recorder
79
+ test_files: []