activerecord-duplicator 0.1.0 → 0.2.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 +26 -0
- data/README.md +11 -0
- data/lib/active_record/duplicator/session.rb +16 -38
- data/lib/active_record/duplicator/version.rb +1 -1
- data/sig/generated/active_record/duplicator/session.rbs +9 -10
- 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: cc6a883e8a354d6b41962cb96eff90d95f083918fee428b0f865d5d254117f5c
|
|
4
|
+
data.tar.gz: 6b04468f4352d8d82f7a3cee8ccf684d19672d86715c4899313992a65519f362
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f3ee11352e9d5612730e3a86f3f11d50e9becff4ac25651f1efef18dc9a6207da379e71751af1cf983e1bd880725200ce44c8c6f86da774242144942076996eb
|
|
7
|
+
data.tar.gz: 90472d33b56c36ab4d3688284cee4f6cacb2d6d3fa1f5cbfc3b44ba8ca5e5f52bcf176da680937d815cc2da0df97c558caee71fb196d03f1f816a2e53ab10f23
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-07-06
|
|
4
|
+
|
|
3
5
|
- Drop support for Ruby 3.2 (EOL)
|
|
4
6
|
- Drop support for Rails 7.2; assume ActiveRecord >= 8.0 so `in_batches(cursor:)` is always available
|
|
5
7
|
|
|
8
|
+
### Removed (breaking)
|
|
9
|
+
|
|
10
|
+
- STI (Single Table Inheritance) dispatch in `Session#duplicate_records`.
|
|
11
|
+
Handlers are now looked up by the input Relation's `klass` (or the first
|
|
12
|
+
element's class for Arrays), with no ancestor walk and no `group_by(&:class)`.
|
|
13
|
+
If you were relying on registering a handler on a base class and expecting
|
|
14
|
+
it to fire for subclass records mixed in one Relation, split your input by
|
|
15
|
+
subclass and register handlers on each concrete class instead.
|
|
16
|
+
|
|
17
|
+
### Changed (breaking)
|
|
18
|
+
|
|
19
|
+
- Handlers now receive an `ActiveRecord::Relation` (not an Array) when the
|
|
20
|
+
input to duplication was a Relation. This lets `api.bulk_insert(klass,
|
|
21
|
+
records, cursor: [...])` take the `in_batches(cursor:)` path and enables
|
|
22
|
+
cursor-based keyset pagination for large child sets.
|
|
23
|
+
- Default (handler-less) dispatch of a Relation now uses `find_in_batches`
|
|
24
|
+
internally, reducing peak memory for large child sets.
|
|
25
|
+
|
|
26
|
+
### Migration
|
|
27
|
+
|
|
28
|
+
- Handlers that previously used Array-only methods (`records.reject!`, etc.)
|
|
29
|
+
on the third argument must convert to Array explicitly:
|
|
30
|
+
`records = records.to_a`.
|
|
31
|
+
|
|
6
32
|
## [0.1.0] - 2026-07-02
|
|
7
33
|
|
|
8
34
|
- 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
|
|
@@ -30,7 +30,6 @@ module ActiveRecord
|
|
|
30
30
|
@record_id_map = Hash.new { |hash, klass| hash[klass] = {} }
|
|
31
31
|
@handlers = handlers
|
|
32
32
|
@user_handlers = {}
|
|
33
|
-
@handlers_cache = {}
|
|
34
33
|
end
|
|
35
34
|
|
|
36
35
|
# Register a handler that overrides Duplicator#on for the current session
|
|
@@ -42,8 +41,6 @@ module ActiveRecord
|
|
|
42
41
|
# Duplicator-level handler is intentional and does not raise.
|
|
43
42
|
#: (Class) { (HandlerApi, Class, untyped) -> void } -> void
|
|
44
43
|
def on(klass, &block)
|
|
45
|
-
@handlers_cache.clear
|
|
46
|
-
|
|
47
44
|
if @user_handlers.key?(klass)
|
|
48
45
|
raise DuplicateHandlerError, "handler for #{klass} is already overridden in this session"
|
|
49
46
|
end
|
|
@@ -145,10 +142,10 @@ module ActiveRecord
|
|
|
145
142
|
#: (ActiveRecord::Base, ?associations: untyped) -> ActiveRecord::Base
|
|
146
143
|
def run(root_record, associations: [])
|
|
147
144
|
root_record.transaction(requires_new: true, joinable: false) do
|
|
148
|
-
duplicate_records([root_record])
|
|
145
|
+
duplicate_records(root_record.class, [root_record])
|
|
149
146
|
|
|
150
147
|
AssociationTraversal.new(root_record, associations).each do |relation|
|
|
151
|
-
duplicate_records(relation)
|
|
148
|
+
duplicate_records(relation.klass, relation)
|
|
152
149
|
end
|
|
153
150
|
|
|
154
151
|
new_id = fetch_new_id(root_record.class, old_id: current_id(root_record))
|
|
@@ -158,45 +155,26 @@ module ActiveRecord
|
|
|
158
155
|
|
|
159
156
|
private
|
|
160
157
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
handler = cache_or_resolve_handler(real_class)
|
|
158
|
+
# Dispatch to a handler (or the default bulk_insert path) for `klass`.
|
|
159
|
+
# All records in `relation_or_records` are assumed to be of `klass`; the
|
|
160
|
+
# caller is responsible for splitting mixed inputs (e.g. STI subclasses)
|
|
161
|
+
# into per-class Relations and passing the matching class in.
|
|
162
|
+
#: (Class, untyped) -> void
|
|
163
|
+
def duplicate_records(klass, relation_or_records)
|
|
164
|
+
handler = resolve_handler(klass)
|
|
169
165
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
end
|
|
166
|
+
if handler
|
|
167
|
+
handler.call(HandlerApi.new(self), klass, relation_or_records)
|
|
168
|
+
else
|
|
169
|
+
bulk_insert(klass, relation_or_records)
|
|
175
170
|
end
|
|
176
171
|
end
|
|
177
172
|
|
|
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.
|
|
173
|
+
# Session-local handlers (Session#on) take precedence over any
|
|
174
|
+
# Duplicator-level handler registered for the exact same class.
|
|
189
175
|
#: (Class) -> Proc?
|
|
190
176
|
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
|
|
177
|
+
@user_handlers[klass] || @handlers[klass]
|
|
200
178
|
end
|
|
201
179
|
|
|
202
180
|
#: (untyped, batch_size: Integer, cursor: untyped) -> untyped
|
|
@@ -90,16 +90,15 @@ module ActiveRecord
|
|
|
90
90
|
|
|
91
91
|
private
|
|
92
92
|
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
# -local handlers (Session#on) take precedence over any
|
|
101
|
-
# handler
|
|
102
|
-
# passes below are deliberate, not a refactor opportunity.
|
|
93
|
+
# Dispatch to a handler (or the default bulk_insert path) for `klass`.
|
|
94
|
+
# All records in `relation_or_records` are assumed to be of `klass`; the
|
|
95
|
+
# caller is responsible for splitting mixed inputs (e.g. STI subclasses)
|
|
96
|
+
# into per-class Relations and passing the matching class in.
|
|
97
|
+
# : (Class, untyped) -> void
|
|
98
|
+
def duplicate_records: (Class, untyped) -> void
|
|
99
|
+
|
|
100
|
+
# Session-local handlers (Session#on) take precedence over any
|
|
101
|
+
# Duplicator-level handler registered for the exact same class.
|
|
103
102
|
# : (Class) -> Proc?
|
|
104
103
|
def resolve_handler: (Class) -> Proc?
|
|
105
104
|
|