activerecord-import 2.3.0 → 3.0.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: 18ab977c921d6e9c51ca329d136998d858ddbe3cae0a186160886cd43ee6b6e9
4
- data.tar.gz: 98d166b037d997de14492f621099ffb63af16505da1c8c36a2e302a903213777
3
+ metadata.gz: 44eca8fc38325819cfc2eeb8c9a6151182f5de96135af6ad3ae1cc145c5f7740
4
+ data.tar.gz: 9f6ef423efde4813da41b8eb5ba218d4166bb3a86beba83d72b4bbf9e70af27f
5
5
  SHA512:
6
- metadata.gz: 2a593859fb0b568b88e29e99e849257b536a78b87c9557e1f98fe196a8c1c0be3116c88676c74c58044f76cb69a2ae17a6093fcc463bd2ad5d914f551229c9b2
7
- data.tar.gz: 10b347057b09a027bbaafc752363553ed117ada314ced7c383276863b9989eda8ec92e9eabee8697099c5b6db7b6e909002a7f86d40df821438b9dabadb15f55
6
+ metadata.gz: f534b0b2a47c29484b8957109794ab8924c758993e93ecd871104f97ce8402856999c40d1e845ff7a2857fb92e12291a0d93259a9bdd17d4b60f81cb20ac5e42
7
+ data.tar.gz: f9e8de71d80edd76c978a1d85807b6a94247da8366dfda54a4c1e2c01ed25c31767d836c2ec487d39377379492dd896f320a986c1a158cfda7f810e7b3ba3a80
data/CHANGELOG.md CHANGED
@@ -1,3 +1,94 @@
1
+ ## Changes in 3.0.0
2
+
3
+ ### Breaking Changes
4
+
5
+ * Drop support for Ruby < 3.1 and ActiveRecord < 6.1. via #918 and #943.
6
+ * MariaDB 10.5+ now imports with `INSERT ... RETURNING`, as PostgreSQL does. Imports return
7
+ `ids` and `results`, imported models get their ids and are marked persisted, and
8
+ `recursive: true` imports associations (it was ignored on MariaDB before). MariaDB doesn't
9
+ report affected rows for `INSERT ... RETURNING`, so `affected_rows` is now `nil` on MariaDB
10
+ 10.5+ unless `no_returning: true` is passed. As on PostgreSQL, `on_duplicate_key_ignore` is
11
+ ignored for recursive imports. MySQL and MariaDB before 10.5 are unchanged. via #926.
12
+ * When only some rows have a value for a column the database fills in (a uuid or other primary
13
+ key with a default function, or a column with a default function), the other rows now get the
14
+ database's default instead of `NULL` on PostgreSQL and MySQL/MariaDB, and assigned primary keys
15
+ are no longer dropped when the first model doesn't have one. SQLite can't use `DEFAULT` in
16
+ `VALUES`, so such a mixed import now raises `ArgumentError` there. Fixes #702, #777, #805 and
17
+ #827 via #929.
18
+ * `on_duplicate_key_update: :all` and `columns: :all` no longer update the timestamps recorded on
19
+ create (`created_at`, `created_on`) when import records timestamps. via #927.
20
+ * Recursive imports now also import existing records that were assigned to a new parent, so they're
21
+ linked to it as `save` would do. Without `on_duplicate_key_update` that raises a duplicate key
22
+ error, where the records used to be silently left unlinked. Fixes #804 via #939.
23
+ * Importing with `synchronize` now keeps the synchronized models' `previous_changes`, rather than
24
+ clearing them. Fixes #738 via #933.
25
+
26
+ ### New Features
27
+
28
+ * Add `require "activerecord-import/without_aliases"` to load the library without the `import` and
29
+ `import!` aliases, for gems such as `elasticsearch-model` that define their own `import`. Use
30
+ `bulk_import`/`bulk_import!` instead. Fixes #592 via #915.
31
+ * `import` accepts array-like arguments such as an `ActiveRecord::Relation` or a `Set`, and invalid
32
+ arguments now raise an error saying what was received. Fixes #785 via #935.
33
+ * Annotations from the current scope (`Model.annotate(...)`) are added to the INSERT statements as
34
+ SQL comments. Fixes #872 via #937.
35
+ * When activerecord-import is required after the database connection is established, import now
36
+ loads the adapter's import support itself instead of inserting one row at a time. Adapters
37
+ without bulk import support log a warning when import falls back to row-by-row inserts.
38
+ via #925 and #941.
39
+ * Validate imported models with `valid?`, so `around` validate callbacks are supported. via #919.
40
+
41
+ ### Fixes
42
+
43
+ * Report `affected_rows` with the mysql2 adapter. It was always `nil` with mysql2, and only
44
+ reported with trilogy. via #944.
45
+ * Clear the query cache after MySQL imports on ActiveRecord < 7.1. via #926.
46
+ * Don't import the associations of models that failed validation. Fixes #857. Thanks to
47
+ @jacob-carlborg-apoex via #858.
48
+ * Fix `NoMethodError` in `returning_selections` when there's no model. Thanks to @mirelon via #892.
49
+ * Don't mark models that failed validation as persisted when `track_validation_failures` is set.
50
+ Fixes #888. Thanks to @OskarEichler via #903.
51
+ * Leave models unchanged when `all_or_none` aborts an import. Thanks to @OskarEichler via #904.
52
+ * Include the owner's foreign key and polymorphic type when importing models with explicit columns
53
+ through an association. Thanks to @OskarEichler via #905.
54
+ * Support string hash keys in association imports. Thanks to @OskarEichler via #909.
55
+ * Use the canonical polymorphic name (e.g. for STI owners) in association imports. Thanks to
56
+ @OskarEichler via #910.
57
+ * Raise `ArgumentError` for rows with fewer values than columns, before running any SQL. Thanks
58
+ to @OskarEichler via #912.
59
+ * Quote PostgreSQL `RETURNING` column names and generated conflict targets. Thanks to
60
+ @OskarEichler via #907 and #908.
61
+ * Skip uniqueness validations on associated records unless `validate_uniqueness: true`. Fixes #841
62
+ via #916.
63
+ * Use each model's own class for `options[:model]` in recursive imports. via #920.
64
+ * Leave columns the database populates (e.g. a uuid with a default function) out of the INSERT
65
+ when no imported model assigns them, as `save` does. via #921.
66
+ * Don't follow an attribute alias when its name is also a column. via #922.
67
+ * Don't modify the options hash passed to `import!`. via #924.
68
+ * Don't send a `RETURNING` clause on PostgreSQL with `no_returning: true`. Fixes #826 via #930.
69
+ * Support `returning: :id` with composite primary keys that include an `id` column. Fixes #842
70
+ via #931.
71
+ * Use the association's `primary_key` for foreign keys in recursive imports. Fixes #878 via #932.
72
+ * Serialize values of STI subclass models with the subclass's attribute types, e.g. for enums
73
+ defined only on the subclass. Fixes #789 via #934.
74
+ * Fix `NoMethodError` when filling in composite `belongs_to` foreign keys. via #938.
75
+
76
+ ### Performance
77
+
78
+ * Match synchronized records in linear rather than quadratic time. Thanks to @OskarEichler via #902.
79
+ * Collect returned ids and results across batches without recopying them. Thanks to @OskarEichler
80
+ via #906.
81
+
82
+ ### Development
83
+
84
+ * Test against MariaDB 10.11 and 11.8 in CI. via #926.
85
+ * Run the composite primary key tests on ActiveRecord 7.1, and speed up the JRuby CI job.
86
+ via #923, #928 and #945.
87
+ * Run the database tests locally against Docker without a hand-written `test/database.yml`.
88
+ via #945.
89
+ * Document the memory use of large imports and what gets logged. via #940.
90
+ * Split `import_helper` into smaller steps. via #947.
91
+
1
92
  ## Changes in 2.3.0
