activerecord-duplicator 0.5.0 → 0.6.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: 017f95574ff90a7fdae2b54b38fd5be60fd6afa33b53a3a59486e159031c9bd1
4
- data.tar.gz: a06425a620bae40393fc97b555f0616a8b5bfc87d6e90806089480f4506d3bdf
3
+ metadata.gz: 0e0c02f84a340928dc7b42bf07eec278113241d6e710c73fed7e2d60c5623617
4
+ data.tar.gz: 3f9a0fad4af18f5d107358c9b17fc757bada05d0ce432b765820db2e39841dff
5
5
  SHA512:
6
- metadata.gz: 6dd54a10072d5521a176d1d5957246e033a42a2671e220ffc718f53f6442c4d6329241d0ab41aa1cc852585c697a36666d629d9c5f60c03d5b7dede47f986a97
7
- data.tar.gz: afc907dc6fb3026b820eb1273500c53f7d51f0bd8ea960092956f81108253faa3d4795473f1a137133cade12f24365a4f8fb8136ff5831a0196e1cc3526f4bad
6
+ metadata.gz: 9271005195b8291621f9e86cb61e79ffabcc67de31519af7167b4538600b5cd7551812079b358def18f399d68b63470ae26345a4981a27be64c340c50ed186c0
7
+ data.tar.gz: 7ad05e575e643f43dba00993b2d835f5e9d3fa00e421da285b0c90617d835efc8a8c4eb271614d71300d228cb6f7fbc2161fbe8f322bdf5dc7204ce494c3637e
data/CHANGELOG.md CHANGED
@@ -1,12 +1,51 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2026-07-08
4
+
5
+ ### Changed (breaking)
6
+
7
+ - Dropped the hard `pg` runtime dependency. The gem is now
8
+ adapter-agnostic: any adapter whose connection reports
9
+ `supports_insert_returning? == true` is supported (PostgreSQL and
10
+ SQLite >= 3.35 today). Applications add the adapter gem they use
11
+ (`pg`, `sqlite3`, ...) to their own Gemfile.
12
+ - `Session#bulk_insert` now raises the new
13
+ `ActiveRecord::Duplicator::UnsupportedAdapterError` when the target
14
+ connection does not implement `supports_insert_returning?`. Previously the
15
+ bulk insert silently returned no rows and left the id map incomplete on
16
+ mysql2 / trilogy. Handlers registered via `Duplicator#on` / `Session#on`
17
+ are unaffected as long as they do not delegate to `bulk_insert`.
18
+
19
+ ### Added
20
+
21
+ - `ActiveRecord::Duplicator::UnsupportedAdapterError`: signals that the
22
+ default duplication path was invoked against a RETURNING-less adapter.
23
+ - The spec suite now runs against SQLite as well as PostgreSQL, driven by an
24
+ `ARCONN` environment variable (`ARCONN=sqlite3 bundle exec rspec`). CI
25
+ exercises both adapters on every push and pull request.
26
+
27
+ ### Renamed (breaking)
28
+
29
+ Public API methods have been renamed to use `duplicate`-prefixed vocabulary
30
+ that matches the gem name and makes the intent clearer:
31
+
32
+ | Old name | New name |
33
+ |---|---|
34
+ | `mark_skip(record)` | `no_duplicate(record)` |
35
+ | `mark_skip_id(klass, *ids)` | `no_duplicate_id(klass, *ids)` |
36
+ | `mark_skip_class(*klasses)` | `no_duplicate_class(*klasses)` |
37
+ | `store_new_id(klass, old_id:, new_id:)` | `store_duplicate_id(klass, old_id:, new_id:)` |
38
+ | `fetch_new_id(klass, old_id:)` | `fetch_duplicate_id(klass, old_id:)` |
39
+
40
+ Applies to `Session`, `HandlerApi`, and `Duplicator` (for `no_duplicate_class`).
41
+
3
42
  ## [0.5.0] - 2026-07-07
4
43
 
5
44
  ### Added
6
45
 
7
- - `Session#mark_skip_id(klass, *ids)` and `HandlerApi#mark_skip_id` —
8
- register specific ids as skipped without loading records. Equivalent to
9
- calling `mark_skip` on each record but avoids the database round-trip
46
+ - `Session#no_duplicate_id(klass, *ids)` and `HandlerApi#no_duplicate_id`:
47
+ exclude specific ids from duplication without loading records. Equivalent to
48
+ calling `no_duplicate` on each record but avoids the database round-trip
10
49
  when the ids are already known. Composite primary key ids are passed as
11
50
  Arrays.
12
51
 
@@ -32,12 +71,12 @@
32
71
 
33
72
  ### Added
34
73
 
35
- - `Duplicator#mark_skip_class(*klasses)` / `Session#mark_skip_class` /
36
- `HandlerApi#mark_skip_class` class-wide shorthand for `mark_skip`.
74
+ - `Duplicator#no_duplicate_class(*klasses)` / `Session#no_duplicate_class` /
75
+ `HandlerApi#no_duplicate_class`: class-wide shorthand for `no_duplicate`.
37
76
  Behaviour is exactly as if every record of the class had been passed
38
- to `mark_skip` beforehand: handlers registered for the class still
77
+ to `no_duplicate` beforehand: handlers registered for the class still
39
78
  fire, the default `bulk_insert` path becomes a no-op, and
40
- `fetch_new_id(klass, old_id: ...)` echoes `old_id` back unchanged.
79
+ `fetch_duplicate_id(klass, old_id: ...)` echoes `old_id` back unchanged.
41
80
  The Duplicator-level registration is inherited by every subsequent
42
81
  `duplicate` run; Session-level additions apply only to the current run.
43
82
 
@@ -72,8 +111,8 @@
72
111
 
73
112
  ## [0.1.0] - 2026-07-02
74
113
 
75
- - Add `ActiveRecord::Duplicator::AssociationTraversal` breadth-first traversal of preload-style association specifications
76
- - Add `ActiveRecord::Duplicator::Session` coordinates one duplication run: record id map, handler registry, `duplicate` / `bulk_insert` / `attributes_for`
77
- - Add `ActiveRecord::Duplicator::HandlerApi` facade passed to `Session#on` handler blocks
114
+ - Add `ActiveRecord::Duplicator::AssociationTraversal`: breadth-first traversal of preload-style association specifications
115
+ - Add `ActiveRecord::Duplicator::Session`: coordinates one duplication run (record id map, handler registry, `duplicate` / `bulk_insert` / `attributes_for`)
116
+ - Add `ActiveRecord::Duplicator::HandlerApi`: facade passed to `Session#on` handler blocks
78
117
  - Add error types under `ActiveRecord::Duplicator::Error`: `InvalidRecordIdError` / `MissingNewIdError` / `DuplicateHandlerError`
