activerecord-import 2.2.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.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +124 -0
  3. data/README.markdown +99 -32
  4. data/lib/activerecord-import/active_record/adapters/mysql2_proxy_adapter.rb +8 -0
  5. data/lib/activerecord-import/active_record/adapters/postgresql_proxy_adapter.rb +8 -0
  6. data/lib/activerecord-import/active_record/adapters/sqlite3_proxy_adapter.rb +8 -0
  7. data/lib/activerecord-import/active_record/adapters/trilogy_proxy_adapter.rb +8 -0
  8. data/lib/activerecord-import/adapters/abstract_adapter.rb +58 -0
  9. data/lib/activerecord-import/adapters/active_record_proxy_adapter.rb +19 -0
  10. data/lib/activerecord-import/adapters/mysql2_proxy_adapter.rb +9 -0
  11. data/lib/activerecord-import/adapters/mysql_adapter.rb +70 -13
  12. data/lib/activerecord-import/adapters/postgresql_adapter.rb +10 -52
  13. data/lib/activerecord-import/adapters/postgresql_proxy_adapter.rb +9 -0
  14. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +7 -3
  15. data/lib/activerecord-import/adapters/sqlite3_proxy_adapter.rb +9 -0
  16. data/lib/activerecord-import/adapters/trilogy_proxy_adapter.rb +9 -0
  17. data/lib/activerecord-import/aliases.rb +42 -0
  18. data/lib/activerecord-import/base.rb +2 -46
  19. data/lib/activerecord-import/core.rb +62 -0
  20. data/lib/activerecord-import/import.rb +612 -369
  21. data/lib/activerecord-import/synchronize.rb +28 -16
  22. data/lib/activerecord-import/version.rb +1 -1
  23. data/lib/activerecord-import/without_aliases.rb +17 -0
  24. metadata +19 -211
  25. data/.github/workflows/test.yaml +0 -165
  26. data/.gitignore +0 -37
  27. data/.rubocop.yml +0 -118
  28. data/.rubocop_todo.yml +0 -30
  29. data/Brewfile +0 -5
  30. data/Dockerfile +0 -23
  31. data/Gemfile +0 -63
  32. data/Rakefile +0 -75
  33. data/activerecord-import.gemspec +0 -28
  34. data/benchmarks/README +0 -32
  35. data/benchmarks/benchmark.rb +0 -72
  36. data/benchmarks/lib/base.rb +0 -143
  37. data/benchmarks/lib/cli_parser.rb +0 -111
  38. data/benchmarks/lib/float.rb +0 -17
  39. data/benchmarks/lib/mysql2_benchmark.rb +0 -21
  40. data/benchmarks/lib/output_to_csv.rb +0 -21
  41. data/benchmarks/lib/output_to_html.rb +0 -66
  42. data/benchmarks/models/test_innodb.rb +0 -5
  43. data/benchmarks/models/test_memory.rb +0 -5
  44. data/benchmarks/models/test_myisam.rb +0 -5
  45. data/benchmarks/schema/mysql2_schema.rb +0 -18
  46. data/docker-compose.yml +0 -34
  47. data/gemfiles/5.2.gemfile +0 -4
  48. data/gemfiles/6.0.gemfile +0 -4
  49. data/gemfiles/6.1.gemfile +0 -4
  50. data/gemfiles/7.0.gemfile +0 -4
  51. data/gemfiles/7.1.gemfile +0 -3
  52. data/gemfiles/7.2.gemfile +0 -3
  53. data/gemfiles/8.0.gemfile +0 -3
  54. data/test/adapters/janus_mysql2.rb +0 -3
  55. data/test/adapters/janus_trilogy.rb +0 -11
  56. data/test/adapters/jdbcmysql.rb +0 -3
  57. data/test/adapters/jdbcpostgresql.rb +0 -3
  58. data/test/adapters/jdbcsqlite3.rb +0 -3
  59. data/test/adapters/makara_postgis.rb +0 -3
  60. data/test/adapters/mysql2.rb +0 -3
  61. data/test/adapters/mysql2_makara.rb +0 -3
  62. data/test/adapters/mysql2_proxy.rb +0 -3
  63. data/test/adapters/mysql2spatial.rb +0 -3
  64. data/test/adapters/postgis.rb +0 -3
  65. data/test/adapters/postgresql.rb +0 -3
  66. data/test/adapters/postgresql_makara.rb +0 -3
  67. data/test/adapters/postgresql_proxy.rb +0 -3
  68. data/test/adapters/seamless_database_pool.rb +0 -3
  69. data/test/adapters/spatialite.rb +0 -3
  70. data/test/adapters/sqlite3.rb +0 -3
  71. data/test/adapters/trilogy.rb +0 -11
  72. data/test/database.yml.sample +0 -73
  73. data/test/github/database.yml +0 -85
  74. data/test/import_test.rb +0 -970
  75. data/test/janus_mysql2/import_test.rb +0 -8
  76. data/test/janus_trilogy/import_test.rb +0 -7
  77. data/test/jdbcmysql/import_test.rb +0 -7
  78. data/test/jdbcpostgresql/import_test.rb +0 -6
  79. data/test/jdbcsqlite3/import_test.rb +0 -6
  80. data/test/makara_postgis/import_test.rb +0 -10
  81. data/test/models/account.rb +0 -5
  82. data/test/models/alarm.rb +0 -4
  83. data/test/models/animal.rb +0 -8
  84. data/test/models/author.rb +0 -9
  85. data/test/models/bike_maker.rb +0 -10
  86. data/test/models/book.rb +0 -18
  87. data/test/models/car.rb +0 -5
  88. data/test/models/card.rb +0 -5
  89. data/test/models/chapter.rb +0 -6
  90. data/test/models/composite_book.rb +0 -19
  91. data/test/models/composite_chapter.rb +0 -12
  92. data/test/models/customer.rb +0 -18
  93. data/test/models/deck.rb +0 -8
  94. data/test/models/dictionary.rb +0 -6
  95. data/test/models/discount.rb +0 -5
  96. data/test/models/end_note.rb +0 -6
  97. data/test/models/group.rb +0 -5
  98. data/test/models/order.rb +0 -17
  99. data/test/models/playing_card.rb +0 -4
  100. data/test/models/promotion.rb +0 -5
  101. data/test/models/question.rb +0 -5
  102. data/test/models/rule.rb +0 -5
  103. data/test/models/tag.rb +0 -12
  104. data/test/models/tag_alias.rb +0 -11
  105. data/test/models/topic.rb +0 -31
  106. data/test/models/user.rb +0 -5
  107. data/test/models/user_token.rb +0 -6
  108. data/test/models/vendor.rb +0 -9
  109. data/test/models/widget.rb +0 -33
  110. data/test/mysql2/import_test.rb +0 -7
  111. data/test/mysql2_makara/import_test.rb +0 -8
  112. data/test/mysql2_proxy/import_test.rb +0 -6
  113. data/test/mysqlspatial2/import_test.rb +0 -8
  114. data/test/postgis/import_test.rb +0 -10
  115. data/test/postgresql/import_test.rb +0 -6
  116. data/test/postgresql_proxy/import_test.rb +0 -6
  117. data/test/schema/generic_schema.rb +0 -230
  118. data/test/schema/jdbcpostgresql_schema.rb +0 -3
  119. data/test/schema/mysql2_schema.rb +0 -21
  120. data/test/schema/postgis_schema.rb +0 -3
  121. data/test/schema/postgresql_schema.rb +0 -94
  122. data/test/schema/sqlite3_schema.rb +0 -15
  123. data/test/schema/version.rb +0 -12
  124. data/test/sqlite3/import_test.rb +0 -6
  125. data/test/support/active_support/test_case_extensions.rb +0 -73
  126. data/test/support/assertions.rb +0 -75
  127. data/test/support/factories.rb +0 -66
  128. data/test/support/generate.rb +0 -31
  129. data/test/support/mysql/import_examples.rb +0 -97
  130. data/test/support/postgresql/import_examples.rb +0 -642
  131. data/test/support/shared_examples/on_duplicate_key_ignore.rb +0 -45
  132. data/test/support/shared_examples/on_duplicate_key_update.rb +0 -437
  133. data/test/support/shared_examples/recursive_import.rb +0 -352
  134. data/test/support/sqlite3/import_examples.rb +0 -232
  135. data/test/synchronize_test.rb +0 -43
  136. data/test/test_helper.rb +0 -91
  137. data/test/trilogy/import_test.rb +0 -7
  138. data/test/value_sets_bytes_parser_test.rb +0 -106
  139. data/test/value_sets_records_parser_test.rb +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7cd19fa557186daf34db96db71f26a7db70b15e66a0a42eaab33be304945ab0
