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.
- checksums.yaml +4 -4
- data/.devcontainer/compose.yml +18 -2
- data/CHANGELOG.md +18 -0
- data/DEVELOPMENT.md +29 -18
- data/README.md +79 -14
- data/Rakefile +50 -15
- data/lib/tape_deck/adapters/base.rb +96 -0
- data/lib/tape_deck/adapters/mysql.rb +20 -0
- data/lib/tape_deck/adapters/postgres.rb +29 -0
- data/lib/tape_deck/adapters/sqlite.rb +10 -0
- data/lib/tape_deck/adapters.rb +22 -0
- data/lib/tape_deck/configuration.rb +116 -3
- data/lib/tape_deck/filter.rb +1 -2
- data/lib/tape_deck/new_rows.rb +168 -0
- data/lib/tape_deck/recorder.rb +97 -90
- data/lib/tape_deck/rows.rb +16 -0
- data/lib/tape_deck/session.rb +73 -30
- data/lib/tape_deck/tape.rb +47 -34
- data/lib/tape_deck/version.rb +1 -1
- data/lib/tape_deck.rb +65 -8
- metadata +10 -3
data/lib/tape_deck/session.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
37
|
+
diff = round_trip(@recorder.diff(@before, after))
|
|
38
|
+
current = @recorder.resolve
|
|
36
39
|
if tape.recording?
|
|
37
|
-
tape.write(
|
|
40
|
+
tape.write(@new_rows.apply(diff), tape_deck: {
|
|
38
41
|
"exclude_tables" => @recorder.excluded_tables,
|
|
39
42
|
"exclude_columns" => @recorder.excluded_columns,
|
|
40
|
-
"
|
|
43
|
+
"include_views" => @recorder.included_views,
|
|
44
|
+
**@new_rows.rules,
|
|
45
|
+
"databases" => current
|
|
41
46
|
})
|
|
42
47
|
else
|
|
43
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
79
|
-
"other tables or
|
|
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(
|
|
88
|
-
|
|
89
|
-
tape_names =
|
|
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
|
-
(
|
|
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
|
data/lib/tape_deck/tape.rb
CHANGED
|
@@ -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
|
|
8
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
23
|
-
|
|
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
|
|
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.
|
|
61
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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] +"
|
|
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
|
|
112
|
-
#
|
|
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,
|
|
115
|
-
databases =
|
|
116
|
-
{
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
131
|
-
databases =
|
|
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
|
-
"
|
|
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
|
-
|
|
148
|
-
databases =
|
|
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
|
data/lib/tape_deck/version.rb
CHANGED
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
|
|
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
|
|
34
|
-
# columns the rules left out
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|
|
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
|
|
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: []
|