2
93
 
3
94
  ### New Features
data/README.markdown CHANGED
@@ -37,6 +37,7 @@ The gem provides the following high-level features:
37
37
  * [ActiveRecord Models](#activerecord-models)
38
38
  * [Batching](#batching)
39
39
  * [Recursive](#recursive)
40
+ * [Annotating the SQL](#annotating-the-sql)
40
41
  * [Options](#options)
41
42
  * [Duplicate Key Ignore](#duplicate-key-ignore)
42
43
  * [Duplicate Key Update](#duplicate-key-update)
@@ -44,6 +45,7 @@ The gem provides the following high-level features:
44
45
  * [Counter Cache](#counter-cache)
45
46
  * [ActiveRecord Timestamps](#activerecord-timestamps)
46
47
  * [Callbacks](#callbacks)
48
+ * [Logging](#logging)
47
49
  * [Supported Adapters](#supported-adapters)
48
50
  * [Additional Adapters](#additional-adapters)
49
51
  * [Requiring](#requiring)
@@ -231,6 +233,16 @@ columns = [ :title ]
231
233
  Book.import columns, books, batch_size: 2
232
234
  ```
233
235
 
236
+ `batch_size` only controls how many rows go into each INSERT statement. The whole import is still prepared up front: every record or row passed to `import`, and the SQL for all of them, is held in memory until the import finishes. For very large imports, load and import the data in chunks, so only one chunk is in memory at a time:
237
+
238
+ ```ruby
239
+ Source.find_in_batches(batch_size: 1_000) do |sources|
240
+ Book.import sources.map { |source| Book.new(title: source.title, author: source.author) }
241
+ end
242
+ ```
243
+
244
+ Each `import` call validates, inserts and returns its own result, so options like `all_or_none` apply to each chunk rather than to all of the data.
245
+
234
246
  If your import is particularly large or slow (possibly due to [callbacks](#callbacks)) whilst batch importing, you might want a way to report back on progress. This is supported by passing a callable as the `batch_progress` option. e.g:
235
247
 
236
248
  ```ruby
@@ -246,7 +258,7 @@ Book.import columns, books, batch_size: 2, batch_progress: my_proc
246
258
  #### Recursive
247
259
 
248
260
  > **Note**
249
- > This only works with PostgreSQL and ActiveRecord objects. This won't work with hashes or arrays as recursive inputs.
261
+ > This only works with PostgreSQL and MariaDB 10.5+, and with ActiveRecord objects. This won't work with hashes or arrays as recursive inputs.
250
262
 
251
263
  Assume that Books <code>has_many</code> Reviews.
252
264
 
@@ -260,12 +272,25 @@ end
260
272
  Book.import books, recursive: true
261
273
  ```
262
274
 
275
+ #### Annotating the SQL
276
+
277
+ To add SQL comments to an import, import through a relation annotated with ActiveRecord's [`annotate`](https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-annotate), or within its `scoping` block. The comments are added to every INSERT statement, including when the import is batched or split into several statements.
278
+
279
+ ```ruby
280
+ Book.annotate("nightly import").import books
281
+ # INSERT INTO "books" (...) VALUES (...) /* nightly import */
282
+
283
+ Book.annotate("nightly import").scoping do
284
+ Book.import books
285
+ end
286
+ ```
287
+
263
288
  ### Options
264
289
 
265
290
  Key | Options | Default | Description
266
291
  ------------------------- | --------------------- | ------------------ | -----------
267
292
  :validate | `true`/`false` | `true` | Whether or not to run `ActiveRecord` validations (uniqueness skipped). This option will always be true when using `import!`.
268
- :validate_uniqueness | `true`/`false` | `false` | Whether or not to run ActiveRecord uniqueness validations. Beware this will incur an sql query per-record (N+1 queries). (requires `>= v0.27.0`).
293
+ :validate_uniqueness | `true`/`false` | `false` | Whether or not to run ActiveRecord uniqueness validations, including on associated records that are validated with the model. Beware this will incur an sql query per-record (N+1 queries). (requires `>= v0.27.0`).
269
294
  :validate_with_context | `Symbol` |`:create`/`:update` | Allows passing an ActiveModel validation context for each model. Default is `:create` for new records and `:update` for existing ones.
270
295
  :track_validation_failures| `true`/`false` | `false` | When this is set to true, `failed_instances` will be an array of arrays, with each inner array having the form `[:index_in_dataset, :object_with_errors]`
271
296
  :on_duplicate_key_ignore | `true`/`false` | `false` | Allows skipping records with duplicate keys. See [here](#duplicate-key-ignore) for more details.
@@ -273,9 +298,9 @@ Key | Options | Default | Descrip
273
298
  :on_duplicate_key_update | :all, `Array`, `Hash` | N/A | Allows upsert logic to be used. See [here](#duplicate-key-update) for more details.
274
299
  :synchronize | `Array` | N/A | An array of ActiveRecord instances. This synchronizes existing instances in memory with updates from the import.
275
300
  :timestamps | `true`/`false` | `true` | Enables/disables timestamps on imported records.
276
- :recursive | `true`/`false` | `false` | Imports has_many/has_one associations (PostgreSQL only).
301
+ :recursive | `true`/`false` | `false` | Imports has_many/has_one associations (PostgreSQL and MariaDB 10.5+ only).
277
302
  :recursive_on_duplicate_key_update | `Hash` | N/A | Allows upsert logic to be used for recursive associations. The hash key is the association name and the value has the same options as `:on_duplicate_key_update`. See [here](#duplicate-key-update) for more details.
278
- :batch_size | `Integer` | total # of records | Max number of records to insert per import
303
+ :batch_size | `Integer` | total # of records | Max number of records to insert per INSERT statement. See [Batching](#batching).
279
304
  :raise_error | `true`/`false` | `false` | Raises an exception at the first invalid record. This means there will not be a result object returned. The `import!` method is a shortcut for this.
280
305
  :all_or_none | `true`/`false` | `false` | Will not import any records if there is a record with validation errors.
281
306
 
@@ -296,7 +321,7 @@ book.reload.title # => "Book1" (stayed the same)
296
321
  book.reload.author # => "George Orwell" (stayed the same)
297
322
  ```
298
323
 
299
- The option `:on_duplicate_key_ignore` is bypassed when `:recursive` is enabled for [PostgreSQL imports](https://rubydoc.info/gems/activerecord-import/ActiveRecord/Base#label-Options) (`:recursive` is not supported with other adapters).
324
+ The option `:on_duplicate_key_ignore` is bypassed when `:recursive` is enabled for [PostgreSQL and MariaDB 10.5+ imports](https://rubydoc.info/gems/activerecord-import/ActiveRecord/Base#label-Options) (`:recursive` is not supported with other databases).
300
325
 
301
326
  #### Duplicate Key Update
302
327
 
@@ -416,7 +441,7 @@ Book.import books, validate_uniqueness: true
416
441
 
417
442
  ### Return Info
418
443
 
419
- The `import` method returns a `Result` object that responds to `failed_instances` and `num_inserts`. Additionally, for users of Postgres, there will be two arrays `ids` and `results` that can be accessed.
444
+ The `import` method returns a `Result` object that responds to `failed_instances` and `num_inserts`. Additionally, for users of Postgres and MariaDB 10.5+, there will be two arrays `ids` and `results` that can be accessed. These are populated with `INSERT ... RETURNING`, which also sets the ids of imported models. On MariaDB this means `affected_rows` isn't available, since MariaDB doesn't report it for `INSERT ... RETURNING`; pass `no_returning: true` to get `affected_rows` instead of `ids`.
420
445
 
421
446
  ```ruby
422
447
  articles = [
@@ -434,11 +459,11 @@ demo.num_inserts
434
459
  => 1,
435
460
 
436
461
  demo.ids
437
- => ["1", "2"] # for Postgres
462
+ => ["1", "2"] # for Postgres and MariaDB 10.5+
438
463
  => [] # for other DBs
439
464
 
440
465
  demo.results
441
- => ["First Article", "Second Article"] # for Postgres
466
+ => ["First Article", "Second Article"] # for Postgres and MariaDB 10.5+
442
467
  => [] # for other DBs
443
468
  ```
444
469
 
@@ -499,12 +524,23 @@ end
499
524
  Book.import valid_books, validate: false
500
525
  ```
501
526
 
527
+ ### Logging
528
+
529
+ Imports are logged like ActiveRecord's own queries, named `<Model> Create Many`. The imported values are part of the SQL rather than bind parameters, so a logged INSERT includes every value it imports. That can make for very long log lines, and ActiveRecord's `filter_attributes` doesn't apply to them, so the values of sensitive columns appear in the log too. ActiveRecord's own `insert_all` and `upsert_all` log the same way.
530
+
531
+ If that's a concern, silence the logger around imports. For example, with Rails' logger:
532
+
533
+ ```ruby
534
+ ActiveRecord::Base.logger.silence { Book.import books }
535
+ ```
536
+
502
537
  ### Supported Adapters
503
538
 
504
539
  The following database adapters are currently supported:
505
540
 
506
541
  * MySQL - supports core import functionality plus on duplicate key update support (included in activerecord-import 0.1.0 and higher)
507
542
  * MySQL2 - supports core import functionality plus on duplicate key update support (included in activerecord-import 0.2.0 and higher)
543
+ * MariaDB - supported through the MySQL2 and Trilogy adapters, with the same functionality as MySQL. MariaDB 10.5+ also supports `ids`, `returning` and recursive imports.
508
544
  * PostgreSQL - supports core import functionality (included in activerecord-import 0.1.0 and higher)
509
545
  * SQLite3 - supports core import functionality (included in activerecord-import 0.1.0 and higher)
510
546
  * Oracle - supports core import functionality through DML trigger (available as an external gem: [activerecord-import-oracle_enhanced](https://github.com/keeguon/activerecord-import-oracle_enhanced))
@@ -550,12 +586,15 @@ You may want to manually load activerecord-import for one reason or another. Fir
550
586
  gem 'activerecord-import', require: false
551
587
  ```
552
588
 
553
- This will allow you to load up activerecord-import in the file or files where you are using it and only load the parts you need.
554
- If you are doing this within Rails and ActiveRecord has established a database connection (such as within a controller), you will need to do extra initialization work:
589
+ This will allow you to load up activerecord-import in the file or files where you are using it and only load the parts you need:
555
590
 
556
591
  ```ruby
557
592
  require 'activerecord-import/base'
558
- # load the appropriate database adapter (postgresql, mysql2, sqlite3, etc)
593
+ ```
594
+
595
+ This works even if ActiveRecord has already established a database connection (such as within a controller): the import support for the connection's adapter (postgresql, mysql2, sqlite3, etc) is loaded the first time you import. If your adapter goes by a name activerecord-import doesn't recognize, load its support yourself:
596
+
597
+ ```ruby
559
598
  require 'activerecord-import/active_record/adapters/postgresql_adapter'
560
599
  ```
561
600
 
@@ -599,6 +638,16 @@ When rubygems pushes the `lib` folder onto the load path a `require` will now fi
599
638
 
600
639
  Activerecord-Import adds the `.import` method onto `ActiveRecord::Base`. There are other gems, such as `elasticsearch-rails`, that do the same thing. In conflicts such as this, there is an aliased method named `.bulk_import` that can be used interchangeably.
601
640
 
641
+ Some gems, such as `elasticsearch-model`, won't define their own `.import` on a model if one already exists. To load activerecord-import without the `import` and `import!` aliases, so only `bulk_import` and `bulk_import!` are defined, require it like so:
642
+
643
+ ```ruby
644
+ gem 'activerecord-import', require: 'activerecord-import/without_aliases'
645
+ ```
646
+
647
+ This takes precedence even if `activerecord-import` or `activerecord-import/base` is also required, for example by another gem, regardless of the order they're required in. To confirm the aliases aren't defined, check that `ActiveRecord::Base.respond_to?(:import)` returns `false` in a console.
648
+
649
+ If you're loading activerecord-import manually (see [Manually Loading](#manually-loading)), require `activerecord-import/without_aliases` in place of `activerecord-import/base`.
650
+
602
651
  If you are using the `apartment` gem, there is a weird triple interaction between that gem, `activerecord-import`, and `activerecord` involving caching of the `sequence_name` of a model. This can be worked around by explicitly setting this value within the model. For example:
603
652
 
604
653
  ```ruby
@@ -632,22 +681,36 @@ To document new information, please add to the README instead of the wiki. See h
632
681
 
633
682
  #### Running Tests
634
683
 
635
- The first thing you need to do is set up your database(s):
684
+ The tests run against each database adapter (`bundle exec rake -T` lists them) and against multiple ActiveRecord versions, chosen with `AR_VERSION`. The SQLite tests need no setup. For MySQL, MariaDB and PostgreSQL, the quickest way to get databases is Docker (see [Docker Setup](#docker-setup)):
685
+
686
+ ```bash
687
+ docker compose up -d
688
+ rm -f Gemfile.lock
689
+ AR_VERSION=8.1 bundle install
690
+ AR_VERSION=8.1 bundle exec rake test:sqlite3 test:postgresql test:mysql2 test:trilogy
691
+ ```
636
692
 
637
- * copy `test/database.yml.sample` to `test/database.yml`
638
- * modify `test/database.yml` for your database settings
639
- * create databases as needed
693
+ Without a `test/database.yml`, the tests use `test/database.default.yml`, which connects to the Docker databases by default and can be pointed elsewhere with environment variables:
640
694
 
641
- After that, you can run the tests. They run against multiple tests and ActiveRecord versions.
695
+ | Variable | Default | |
696
+ | --- | --- | --- |
697
+ | `DB_HOST` | `127.0.0.1` | Host for all databases |
698
+ | `DB_DATABASE` | `activerecord_import_test` | Database name |
699
+ | `MYSQL_HOST`, `PG_HOST` | `DB_HOST` | Host for MySQL/MariaDB or PostgreSQL |
700
+ | `MYSQL_PORT` | `13306` | MySQL/MariaDB port; Docker's MariaDB is on `13307` |
701
+ | `PG_PORT` | `15432` | PostgreSQL port |
702
+ | `MYSQL_USERNAME`, `MYSQL_PASSWORD` | `root`, empty | |
703
+ | `PG_USERNAME`, `PG_PASSWORD` | `postgres`, empty | |
642
704
 
643
- This is one example of how to run the tests:
705
+ For example, to run the MySQL tests against Docker's MariaDB, or against a MySQL server you run yourself:
644
706
 
645
707
  ```bash
646
- rm Gemfile.lock
647
- AR_VERSION=7.0 bundle install
648
- AR_VERSION=7.0 bundle exec rake test:postgresql test:sqlite3 test:mysql2
708
+ MYSQL_PORT=13307 AR_VERSION=8.1 bundle exec rake test:mysql2
709
+ MYSQL_PORT=3306 MYSQL_PASSWORD=secret AR_VERSION=8.1 bundle exec rake test:mysql2
649
710
  ```
650
711
 
712
+ The PostgreSQL tests need the `hstore`, `pgcrypto` and `uuid-ossp` extensions, which they create themselves when connecting as a superuser, and `test:postgis` also needs PostGIS. If you'd rather keep your settings in a file, copy `test/database.yml.sample` to `test/database.yml` and edit it; when that file exists it is used instead (it can also use ERB).
713
+
651
714
  Once you have pushed up your changes, you can find your CI results [here](https://github.com/zdennis/activerecord-import/actions).
652
715
 
653
716
  #### Docker Setup
@@ -657,11 +720,15 @@ Before you begin, make sure you have [Docker](https://www.docker.com/products/do
657
720
  ```bash
658
721
  brew install docker && brew install docker-compose
659
722
  ```
660
- ##### Steps
661
723
 
662
- 1. In your terminal run `docker-compose up --build`
663
- 1. In another tab/window run `docker-compose exec app bash`
664
- 1. In that same terminal run the mysql2 test by running `bundle exec rake test:mysql2`
724
+ `docker compose up -d` starts MySQL 8.0, MariaDB 11.8 and PostgreSQL with PostGIS (the image CI uses), listening on localhost ports 13306, 13307 and 15432 so they don't collide with databases you may already run. To use other ports, set `MYSQL_PORT`, `MARIADB_PORT` or `PG_PORT` when starting them, and the same variables (with `MYSQL_PORT` set to the MariaDB port to test MariaDB) when running the tests. `docker compose down -v` stops them and deletes their data.
725
+
726
+ To run the tests inside Docker instead of with a local Ruby:
727
+
728
+ 1. In your terminal run `docker compose --profile app up -d --build`
729
+ 1. Then run `docker compose exec app bash`
730
+ 1. In that shell run `bundle install` (the image's gems are for `AR_VERSION=8.1`; to test another version, `rm Gemfile.lock` and run `AR_VERSION=7.2 bundle install`, then set `AR_VERSION` when running the tests too)
731
+ 1. Run the mysql2 tests with `bundle exec rake test:mysql2` (set `MYSQL_HOST=mariadb` to use MariaDB)
665
732
 
666
733
  ## Issue Triage [![Open Source Helpers](https://www.codetriage.com/zdennis/activerecord-import/badges/users.svg)](https://www.codetriage.com/zdennis/activerecord-import)
667
734
 
@@ -62,6 +62,59 @@ module ActiveRecord::Import::AbstractAdapter
62
62
  post_sql_statements
63
63
  end
64
64
 
65
+ # Returns the selections for an INSERT ... RETURNING clause: the primary key
66
+ # and any +:returning+ columns. Known columns are quoted, anything else (such
67
+ # as raw SQL) is returned unchanged.
68
+ def returning_selections(options)
69
+ selections = []
70
+ column_names = Array(options[:model]&.column_names)
71
+
72
+ selections += Array(options[:primary_key]) if options[:primary_key].present?
73
+ selections += Array(options[:returning]) if options[:returning].present?
74
+
75
+ selections.map do |selection|
76
+ column_names.include?(selection.to_s) ? quote_column_name(selection) : selection
77
+ end
78
+ end
79
+
80
+ # Splits the values returned by an INSERT ... RETURNING into the ids and the
81
+ # +:returning+ results. +returned_values+ is a Hash with the returned
82
+ # +:values+, and the +:columns+ when more than one column was returned.
83
+ def ids_and_results_from_returned_values( returned_values, options ) # :nodoc:
84
+ if options[:returning].blank?
85
+ [Array(returned_values[:values]), []]
86
+ elsif options[:primary_key].blank?
87
+ options[:returning_columns] ||= returned_values[:columns]
88
+ [[], Array(returned_values[:values])]
89
+ else
90
+ # split primary key and returning columns
91
+ ids, results, options[:returning_columns] = split_ids_and_results(returned_values, options)
92
+ [ids, results]
93
+ end
94
+ end
95
+
96
+ def split_ids_and_results( selections, options )
97
+ ids = []
98
+ returning_values = []
99
+
100
+ columns = Array(selections[:columns])
101
+ values = Array(selections[:values])
102
+ id_indexes = Array(options[:primary_key]).map { |key| columns.index(key) }
103
+ returning_columns = columns.reject.with_index { |_, index| id_indexes.include?(index) }
104
+ returning_indexes = returning_columns.map { |column| columns.index(column) }
105
+
106
+ values.each do |value|
107
+ value_array = Array(value)
108
+ ids << id_indexes.map { |index| value_array[index] }
109
+ returning_values << returning_indexes.map { |index| value_array[index] }
110
+ end
111
+
112
+ ids.map!(&:first) if id_indexes.size == 1
113
+ returning_values.map!(&:first) if returning_columns.size == 1
114
+
115
+ [ids, returning_values, returning_columns]
116
+ end
117
+
65
118
  def increment_locking_column!(table_name, results, locking_column)
66
119
  if locking_column.present?
67
120
  results << "\"#{locking_column}\"=#{table_name}.\"#{locking_column}\"+1"
@@ -71,5 +124,10 @@ module ActiveRecord::Import::AbstractAdapter
71
124
  def supports_on_duplicate_key_update?
72
125
  false
73
126
  end
127
+
128
+ # Whether a value in an INSERT's VALUES can be DEFAULT, to use the column's default
129
+ def supports_default_keyword_in_values?
130
+ true
131
+ end
74
132
  end
75
133
  end
@@ -6,6 +6,7 @@ module ActiveRecord::Import::MysqlAdapter
6
6
 
7
7
  NO_MAX_PACKET = 0
8
8
  QUERY_OVERHEAD = 8 # This was shown to be true for MySQL, but it's not clear where the overhead is from.
9
+ MIN_MARIADB_VERSION_FOR_RETURNING = "10.5.0"
9
10
 
10
11
  # +sql+ can be a single string or an array. If it is an array all
11
12
  # elements that are in position >= 1 will be appended to the final SQL.
@@ -13,6 +14,9 @@ module ActiveRecord::Import::MysqlAdapter
13
14
  # the number of inserts default
14
15
  number_of_inserts = 0
15
16
  affected_rows = nil
17
+ returning = import_returning?( options )
18
+ returned_columns = []
19
+ returned_rows = []
16
20
 
17
21
  base_sql, post_sql = case sql
18
22
  when String
@@ -34,32 +38,55 @@ module ActiveRecord::Import::MysqlAdapter
34
38
 
35
39
  max = max_allowed_packet
36
40
 
37
- # if we can insert it all as one statement
38
- if max == NO_MAX_PACKET || total_bytes <= max || options[:force_single_insert]
41
+ insert_value_set = lambda do |value_set|
39
42
  number_of_inserts += 1
40
- sql2insert = base_sql + values.join( ',' ) + post_sql
41
- result = execute( sql2insert, *args )
42
- if result.respond_to?(:affected_rows)
43
- affected_rows = (affected_rows || 0) + result.affected_rows
43
+ sql2insert = base_sql + value_set.join( ',' ) + post_sql
44
+
45
+ if returning
46
+ result = exec_query( sql2insert, *args )
47
+ returned_columns = result.columns
48
+ returned_rows.concat(result.rows)
49
+
50
+ if result.respond_to?(:affected_rows) && result.affected_rows
51
+ affected_rows = (affected_rows || 0) + result.affected_rows
52
+ end
53
+ else
54
+ # Unlike #execute, whose result only has the affected rows with some drivers
55
+ # (e.g. trilogy, but not mysql2), #exec_update returns them with every driver
56
+ count = exec_update( sql2insert, *args )
57
+ affected_rows = (affected_rows || 0) + count if count.is_a?(Integer)
44
58
  end
59
+ end
60
+
61
+ # if we can insert it all as one statement
62
+ if max == NO_MAX_PACKET || total_bytes <= max || options[:force_single_insert]
63
+ insert_value_set.call(values)
45
64
  else
46
65
  value_sets = ::ActiveRecord::Import::ValueSetsBytesParser.parse(values,
47
66
  reserved_bytes: sql_size,
48
67
  max_bytes: max)
49
68
 
50
69
  transaction(requires_new: true) do
51
- value_sets.each do |value_set|
52
- number_of_inserts += 1
53
- sql2insert = base_sql + value_set.join( ',' ) + post_sql
54
- result = execute( sql2insert, *args )
55
- if result.respond_to?(:affected_rows)
56
- affected_rows = (affected_rows || 0) + result.affected_rows
57
- end
58
- end
70
+ value_sets.each { |value_set| insert_value_set.call(value_set) }
59
71
  end
60
72
  end
61
73
 
62
- ActiveRecord::Import::Result.new([], number_of_inserts, [], [], affected_rows)
74
+ # ActiveRecord < 7.1 doesn't clear the query cache for #execute or #exec_query
75
+ clear_query_cache if query_cache_enabled
76
+
77
+ ids = []
78
+ results = []
79
+
80
+ if returning
81
+ returned_values = if returned_columns.size > 1
82
+ { values: returned_rows, columns: returned_columns }
83
+ else
84
+ { values: returned_rows.map(&:first) }
85
+ end
86
+ ids, results = ids_and_results_from_returned_values(returned_values, options)
87
+ end
88
+
89
+ ActiveRecord::Import::Result.new([], number_of_inserts, ids, results, affected_rows)
63
90
  end
64
91
 
65
92
  # Returns the maximum number of bytes that the server will allow
@@ -75,10 +102,33 @@ module ActiveRecord::Import::MysqlAdapter
75
102
 
76
103
  def pre_sql_statements( options)
77
104
  sql = []
78
- sql << "IGNORE" if options[:ignore] || options[:on_duplicate_key_ignore]
105
+ # Recursive imports need the id of every model, which INSERT IGNORE doesn't return
106
+ # for ignored rows, so :ignore and :on_duplicate_key_ignore don't apply to them.
107
+ if (options[:ignore] || options[:on_duplicate_key_ignore]) && !(options[:recursive] && supports_setting_primary_key_of_imported_objects?)
108
+ sql << "IGNORE"
109
+ end
79
110
  sql + super
80
111
  end
81
112
 
113
+ def post_sql_statements( table_name, options ) # :nodoc:
114
+ sql = super
115
+ sql << "RETURNING #{returning_selections(options).join(', ')}" if import_returning?( options )
116
+ sql
117
+ end
118
+
119
+ # MariaDB 10.5+ supports INSERT ... RETURNING, so the ids of imported models
120
+ # can be set. MySQL doesn't support it.
121
+ def supports_setting_primary_key_of_imported_objects?
122
+ respond_to?(:mariadb?) && mariadb? && database_version >= MIN_MARIADB_VERSION_FOR_RETURNING
123
+ end
124
+
125
+ # Returns true if the import should use INSERT ... RETURNING
126
+ def import_returning?( options ) # :nodoc:
127
+ supports_setting_primary_key_of_imported_objects? &&
128
+ returning_selections(options).present? &&
129
+ !(options[:no_returning] && !options[:recursive])
130
+ end
131
+
82
132
  # Add a column to be updated on duplicate key update
83
133
  def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
84
134
  if (columns = options[:on_duplicate_key_update])
@@ -109,7 +159,7 @@ module ActiveRecord::Import::MysqlAdapter
109
159
 
110
160
  def sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, arr ) # :nodoc:
111
161
  results = arr.map do |column|
112
- original_column_name = model.attribute_alias?( column ) ? model.attribute_alias( column ) : column
162
+ original_column_name = model.column_name_for_import( column )
113
163
  qc = quote_column_name( original_column_name )
114
164
  "#{table_name}.#{qc}=VALUES(#{qc})"
115
165
  end
@@ -119,10 +169,10 @@ module ActiveRecord::Import::MysqlAdapter
119
169
 
120
170
  def sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, hsh ) # :nodoc:
121
171
  results = hsh.map do |column1, column2|
122
- original_column1_name = model.attribute_alias?( column1 ) ? model.attribute_alias( column1 ) : column1
172
+ original_column1_name = model.column_name_for_import( column1 )
123
173
  qc1 = quote_column_name( original_column1_name )
124
174
 
125
- original_column2_name = model.attribute_alias?( column2 ) ? model.attribute_alias( column2 ) : column2
175
+ original_column2_name = model.column_name_for_import( column2 )
126
176
  qc2 = quote_column_name( original_column2_name )
127
177
 
128
178
  "#{table_name}.#{qc1}=VALUES( #{qc2} )"