activerecord-duplicator 0.1.0 → 0.3.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/CHANGELOG.md +47 -0
- data/README.md +37 -0
- data/lib/active_record/duplicator/handler_api.rb +5 -0
- data/lib/active_record/duplicator/session.rb +61 -46
- data/lib/active_record/duplicator/version.rb +1 -1
- data/lib/active_record/duplicator.rb +14 -1
- data/sig/generated/active_record/duplicator/handler_api.rbs +3 -0
- data/sig/generated/active_record/duplicator/session.rbs +44 -16
- data/sig/generated/active_record/duplicator.rbs +10 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee612e84f80aefec6371c6e900d058e9934b7b7d095a981e2f52d22291586210
|
|
4
|
+
data.tar.gz: 2aafb86816d74188a18a82e36c74e1a6c1dcb65b52523c3ee4bafa9db45e1fa3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f167f08df7e50cc605ff85d3663f3e51cb1b2a11ea88201bf50726b20721e263b8b9f96fee38ad64e42b97cd895f2217fb1968760bdf085351959632c7fec0d1
|
|
7
|
+
data.tar.gz: 652d33cc38a670dc0eafad296157d6f56c5fd3e4a435082b1bec45706c9a01624730be36992ebd22ac88f335a70def57aad3234480d77816297f879460462cda
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,55 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-07-06
|
|
4
|
+
|
|
5
|
+
### Changed (breaking)
|
|
6
|
+
|
|
7
|
+
- `Session#run` now takes a `transaction_options: {}` keyword and forwards
|
|
8
|
+
it verbatim to `root_record.transaction`. The previous hard-coded
|
|
9
|
+
`requires_new: true, joinable: false` defaults are removed; callers that
|
|
10
|
+
relied on them must pass those flags explicitly:
|
|
11
|
+
`session.run(root, transaction_options: { requires_new: true, joinable: false })`.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `Duplicator#mark_skip_class(*klasses)` / `Session#mark_skip_class` /
|
|
16
|
+
`HandlerApi#mark_skip_class` — class-wide shorthand for `mark_skip`.
|
|
17
|
+
Behaviour is exactly as if every record of the class had been passed
|
|
18
|
+
to `mark_skip` beforehand: handlers registered for the class still
|
|
19
|
+
fire, the default `bulk_insert` path becomes a no-op, and
|
|
20
|
+
`fetch_new_id(klass, old_id: ...)` echoes `old_id` back unchanged.
|
|
21
|
+
The Duplicator-level registration is inherited by every subsequent
|
|
22
|
+
`duplicate` run; Session-level additions apply only to the current run.
|
|
23
|
+
|
|
24
|
+
## [0.2.0] - 2026-07-06
|
|
25
|
+
|
|
3
26
|
- Drop support for Ruby 3.2 (EOL)
|
|
4
27
|
- Drop support for Rails 7.2; assume ActiveRecord >= 8.0 so `in_batches(cursor:)` is always available
|
|
5
28
|
|
|
29
|
+
### Removed (breaking)
|
|
30
|
+
|
|
31
|
+
- STI (Single Table Inheritance) dispatch in `Session#duplicate_records`.
|
|
32
|
+
Handlers are now looked up by the input Relation's `klass` (or the first
|
|
33
|
+
element's class for Arrays), with no ancestor walk and no `group_by(&:class)`.
|
|
34
|
+
If you were relying on registering a handler on a base class and expecting
|
|
35
|
+
it to fire for subclass records mixed in one Relation, split your input by
|
|
36
|
+
subclass and register handlers on each concrete class instead.
|
|
37
|
+
|
|
38
|
+
### Changed (breaking)
|
|
39
|
+
|
|
40
|
+
- Handlers now receive an `ActiveRecord::Relation` (not an Array) when the
|
|
41
|
+
input to duplication was a Relation. This lets `api.bulk_insert(klass,
|
|
42
|
+
records, cursor: [...])` take the `in_batches(cursor:)` path and enables
|
|
43
|
+
cursor-based keyset pagination for large child sets.
|
|
44
|
+
- Default (handler-less) dispatch of a Relation now uses `find_in_batches`
|
|
45
|
+
internally, reducing peak memory for large child sets.
|
|
46
|
+
|
|
47
|
+
### Migration
|
|
48
|
+
|
|
49
|
+
- Handlers that previously used Array-only methods (`records.reject!`, etc.)
|
|
50
|
+
on the third argument must convert to Array explicitly:
|
|
51
|
+
`records = records.to_a`.
|
|
52
|
+
|
|
6
53
|
## [0.1.0] - 2026-07-02
|
|
7
54
|
|
|
8
55
|
- Add `ActiveRecord::Duplicator::AssociationTraversal` — breadth-first traversal of preload-style association specifications
|
data/README.md
CHANGED
|
@@ -26,6 +26,17 @@ gem install activerecord-duplicator
|
|
|
26
26
|
- PostgreSQL (via `pg` gem). The gem relies on `insert_all!` returning inserted
|
|
27
27
|
primary keys.
|
|
28
28
|
|
|
29
|
+
## Non-goals
|
|
30
|
+
|
|
31
|
+
### STI dispatch
|
|
32
|
+
|
|
33
|
+
This gem dispatches based on the class of the input Relation (or the first
|
|
34
|
+
element for Arrays). If your Relation mixes STI subclasses and you need
|
|
35
|
+
per-subclass handlers, split the input into per-subclass Relations before
|
|
36
|
+
passing them in. Ancestor-walking dispatch (register a handler on a base class,
|
|
37
|
+
have it fire for subclass records mixed in one Relation) is intentionally not
|
|
38
|
+
supported; see CHANGELOG for details.
|
|
39
|
+
|
|
29
40
|
## Usage
|
|
30
41
|
|
|
31
42
|
### Basic duplication
|
|
@@ -60,6 +71,32 @@ duplicator.duplicate(old_project, associations: [tasks: :attachments])
|
|
|
60
71
|
Skipped records get `new_id == old_id` in the internal map, so children that
|
|
61
72
|
reference them are inserted with the original id unchanged.
|
|
62
73
|
|
|
74
|
+
### Skipping a whole model (`mark_skip_class`)
|
|
75
|
+
|
|
76
|
+
When every row of a model should be reused by id (a shared lookup table,
|
|
77
|
+
a tenant-wide catalog, an enum-backed table with dozens of entries), skip
|
|
78
|
+
the class itself instead of enumerating rows. `mark_skip_class` exists at
|
|
79
|
+
both scopes and mirrors `on`: register once on the Duplicator to inherit
|
|
80
|
+
across every run, or add it in the Session block for a single run.
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
duplicator = ActiveRecord::Duplicator.new
|
|
84
|
+
duplicator.mark_skip_class(Tag) # applies to every subsequent run
|
|
85
|
+
|
|
86
|
+
duplicator.duplicate(old_post, associations: [:taggings])
|
|
87
|
+
|
|
88
|
+
duplicator.duplicate(other_post, associations: [:taggings]) do |session|
|
|
89
|
+
session.mark_skip_class(Currency) # extra skip, this run only
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Behaviour is exactly as if you had passed every row to `mark_skip` up front:
|
|
94
|
+
handlers registered for the class still fire (so custom copy logic keeps
|
|
95
|
+
working), but the default `bulk_insert` path becomes a no-op and any
|
|
96
|
+
`fetch_new_id(klass, old_id: ...)` from a child's foreign-key reassignment
|
|
97
|
+
echoes the old id back unchanged. `mark_skip_class` only differs from
|
|
98
|
+
per-record `mark_skip` in cost: no enumeration is needed to opt each row in.
|
|
99
|
+
|
|
63
100
|
### Registering an id from outside (`store_new_id` / `fetch_new_id`)
|
|
64
101
|
|
|
65
102
|
If you produce a duplicate yourself (for example a model with a uniqueness
|
|
@@ -19,6 +19,11 @@ module ActiveRecord
|
|
|
19
19
|
@session.mark_skip(...)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
#: (*Class) -> void
|
|
23
|
+
def mark_skip_class(...)
|
|
24
|
+
@session.mark_skip_class(...)
|
|
25
|
+
end
|
|
26
|
+
|
|
22
27
|
#: (Class, old_id: untyped, new_id: untyped) -> void
|
|
23
28
|
def store_new_id(...)
|
|
24
29
|
@session.store_new_id(...)
|
|
@@ -24,13 +24,16 @@ module ActiveRecord
|
|
|
24
24
|
# @rbs @record_id_map: Hash[Class, Hash[untyped, untyped]]
|
|
25
25
|
# @rbs @handlers: Hash[Class, Proc]
|
|
26
26
|
# @rbs @user_handlers: Hash[Class, Proc]
|
|
27
|
+
# @rbs @skipped_classes: Set[Class]
|
|
27
28
|
|
|
28
|
-
#: (?handlers: Hash[Class, Proc]) -> void
|
|
29
|
-
def initialize(handlers: {})
|
|
29
|
+
#: (?handlers: Hash[Class, Proc], ?skipped_classes: Set[Class]) -> void
|
|
30
|
+
def initialize(handlers: {}, skipped_classes: Set.new)
|
|
30
31
|
@record_id_map = Hash.new { |hash, klass| hash[klass] = {} }
|
|
31
32
|
@handlers = handlers
|
|
32
33
|
@user_handlers = {}
|
|
33
|
-
|
|
34
|
+
# dup so that Session#mark_skip_class additions never leak back to the
|
|
35
|
+
# Duplicator-level set shared across every run.
|
|
36
|
+
@skipped_classes = skipped_classes.dup
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
# Register a handler that overrides Duplicator#on for the current session
|
|
@@ -42,8 +45,6 @@ module ActiveRecord
|
|
|
42
45
|
# Duplicator-level handler is intentional and does not raise.
|
|
43
46
|
#: (Class) { (HandlerApi, Class, untyped) -> void } -> void
|
|
44
47
|
def on(klass, &block)
|
|
45
|
-
@handlers_cache.clear
|
|
46
|
-
|
|
47
48
|
if @user_handlers.key?(klass)
|
|
48
49
|
raise DuplicateHandlerError, "handler for #{klass} is already overridden in this session"
|
|
49
50
|
end
|
|
@@ -62,6 +63,21 @@ module ActiveRecord
|
|
|
62
63
|
end
|
|
63
64
|
end
|
|
64
65
|
|
|
66
|
+
# Class-wide equivalent of mark_skip: it is exactly as if every record of
|
|
67
|
+
# each given class had been passed to mark_skip. Handlers still fire
|
|
68
|
+
# (matching the mark_skip case where a handler for the class is invoked
|
|
69
|
+
# with a Relation that happens to contain pre-skipped records); the
|
|
70
|
+
# default bulk_insert path becomes a no-op, and fetch_new_id echoes the
|
|
71
|
+
# queried old_id back unchanged.
|
|
72
|
+
#
|
|
73
|
+
# Use when the target database is expected to already hold every
|
|
74
|
+
# referenced row (shared lookup tables, tenant-wide catalogs, etc.), so
|
|
75
|
+
# per-record mark_skip would only differ from this in cost, not effect.
|
|
76
|
+
#: (*Class) -> void
|
|
77
|
+
def mark_skip_class(*klasses)
|
|
78
|
+
@skipped_classes.merge(klasses)
|
|
79
|
+
end
|
|
80
|
+
|
|
65
81
|
# Record a mapping from an old_id to a new_id. Called automatically from
|
|
66
82
|
# bulk_insert; called explicitly by user code (or a handler) when new
|
|
67
83
|
# records are produced outside of the default duplication path.
|
|
@@ -92,6 +108,8 @@ module ActiveRecord
|
|
|
92
108
|
# associations tree or register/skip it explicitly.
|
|
93
109
|
#: (Class, old_id: untyped) -> untyped
|
|
94
110
|
def fetch_new_id(klass, old_id:)
|
|
111
|
+
return old_id if @skipped_classes.include?(klass)
|
|
112
|
+
|
|
95
113
|
@record_id_map[klass].fetch(old_id) do
|
|
96
114
|
raise MissingNewIdError, "no new_id registered for #{klass}##{old_id.inspect}"
|
|
97
115
|
end
|
|
@@ -100,8 +118,14 @@ module ActiveRecord
|
|
|
100
118
|
# Bulk-insert copies of the given records into klass, skipping any that
|
|
101
119
|
# are already present in the id map. Handlers can reuse this via HandlerApi
|
|
102
120
|
# to defer a subset of records to the default duplication path.
|
|
121
|
+
#
|
|
122
|
+
# When klass has been passed to mark_skip_class the call is a no-op that
|
|
123
|
+
# never touches the DB: it is equivalent to having pre-registered every
|
|
124
|
+
# incoming record via mark_skip, only without the per-record enumeration.
|
|
103
125
|
#: (Class, untyped, ?batch_size: Integer, ?cursor: untyped) -> void
|
|
104
126
|
def bulk_insert(klass, records, batch_size: DEFAULT_BATCH_SIZE, cursor: nil)
|
|
127
|
+
return if @skipped_classes.include?(klass)
|
|
128
|
+
|
|
105
129
|
batches = enumerate_batches(records, batch_size:, cursor:)
|
|
106
130
|
|
|
107
131
|
batches.each do |batch|
|
|
@@ -140,15 +164,21 @@ module ActiveRecord
|
|
|
140
164
|
attrs
|
|
141
165
|
end
|
|
142
166
|
|
|
143
|
-
# Run the duplication inside a
|
|
144
|
-
#
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
167
|
+
# Run the duplication inside a transaction and return the freshly loaded
|
|
168
|
+
# root record. Normally invoked by Duplicator#duplicate.
|
|
169
|
+
#
|
|
170
|
+
# transaction_options is passed through verbatim to
|
|
171
|
+
# `root_record.transaction`, so callers can request `requires_new: true`
|
|
172
|
+
# / `joinable: false` / `isolation: :serializable` etc. as fits the
|
|
173
|
+
# surrounding call site. It defaults to `{}`, i.e. plain
|
|
174
|
+
# `transaction do ... end` behaviour.
|
|
175
|
+
#: (ActiveRecord::Base, ?associations: untyped, ?transaction_options: Hash[Symbol, untyped]) -> ActiveRecord::Base
|
|
176
|
+
def run(root_record, associations: [], transaction_options: {})
|
|
177
|
+
root_record.transaction(**transaction_options) do
|
|
178
|
+
duplicate_records(root_record.class, [root_record])
|
|
149
179
|
|
|
150
180
|
AssociationTraversal.new(root_record, associations).each do |relation|
|
|
151
|
-
duplicate_records(relation)
|
|
181
|
+
duplicate_records(relation.klass, relation)
|
|
152
182
|
end
|
|
153
183
|
|
|
154
184
|
new_id = fetch_new_id(root_record.class, old_id: current_id(root_record))
|
|
@@ -158,45 +188,30 @@ module ActiveRecord
|
|
|
158
188
|
|
|
159
189
|
private
|
|
160
190
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
191
|
+
# Dispatch to a handler (or the default bulk_insert path) for `klass`.
|
|
192
|
+
# All records in `relation_or_records` are assumed to be of `klass`; the
|
|
193
|
+
# caller is responsible for splitting mixed inputs (e.g. STI subclasses)
|
|
194
|
+
# into per-class Relations and passing the matching class in.
|
|
195
|
+
#
|
|
196
|
+
# mark_skip_class does NOT short-circuit here: handlers must still fire
|
|
197
|
+
# (mirroring the per-record mark_skip case), and the class-wide skip is
|
|
198
|
+
# applied inside bulk_insert instead.
|
|
199
|
+
#: (Class, untyped) -> void
|
|
200
|
+
def duplicate_records(klass, relation_or_records)
|
|
201
|
+
handler = resolve_handler(klass)
|
|
202
|
+
|
|
203
|
+
if handler
|
|
204
|
+
handler.call(HandlerApi.new(self), klass, relation_or_records)
|
|
205
|
+
else
|
|
206
|
+
bulk_insert(klass, relation_or_records)
|
|
175
207
|
end
|
|
176
208
|
end
|
|
177
209
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
@handlers_cache[klass] = resolve_handler(klass) unless @handlers_cache.key?(klass)
|
|
181
|
-
|
|
182
|
-
@handlers_cache[klass]
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Look up a handler for `klass` by walking its ancestor chain. Session
|
|
186
|
-
# -local handlers (Session#on) take precedence over any Duplicator-level
|
|
187
|
-
# handler, even one registered closer in the ancestor chain: the two
|
|
188
|
-
# passes below are deliberate, not a refactor opportunity.
|
|
210
|
+
# Session-local handlers (Session#on) take precedence over any
|
|
211
|
+
# Duplicator-level handler registered for the exact same class.
|
|
189
212
|
#: (Class) -> Proc?
|
|
190
213
|
def resolve_handler(klass)
|
|
191
|
-
|
|
192
|
-
klass.ancestors.each do |ancestor|
|
|
193
|
-
return @user_handlers[ancestor] if @user_handlers.key?(ancestor)
|
|
194
|
-
end
|
|
195
|
-
klass.ancestors.each do |ancestor|
|
|
196
|
-
return @handlers[ancestor] if @handlers.key?(ancestor)
|
|
197
|
-
end
|
|
198
|
-
# rubocop:enable Style/CombinableLoops
|
|
199
|
-
nil
|
|
214
|
+
@user_handlers[klass] || @handlers[klass]
|
|
200
215
|
end
|
|
201
216
|
|
|
202
217
|
#: (untyped, batch_size: Integer, cursor: untyped) -> untyped
|
|
@@ -19,10 +19,12 @@ module ActiveRecord
|
|
|
19
19
|
# end
|
|
20
20
|
class Duplicator
|
|
21
21
|
# @rbs @handlers: Hash[Class, Proc]
|
|
22
|
+
# @rbs @skipped_classes: Set[Class]
|
|
22
23
|
|
|
23
24
|
#: () -> void
|
|
24
25
|
def initialize
|
|
25
26
|
@handlers = {}
|
|
27
|
+
@skipped_classes = Set.new
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
# Register a custom handler for the given model class. See HandlerApi for
|
|
@@ -36,11 +38,22 @@ module ActiveRecord
|
|
|
36
38
|
@handlers[klass] = block
|
|
37
39
|
end
|
|
38
40
|
|
|
41
|
+
# Declare that every record of the given classes must not be duplicated in
|
|
42
|
+
# any subsequent run. See Session#mark_skip_class for the semantics; the
|
|
43
|
+
# Duplicator-level registration is applied to every fresh Session as its
|
|
44
|
+
# baseline, and Session#mark_skip_class can extend it for a single run.
|
|
45
|
+
# Repeated calls with the same class are idempotent.
|
|
46
|
+
#: (*Class) -> void
|
|
47
|
+
def mark_skip_class(*klasses)
|
|
48
|
+
@skipped_classes.merge(klasses)
|
|
49
|
+
end
|
|
50
|
+
|
|
39
51
|
# Prevent further registrations. Also freezes the internal handler map so
|
|
40
52
|
# accidental mutation later fails loudly.
|
|
41
53
|
#: () -> self
|
|
42
54
|
def freeze
|
|
43
55
|
@handlers.freeze
|
|
56
|
+
@skipped_classes.freeze
|
|
44
57
|
super
|
|
45
58
|
end
|
|
46
59
|
|
|
@@ -52,7 +65,7 @@ module ActiveRecord
|
|
|
52
65
|
# were duplicated elsewhere.
|
|
53
66
|
#: (ActiveRecord::Base, ?associations: untyped) ?{ (Session) -> void } -> ActiveRecord::Base
|
|
54
67
|
def duplicate(root_record, associations: [], &block)
|
|
55
|
-
session = Session.new(handlers: @handlers)
|
|
68
|
+
session = Session.new(handlers: @handlers, skipped_classes: @skipped_classes)
|
|
56
69
|
block&.call(session)
|
|
57
70
|
session.run(root_record, associations:)
|
|
58
71
|
end
|
|
@@ -15,6 +15,9 @@ module ActiveRecord
|
|
|
15
15
|
# : (*ActiveRecord::Base) -> void
|
|
16
16
|
def mark_skip: (*ActiveRecord::Base) -> void
|
|
17
17
|
|
|
18
|
+
# : (*Class) -> void
|
|
19
|
+
def mark_skip_class: (*Class) -> void
|
|
20
|
+
|
|
18
21
|
# : (Class, old_id: untyped, new_id: untyped) -> void
|
|
19
22
|
def store_new_id: (Class, old_id: untyped, new_id: untyped) -> void
|
|
20
23
|
|
|
@@ -20,14 +20,16 @@ module ActiveRecord
|
|
|
20
20
|
class Session
|
|
21
21
|
DEFAULT_BATCH_SIZE: ::Integer
|
|
22
22
|
|
|
23
|
+
@skipped_classes: Set[Class]
|
|
24
|
+
|
|
23
25
|
@user_handlers: Hash[Class, Proc]
|
|
24
26
|
|
|
25
27
|
@handlers: Hash[Class, Proc]
|
|
26
28
|
|
|
27
29
|
@record_id_map: Hash[Class, Hash[untyped, untyped]]
|
|
28
30
|
|
|
29
|
-
# : (?handlers: Hash[Class, Proc]) -> void
|
|
30
|
-
def initialize: (?handlers: Hash[Class, Proc]) -> void
|
|
31
|
+
# : (?handlers: Hash[Class, Proc], ?skipped_classes: Set[Class]) -> void
|
|
32
|
+
def initialize: (?handlers: Hash[Class, Proc], ?skipped_classes: Set[Class]) -> void
|
|
31
33
|
|
|
32
34
|
# Register a handler that overrides Duplicator#on for the current session
|
|
33
35
|
# only. Useful when a single duplication run needs a special copy of a
|
|
@@ -45,6 +47,19 @@ module ActiveRecord
|
|
|
45
47
|
# : (*ActiveRecord::Base) -> void
|
|
46
48
|
def mark_skip: (*ActiveRecord::Base) -> void
|
|
47
49
|
|
|
50
|
+
# Class-wide equivalent of mark_skip: it is exactly as if every record of
|
|
51
|
+
# each given class had been passed to mark_skip. Handlers still fire
|
|
52
|
+
# (matching the mark_skip case where a handler for the class is invoked
|
|
53
|
+
# with a Relation that happens to contain pre-skipped records); the
|
|
54
|
+
# default bulk_insert path becomes a no-op, and fetch_new_id echoes the
|
|
55
|
+
# queried old_id back unchanged.
|
|
56
|
+
#
|
|
57
|
+
# Use when the target database is expected to already hold every
|
|
58
|
+
# referenced row (shared lookup tables, tenant-wide catalogs, etc.), so
|
|
59
|
+
# per-record mark_skip would only differ from this in cost, not effect.
|
|
60
|
+
# : (*Class) -> void
|
|
61
|
+
def mark_skip_class: (*Class) -> void
|
|
62
|
+
|
|
48
63
|
# Record a mapping from an old_id to a new_id. Called automatically from
|
|
49
64
|
# bulk_insert; called explicitly by user code (or a handler) when new
|
|
50
65
|
# records are produced outside of the default duplication path.
|
|
@@ -69,6 +84,10 @@ module ActiveRecord
|
|
|
69
84
|
# Bulk-insert copies of the given records into klass, skipping any that
|
|
70
85
|
# are already present in the id map. Handlers can reuse this via HandlerApi
|
|
71
86
|
# to defer a subset of records to the default duplication path.
|
|
87
|
+
#
|
|
88
|
+
# When klass has been passed to mark_skip_class the call is a no-op that
|
|
89
|
+
# never touches the DB: it is equivalent to having pre-registered every
|
|
90
|
+
# incoming record via mark_skip, only without the per-record enumeration.
|
|
72
91
|
# : (Class, untyped, ?batch_size: Integer, ?cursor: untyped) -> void
|
|
73
92
|
def bulk_insert: (Class, untyped, ?batch_size: Integer, ?cursor: untyped) -> void
|
|
74
93
|
|
|
@@ -83,23 +102,32 @@ module ActiveRecord
|
|
|
83
102
|
# : (Class, ActiveRecord::Base) -> Hash[String, untyped]
|
|
84
103
|
def attributes_for: (Class, ActiveRecord::Base) -> Hash[String, untyped]
|
|
85
104
|
|
|
86
|
-
# Run the duplication inside a
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
|
|
105
|
+
# Run the duplication inside a transaction and return the freshly loaded
|
|
106
|
+
# root record. Normally invoked by Duplicator#duplicate.
|
|
107
|
+
#
|
|
108
|
+
# transaction_options is passed through verbatim to
|
|
109
|
+
# `root_record.transaction`, so callers can request `requires_new: true`
|
|
110
|
+
# / `joinable: false` / `isolation: :serializable` etc. as fits the
|
|
111
|
+
# surrounding call site. It defaults to `{}`, i.e. plain
|
|
112
|
+
# `transaction do ... end` behaviour.
|
|
113
|
+
# : (ActiveRecord::Base, ?associations: untyped, ?transaction_options: Hash[Symbol, untyped]) -> ActiveRecord::Base
|
|
114
|
+
def run: (ActiveRecord::Base, ?associations: untyped, ?transaction_options: Hash[Symbol, untyped]) -> ActiveRecord::Base
|
|
90
115
|
|
|
91
116
|
private
|
|
92
117
|
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
|
|
118
|
+
# Dispatch to a handler (or the default bulk_insert path) for `klass`.
|
|
119
|
+
# All records in `relation_or_records` are assumed to be of `klass`; the
|
|
120
|
+
# caller is responsible for splitting mixed inputs (e.g. STI subclasses)
|
|
121
|
+
# into per-class Relations and passing the matching class in.
|
|
122
|
+
#
|
|
123
|
+
# mark_skip_class does NOT short-circuit here: handlers must still fire
|
|
124
|
+
# (mirroring the per-record mark_skip case), and the class-wide skip is
|
|
125
|
+
# applied inside bulk_insert instead.
|
|
126
|
+
# : (Class, untyped) -> void
|
|
127
|
+
def duplicate_records: (Class, untyped) -> void
|
|
128
|
+
|
|
129
|
+
# Session-local handlers (Session#on) take precedence over any
|
|
130
|
+
# Duplicator-level handler registered for the exact same class.
|
|
103
131
|
# : (Class) -> Proc?
|
|
104
132
|
def resolve_handler: (Class) -> Proc?
|
|
105
133
|
|
|
@@ -18,6 +18,8 @@ module ActiveRecord
|
|
|
18
18
|
class Duplicator
|
|
19
19
|
@handlers: Hash[Class, Proc]
|
|
20
20
|
|
|
21
|
+
@skipped_classes: Set[Class]
|
|
22
|
+
|
|
21
23
|
# : () -> void
|
|
22
24
|
def initialize: () -> void
|
|
23
25
|
|
|
@@ -28,6 +30,14 @@ module ActiveRecord
|
|
|
28
30
|
# : (Class) { (HandlerApi, Class, untyped) -> void } -> void
|
|
29
31
|
def on: (Class) { (HandlerApi, Class, untyped) -> void } -> void
|
|
30
32
|
|
|
33
|
+
# Declare that every record of the given classes must not be duplicated in
|
|
34
|
+
# any subsequent run. See Session#mark_skip_class for the semantics; the
|
|
35
|
+
# Duplicator-level registration is applied to every fresh Session as its
|
|
36
|
+
# baseline, and Session#mark_skip_class can extend it for a single run.
|
|
37
|
+
# Repeated calls with the same class are idempotent.
|
|
38
|
+
# : (*Class) -> void
|
|
39
|
+
def mark_skip_class: (*Class) -> void
|
|
40
|
+
|
|
31
41
|
# Prevent further registrations. Also freezes the internal handler map so
|
|
32
42
|
# accidental mutation later fails loudly.
|
|
33
43
|
# : () -> self
|