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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4bf58194f42894cf69c9ff83e02cd0f9f76fe38565e671e24ebb232221e8fa31
4
- data.tar.gz: 1d20e02d28ef2bffcf831654d2b71fe73461d3eee0fa3b11ce5e95a15bac259a
3
+ metadata.gz: cc6a883e8a354d6b41962cb96eff90d95f083918fee428b0f865d5d254117f5c
4
+ data.tar.gz: 6b04468f4352d8d82f7a3cee8ccf684d19672d86715c4899313992a65519f362
5
5
  SHA512:
6
- metadata.gz: ba02707cbdfdf479e2663bfcbf9b7d4012e1b1d43a9a483a041b76debff00d013e8b4f36efd7e81cb8e2e09d30a60b374a87345f625216dad0c75fee6898f1f0
7
- data.tar.gz: 6871e902554ea9fc435b17bfa83e978a1034898bae00cf744edb61b191a89dc05bc479d316c6785a172a722f7d87b794114e173b819ed8176ccdffca380bd430
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
- #: (untyped) -> void
162
- def duplicate_records(relation_or_records)
163
- # Group by the real class of each record so a handler registered on an
164
- # STI subclass fires only for records of that subclass. For non-STI
165
- # models every record shares the given `klass`, so a single group is
166
- # produced.
167
- relation_or_records.group_by(&:class).each do |real_class, records|
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
- if handler
171
- handler.call(HandlerApi.new(self), real_class, records)
172
- else
173
- bulk_insert(real_class, records)
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
- #: (Class) -> Proc?
179
- def cache_or_resolve_handler(klass)
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
- # rubocop:disable Style/CombinableLoops
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  class Duplicator
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -90,16 +90,15 @@ module ActiveRecord
90
90
 
91
91
  private
92
92
 
93
- # : (untyped) -> void
94
- def duplicate_records: (untyped) -> void
95
-
96
- # : (Class) -> Proc?
97
- def cache_or_resolve_handler: (Class) -> Proc?
98
-
99
- # Look up a handler for `klass` by walking its ancestor chain. Session
100
- # -local handlers (Session#on) take precedence over any Duplicator-level
101
- # handler, even one registered closer in the ancestor chain: the two
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
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-duplicator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SmartHR