tape_deck 0.1.0.beta1 → 0.1.0

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.
@@ -13,11 +13,14 @@ module TapeDeck
13
13
  # @param recorder [Recorder]
14
14
  # @param filter [Filter] narrows what is compared when verifying
15
15
  # @param on_schema_change [Symbol] see {Configuration#on_schema_change}
16
- def initialize(tape, recorder, filter, on_schema_change: :error)
16
+ # @param new_rows [NewRows] replaces the ids of new rows with placeholders when recording. Verifying uses the rules
17
+ # the tape was recorded with.
18
+ def initialize(tape, recorder, filter, on_schema_change: :error, new_rows: NewRows.new(recorder))
17
19
  @tape = tape
18
20
  @recorder = recorder
19
21
  @filter = filter
20
22
  @on_schema_change = on_schema_change
23
+ @new_rows = new_rows
21
24
  @before = recorder.snapshot
22
25
  end
23
26
 
@@ -31,16 +34,20 @@ module TapeDeck
31
34
  return if discard
32
35
 
33
36
  after = @recorder.snapshot(previous: @before)
34
- actual = round_trip(@recorder.diff(@before, after))
35
- exclusions = @recorder.exclusions
37
+ diff = round_trip(@recorder.diff(@before, after))
38
+ current = @recorder.resolve
36
39
  if tape.recording?