79
118
  - Support composite primary keys (Rails 7.1+)
data/README.md CHANGED
@@ -23,8 +23,20 @@ gem install activerecord-duplicator
23
23
 
24
24
  - Ruby >= 3.3
25
25
  - ActiveRecord >= 8.0 (tested against 8.0, 8.1)
26
- - PostgreSQL (via `pg` gem). The gem relies on `insert_all!` returning inserted
27
- primary keys.
26
+ - A database adapter whose connection reports
27
+ `supports_insert_returning? == true`. The default duplication path uses
28
+ `insert_all!` and needs the adapter to echo inserted primary keys back so
29
+ the id map can be populated in a single round trip.
30
+ - PostgreSQL (via `pg`): always supported.
31
+ - SQLite (via `sqlite3`): SQLite >= 3.35 is required; the modern
32
+ `sqlite3` gem ships a compatible native binary.
33
+ - MySQL / Trilogy: not supported by the default path because those
34
+ adapters do not implement RETURNING. Duplication of specific models can
35
+ still be done through a custom handler registered with `Duplicator#on`
36
+ (see below).
37
+
38
+ The gem does not depend on a specific adapter gem; applications add whichever
39
+ one they use (`pg`, `sqlite3`, ...) to their own Gemfile.
28
40
 
29
41
  ## Non-goals
30
42
 
@@ -54,16 +66,16 @@ new_post = duplicator.duplicate(
54
66
  `associations`), rewires every foreign key to point at the freshly created
55
67
  parents, and returns the new root record.
56
68
 
57
- ### Keeping records referenced but not copied (`mark_skip`)
69
+ ### Keeping records referenced but not copied (`no_duplicate`)
58
70
 
59
71
  When you want the duplicated rows to keep pointing at an existing record
60
72
  (for example the current tenant, current user, or a shared lookup table),
61
- mark that record as skipped before calling `duplicate`:
73
+ call no_duplicate on that record before calling `duplicate`:
62
74
 
63
75
  ```ruby
64
76
  duplicator = ActiveRecord::Duplicator.new
65
- duplicator.mark_skip(tenant)
66
- duplicator.mark_skip(current_user)
77
+ duplicator.no_duplicate(tenant)
78
+ duplicator.no_duplicate(current_user)
67
79
 
68
80
  duplicator.duplicate(old_project, associations: [tasks: :attachments])
69
81
  ```
@@ -71,55 +83,55 @@ duplicator.duplicate(old_project, associations: [tasks: :attachments])
71
83
  Skipped records get `new_id == old_id` in the internal map, so children that
72
84
  reference them are inserted with the original id unchanged.
73
85
 
74
- ### Skipping a whole model (`mark_skip_class`)
86
+ ### Skipping a whole model (`no_duplicate_class`)
75
87
 
76
88
  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
89
+ a tenant-wide catalog, an enum-backed table with dozens of entries), call no_duplicate_class
90
+ the class itself instead of enumerating rows. `no_duplicate_class` exists at
79
91
  both scopes and mirrors `on`: register once on the Duplicator to inherit
80
92
  across every run, or add it in the Session block for a single run.
81
93
 
82
94
  ```ruby
83
95
  duplicator = ActiveRecord::Duplicator.new
84
- duplicator.mark_skip_class(Tag) # applies to every subsequent run
96
+ duplicator.no_duplicate_class(Tag) # applies to every subsequent run
85
97
 
86
98
  duplicator.duplicate(old_post, associations: [:taggings])
87
99
 
88
100
  duplicator.duplicate(other_post, associations: [:taggings]) do |session|
89
- session.mark_skip_class(Currency) # extra skip, this run only
101
+ session.no_duplicate_class(Currency) # extra exclusion, this run only
90
102
  end
91
103
  ```
92
104
 
