database_recorder 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d51e080ba82dada7af93fbb37380af72e25fef001ecc3f691080048b951f67f
4
- data.tar.gz: 5a902b17cab7f30b5cedae2b1e00e374fdc58b3e992cf3bc08d1be8421f37421
3
+ metadata.gz: d0b2575a34a9e68ee59f68884a492a33c071a05b9d9999caf3baeca5c070c11d
4
+ data.tar.gz: 29b78920c82393f3e9a23d334b7675d29dc96f3b77f13ff1bf33384cc3206a0d
5
5
  SHA512:
6
- metadata.gz: 936d740c398c162be09d7f9631c64ad3c516a55ab787991104df7e32bd53299ebf1d7b50d0f6b605f7118e495a7c7e1d551f2029085d123451291a6c054547f5
7
- data.tar.gz: 260f2d6c7bd1d3b65c67a25db41fce5efbd65dc19f294af7e852003c08e78126d58d0f6679b48299ff2ad0646eb96577f8ffc2383e137278e44f222d13326f70
6
+ metadata.gz: 0c7c56cc0c95e24b410ec5d63ac64b1944a20d7084d63ae64b0d80ea265b815435d2c3ce14eec3fb9d2f69261c567ddd59eaba24b4ca0c28f3993272fbb2e33b
7
+ data.tar.gz: 78db7a6377ba564f5e039bd74f32ffe4c4f6d6c6ff5719340818738a7a4ea40caf4f2164da1cf72e1ebb3800863e1921d3c2bbf32952ff00dc1154487367fe4a
data/README.md CHANGED
@@ -1,34 +1,66 @@
1
1
  # Database Recorder