37
- tape.write(actual, recorded: {
40
+ tape.write(@new_rows.apply(diff), tape_deck: {
38
41
  "exclude_tables" => @recorder.excluded_tables,
39
42
  "exclude_columns" => @recorder.excluded_columns,
40
- "databases" => exclusions
43
+ "include_views" => @recorder.included_views,
44
+ **@new_rows.rules,
45
+ "databases" => current
41
46
  })
42
47
  else
43
- verify(actual, exclusions)
48
+ # Numbering only changes how ids are written, so the tape's own rules apply, whatever the configuration says now
49
+ actual = NewRows.recorded_with(@recorder, tape.read.fetch("tape_deck")).apply(diff)
50
+ verify(actual, current)
44
51
  end
45
52
  end
46
53
 
@@ -49,34 +56,50 @@ module TapeDeck
49
56
  # @return [Hash] the diff with the same shape as a tape read from disk
50
57
  def round_trip(diff) = YAML.safe_load(Tape.serialize(diff))
51
58
 
52
- # Compares the watched databases only. What is excluded now, but is on the tape, is dropped from both sides.
53
- def verify(actual, exclusions)
54
- recorded = tape.read.fetch("recorded")
55
- tape_exclusions = recorded.fetch("databases").slice(*exclusions.keys)
56
- report_schema_changes(recorded, tape_exclusions)
57
- filter = @filter.ignoring(newly_excluded(tape_exclusions, exclusions))
58
- expected = filter.apply(tape.read.fetch("diff").slice(*exclusions.keys))
59
- actual = filter.apply(actual)
59
+ # Compares the watched databases only. What the current rules leave out, but is on the tape, is dropped from both
60
+ # sides. So are views a schema change added.
61
+ #
62
+ # @param current [Hash] what the current rules resolve to, see {Recorder#resolve}
63
+ def verify(actual, current)
64
+ recorded = tape.read.fetch("tape_deck")
65
+ on_tape = recorded.fetch("databases").slice(*current.keys)
66
+ by_recorded_rules = @recorder.resolve(
67
+ exclude_tables: recorded.fetch("exclude_tables"),
68
+ exclude_columns: recorded.fetch("exclude_columns"),
69
+ include_views: recorded.fetch("include_views")
70
+ )
71
+ report_schema_changes(on_tape, by_recorded_rules)
72
+ ignored = newly_excluded(on_tape, current) + changed_views(on_tape, current, by_recorded_rules)
73
+ filter = @filter.ignoring(ignored)
74
+ expected = in_stable_order(filter.apply(tape.read.fetch("diff").slice(*current.keys)))
75
+ actual = in_stable_order(filter.apply(actual))
60
76
  raise UnexpectedDiffError.new(tape, expected, actual) unless expected == actual
61
77
  end
62
78
 
63
- # The recorded exclude rules, applied to the current schema, must leave out what they did when recording.
64
- def report_schema_changes(recorded, tape_exclusions)
79
+ # The order of rows on a tape is for reading only, so it must not decide whether the tape matches. Filtered rows
80
+ # need it too, because their order may have depended on values that are ignored now.
81
+ def in_stable_order(diff)
82
+ diff.transform_values do |tables|
83
+ tables.transform_values { |changes| changes.transform_values { Rows.sort(_1) } }
84
+ end
85
+ end
86
+
87
+ # The recorded rules, applied to the current schema, must leave out and include what they did when recording.
88
+ def report_schema_changes(on_tape, by_recorded_rules)
65
89
  return if @on_schema_change == :ignore
66
90
 
67
- now = @recorder.exclusions(
68
- exclude_tables: recorded.fetch("exclude_tables"),
69
- exclude_columns: recorded.fetch("exclude_columns")
70
- )
71
- changes = tape_exclusions.filter_map do |db_name, names|
72
- added = now.fetch(db_name) - names
73
- removed = names - now.fetch(db_name)
74
- "#{db_name}: added #{added.inspect}, removed #{removed.inspect}" unless added.empty? && removed.empty?
91
+ changes = on_tape.flat_map do |db_name, lists|
92
+ Tape::DATABASE_LISTS.filter_map do |list|
93
+ now = by_recorded_rules.fetch(db_name).fetch(list)
94
+ added = now - lists.fetch(list)
95
+ removed = lists.fetch(list) - now
96
+ "#{db_name} #{list}: added #{added.inspect}, removed #{removed.inspect}" unless added.empty? && removed.empty?
97
+ end
75
98
  end
76
99
  return if changes.empty?
77
100
 
78
- message = "schema changed since tape #{tape.file} was recorded, its exclude rules now match " \
79
- "other tables or columns (#{changes.join(", ")})"
101
+ message = "schema changed since tape #{tape.file} was recorded, its recording rules now match " \
102
+ "other tables, columns or views (#{changes.join(", ")})"
80
103
  raise IncompatibleTapeError, message if @on_schema_change == :error
81
104
 
82
105
  warn "TapeDeck: #{message}"
@@ -84,16 +107,36 @@ module TapeDeck
84
107
 
85
108
  # @return [Array<String>] patterns for what is excluded now, but was recorded on the tape
86
109
  # @raise [IncompatibleTapeError] if the tape left out something that is no longer excluded
87
- def newly_excluded(tape_exclusions, exclusions)
88
- exclusions.flat_map do |db_name, names|
89
- tape_names = tape_exclusions.fetch(db_name)
110
+ def newly_excluded(on_tape, current)
111
+ current.flat_map do |db_name, lists|
112
+ tape_names = on_tape.fetch(db_name).fetch("excluded")
90
113
  no_longer_excluded = tape_names.reject { @recorder.excluded?(db_name, _1) }
91
114
  unless no_longer_excluded.empty?
92
115
  raise IncompatibleTapeError, "tape #{tape.file} left out #{no_longer_excluded.inspect} of #{db_name}, " \
93
116
  "which is no longer excluded. Re-record the tape."
94
117
  end
95
118
 
96
- (names - tape_names).map { Patterns.escape("#{db_name}.#{_1}") }
119
+ (lists.fetch("excluded") - tape_names).map { Patterns.escape("#{db_name}.#{_1}") }
120
+ end
121
+ end
122
+
123
+ # A view on the tape that is no longer included is not compared. Neither is one included now because a schema
124
+ # change added it, which {#report_schema_changes} reports.
125
+ #
126
+ # @return [Array<String>] patterns for the views not to compare
127
+ # @raise [IncompatibleTapeError] if a view is included now by changed rules, so the tape lacks it
128
+ def changed_views(on_tape, current, by_recorded_rules)
129
+ current.flat_map do |db_name, lists|
130
+ tape_views = on_tape.fetch(db_name).fetch("views")
131
+ added = lists.fetch("views") - tape_views
132
+ newly_included = added - by_recorded_rules.fetch(db_name).fetch("views")
133
+ unless newly_included.empty?
134
+ raise IncompatibleTapeError, "tape #{tape.file} has no recording of the views #{newly_included.inspect} " \
135
+ "of #{db_name}, which are included now. Re-record the tape."
136
+ end
137
+
138
+ no_longer_included = tape_views - lists.fetch("views")
139
+ (no_longer_included + added).map { Patterns.escape("#{db_name}.#{_1}") }
97
140
  end
98
141
  end
99
142
  end
@@ -4,23 +4,33 @@ module TapeDeck
4
4
  # One recorded diff, stored as a YAML file. Like a +File+, a tape knows where it lives, whether it is there to be
5
5
  # recorded or verified, and how to read and write itself. It never touches a database.
6
6
  #
7
- # Next to the diff, a tape stores what was recorded: the exclude rules, the watched databases, and per database the
8
- # tables and columns the rules left out. Empty lists are left out of the file.
7
+ # Next to the diff, a tape stores everything that shaped it: the TapeDeck version that recorded it, the recording
8
+ # rules, the watched databases, and per database the tables and columns the rules left out and the views they
9
+ # included. Empty lists are left out of the file.
9
10
  #
10
- # recorded:
11
+ # tape_deck:
12
+ # version: 0.1.0
11
13
  # exclude_tables: [shop.sessions]
12
14
  # exclude_columns: ["*.created_at"]
15
+ # include_views: [shop.order_totals]
13
16
  # databases:
14
17
  # shop:
15
18
  # excluded: [orders.created_at, sessions]
19
+ # views: [order_totals]
16
20
  # billing:
17
21
  # diff:
18
22
  # shop:
19
23
  # orders:
20
24
  # inserted: [...]
21
25
  class Tape
22
- # Lists in the +recorded+ section of a tape file, written on one line.
23
- RECORDED_LISTS = %w[exclude_tables exclude_columns excluded].freeze
26
+ # Recording rules at the top of the +tape_deck+ section that are lists of patterns.
27
+ RULES = %w[exclude_tables exclude_columns include_views keep_new_ids number_by_content generated_keys].freeze
28
+
29
+ # Recording rules at the top of the +tape_deck+ section that map a table to patterns.
30
+ TABLE_RULES = %w[references_to].freeze
31
+
32
+ # Lists per database in the +tape_deck+ section.
33
+ DATABASE_LISTS = %w[excluded views].freeze
24
34
 
25
35
  # Supported values for the +:record+ option:
26
36
  # [+:once+] record if the tape file is missing, otherwise verify
@@ -50,21 +60,16 @@ module TapeDeck
50
60
  raise ArgumentError, "invalid record mode #{mode.inspect}, use one of #{RECORD_MODES.inspect}"
51
61
  end
52
62
 
53
- # Like {.serialize}, but writes the {RECORDED_LISTS} on one line, so the +recorded+ section stays short.
63
+ # Like {.serialize}, but writes the lists of the +tape_deck+ section on one line, so it stays short.
54
64
  #
55
65
  # @param content [Hash] see {#read}
56
66
  # @return [String] the YAML stored in a tape file
57
67
  def self.dump(content)
58
68
  tree = Psych::Visitors::YAMLTree.create(line_width: -1)
59
69
  tree << content
60
- tree.tree.each do |node|
61
- next unless node.is_a?(Psych::Nodes::Mapping)
62
-
63
- node.children.each_slice(2) do |key, value|
64
- recorded_list = RECORDED_LISTS.include?(key.value) && value.is_a?(Psych::Nodes::Sequence)
65
- value.style = Psych::Nodes::Sequence::FLOW if recorded_list
66
- end
67
- end
70
+ root = tree.tree.children.first.children.first
71
+ section = root.children.each_slice(2).find { |key, _| key.value == "tape_deck" }&.last
72
+ section&.each { _1.style = Psych::Nodes::Sequence::FLOW if _1.is_a?(Psych::Nodes::Sequence) }
68
73
  tree.tree.yaml(nil, line_width: -1)
69
74
  end
70
75
 
@@ -95,29 +100,37 @@ module TapeDeck
95
100
  end
96
101
 
97
102
  # @return [String] the tape name with {UNSAFE_FILE_NAME_CHARS} replaced, plus +.yml+
98
- def file_name
99
- "#{name.gsub(UNSAFE_FILE_NAME_CHARS, "_")}.yml"
100
- end
103
+ def file_name = self.class.file_name_for(name)
104
+
105
+ # Different tape names can share a file name, e.g. +"an order"+ and +"an_order"+.
106
+ #
107
+ # @param name [String] tape name
108
+ # @return [String] the name with {UNSAFE_FILE_NAME_CHARS} replaced, plus +.yml+
109
+ def self.file_name_for(name) = "#{name.to_s.gsub(UNSAFE_FILE_NAME_CHARS, "_")}.yml"
101
110
 
102
111
  # Reads the tape file once, later calls return the same content.
103
112
  #
104
- # @return [Hash] +"recorded"+: see {#write}, and +"diff"+: the stored diff
113
+ # @return [Hash] +"tape_deck"+: see {#write}, and +"diff"+: the stored diff
105
114
  # @raise [TapeLoadingError] if the file is not valid YAML or does not hold a recording
106
115
  def read
107
116
  @content ||= load
108
117
  end
109
118
 
119
+ # Also stores the version of TapeDeck that recorded the tape.
120
+ #
110
121
  # @param diff [Hash] the diff to store in the tape file, replacing its content
111
- # @param recorded [Hash] +"exclude_tables"+ and +"exclude_columns"+: the exclude rules, +"databases"+: database ->
112
- # names the rules left out of it, as returned by {Recorder#exclusions}
122
+ # @param tape_deck [Hash] the {RULES} and {TABLE_RULES}, and +"databases"+: what they resolved to in each
123
+ # database, as returned by {Recorder#resolve}
113
124
  # @return [void]
114
- def write(diff, recorded:)
115
- databases = recorded.fetch("databases").transform_values do |names|
116
- {"excluded" => names} unless names.empty?
125
+ def write(diff, tape_deck:)
126
+ databases = tape_deck.fetch("databases").transform_values do |lists|
127
+ non_empty = lists.slice(*DATABASE_LISTS).reject { |_, list| list.empty? }
128
+ non_empty unless non_empty.empty?
117
129
  end
118
- patterns = recorded.slice("exclude_tables", "exclude_columns").reject { |_, list| list.empty? }
130
+ rules = tape_deck.slice(*RULES, *TABLE_RULES).reject { |_, rule| rule.empty? }
131
+ content = {"tape_deck" => {"version" => VERSION, **rules, "databases" => databases}, "diff" => diff}
119
132
  FileUtils.mkdir_p(File.dirname(file))
120
- File.write(file, self.class.dump("recorded" => {**patterns, "databases" => databases}, "diff" => diff))
133
+ File.write(file, self.class.dump(content))
121
134
  @content = nil
122
135
  end
123
136
 
@@ -127,14 +140,14 @@ module TapeDeck
127
140
  content = YAML.safe_load_file(file)
128
141
  raise TapeLoadingError, "tape #{file} does not hold a recording" unless valid_content?(content)
129
142
 
130
- recorded = content["recorded"]
131
- databases = recorded["databases"].transform_values { Array(_1&.dig("excluded")) }
143
+ section = content["tape_deck"]
144
+ databases = section["databases"].transform_values do |lists|
145
+ DATABASE_LISTS.to_h { [_1, Array(lists&.dig(_1))] }
146
+ end
147
+ rules = RULES.to_h { [_1, Array(section[_1])] }
148
+ table_rules = TABLE_RULES.to_h { [_1, section[_1].to_h.transform_values { |patterns| Array(patterns) }] }
132
149
  {
133
- "recorded" => {
134
- "exclude_tables" => Array(recorded["exclude_tables"]),
135
- "exclude_columns" => Array(recorded["exclude_columns"]),
136
- "databases" => databases
137
- },
150
+ "tape_deck" => {"version" => section["version"], **rules, **table_rules, "databases" => databases},
138
151
  "diff" => content["diff"]
139
152
  }
140
153
  rescue Psych::Exception => e
@@ -144,8 +157,8 @@ module TapeDeck
144
157
  def valid_content?(content)
145
158
  return false unless content.is_a?(Hash) && content["diff"].is_a?(Hash)
146
159
 
147
- recorded = content["recorded"]
148
- databases = recorded["databases"] if recorded.is_a?(Hash)
160
+ section = content["tape_deck"]
161
+ databases = section["databases"] if section.is_a?(Hash)
149
162
  databases.is_a?(Hash) && databases.values.all? { _1.nil? || _1.is_a?(Hash) }
150
163
  end
151
164
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module TapeDeck
4
4
  # Version of the gem.
5
- VERSION = "0.1.0.beta1"
5
+ VERSION = "0.1.0"
6
6
  end
data/lib/tape_deck.rb CHANGED
@@ -6,6 +6,7 @@ require "date"
6
6
  require "time"
7
7
  require "digest"
8
8
  require "yaml"
9
+ require "json"
9
10
  require "fileutils"
10
11
 
11
12
  require_relative "tape_deck/version"
@@ -13,11 +14,14 @@ require_relative "tape_deck/errors"
13
14
  require_relative "tape_deck/configuration"
14
15
  require_relative "tape_deck/tape"
15
16
  require_relative "tape_deck/patterns"
17
+ require_relative "tape_deck/rows"
18
+ require_relative "tape_deck/new_rows"
16
19
  require_relative "tape_deck/filter"
17
20
  require_relative "tape_deck/session"
21
+ require_relative "tape_deck/adapters"
18
22
  require_relative "tape_deck/recorder"
19
23
 
20
- # Records what a block of code changes in several MySQL databases, and checks later runs against that recording.
24
+ # Records what a block of code changes in several databases, and checks later runs against that recording.
21
25
  # API, naming and flow inspired by the VCR gem.
22
26
  #
23
27
  # == Tapes
@@ -30,15 +34,19 @@ require_relative "tape_deck/recorder"
30
34
  # statements that caused it or their order, so INSERT/UPDATE/DELETE statements generated from it can go wrong in many
31
35
  # ways: foreign key constraints, generated columns, triggers.
32
36
  #
33
- # Next to the diff, a tape stores what was recorded: the exclude rules, the watched databases, and the tables and
34
- # columns the rules left out of each.
37
+ # Next to the diff, a tape stores everything that shaped it: the TapeDeck version that recorded it, the recording
38
+ # rules, the watched databases, and per database the tables and columns the rules left out and the views they
39
+ # included. Empty lists are left out of the file.
35
40
  #
36
- # recorded:
41
+ # tape_deck:
42
+ # version: 0.1.0
37
43
  # exclude_tables: [shop.sessions]
38
44
  # exclude_columns: ["*.created_at"]
45
+ # include_views: [shop.order_totals]
39
46
  # databases:
40
47
  # shop:
41
48
  # excluded: [orders.created_at, sessions]
49
+ # views: [order_totals]
42
50
  # billing:
43
51
  # diff:
44
52
  # shop:
@@ -92,6 +100,45 @@ require_relative "tape_deck/recorder"
92
100
  # matches dots), so +"*.created_at"+ matches that column in every table of every database, and +"shop.*"+ matches
93
101
  # everything in +shop+.
94
102
  #
103
+ # == New rows
104
+ #
105
+ # The ids a database hands out depend on where its counters stand, and those move with every insert, also with ones
106
+ # that were rolled back. So a tape doesn't store the generated keys of inserted rows. It numbers them per table in the
107
+ # order the rows were created, and stores placeholders like +"new shop.orders 1"+. Declared foreign keys to those rows
108
+ # get the same placeholder, in inserted rows and in updated values. Ids of rows that existed before stay as they are.
109
+ #
110
+ # orders:
111
+ # inserted:
112
+ # - id: new shop.orders 1
113
+ # items:
114
+ # inserted:
115
+ # - id: new shop.items 1
116
+ # order_id: new shop.orders 1
117
+ #
118
+ # A key is generated if it is the single, auto increment primary key of its table. Rows are numbered in creation
119
+ # order, so creating the same rows in a different order fails verification. Where that order doesn't matter, like an
120
+ # import, {Configuration#number_by_content} numbers a table's rows by their content instead.
121
+ # {Configuration#keep_new_ids} keeps the real ids of some tables.
122
+ #
123
+ # References across databases, and ones without a foreign key, need {Configuration#references_to}. Keys that are
124
+ # generated, but not auto increment, like UUIDs, need {Configuration#generated_keys}. Those have no creation order, so
125
+ # their rows are numbered by content.
126
+ #
127
+ # The numbering rules are stored on the tape, and verifying uses them, not the current configuration. Numbering only
128
+ # changes how ids are written, so a tape keeps verifying the way it was recorded. New rules apply to tapes recorded
129
+ # afterwards.
130
+ #
131
+ # == Views
132
+ #
133
+ # Views are left out by default. A view holds no data of its own, so the changes of the tables behind it are already
134
+ # on the tape. {Configuration#include_views} adds views to snapshots, to also notice a changed view definition, or data
135
+ # a view reads from tables that are not recorded. Included views are read in full on every snapshot, and compared as a
136
+ # multiset of rows (+inserted+/+deleted+ only), because they have no primary key.
137
+ #
138
+ # Like the exclude rules, the tape stores the include rules and the views they matched. When verifying, a view on the
139
+ # tape that is no longer included is not compared. A view that is included now, but not on the tape, raises
140
+ # {IncompatibleTapeError}, unless the recorded rules match it too: then a schema change added it, see below.
141
+ #
95
142
  # == Excluding and ignoring
96
143
  #
97
144
  # There are two ways to leave data out, and they act at different moments.
@@ -103,7 +150,8 @@ require_relative "tape_deck/recorder"
103
150
  # {IncompatibleTapeError} and the tape needs re-recording.
104
151
  #
105
152
  # The stored rules also reveal schema changes. When verifying, they are applied to the current schema again. If they
106
- # leave out other tables or columns than when recording, a table or column matching them was added or removed.
153
+ # leave out other tables or columns, or include other views than when recording, a matching table, column or view was
154
+ # added or removed.
107
155
  # {Configuration#on_schema_change} decides whether that raises {IncompatibleTapeError}, warns, or is ignored.
108
156
  #
109
157
  # *Ignore* means recorded, but not compared. A tape always holds everything that is not excluded. When verifying, one
@@ -122,6 +170,7 @@ require_relative "tape_deck/recorder"
122
170
  # c.default_record_mode = :once
123
171
  # c.exclude_tables "shop.sessions", "*.schema_migrations"
124
172
  # c.exclude_columns "*.created_at", "*.updated_at", "shop.users.last_seen_at"
173
+ # c.include_views "shop.order_totals"
125
174
  # c.on_schema_change = :warn
126
175
  # c.configure_rspec_metadata!
127
176
  # end
@@ -224,10 +273,18 @@ module TapeDeck
224
273
  recorder = Recorder.new(
225
274
  select_databases(tape_databases(tape, databases)),
226
275
  exclude_tables: configuration.excluded_tables,
227
- exclude_columns: configuration.excluded_columns
276
+ exclude_columns: configuration.excluded_columns,
277
+ include_views: configuration.included_views
228
278
  )
229
279
  filter = Filter.new(only: only, ignore: ignore)
230
- sessions.push(Session.new(tape, recorder, filter, on_schema_change: configuration.on_schema_change))
280
+ new_rows = NewRows.new(
281
+ recorder,
282
+ keep: configuration.kept_new_ids,
283
+ by_content: configuration.numbered_by_content,
284
+ generated_keys: configuration.generated_key_columns,
285
+ references: configuration.reference_columns
286
+ )
287
+ sessions.push(Session.new(tape, recorder, filter, on_schema_change: configuration.on_schema_change, new_rows: new_rows))
231
288
  tape
232
289
  end
233
290
 
@@ -266,7 +323,7 @@ module TapeDeck
266
323
  def tape_databases(tape, names)
267
324
  return names if tape.recording?
268
325
 
269
- recorded = tape.read.fetch("recorded").fetch("databases").keys
326
+ recorded = tape.read.fetch("tape_deck").fetch("databases").keys
270
327
  names = Array(names || recorded).map(&:to_s)
271
328
  missing = names - recorded
272
329
  unless missing.empty?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tape_deck
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.beta1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Schulze
@@ -40,7 +40,7 @@ dependencies:
40
40
  description: Snapshots your databases around an action and stores the row-level diff
41
41
  as a YAML tape. Later runs of the same action are verified against the tape, so
42
42
  you notice when a change or update alters what gets written. VCR-style API, works
43
- with Sequel and MySQL.
43
+ with Sequel and MySQL, MariaDB, PostgreSQL or SQLite.
44
44
  email:
45
45
  - robert@dotless.de
46
46
  executables: []
@@ -57,11 +57,18 @@ files:
57
57
  - README.md
58
58
  - Rakefile
59
59
  - lib/tape_deck.rb
60
+ - lib/tape_deck/adapters.rb
61
+ - lib/tape_deck/adapters/base.rb
62
+ - lib/tape_deck/adapters/mysql.rb
63
+ - lib/tape_deck/adapters/postgres.rb
64
+ - lib/tape_deck/adapters/sqlite.rb
60
65
  - lib/tape_deck/configuration.rb
61
66
  - lib/tape_deck/errors.rb
62
67
  - lib/tape_deck/filter.rb
68
+ - lib/tape_deck/new_rows.rb
63
69
  - lib/tape_deck/patterns.rb
64
70
  - lib/tape_deck/recorder.rb
71
+ - lib/tape_deck/rows.rb
65
72
  - lib/tape_deck/session.rb
66
73
  - lib/tape_deck/tape.rb
67
74
  - lib/tape_deck/version.rb
@@ -71,7 +78,7 @@ licenses:
71
78
  - MIT
72
79
  metadata:
73
80
  homepage_uri: https://github.com/fnordfish/tape_deck
74
- documentation_uri: https://fnordfish.github.io/tape_deck/docs/v0.1.0.beta1/
81
+ documentation_uri: https://fnordfish.github.io/tape_deck/docs/v0.1.0/
75
82
  changelog_uri: https://github.com/fnordfish/tape_deck/blob/main/CHANGELOG.md
76
83
  rubygems_mfa_required: 'true'
77
84
  rdoc_options: []