4
- data.tar.gz: ff8afe763d437210ea1c78e21bacd0b4ae84f113fbadbda087077bf402f08e64
3
+ metadata.gz: 44eca8fc38325819cfc2eeb8c9a6151182f5de96135af6ad3ae1cc145c5f7740
4
+ data.tar.gz: 9f6ef423efde4813da41b8eb5ba218d4166bb3a86beba83d72b4bbf9e70af27f
5
5
  SHA512:
6
- metadata.gz: 402d2087c8c35a2adfa3787ee4e183883df74cc1f4ad2276de1227ba9778b630bfbee546faa3b53b7dcbe1142232bd7d03de046ccab5c6caa5c475a3bc4f15c1
7
- data.tar.gz: ef5ce7d4dba744b3c58a0affbe326b50d764c7e4c4726b468ad4d18eacee13fbaa8d7b5d78459bc048e8eeb0628cf34b52a59f759f0a1f4d9d491b7439d5845a
6
+ metadata.gz: f534b0b2a47c29484b8957109794ab8924c758993e93ecd871104f97ce8402856999c40d1e845ff7a2857fb92e12291a0d93259a9bdd17d4b60f81cb20ac5e42
7
+ data.tar.gz: f9e8de71d80edd76c978a1d85807b6a94247da8366dfda54a4c1e2c01ed25c31767d836c2ec487d39377379492dd896f320a986c1a158cfda7f810e7b3ba3a80
data/CHANGELOG.md CHANGED
@@ -1,3 +1,127 @@
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
+
92
+ ## Changes in 2.3.0
93
+
94
+ ### New Features
95
+
96
+ * Add Support for trilogy_proxy from [active_record_proxy_adapters](https://rubygems.org/gems/active_record_proxy_adapters) by @mateuscruz via #887.
97
+ * Add Support for sqlite3_proxy from [active_record_proxy_adapters](https://rubygems.org/gems/active_record_proxy_adapters) by @mateuscruz via #887.
98
+ * Add affected rows support for the MySQL adapter. `ActiveRecord::Import::Result` now carries an
99
+ `affected_rows` member, which reports the number of rows the server actually reported as
100
+ affected when the adapter exposes it (`nil` otherwise). This is most useful with
101
+ `on_duplicate_key_update`, where the count distinguishes inserted from updated rows.
102
+ Thanks to @ehasrouni via #882 and #885.
103
+
104
+ ### Fixes
105
+
106
+ * Fix `NoMethodError: undefined method 'validation_context='` when importing with validations
107
+ against Rails main (8.2.0.alpha). Rails removed ActiveModel's private `validation_context=`
108
+ writer in rails/rails@0f9d1270834a6407a59637650bf910d8ae826169 in favor of a validation
109
+ context object reachable through `context_for_validation`. `ActiveRecord::Import::Validator`
110
+ now sets and restores the validation context through whichever API the loaded ActiveModel
111
+ provides. Thanks to @yu-yaba via #900.
112
+ * Fix adapter-specific import support (e.g. `on_duplicate_key_update`) never loading for
113
+ a connection established via `connects_to`/`ActiveRecord::ConnectionAdapters::
114
+ ConnectionHandler#establish_connection` instead of `ActiveRecord::Base.establish_connection`
115
+ directly - most commonly hit with a Rails multi-database secondary connection. The lazy
116
+ adapter loader now hooks `ConnectionHandler#establish_connection` instead of
117
+ `ActiveRecord::Base.establish_connection`, so it fires for every connection regardless of
118
+ how it was established. Thanks to @mateuscruz via #886.
119
+ * Fix `active_record_proxy_adapters` support, which was not loading the import adapter for
120
+ proxied connections. Thanks to @mateuscruz via #880.
121
+ * Use `with_connection` instead of `ActiveRecord::Base.connection`, which is deprecated as of
122
+ ActiveRecord 7.2. Thanks to @nerdrew via #875 and #876.
123
+ * Reduce the size of the published gem by excluding test files. Thanks to @yuri-zubov via #893.
124
+
1
125
  ## Changes in 2.2.0
2
126
 
3
127
  ### 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)
@@ -121,20 +123,20 @@ Book.import values
121
123
  The `import` method can take an array of column names and an array of hash objects. The column names are used to determine what fields of data should be imported. The following example will only import books with the `title` field:
122
124
 
123
125
  ```ruby
124
- books = [
126
+ values = [
125
127
  { title: "Book 1", author: "George Orwell" },
126
128
  { title: "Book 2", author: "Bob Jones" }
127
129
  ]
128
130
  columns = [ :title ]
129
131
 
130
132
  # without validations
131
- Book.import columns, books, validate: false
133
+ Book.import columns, values, validate: false
132
134
 
133
135
  # with validations
134
- Book.import columns, books, validate: true
136
+ Book.import columns, values, validate: true
135
137
 
136
138
  # when not specified :validate defaults to true
137
- Book.import columns, books
139
+ Book.import columns, values
138
140
 
139
141
  # result in table books
140
142
  # title | author
@@ -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://github.com/zdennis/activerecord-import/wiki#recursive-example-postgresql-only).
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,18 +524,29 @@ 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
- * 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)
511
- * SQL Server - supports core import functionality (available as an external gem: [activerecord-import-sqlserver](https://github.com/keeguon/activerecord-import-sqlserver)
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))
547
+ * SQL Server - supports core import functionality (available as an external gem: [activerecord-import-sqlserver](https://github.com/keeguon/activerecord-import-sqlserver))
512
548
 
513
- If your adapter isn't listed here, please consider creating an external gem as described in the README to provide support. If you do, feel free to update this wiki to include a link to the new adapter's repository!
549
+ If your adapter isn't listed here, please consider creating an external gem as described in the README to provide support. If you do, feel free to update this README to include a link to the new adapter's repository!
514
550
 
515
551
  To test which features are supported by your adapter, use the following methods on a model class:
516
552
  * `supports_import?(*args)`
@@ -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
@@ -624,7 +673,7 @@ See https://github.com/zdennis/activerecord-import/issues/233 for further discus
624
673
 
625
674
  ### More Information
626
675
 
627
- For more information on Activerecord-Import please see its wiki: https://github.com/zdennis/activerecord-import/wiki
676
+ For more information on Activerecord-Import please see its docs: https://rubydoc.info/gems/activerecord-import/ActiveRecord/Base
628
677
 
629
678
  To document new information, please add to the README instead of the wiki. See https://github.com/zdennis/activerecord-import/issues/397 for discussion.
630
679
 
@@ -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
 
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/connection_adapters/mysql2_proxy_adapter"
4
+ require "activerecord-import/adapters/mysql2_proxy_adapter"
5
+
6
+ ActiveSupport.on_load(:active_record_mysql2proxyadapter) do |klass|
7
+ klass.include(ActiveRecord::Import::Mysql2ProxyAdapter)
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/connection_adapters/postgresql_proxy_adapter"
4
+ require "activerecord-import/adapters/postgresql_proxy_adapter"
5
+
6
+ ActiveSupport.on_load(:active_record_postgresqlproxyadapter) do |klass|
7
+ klass.include(ActiveRecord::Import::PostgreSQLProxyAdapter)
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/connection_adapters/sqlite3_proxy_adapter"
4
+ require "activerecord-import/adapters/sqlite3_proxy_adapter"
5
+
6
+ ActiveSupport.on_load(:active_record_sqlite3proxyadapter) do |klass|
7
+ klass.include(ActiveRecord::Import::SQLite3ProxyAdapter)
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/connection_adapters/trilogy_proxy_adapter"
4
+ require "activerecord-import/adapters/trilogy_proxy_adapter"
5
+
6
+ ActiveSupport.on_load(:active_record_trilogyproxyadapter) do |klass|
7
+ klass.include(ActiveRecord::Import::TrilogyProxyAdapter)
8
+ end
@@ -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
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord::Import::ActiveRecordProxyAdapter
4
+ def insert_many(*args, **kwargs, &block)
5
+ sticking_to_primary { super(*args, **kwargs, &block) }
6
+ end
7
+
8
+ def insert(*args, **kwargs, &block)
9
+ sticking_to_primary { super(*args, **kwargs, &block) }
10
+ end
11
+
12
+ def sticking_to_primary(&block)
13
+ ActiveRecord::Base.connected_to(role: writing_role, &block)
14
+ end
15
+
16
+ def writing_role
17
+ ActiveRecord.try(:writing_role) || ActiveRecord::Base.try(:writing_role) || :writing
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "activerecord-import/adapters/mysql2_adapter"
4
+ require "activerecord-import/adapters/active_record_proxy_adapter"
5
+
6
+ module ActiveRecord::Import::Mysql2ProxyAdapter
7
+ include ActiveRecord::Import::Mysql2Adapter
8
+ include ActiveRecord::Import::ActiveRecordProxyAdapter
9
+ end