2
+ [![Gem Version](https://badge.fury.io/rb/database_recorder.svg)](https://badge.fury.io/rb/database_recorder)
3
+ [![Linters](https://github.com/blocknotes/database_recorder/actions/workflows/linters.yml/badge.svg)](https://github.com/blocknotes/database_recorder/actions/workflows/linters.yml)
4
+ [![Specs ActiveRecord](https://github.com/blocknotes/database_recorder/actions/workflows/specs_active_record.yml/badge.svg)](https://github.com/blocknotes/database_recorder/actions/workflows/specs_active_record.yml)
5
+ [![Specs MySQL](https://github.com/blocknotes/database_recorder/actions/workflows/specs_mysql.yml/badge.svg)](https://github.com/blocknotes/database_recorder/actions/workflows/specs_mysql.yml)
6
+ [![Specs PostgreSQL](https://github.com/blocknotes/database_recorder/actions/workflows/specs_postgres.yml/badge.svg)](https://github.com/blocknotes/database_recorder/actions/workflows/specs_postgres.yml)
2
7
 
3
- Record database queries for testing and development purposes only.
4
- Support only RSpec at the moment, storing logs data on files or Redis.
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
- See below for more details.
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
- - 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
+ - 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
+ ![image1](extra/image1.png)
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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseRecorder
4
+ module ActiveRecord
5
+ module AbstractAdapterExt
6
+ def log(*args)
7
+ Recorder.record(self, *args) do
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -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
- attr_accessor :db_driver, :print_queries, :replay_recordings, :storage
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, :db_driver=, :print_queries, :print_queries=, :replay_recordings,
16
- :replay_recordings=, :storage
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 = :active_record
20
- instance.print_queries = false # false | true | :color
29
+ instance.db_driver = DEFAULT_DB_DRIVER
30
+ instance.print_queries = false
21
31
  instance.replay_recordings = false
22
- self.storage = :file # :file | :redis
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
- 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}"
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
- 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
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 # < ::Mysql2::Result
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['count']
19
- @fields = data['fields']
20
- @entries = data['values']
21
- # @values = data['values']
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
- def query(sql, options = {})
7
- Mysql2.record(self, sql: sql) do
8
- super
9
- end
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
- module_function
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
- 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
27
+ def setup
28
+ ::Mysql2::Client.class_eval do
29
+ prepend ClientExt
30
+ end
21
31
 
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)
32
+ ::Mysql2::Statement.class_eval do
33
+ prepend StatementExt
43
34
  end
44
35
  end
45
- end
46
36
 
47
- def setup
48
- ::Mysql2::Client.class_eval do
49
- prepend Recorder
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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseRecorder
4
+ module Mysql2
5
+ module StatementExt
6
+ def execute(*args, **kwargs)
7
+ Recorder.store_prepared_statement(self, source: :execute, binds: args) do
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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['count']
15
- @fields = data['fields']
16
- @values = data['values']
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
- def async_exec(sql)
7
- PG.record(sql: sql, source: :async_exec) do
8
- super
9
- end
10
- end
6
+ module_function
11
7
 
12
- def sync_exec(sql)
13
- PG.record(sql: sql, source: :sync_exec) do
14
- super
15
- end
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
- # def exec(*args)
19
- # puts "-E- #{args[0]}"
20
- # super
21
- # end
14
+ def format_result(result)
15
+ { count: result.count, fields: result.fields, values: result.values } if result
16
+ end
22
17
 
23
- # def query(*args)
24
- # puts "-Q- #{args[0]}"
25
- # super
26
- # end
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 exec_params(*args)
29
- PG.record(sql: args[0], binds: args[1], source: :exec_params) do
30
- super
23
+ def setup
24
+ ::PG::Connection.class_eval do
25
+ prepend ConnectionExt
31
26
  end
32
27
  end
33
28
 
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
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
- def exec_prepared(*args)
45
- PG.record(sql: args[0], binds: args[1], source: :exec_prepared) do
46
- super
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
- module_function
47
+ def store_query(name: nil, sql: nil, binds: nil, source: nil)
48
+ return yield if ignore_query?(sql)
52
49
 
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
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
- 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)
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['sql'] == sql # TODo: try matching by normalized query (no values)
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('model' => model, 'id' => id)
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, name: nil)
42
- query = { 'name' => name, 'sql' => sql, 'binds' => binds, 'result' => result }.compact
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 { _1['sql'] } }
54
- result[:stored_queries] = cache.map { _1['sql'] } if from_cache
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, :pull_entity, :push
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])
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseRecorder
4
+ module Storage
5
+ class Base
6
+ def initialize(recording, name:, options: {})
7
+ @recording = recording
8
+ @name = name
9
+ @options = options
10
+ end
11
+ end
12
+ end
13
+ end
@@ -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?(record_file) ? ::File.read(record_file) : false
7
+ stored_data = ::File.exist?(storage_path) ? ::File.read(storage_path) : false
13
8
  if stored_data
14
- data = YAML.load(stored_data)
15
- @recording.cache = data['queries']
16
- @recording.entities = data['entities']
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 = { '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)
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 initialize(recording, name:)
7
- @recording = recording
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 = self.class.connection.get(@name)
11
+ stored_data = connection.get(@name)
13
12
  if stored_data
14
- data = JSON.parse(stored_data)
15
- @recording.cache = data['queries']
16
- @recording.entities = data['entities']
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 = { 'queries' => @recording.queries }
25
- data['entities'] = @recording.entities if @recording.entities.any?
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
- self.class.connection.set(@name, serialized_data)
28
- end
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :nocov:
3
4
  module DatabaseRecorder
4
- VERSION = '0.1.0'
5
+ VERSION = '0.2.1'
5
6
  end
7
+ # :nocov:
@@ -3,22 +3,22 @@
3
3
  require_relative 'database_recorder/core'
4
4
 
5
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'
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
- if defined? ::Mysql2
13
- require_relative 'database_recorder/mysql2/recorded_result'
14
- require_relative 'database_recorder/mysql2/recorder'
15
- end
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
- if defined? ::PG
18
- require_relative 'database_recorder/pg/recorded_result'
19
- require_relative 'database_recorder/pg/recorder'
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.0
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-08 00:00:00.000000000 Z
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/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
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