93
- Behaviour is exactly as if you had passed every row to `mark_skip` up front:
105
+ Behaviour is exactly as if you had passed every row to `no_duplicate` up front:
94
106
  handlers registered for the class still fire (so custom copy logic keeps
95
107
  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.
108
+ `fetch_duplicate_id(klass, old_id: ...)` from a child's foreign-key reassignment
109
+ echoes the old id back unchanged. `no_duplicate_class` only differs from
110
+ per-record `no_duplicate` in cost: no enumeration is needed to opt each row in.
99
111
 
100
- ### Skipping by id without loading records (`mark_skip_id`)
112
+ ### Skipping by id without loading records (`no_duplicate_id`)
101
113
 
102
- When the ids to skip are already known (a foreign key column, a prior
103
- pluck, a config constant), use `mark_skip_id` to register them directly
114
+ When the ids are already known (a foreign key column, a prior
115
+ pluck, a config constant), use `no_duplicate_id` to register them directly
104
116
  without issuing a `SELECT`:
105
117
 
106
118
  ```ruby
107
119
  duplicator.duplicate(old_post, associations: [:taggings]) do |session|
108
- session.mark_skip_id(Tag, 1, 2, 3)
120
+ session.no_duplicate_id(Tag, 1, 2, 3)
109
121
  end
110
122
  ```
111
123
 
112
124
  For composite primary keys pass each id as an Array:
113
125
 
114
126
  ```ruby
115
- session.mark_skip_id(Item, [tenant_id, 10], [tenant_id, 11])
127
+ session.no_duplicate_id(Item, [tenant_id, 10], [tenant_id, 11])
116
128
  ```
117
129
 
118
- `mark_skip_id` has the same effect as calling `mark_skip` on the
119
- corresponding records: `fetch_new_id` returns the old id unchanged for
130
+ `no_duplicate_id` has the same effect as calling `no_duplicate` on the
131
+ corresponding records: `fetch_duplicate_id` returns the old id unchanged for
120
132
  each registered id.
121
133
 
122
- ### Registering an id from outside (`store_new_id` / `fetch_new_id`)
134
+ ### Registering an id from outside (`store_duplicate_id` / `fetch_duplicate_id`)
123
135
 
124
136
  If you produce a duplicate yourself (for example a model with a uniqueness
125
137
  constraint that has to be reshaped before insert), tell the session about the
@@ -131,11 +143,11 @@ new_project.slug = "#{old_project.slug}-copy"
131
143
  new_project.save!
132
144
 
133
145
  duplicator = ActiveRecord::Duplicator.new
134
- duplicator.store_new_id(Project, old_id: old_project.id, new_id: new_project.id)
146
+ duplicator.store_duplicate_id(Project, old_id: old_project.id, new_id: new_project.id)
135
147
  duplicator.duplicate(old_project, associations: [:tasks])
136
148
  ```
137
149
 
138
- `fetch_new_id(klass, old_id:)` returns the mapping (scalar for a single-column
150
+ `fetch_duplicate_id(klass, old_id:)` returns the mapping (scalar for a single-column
139
151
  primary key, Array for a composite one) and raises `MissingNewIdError` if the
140
152
  parent has not been duplicated yet.
141
153
 
@@ -150,16 +162,16 @@ called instead of the default duplication path for that class:
150
162
  duplicator.on(Attachment) do |api, klass, records|
151
163
  records.find_each do |record|
152
164
  new_record = klass.create!(
153
- post_id: api.fetch_new_id(Post, old_id: record.post_id),
165
+ post_id: api.fetch_duplicate_id(Post, old_id: record.post_id),
154
166
  )
155
167
  new_record.file.attach(record.file.blob)
156
- api.store_new_id(klass, old_id: record.id, new_id: new_record.id)
168
+ api.store_duplicate_id(klass, old_id: record.id, new_id: new_record.id)
157
169
  end
158
170
  end
159
171
  ```
160
172
 
161
173
  `api` is a `HandlerApi` facade over the session that exposes
162
- `mark_skip` / `store_new_id` / `fetch_new_id` / `bulk_insert` /
174
+ `no_duplicate` / `store_duplicate_id` / `fetch_duplicate_id` / `bulk_insert` /
163
175
  `attributes_for`. Registering two handlers for the same class raises
164
176
  `DuplicateHandlerError`.
165
177
 
@@ -175,11 +187,16 @@ tuple `WHERE (a, b) IN ((v1, v2), ...)` syntax.
175
187
 
176
188
  All errors inherit from `ActiveRecord::Duplicator::Error`:
177
189
 
178
- - `InvalidRecordIdError` the same `old_id` was registered with a different `new_id`.
179
- - `MissingNewIdError` `fetch_new_id` had no mapping. Usually means a
190
+ - `InvalidRecordIdError`: the same `old_id` was registered with a different `new_id`.
191
+ - `MissingNewIdError`: `fetch_duplicate_id` had no mapping. Usually means a
180
192
  belongs_to parent was not duplicated yet; place it earlier in the
181
- associations tree or mark it skipped.
182
- - `DuplicateHandlerError` `on(klass)` was called twice for the same class.
193
+ associations tree, or call no_duplicate / no_duplicate_id on it.
194
+ - `DuplicateHandlerError`: `on(klass)` was called twice for the same class.
195
+ - `UnsupportedAdapterError`: the default `bulk_insert` was invoked against a
196
+ connection whose adapter does not implement `supports_insert_returning?`
197
+ (for example mysql2 or trilogy). Register a handler with
198
+ `Duplicator#on` / `Session#on` that uses `create!` (or another
199
+ RETURNING-free path) for that model.
183
200
 
184
201
  Because they share a common ancestor, one `rescue` clause catches any
185
202
  duplication-specific failure:
@@ -202,8 +219,8 @@ smaller root records or file an issue.
202
219
 
203
220
  ## Development
204
221
 
205
- The gem tests against a real PostgreSQL instance. `compose.yml` at the repo
206
- root starts one for you on port 55432:
222
+ The gem tests against a real database. `compose.yml` at the repo root starts a
223
+ PostgreSQL instance for you on port 55432:
207
224
 
208
225
  ```bash
209
226
  docker compose up -d
@@ -213,12 +230,22 @@ bundle exec rake # rspec + rubocop
213
230
  bundle exec rspec # spec only
214
231
  ```
215
232
 
216
- Override the connection with `DATABASE_URL` (used by CI):
233
+ Switch adapters with `ARCONN`; SQLite runs against an in-memory database and
234
+ needs no external service:
235
+
236
+ ```bash
237
+ ARCONN=postgresql bundle exec rspec # default (uses DATABASE_URL)
238
+ ARCONN=sqlite3 bundle exec rspec
239
+ ```
240
+
241
+ Override the PostgreSQL connection with `DATABASE_URL` (used by CI):
217
242
 
218
243
  ```bash
219
244
  DATABASE_URL=postgres://user:pass@host:port/db bundle exec rspec
220
245
  ```
221
246
 
247
+ Both adapters are exercised in CI on every push and pull request.
248
+
222
249
  ## Contributing
223
250
 
224
251
  Bug reports and pull requests are welcome on GitHub at
@@ -4,18 +4,28 @@ module ActiveRecord
4
4
  class Duplicator
5
5
  class Error < StandardError; end
6
6
 
7
- # Raised when store_new_id (or mark_skip) tries to overwrite an existing
8
- # mapping with a different new_id. This typically indicates a bug in the
9
- # calling code or a handler.
7
+ # Raised when store_duplicate_id (or no_duplicate) tries to overwrite an
8
+ # existing mapping with a different new_id. This typically indicates a bug
9
+ # in the calling code or a handler.
10
10
  class InvalidRecordIdError < Error; end
11
11
 
12
- # Raised when fetch_new_id cannot find a mapping for the given old_id.
12
+ # Raised when fetch_duplicate_id cannot find a mapping for the given old_id.
13
13
  # This usually means a belongs_to parent has not been duplicated yet;
14
- # place the parent earlier in the associations tree, or register/skip it
15
- # explicitly.
14
+ # place the parent earlier in the associations tree, or call no_duplicate /
15
+ # no_duplicate_id on it explicitly.
16
16
  class MissingNewIdError < Error; end
17
17
 
18
18
  # Raised when Session#on is called twice for the same model class.
19
19
  class DuplicateHandlerError < Error; end
20
+
21
+ # Raised when a bulk_insert is attempted against a connection whose adapter
22
+ # does not implement `supports_insert_returning?`. The default duplication
23
+ # path relies on `insert_all!` returning the inserted primary keys so the
24
+ # id map can be populated in one round trip; adapters that do not support
25
+ # RETURNING (e.g. mysql2, trilogy) cannot participate in this flow.
26
+ #
27
+ # Register a handler with `Duplicator#on` / `Session#on` that uses
28
+ # `create!` (or similar) if you need to duplicate rows on such an adapter.
29
+ class UnsupportedAdapterError < Error; end
20
30
  end
21
31
  end
@@ -15,28 +15,28 @@ module ActiveRecord
15
15
  end
16
16
 
17
17
  #: (*ActiveRecord::Base) -> void
18
- def mark_skip(...)
19
- @session.mark_skip(...)
18
+ def no_duplicate(...)
19
+ @session.no_duplicate(...)
20
20
  end
21
21
 
22
22
  #: (Class, *untyped) -> void
23
- def mark_skip_id(...)
24
- @session.mark_skip_id(...)
23
+ def no_duplicate_id(...)
24
+ @session.no_duplicate_id(...)
25
25
  end
26
26
 
27
27
  #: (*Class) -> void
28
- def mark_skip_class(...)
29
- @session.mark_skip_class(...)
28
+ def no_duplicate_class(...)
29
+ @session.no_duplicate_class(...)
30
30
  end
31
31
 
32
32
  #: (Class, old_id: untyped, new_id: untyped) -> void
33
- def store_new_id(...)
34
- @session.store_new_id(...)
33
+ def store_duplicate_id(...)
34
+ @session.store_duplicate_id(...)
35
35
  end
36
36
 
37
37
  #: (Class, old_id: untyped) -> untyped
38
- def fetch_new_id(...)
39
- @session.fetch_new_id(...)
38
+ def fetch_duplicate_id(...)
39
+ @session.fetch_duplicate_id(...)
40
40
  end
41
41
 
42
42
  #: (Class, untyped, ?batch_size: Integer, ?cursor: untyped) -> void
@@ -5,13 +5,13 @@ module ActiveRecord
5
5
  # One-shot execution context for a duplication run.
6
6
  #
7
7
  # A Session owns the record id map (old_id -> new_id, per class) and
8
- # exposes the API a handler needs: mark_skip / store_new_id / fetch_new_id
9
- # / bulk_insert / attributes_for.
8
+ # exposes the API a handler needs: no_duplicate / store_duplicate_id /
9
+ # fetch_duplicate_id / bulk_insert / attributes_for.
10
10
  #
11
11
  # Users normally do not instantiate Session directly; a fresh Session is
12
12
  # created by Duplicator#duplicate for every run, so state does not leak
13
13
  # across runs. Duplicator#duplicate yields the Session to its block so
14
- # callers can mark_skip / store_new_id before the run starts.
14
+ # callers can no_duplicate / store_duplicate_id before the run starts.
15
15
  #
16
16
  # Composite primary keys (Rails 7.1+ `self.primary_key = [:a, :b]`) are
17
17
  # supported. An id is a scalar for a single-column primary key and an
@@ -24,16 +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
+ # @rbs @no_duplicate_classes: Set[Class]
28
28
 
29
- #: (?handlers: Hash[Class, Proc], ?skipped_classes: Set[Class]) -> void
30
- def initialize(handlers: {}, skipped_classes: Set.new)
29
+ #: (?handlers: Hash[Class, Proc], ?no_duplicate_classes: Set[Class]) -> void
30
+ def initialize(handlers: {}, no_duplicate_classes: Set.new)
31
31
  @record_id_map = Hash.new { |hash, klass| hash[klass] = {} }
32
32
  @handlers = handlers
33
33
  @user_handlers = {}
34
- # dup so that Session#mark_skip_class additions never leak back to the
34
+ # dup so that Session#no_duplicate_class additions never leak back to the
35
35
  # Duplicator-level set shared across every run.
36
- @skipped_classes = skipped_classes.dup
36
+ @no_duplicate_classes = no_duplicate_classes.dup
37
37
  end
38
38
 
39
39
  # Register a handler that overrides Duplicator#on for the current session
@@ -54,36 +54,36 @@ module ActiveRecord
54
54
  # is set equal to their old_id, so any record duplicated later that
55
55
  # references them will keep pointing at the same rows.
56
56
  #: (*ActiveRecord::Base) -> void
57
- def mark_skip(*records)
57
+ def no_duplicate(*records)
58
58
  records.each do |record|
59
59
  id = current_id(record)
60
- store_new_id(record.class, old_id: id, new_id: id)
60
+ store_duplicate_id(record.class, old_id: id, new_id: id)
61
61
  end
62
62
  end
63
63
 
64
64
  # Declare that the given ids of the given class must not be duplicated,
65
- # without loading the records. Equivalent to calling mark_skip on each
65
+ # without loading the records. Equivalent to calling no_duplicate on each
66
66
  # record but avoids the database round-trip when the ids are already
67
67
  # known. Each id should have the same shape as the class's primary key
68
68
  # (scalar for a single-column pk, Array for a composite pk).
69
69
  #: (Class, *untyped) -> void
70
- def mark_skip_id(klass, *ids)
71
- ids.each { |id| store_new_id(klass, old_id: id, new_id: id) }
70
+ def no_duplicate_id(klass, *ids)
71
+ ids.each { |id| store_duplicate_id(klass, old_id: id, new_id: id) }
72
72
  end
73
73
 
74
- # Class-wide equivalent of mark_skip: it is exactly as if every record of
75
- # each given class had been passed to mark_skip. Handlers still fire
76
- # (matching the mark_skip case where a handler for the class is invoked
77
- # with a Relation that happens to contain pre-skipped records); the
78
- # default bulk_insert path becomes a no-op, and fetch_new_id echoes the
79
- # queried old_id back unchanged.
74
+ # Class-wide equivalent of no_duplicate: it is exactly as if every record
75
+ # of each given class had been passed to no_duplicate. Handlers still fire
76
+ # (matching the no_duplicate case where a handler for the class is invoked
77
+ # with a Relation that happens to contain no_duplicate records); the
78
+ # default bulk_insert path becomes a no-op, and fetch_duplicate_id echoes
79
+ # the queried old_id back unchanged.
80
80
  #
81
81
  # Use when the target database is expected to already hold every
82
82
  # referenced row (shared lookup tables, tenant-wide catalogs, etc.), so
83
- # per-record mark_skip would only differ from this in cost, not effect.
83
+ # per-record no_duplicate would only differ from this in cost, not effect.
84
84
  #: (*Class) -> void
85
- def mark_skip_class(*klasses)
86
- @skipped_classes.merge(klasses)
85
+ def no_duplicate_class(*klasses)
86
+ @no_duplicate_classes.merge(klasses)
87
87
  end
88
88
 
89
89
  # Record a mapping from an old_id to a new_id. Called automatically from
@@ -96,7 +96,7 @@ module ActiveRecord
96
96
  # Raises InvalidRecordIdError when the same old_id is already mapped to
97
97
  # a different new_id.
98
98
  #: (Class, old_id: untyped, new_id: untyped) -> void
99
- def store_new_id(klass, old_id:, new_id:)
99
+ def store_duplicate_id(klass, old_id:, new_id:)
100
100
  map = @record_id_map[klass]
101
101
 
102
102
  if !map.key?(old_id)
@@ -113,26 +113,28 @@ module ActiveRecord
113
113
  #
114
114
  # Raises MissingNewIdError when no mapping exists. This usually means a
115
115
  # belongs_to parent has not been duplicated yet; place it earlier in the
116
- # associations tree or register/skip it explicitly.
116
+ # associations tree, or call no_duplicate / no_duplicate_id explicitly.
117
117
  #: (Class, old_id: untyped) -> untyped
118
- def fetch_new_id(klass, old_id:)
119
- return old_id if @skipped_classes.include?(klass)
118
+ def fetch_duplicate_id(klass, old_id:)
119
+ return old_id if @no_duplicate_classes.include?(klass)
120
120
 
121
121
  @record_id_map[klass].fetch(old_id) do
122
- raise MissingNewIdError, "no new_id registered for #{klass}##{old_id.inspect}"
122
+ raise MissingNewIdError, "no duplicate id registered for #{klass}##{old_id.inspect}"
123
123
  end
124
124
  end
125
125
 
126
- # Bulk-insert copies of the given records into klass, skipping any that
126
+ # Bulk-insert copies of the given records into klass, omitting any that
127
127
  # are already present in the id map. Handlers can reuse this via HandlerApi
128
128
  # to defer a subset of records to the default duplication path.
129
129
  #
130
- # When klass has been passed to mark_skip_class the call is a no-op that
130
+ # When klass has been passed to no_duplicate_class the call is a no-op that
131
131
  # never touches the DB: it is equivalent to having pre-registered every
132
- # incoming record via mark_skip, only without the per-record enumeration.
132
+ # incoming record via no_duplicate, only without the per-record enumeration.
133
133
  #: (Class, untyped, ?batch_size: Integer, ?cursor: untyped) -> void
134
134
  def bulk_insert(klass, records, batch_size: DEFAULT_BATCH_SIZE, cursor: nil)
135
- return if @skipped_classes.include?(klass)
135
+ return if @no_duplicate_classes.include?(klass)
136
+
137
+ ensure_insert_returning_supported!(klass)
136
138
 
137
139
  batches = enumerate_batches(records, batch_size:, cursor:)
138
140
 
@@ -147,7 +149,7 @@ module ActiveRecord
147
149
  inserted.to_a.each.with_index do |returned_row, i|
148
150
  new_id = extract_id_from_row(klass, returned_row)
149
151
  old_id = current_id(old_records[i])
150
- store_new_id(klass, old_id:, new_id:)
152
+ store_duplicate_id(klass, old_id:, new_id:)
151
153
  end
152
154
  end
153
155
  end
@@ -189,7 +191,7 @@ module ActiveRecord
189
191
  duplicate_records(relation.klass, relation)
190
192
  end
191
193
 
192
- new_id = fetch_new_id(root_record.class, old_id: current_id(root_record))
194
+ new_id = fetch_duplicate_id(root_record.class, old_id: current_id(root_record))
193
195
  root_record.class.find(new_id)
194
196
  end
195
197
  end
@@ -201,9 +203,9 @@ module ActiveRecord
201
203
  # caller is responsible for splitting mixed inputs (e.g. STI subclasses)
202
204
  # into per-class Relations and passing the matching class in.
203
205
  #
204
- # mark_skip_class does NOT short-circuit here: handlers must still fire
205
- # (mirroring the per-record mark_skip case), and the class-wide skip is
206
- # applied inside bulk_insert instead.
206
+ # no_duplicate_class does NOT short-circuit here: handlers must still fire
207
+ # (mirroring the per-record no_duplicate case), and the class-wide exclusion
208
+ # is applied inside bulk_insert instead.
207
209
  #: (Class, untyped) -> void
208
210
  def duplicate_records(klass, relation_or_records)
209
211
  handler = resolve_handler(klass)
@@ -266,26 +268,46 @@ module ActiveRecord
266
268
  if old_values.all?(&:nil?)
267
269
  fk.each { |c| attrs[c] = nil }
268
270
  else
269
- new_values = fetch_new_id(reflection.klass, old_id: old_values)
271
+ new_values = fetch_duplicate_id(reflection.klass, old_id: old_values)
270
272
  fk.zip(new_values).each { |c, v| attrs[c] = v }
271
273
  end
272
274
  else
273
275
  old_value = old_record[fk]
274
- attrs[fk] = old_value.nil? ? nil : fetch_new_id(reflection.klass, old_id: old_value)
276
+ attrs[fk] = old_value.nil? ? nil : fetch_duplicate_id(reflection.klass, old_id: old_value)
275
277
  end
276
278
  end
277
279
 
278
280
  # Primary key columns whose values are filled in by the database (e.g. a
279
- # bigserial `id`). These must be stripped from the attributes before
280
- # calling insert_all! so the DB can assign fresh values. Composite pk
281
- # columns without a default_function (e.g. tenant_id) are kept.
281
+ # bigserial `id`, a SQLite INTEGER PRIMARY KEY / ROWID alias, a column
282
+ # with a `default_function` such as `gen_random_uuid()`). These must be
283
+ # stripped from the attributes before calling insert_all! so the DB can
284
+ # assign fresh values. Composite pk columns without a default (e.g.
285
+ # tenant_id) are kept.
282
286
  #: (Class) -> Array[String]
283
287
  def auto_generated_pk_columns(klass)
284
288
  Array.wrap(klass.primary_key).select do |key|
285
289
  column = klass.column_for_attribute(key)
286
- column.default_function.present?
290
+ column.auto_populated?
287
291
  end
288
292
  end
293
+
294
+ # Guard against adapters that cannot return inserted primary keys.
295
+ # The default path relies on `insert_all!(...)` echoing back the pk of
296
+ # every inserted row so the id map can be populated in one round trip;
297
+ # adapters that do not support RETURNING silently return an empty
298
+ # inserted-row set and would leave the map incomplete.
299
+ #: (Class) -> void
300
+ def ensure_insert_returning_supported!(klass)
301
+ connection = klass.connection
302
+ return if connection.supports_insert_returning?
303
+
304
+ raise UnsupportedAdapterError,
305
+ "#{connection.adapter_name} does not support insert with RETURNING. " \
306
+ "activerecord-duplicator requires an adapter whose " \
307
+ "`supports_insert_returning?` returns true (e.g. PostgreSQL, or " \
308
+ "SQLite >= 3.35). Register a custom handler with `on(#{klass})` " \
309
+ "if you need to duplicate #{klass} on this adapter."
310
+ end
289
311
  end
290
312
  end
291
313
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  class Duplicator
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
@@ -15,16 +15,16 @@ module ActiveRecord
15
15
  # duplicator.freeze
16
16
  #
17
17
  # new_root = duplicator.duplicate(old_root, associations: [...]) do |session|
18
- # session.mark_skip(tenant)
18
+ # session.no_duplicate(tenant)
19
19
  # end
20
20
  class Duplicator
21
21
  # @rbs @handlers: Hash[Class, Proc]
22
- # @rbs @skipped_classes: Set[Class]
22
+ # @rbs @no_duplicate_classes: Set[Class]
23
23
 
24
24
  #: () -> void
25
25
  def initialize
26
26
  @handlers = {}
27
- @skipped_classes = Set.new
27
+ @no_duplicate_classes = Set.new
28
28
  end
29
29
 
30
30
  # Register a custom handler for the given model class. See HandlerApi for
@@ -39,13 +39,13 @@ module ActiveRecord
39
39
  end
40
40
 
41
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
42
+ # any subsequent run. See Session#no_duplicate_class for the semantics; the
43
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.
44
+ # baseline, and Session#no_duplicate_class can extend it for a single run.
45
45
  # Repeated calls with the same class are idempotent.
46
46
  #: (*Class) -> void
47
- def mark_skip_class(*klasses)
48
- @skipped_classes.merge(klasses)
47
+ def no_duplicate_class(*klasses)
48
+ @no_duplicate_classes.merge(klasses)
49
49
  end
50
50
 
51
51
  # Prevent further registrations. Also freezes the internal handler map so
@@ -53,7 +53,7 @@ module ActiveRecord
53
53
  #: () -> self
54
54
  def freeze
55
55
  @handlers.freeze
56
- @skipped_classes.freeze
56
+ @no_duplicate_classes.freeze
57
57
  super
58
58
  end
59
59
 
@@ -61,15 +61,15 @@ module ActiveRecord
61
61
  # created for every invocation, so state does not leak between runs.
62
62
  #
63
63
  # If a block is given it is called with the new Session before duplication
64
- # starts, letting the caller mark_skip or store_new_id for records that
65
- # were duplicated elsewhere.
64
+ # starts, letting the caller no_duplicate or store_duplicate_id for records
65
+ # that were duplicated elsewhere.
66
66
  #
67
67
  # transaction_options is forwarded verbatim to Session#run, which forwards
68
68
  # it to `root_record.transaction`. Default is `{}` (plain
69
69
  # `transaction do ... end`).
70
70
  #: (ActiveRecord::Base, ?associations: untyped, ?transaction_options: Hash[Symbol, untyped]) ?{ (Session) -> void } -> ActiveRecord::Base
71
71
  def duplicate(root_record, associations: [], transaction_options: {}, &block)
72
- session = Session.new(handlers: @handlers, skipped_classes: @skipped_classes)
72
+ session = Session.new(handlers: @handlers, no_duplicate_classes: @no_duplicate_classes)
73
73
  block&.call(session)
74
74
  session.run(root_record, associations:, transaction_options:)
75
75
  end
@@ -5,21 +5,32 @@ module ActiveRecord
5
5
  class Error < StandardError
6
6
  end
7
7
 
8
- # Raised when store_new_id (or mark_skip) tries to overwrite an existing
9
- # mapping with a different new_id. This typically indicates a bug in the
10
- # calling code or a handler.
8
+ # Raised when store_duplicate_id (or no_duplicate) tries to overwrite an
9
+ # existing mapping with a different new_id. This typically indicates a bug
10
+ # in the calling code or a handler.
11
11
  class InvalidRecordIdError < Error
12
12
  end
13
13
 
14
- # Raised when fetch_new_id cannot find a mapping for the given old_id.
14
+ # Raised when fetch_duplicate_id cannot find a mapping for the given old_id.
15
15
  # This usually means a belongs_to parent has not been duplicated yet;
16
- # place the parent earlier in the associations tree, or register/skip it
17
- # explicitly.
16
+ # place the parent earlier in the associations tree, or call no_duplicate /
17
+ # no_duplicate_id on it explicitly.
18
18
  class MissingNewIdError < Error
19
19
  end
20
20
 
21
21
  # Raised when Session#on is called twice for the same model class.
22
22
  class DuplicateHandlerError < Error
23
23
  end
24
+
25
+ # Raised when a bulk_insert is attempted against a connection whose adapter
26
+ # does not implement `supports_insert_returning?`. The default duplication
27
+ # path relies on `insert_all!` returning the inserted primary keys so the
28
+ # id map can be populated in one round trip; adapters that do not support
29
+ # RETURNING (e.g. mysql2, trilogy) cannot participate in this flow.
30
+ #
31
+ # Register a handler with `Duplicator#on` / `Session#on` that uses
32
+ # `create!` (or similar) if you need to duplicate rows on such an adapter.
33
+ class UnsupportedAdapterError < Error
34
+ end
24
35
  end
25
36
  end
@@ -13,19 +13,19 @@ module ActiveRecord
13
13
  def initialize: (Session) -> void
14
14
 
15
15
  # : (*ActiveRecord::Base) -> void
16
- def mark_skip: (*ActiveRecord::Base) -> void
16
+ def no_duplicate: (*ActiveRecord::Base) -> void
17
17
 
18
18
  # : (Class, *untyped) -> void
19
- def mark_skip_id: (Class, *untyped) -> void
19
+ def no_duplicate_id: (Class, *untyped) -> void
20
20
 
21
21
  # : (*Class) -> void
22
- def mark_skip_class: (*Class) -> void
22
+ def no_duplicate_class: (*Class) -> void
23
23
 
24
24
  # : (Class, old_id: untyped, new_id: untyped) -> void
25
- def store_new_id: (Class, old_id: untyped, new_id: untyped) -> void
25
+ def store_duplicate_id: (Class, old_id: untyped, new_id: untyped) -> void
26
26
 
27
27
  # : (Class, old_id: untyped) -> untyped
28
- def fetch_new_id: (Class, old_id: untyped) -> untyped
28
+ def fetch_duplicate_id: (Class, old_id: untyped) -> untyped
29
29
 
30
30
  # : (Class, untyped, ?batch_size: Integer, ?cursor: untyped) -> void
31
31
  def bulk_insert: (Class, untyped, ?batch_size: Integer, ?cursor: untyped) -> void
@@ -5,13 +5,13 @@ module ActiveRecord
5
5
  # One-shot execution context for a duplication run.
6
6
  #
7
7
  # A Session owns the record id map (old_id -> new_id, per class) and
8
- # exposes the API a handler needs: mark_skip / store_new_id / fetch_new_id
9
- # / bulk_insert / attributes_for.
8
+ # exposes the API a handler needs: no_duplicate / store_duplicate_id /
9
+ # fetch_duplicate_id / bulk_insert / attributes_for.
10
10
  #
11
11
  # Users normally do not instantiate Session directly; a fresh Session is
12
12
  # created by Duplicator#duplicate for every run, so state does not leak
13
13
  # across runs. Duplicator#duplicate yields the Session to its block so
14
- # callers can mark_skip / store_new_id before the run starts.
14
+ # callers can no_duplicate / store_duplicate_id before the run starts.
15
15
  #
16
16
  # Composite primary keys (Rails 7.1+ `self.primary_key = [:a, :b]`) are
17
17
  # supported. An id is a scalar for a single-column primary key and an
@@ -20,7 +20,7 @@ module ActiveRecord
20
20
  class Session
21
21
  DEFAULT_BATCH_SIZE: ::Integer
22
22
 
23
- @skipped_classes: Set[Class]
23
+ @no_duplicate_classes: Set[Class]
24
24
 
25
25
  @user_handlers: Hash[Class, Proc]
26
26
 
@@ -28,8 +28,8 @@ module ActiveRecord
28
28
 
29
29
  @record_id_map: Hash[Class, Hash[untyped, untyped]]
30
30
 
31
- # : (?handlers: Hash[Class, Proc], ?skipped_classes: Set[Class]) -> void
32
- def initialize: (?handlers: Hash[Class, Proc], ?skipped_classes: Set[Class]) -> void
31
+ # : (?handlers: Hash[Class, Proc], ?no_duplicate_classes: Set[Class]) -> void
32
+ def initialize: (?handlers: Hash[Class, Proc], ?no_duplicate_classes: Set[Class]) -> void
33
33
 
34
34
  # Register a handler that overrides Duplicator#on for the current session
35
35
  # only. Useful when a single duplication run needs a special copy of a
@@ -45,28 +45,28 @@ module ActiveRecord
45
45
  # is set equal to their old_id, so any record duplicated later that
46
46
  # references them will keep pointing at the same rows.
47
47
  # : (*ActiveRecord::Base) -> void
48
- def mark_skip: (*ActiveRecord::Base) -> void
48
+ def no_duplicate: (*ActiveRecord::Base) -> void
49
49
 
50
50
  # Declare that the given ids of the given class must not be duplicated,
51
- # without loading the records. Equivalent to calling mark_skip on each
51
+ # without loading the records. Equivalent to calling no_duplicate on each
52
52
  # record but avoids the database round-trip when the ids are already
53
53
  # known. Each id should have the same shape as the class's primary key
54
54
  # (scalar for a single-column pk, Array for a composite pk).
55
55
  # : (Class, *untyped) -> void
56
- def mark_skip_id: (Class, *untyped) -> void
57
-
58
- # Class-wide equivalent of mark_skip: it is exactly as if every record of
59
- # each given class had been passed to mark_skip. Handlers still fire
60
- # (matching the mark_skip case where a handler for the class is invoked
61
- # with a Relation that happens to contain pre-skipped records); the
62
- # default bulk_insert path becomes a no-op, and fetch_new_id echoes the
63
- # queried old_id back unchanged.
56
+ def no_duplicate_id: (Class, *untyped) -> void
57
+
58
+ # Class-wide equivalent of no_duplicate: it is exactly as if every record
59
+ # of each given class had been passed to no_duplicate. Handlers still fire
60
+ # (matching the no_duplicate case where a handler for the class is invoked
61
+ # with a Relation that happens to contain no_duplicate records); the
62
+ # default bulk_insert path becomes a no-op, and fetch_duplicate_id echoes
63
+ # the queried old_id back unchanged.
64
64
  #
65
65
  # Use when the target database is expected to already hold every
66
66
  # referenced row (shared lookup tables, tenant-wide catalogs, etc.), so
67
- # per-record mark_skip would only differ from this in cost, not effect.
67
+ # per-record no_duplicate would only differ from this in cost, not effect.
68
68
  # : (*Class) -> void
69
- def mark_skip_class: (*Class) -> void
69
+ def no_duplicate_class: (*Class) -> void
70
70
 
71
71
  # Record a mapping from an old_id to a new_id. Called automatically from
72
72
  # bulk_insert; called explicitly by user code (or a handler) when new
@@ -78,24 +78,24 @@ module ActiveRecord
78
78
  # Raises InvalidRecordIdError when the same old_id is already mapped to
79
79
  # a different new_id.
80
80
  # : (Class, old_id: untyped, new_id: untyped) -> void
81
- def store_new_id: (Class, old_id: untyped, new_id: untyped) -> void
81
+ def store_duplicate_id: (Class, old_id: untyped, new_id: untyped) -> void
82
82
 
83
83
  # Look up a new_id by (klass, old_id). Returns a scalar for a
84
84
  # single-column pk and an Array for a composite pk.
85
85
  #
86
86
  # Raises MissingNewIdError when no mapping exists. This usually means a
87
87
  # belongs_to parent has not been duplicated yet; place it earlier in the
88
- # associations tree or register/skip it explicitly.
88
+ # associations tree, or call no_duplicate / no_duplicate_id explicitly.
89
89
  # : (Class, old_id: untyped) -> untyped
90
- def fetch_new_id: (Class, old_id: untyped) -> untyped
90
+ def fetch_duplicate_id: (Class, old_id: untyped) -> untyped
91
91
 
92
- # Bulk-insert copies of the given records into klass, skipping any that
92
+ # Bulk-insert copies of the given records into klass, omitting any that
93
93
  # are already present in the id map. Handlers can reuse this via HandlerApi
94
94
  # to defer a subset of records to the default duplication path.
95
95
  #
96
- # When klass has been passed to mark_skip_class the call is a no-op that
96
+ # When klass has been passed to no_duplicate_class the call is a no-op that
97
97
  # never touches the DB: it is equivalent to having pre-registered every
98
- # incoming record via mark_skip, only without the per-record enumeration.
98
+ # incoming record via no_duplicate, only without the per-record enumeration.
99
99
  # : (Class, untyped, ?batch_size: Integer, ?cursor: untyped) -> void
100
100
  def bulk_insert: (Class, untyped, ?batch_size: Integer, ?cursor: untyped) -> void
101
101
 
@@ -128,9 +128,9 @@ module ActiveRecord
128
128
  # caller is responsible for splitting mixed inputs (e.g. STI subclasses)
129
129
  # into per-class Relations and passing the matching class in.
130
130
  #
131
- # mark_skip_class does NOT short-circuit here: handlers must still fire
132
- # (mirroring the per-record mark_skip case), and the class-wide skip is
133
- # applied inside bulk_insert instead.
131
+ # no_duplicate_class does NOT short-circuit here: handlers must still fire
132
+ # (mirroring the per-record no_duplicate case), and the class-wide exclusion
133
+ # is applied inside bulk_insert instead.
134
134
  # : (Class, untyped) -> void
135
135
  def duplicate_records: (Class, untyped) -> void
136
136
 
@@ -156,11 +156,21 @@ module ActiveRecord
156
156
  def assign_foreign_key: (Hash[String, untyped], untyped, ActiveRecord::Base) -> void
157
157
 
158
158
  # Primary key columns whose values are filled in by the database (e.g. a
159
- # bigserial `id`). These must be stripped from the attributes before
160
- # calling insert_all! so the DB can assign fresh values. Composite pk
161
- # columns without a default_function (e.g. tenant_id) are kept.
159
+ # bigserial `id`, a SQLite INTEGER PRIMARY KEY / ROWID alias, a column
160
+ # with a `default_function` such as `gen_random_uuid()`). These must be
161
+ # stripped from the attributes before calling insert_all! so the DB can
162
+ # assign fresh values. Composite pk columns without a default (e.g.
163
+ # tenant_id) are kept.
162
164
  # : (Class) -> Array[String]
163
165
  def auto_generated_pk_columns: (Class) -> Array[String]
166
+
167
+ # Guard against adapters that cannot return inserted primary keys.
168
+ # The default path relies on `insert_all!(...)` echoing back the pk of
169
+ # every inserted row so the id map can be populated in one round trip;
170
+ # adapters that do not support RETURNING silently return an empty
171
+ # inserted-row set and would leave the map incomplete.
172
+ # : (Class) -> void
173
+ def ensure_insert_returning_supported!: (Class) -> void
164
174
  end
165
175
  end
166
176
  end
@@ -13,12 +13,12 @@ module ActiveRecord
13
13
  # duplicator.freeze
14
14
  #
15
15
  # new_root = duplicator.duplicate(old_root, associations: [...]) do |session|
16
- # session.mark_skip(tenant)
16
+ # session.no_duplicate(tenant)
17
17
  # end
18
18
  class Duplicator
19
19
  @handlers: Hash[Class, Proc]
20
20
 
21
- @skipped_classes: Set[Class]
21
+ @no_duplicate_classes: Set[Class]
22
22
 
23
23
  # : () -> void
24
24
  def initialize: () -> void
@@ -31,12 +31,12 @@ module ActiveRecord
31
31
  def on: (Class) { (HandlerApi, Class, untyped) -> void } -> void
32
32
 
33
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
34
+ # any subsequent run. See Session#no_duplicate_class for the semantics; the
35
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.
36
+ # baseline, and Session#no_duplicate_class can extend it for a single run.
37
37
  # Repeated calls with the same class are idempotent.
38
38
  # : (*Class) -> void
39
- def mark_skip_class: (*Class) -> void
39
+ def no_duplicate_class: (*Class) -> void
40
40
 
41
41
  # Prevent further registrations. Also freezes the internal handler map so
42
42
  # accidental mutation later fails loudly.
@@ -47,8 +47,8 @@ module ActiveRecord
47
47
  # created for every invocation, so state does not leak between runs.
48
48
  #
49
49
  # If a block is given it is called with the new Session before duplication
50
- # starts, letting the caller mark_skip or store_new_id for records that
51
- # were duplicated elsewhere.
50
+ # starts, letting the caller no_duplicate or store_duplicate_id for records
51
+ # that were duplicated elsewhere.
52
52
  #
53
53
  # transaction_options is forwarded verbatim to Session#run, which forwards
54
54
  # it to `root_record.transaction`. Default is `{}` (plain
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SmartHR
@@ -23,20 +23,6 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '8.0'
26
- - !ruby/object:Gem::Dependency
27
- name: pg
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: '1.5'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: '1.5'
40
26
  description: |
41
27
  activerecord-duplicator copies an ActiveRecord record and its associated rows
42
28
  (has_many, has_one, belongs_to, has_many :through) in one call. It rewires