activerecord-import 1.5.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 866dd09466fbda981329e13916e89986cb47d65259f0889b8a3b53aeca1aee32
4
- data.tar.gz: 258a0d2fc34bbb928500e2f8a83df4f5bdc8901c25e2e123a569c56d587d3f53
3
+ metadata.gz: d1478b29daf1e757c91a0ca8693bb1b03f9a90daa2fe02f0959576bcec900d85
4
+ data.tar.gz: dc938a1936c931182675a118ad24a37e2a7c5a27c0419e5bd947e0ec35aa4334
5
5
  SHA512:
6
- metadata.gz: 989562e1fb64d669a96211c6b80df5b89d8a43aa38575ec6706d0633b405983a63849bd5fb78d4031ed7e4b2c239192f62d4b3d9e720f4c474cd3801600d11ef
7
- data.tar.gz: 22e6ccbb750bb7851930d98c2b90f496c759d35503c8c1e58b841f4a928a283c486caec3f22cb57e6948b6960e6fff2a598499f0670f59b281e321dded20c183
6
+ metadata.gz: e276196113f433c8989a9a06cfad36df650105183a5559a548bdd8a04bd2da22db36ca996d2d0d04c4561b33ceb44559b50b4ad98e0a2d9f050b8084ee99c8e6
7
+ data.tar.gz: e699052ac2499bac23a9955fef71df853050767785d573523e23a1d216cfba9cb75b81b0db9f5e2afac02afbcb416563af6e7a2bf69f6d1288c6e6c71aeb7e5c
@@ -16,17 +16,50 @@ jobs:
16
16
  --health-interval 10s
17
17
  --health-timeout 5s
18
18
  --health-retries 5
19
+ mysql:
20
+ image: mysql:5.7
21
+ ports:
22
+ - 3306:3306
23
+ env:
24
+ MYSQL_ROOT_PASSWORD: root
25
+ MYSQL_USER: github
26
+ MYSQL_PASSWORD: github
27
+ MYSQL_DATABASE: activerecord_import_test
28
+ options: >-
29
+ --health-cmd "mysqladmin ping -h localhost"
30
+ --health-interval 10s
31
+ --health-timeout 5s
32
+ --health-retries 5
19
33
  strategy:
20
34
  fail-fast: false
21
35
  matrix:
22
36
  ruby:
23
- - 3.2
37
+ - 3.3
24
38
  env:
39
+ - AR_VERSION: '7.2'
40
+ RUBYOPT: --enable-frozen-string-literal
41
+ - AR_VERSION: '7.1'
42
+ RUBYOPT: --enable-frozen-string-literal
25
43
  - AR_VERSION: '7.0'
26
44
  RUBYOPT: --enable-frozen-string-literal
27
45
  - AR_VERSION: 6.1
28
46
  RUBYOPT: --enable-frozen-string-literal
29
47
  include:
48
+ - ruby: 3.2
49
+ env:
50
+ AR_VERSION: '7.2'
51
+ - ruby: 3.2
52
+ env:
53
+ AR_VERSION: '7.1'
54
+ - ruby: 3.2
55
+ env:
56
+ AR_VERSION: '7.0'
57
+ - ruby: 3.2
58
+ env:
59
+ AR_VERSION: 6.1
60
+ - ruby: 3.1
61
+ env:
62
+ AR_VERSION: '7.1'
30
63
  - ruby: 3.1
31
64
  env:
32
65
  AR_VERSION: '7.0'
@@ -39,6 +72,9 @@ jobs:
39
72
  - ruby: '3.0'
40
73
  env:
41
74
  AR_VERSION: 6.1
75
+ - ruby: jruby-9.4.5.0
76
+ env:
77
+ AR_VERSION: '7.0'
42
78
  - ruby: 2.7
43
79
  env:
44
80
  AR_VERSION: '7.0'
@@ -48,28 +84,23 @@ jobs:
48
84
  - ruby: 2.7
49
85
  env:
50
86
  AR_VERSION: '6.0'
51
- - ruby: 2.6
87
+ - ruby: jruby-9.3.10.0
52
88
  env:
53
- AR_VERSION: 5.2
89
+ AR_VERSION: '6.1'
54
90
  - ruby: 2.6
55
91
  env:
56
- AR_VERSION: 5.1
57
- - ruby: 2.4
58
- env:
59
- AR_VERSION: '5.0'
60
- - ruby: 2.4
61
- env:
62
- AR_VERSION: 4.2
92
+ AR_VERSION: 5.2
63
93
  runs-on: ubuntu-latest
64
94
  env:
65
95
  AR_VERSION: ${{ matrix.env.AR_VERSION }}
66
96
  DB_DATABASE: activerecord_import_test
67
97
  steps:
68
- - uses: actions/checkout@v2
98
+ - uses: actions/checkout@v4
69
99
  - uses: ruby/setup-ruby@v1
70
100
  with:
71
101
  ruby-version: ${{ matrix.ruby }}
72
102
  bundler-cache: true
103
+ rubygems: latest
73
104
  - name: Set up databases
74
105
  run: |
75
106
  sudo /etc/init.d/mysql start
@@ -99,12 +130,15 @@ jobs:
99
130
  run: |
100
131
  bundle exec rake test:spatialite
101
132
  bundle exec rake test:sqlite3
133
+ - name: Run trilogy tests
134
+ if: ${{ matrix.env.AR_VERSION >= '7.0' && !startsWith(matrix.ruby, 'jruby') }}
135
+ run: bundle exec rake test:trilogy
102
136
  lint:
103
137
  runs-on: ubuntu-latest
104
138
  env:
105
139
  AR_VERSION: '7.0'
106
140
  steps:
107
- - uses: actions/checkout@v2
141
+ - uses: actions/checkout@v4
108
142
  - uses: ruby/setup-ruby@v1
109
143
  with:
110
144
  ruby-version: 2.7
data/.gitignore CHANGED
@@ -13,12 +13,16 @@ tmtags
13
13
  ## VIM
14
14
  *.swp
15
15
 
16
+ ## Idea
17
+ .idea
18
+
16
19
  ## PROJECT::GENERAL
17
20
  coverage
18
21
  rdoc
19
22
  pkg
20
23
  *.gem
21
24
  *.lock
25
+ .byebug_history
22
26
 
23
27
  ## PROJECT::SPECIFIC
24
28
  log/*.log
data/.rubocop_todo.yml CHANGED
@@ -24,3 +24,7 @@ Lint/UnusedMethodArgument:
24
24
  Style/CombinableLoops:
25
25
  Exclude:
26
26
  - 'test/support/shared_examples/recursive_import.rb'
27
+
28
+ Naming/FileName:
29
+ Exclude:
30
+ - 'lib/activerecord-import.rb'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## Changes in 1.8.0
2
+
3
+ ### New Features
4
+
5
+ * Add support for ActiveRecord 7.2 via \##845.
6
+
7
+ ## Changes in 1.7.0
8
+
9
+ ### New Features
10
+
11
+ * Add support for ActiveRecord 7.1 composite primary keys. Thanks to @fragkakis via \##837.
12
+ * Add support for upserting associations when doing recursive imports. Thanks to @ramblex via \##778.
13
+
14
+ ## Changes in 1.6.0
15
+
16
+ ### New Features
17
+
18
+ * Add trilogy adapter support. Thanks to @zmariscal via \##825.
19
+
20
+ ### Fixes
21
+
22
+ * Use the locking_enabled? method provided by activerecord to decide whether the lock field should be updated. Thanks to @dombesz via \##822.
23
+
24
+ ## Changes in 1.5.1
25
+
26
+ ### Fixes
27
+
28
+ * Stop memoizing schema_columns_hash so dynamic schema changes are picked up. Thanks to @koshigoe via \##812.
29
+
1
30
  ## Changes in 1.5.0
2
31
 
3
32
  ### New Features
data/Dockerfile ADDED
@@ -0,0 +1,23 @@
1
+ # Use the official Ruby 3.2 image as a base image
2
+ ARG RUBY_VERSION=3.2
3
+ FROM ruby:${RUBY_VERSION}-bullseye
4
+
5
+ # Set the working directory
6
+ WORKDIR /usr/src/app
7
+
8
+ # Install system packages
9
+ RUN apt-get update -qq && \
10
+ apt-get install -y default-mysql-client postgresql postgresql-contrib vim && \
11
+ apt-get clean
12
+
13
+ # Set environment variables
14
+ ENV AR_VERSION=7.0
15
+
16
+ # Copy all files
17
+ COPY . .
18
+
19
+ # Move sample database.yml and install gems
20
+ RUN mv test/database.yml.sample test/database.yml && \
21
+ bundle install
22
+
23
+ CMD ["irb"]
data/Gemfile CHANGED
@@ -26,18 +26,26 @@ platforms :ruby do
26
26
  gem "sqlite3", "~> #{sqlite3_version}"
27
27
  # seamless_database_pool requires Ruby ~> 2.0
28
28
  gem "seamless_database_pool", "~> 1.0.20" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.0.0')
29
+ gem "trilogy" if version >= 6.0
30
+ if version >= 6.0 && version <= 7.0
31
+ gem "activerecord-trilogy-adapter"
32
+ end
29
33
  end
30
34
 
31
35
  platforms :jruby do
32
36
  gem "jdbc-mysql"
33
37
  gem "jdbc-postgres"
34
- gem "activerecord-jdbcsqlite3-adapter", "~> 1.3"
35
- gem "activerecord-jdbcmysql-adapter", "~> 1.3"
36
- gem "activerecord-jdbcpostgresql-adapter", "~> 1.3"
38
+ gem "activerecord-jdbcsqlite3-adapter"
39
+ gem "activerecord-jdbcmysql-adapter"
40
+ gem "activerecord-jdbcpostgresql-adapter"
37
41
  end
38
42
 
39
43
  # Support libs
40
- gem "factory_bot"
44
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.0.0")
45
+ gem "factory_bot"
46
+ else
47
+ gem "factory_bot", "~> 5", "< 6.4.5"
48
+ end
41
49
  gem "timecop"
42
50
  gem "chronic"
43
51
  gem "mocha", "~> 2.1.0"
data/README.markdown CHANGED
@@ -265,7 +265,7 @@ Book.import books, recursive: true
265
265
  Key | Options | Default | Description
266
266
  ------------------------- | --------------------- | ------------------ | -----------
267
267
  :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 uniqueness validations, has potential pitfalls, use with caution (requires `>= v0.27.0`).
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`).
269
269
  :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
270
  :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
271
  :on_duplicate_key_ignore | `true`/`false` | `false` | Allows skipping records with duplicate keys. See [here](#duplicate-key-ignore) for more details.
@@ -274,6 +274,7 @@ Key | Options | Default | Descrip
274
274
  :synchronize | `Array` | N/A | An array of ActiveRecord instances. This synchronizes existing instances in memory with updates from the import.
275
275
  :timestamps | `true`/`false` | `true` | Enables/disables timestamps on imported records.
276
276
  :recursive | `true`/`false` | `false` | Imports has_many/has_one associations (PostgreSQL only).
277
+ :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.
277
278
  :batch_size | `Integer` | total # of records | Max number of records to insert per import
278
279
  :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.
279
280
  :all_or_none | `true`/`false` | `false` | Will not import any records if there is a record with validation errors.
@@ -364,6 +365,29 @@ book.reload.title # => "Book1" (stayed the same)
364
365
  book.reload.author # => "Bob Barker" (changed)
365
366
  ```
366
367
 
368
+ PostgreSQL Using partial indexes
369
+
370
+ ```ruby
371
+ book = Book.create! title: "Book1", author: "George Orwell", published_at: Time.now
372
+ book.author = "Bob Barker"
373
+
374
+ # in migration
375
+ execute <<-SQL
376
+ CREATE INDEX books_published_at_index ON books (published_at) WHERE published_at IS NOT NULL;
377
+ SQL
378
+
379
+ # PostgreSQL version
380
+ Book.import [book], on_duplicate_key_update: {
381
+ conflict_target: [:id],
382
+ index_predicate: "published_at IS NOT NULL",
383
+ columns: [:author]
384
+ }
385
+
386
+ book.reload.title # => "Book1" (stayed the same)
387
+ book.reload.author # => "Bob Barker" (changed)
388
+ book.reload.published_at # => 2017-10-09 (stayed the same)
389
+ ```
390
+
367
391
  PostgreSQL Using constraints
368
392
 
369
393
  ```ruby
@@ -626,6 +650,19 @@ AR_VERSION=7.0 bundle exec rake test:postgresql test:sqlite3 test:mysql2
626
650
 
627
651
  Once you have pushed up your changes, you can find your CI results [here](https://github.com/zdennis/activerecord-import/actions).
628
652
 
653
+ #### Docker Setup
654
+
655
+ Before you begin, make sure you have [Docker](https://www.docker.com/products/docker-desktop/) and [Docker Compose](https://docs.docker.com/compose/) installed on your machine. If you don't, you can install both via Homebrew using the following command:
656
+
657
+ ```bash
658
+ brew install docker && brew install docker-compose
659
+ ```
660
+ ##### Steps
661
+
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`
665
+
629
666
  ## Issue Triage [![Open Source Helpers](https://www.codetriage.com/zdennis/activerecord-import/badges/users.svg)](https://www.codetriage.com/zdennis/activerecord-import)
630
667
 
631
668
  You can triage issues which may include reproducing bug reports or asking for vital information, such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to [subscribe to activerecord-import on CodeTriage](https://www.codetriage.com/zdennis/activerecord-import).
data/Rakefile CHANGED
@@ -29,6 +29,7 @@ ADAPTERS = %w(
29
29
  sqlite3
30
30
  spatialite
31
31
  seamless_database_pool
32
+ trilogy
32
33
  ).freeze
33
34
  ADAPTERS.each do |adapter|
34
35
  namespace :test do
@@ -10,6 +10,10 @@ Gem::Specification.new do |gem|
10
10
  gem.homepage = "https://github.com/zdennis/activerecord-import"
11
11
  gem.license = "MIT"
12
12
 
13
+ gem.metadata = {
14
+ "changelog_uri" => "https://github.com/zdennis/activerecord-import/blob/master/CHANGELOG.md"
15
+ }
16
+
13
17
  gem.files = `git ls-files`.split($\)
14
18
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
15
19
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
@@ -0,0 +1,34 @@
1
+ version: "3.5"
2
+
3
+ services:
4
+ mysql:
5
+ platform: linux/x86_64
6
+ image: mysql:5.7
7
+ volumes:
8
+ - mysql-data:/var/lib/mysql
9
+ ports:
10
+ - "3306:3306"
11
+
12
+ postgresql:
13
+ image: postgres:latest
14
+ volumes:
15
+ - postgresql-data:/var/lib/postgresql/data
16
+ ports:
17
+ - "5432:5432"
18
+
19
+ app:
20
+ build:
21
+ context: .
22
+ environment:
23
+ DB_HOST: mysql
24
+ AR_VERSION: 7.0
25
+ volumes:
26
+ - .:/usr/src/app
27
+ depends_on:
28
+ - mysql
29
+ - postgresql
30
+ command: tail -f /dev/null
31
+
32
+ volumes:
33
+ mysql-data:
34
+ postgresql-data:
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem 'activerecord', '~> 7.1.0'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem 'activerecord', '~> 7.2.0'
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/connection_adapters/trilogy_adapter"
4
+ require "activerecord-import/adapters/trilogy_adapter"
5
+
6
+ class ActiveRecord::ConnectionAdapters::TrilogyAdapter
7
+ include ActiveRecord::Import::TrilogyAdapter
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "activerecord-import/adapters/mysql_adapter"
4
+
5
+ module ActiveRecord::Import::TrilogyAdapter
6
+ include ActiveRecord::Import::MysqlAdapter
7
+ end
@@ -62,6 +62,7 @@ module ActiveRecord::Import # :nodoc:
62
62
  if @validate_callbacks.respond_to?(:chain, true)
63
63
  @validate_callbacks.send(:chain).tap do |chain|
64
64
  callback.instance_variable_set(:@filter, filter)
65
+ callback.instance_variable_set(:@compiled, nil)
65
66
  chain[i] = callback
66
67
  end
67
68
  else
@@ -557,7 +558,7 @@ class ActiveRecord::Base
557
558
  options.merge!( args.pop ) if args.last.is_a? Hash
558
559
  # making sure that current model's primary key is used
559
560
  options[:primary_key] = primary_key
560
- options[:locking_column] = locking_column if attribute_names.include?(locking_column)
561
+ options[:locking_column] = locking_column if locking_enabled?
561
562
 
562
563
  is_validating = options[:validate_with_context].present? ? true : options[:validate]
563
564
  validator = ActiveRecord::Import::Validator.new(self, options)
@@ -857,6 +858,15 @@ class ActiveRecord::Base
857
858
 
858
859
  private
859
860
 
861
+ def associated_options(options, associated_class)
862
+ return options unless options.key?(:recursive_on_duplicate_key_update)
863
+
864
+ table_name = associated_class.arel_table.name.to_sym
865
+ options.merge(
866
+ on_duplicate_key_update: options[:recursive_on_duplicate_key_update][table_name]
867
+ )
868
+ end
869
+
860
870
  def set_attributes_and_mark_clean(models, import_result, timestamps, options)
861
871
  return if models.nil?
862
872
  models -= import_result.failed_instances
@@ -941,7 +951,7 @@ class ActiveRecord::Base
941
951
  association = association.target
942
952
  next if association.blank? || model.public_send(column_name).present?
943
953
 
944
- association_primary_key = Array(association_reflection.association_primary_key)[column_index]
954
+ association_primary_key = Array(association_reflection.association_primary_key.tr("[]:", "").split(", "))[column_index]
945
955
  model.public_send("#{column_name}=", association.send(association_primary_key))
946
956
  end
947
957
  end
@@ -963,13 +973,17 @@ class ActiveRecord::Base
963
973
 
964
974
  associated_objects_by_class.each_value do |associations|
965
975
  associations.each_value do |associated_records|
966
- associated_records.first.class.bulk_import(associated_records, options) unless associated_records.empty?
976
+ next if associated_records.empty?
977
+
978
+ associated_class = associated_records.first.class
979
+ associated_class.bulk_import(associated_records,
980
+ associated_options(options, associated_class))
967
981
  end
968
982
  end
969
983
  end
970
984
 
971
985
  def schema_columns_hash
972
- @schema_columns_hash ||= if respond_to?(:ignored_columns) && ignored_columns.any?
986
+ if respond_to?(:ignored_columns) && ignored_columns.any?
973
987
  connection.schema_cache.columns_hash(table_name)
974
988
  else
975
989
  columns_hash
@@ -996,7 +1010,10 @@ class ActiveRecord::Base
996
1010
 
997
1011
  changed_objects = association.select { |a| a.new_record? || a.changed? }
998
1012
  changed_objects.each do |child|
999
- child.public_send("#{association_reflection.foreign_key}=", model.id)
1013
+ Array(association_reflection.inverse_of&.foreign_key || association_reflection.foreign_key).each_with_index do |column, index|
1014
+ child.public_send("#{column}=", Array(model.id)[index])
1015
+ end
1016
+
1000
1017
  # For polymorphic associations
1001
1018
  association_name = if model.class.respond_to?(:polymorphic_name)
1002
1019
  model.class.polymorphic_name
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Import
5
- VERSION = "1.5.0"
5
+ VERSION = "1.8.0"
6
6
  end
7
7
  end
@@ -1,4 +1,3 @@
1
- # rubocop:disable Naming/FileName
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require "active_support/lazy_load_hooks"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ ENV["ARE_DB"] = "trilogy"
4
+
5
+ if ENV['AR_VERSION'].to_f <= 7.0
6
+ require "activerecord-trilogy-adapter"
7
+ require "trilogy_adapter/connection"
8
+ ActiveRecord::Base.extend TrilogyAdapter::Connection
9
+ end
@@ -8,6 +8,7 @@ common: &common
8
8
  mysql2: &mysql2
9
9
  <<: *common
10
10
  adapter: mysql2
11
+ host: mysql
11
12
 
12
13
  mysql2spatial:
13
14
  <<: *mysql2
@@ -19,6 +20,7 @@ postgresql: &postgresql
19
20
  <<: *common
20
21
  username: postgres
21
22
  adapter: postgresql
23
+ host: postgresql
22
24
  min_messages: warning
23
25
 
24
26
  postresql_makara:
@@ -50,3 +52,8 @@ sqlite3: &sqlite3
50
52
 
51
53
  spatialite:
52
54
  <<: *sqlite3
55
+
56
+ trilogy:
57
+ <<: *common
58
+ adapter: trilogy
59
+ host: mysql
@@ -66,3 +66,7 @@ sqlite3: &sqlite3
66
66
 
67
67
  spatialite:
68
68
  <<: *sqlite3
69
+
70
+ trilogy:
71
+ <<: *common
72
+ adapter: trilogy
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Author < ActiveRecord::Base
4
+ if ENV['AR_VERSION'].to_f >= 7.1
5
+ has_many :composite_books, query_constraints: [:id, :author_id], inverse_of: :author
6
+ end
7
+ end
data/test/models/book.rb CHANGED
@@ -2,8 +2,11 @@
2
2
 
3
3
  class Book < ActiveRecord::Base
4
4
  belongs_to :topic, inverse_of: :books
5
- belongs_to :tag, foreign_key: [:tag_id, :parent_id]
6
-
5
+ if ENV['AR_VERSION'].to_f <= 7.0
6
+ belongs_to :tag, foreign_key: [:tag_id, :parent_id] unless ENV["SKIP_COMPOSITE_PK"]
7
+ else
8
+ belongs_to :tag, query_constraints: [:tag_id, :parent_id] unless ENV["SKIP_COMPOSITE_PK"]
9
+ end
7
10
  has_many :chapters, inverse_of: :book
8
11
  has_many :discounts, as: :discountable
9
12
  has_many :end_notes, inverse_of: :book
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CompositeBook < ActiveRecord::Base
4
+ self.primary_key = %i[id author_id]
5
+ belongs_to :author
6
+ if ENV['AR_VERSION'].to_f <= 7.0
7
+ unless ENV["SKIP_COMPOSITE_PK"]
8
+ has_many :composite_chapters, inverse_of: :composite_book,
9
+ foreign_key: [:id, :author_id]
10
+ end
11
+ else
12
+ has_many :composite_chapters, inverse_of: :composite_book,
13
+ query_constraints: [:id, :author_id]
14
+ end
15
+
16
+ def self.sequence_name
17
+ "composite_book_id_seq"
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CompositeChapter < ActiveRecord::Base
4
+ if ENV['AR_VERSION'].to_f >= 7.1
5
+ belongs_to :composite_book, inverse_of: :composite_chapters,
6
+ query_constraints: [:composite_book_id, :author_id]
7
+ end
8
+ validates :title, presence: true
9
+ end
@@ -1,8 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Customer < ActiveRecord::Base
4
- has_many :orders,
5
- inverse_of: :customer,
6
- primary_key: %i(account_id id),
7
- foreign_key: %i(account_id customer_id)
4
+ unless ENV["SKIP_COMPOSITE_PK"]
5
+ if ENV['AR_VERSION'].to_f <= 7.0
6
+ has_many :orders,
7
+ inverse_of: :customer,
8
+ primary_key: %i(account_id id),
9
+ foreign_key: %i(account_id customer_id)
10
+ else
11
+ has_many :orders,
12
+ inverse_of: :customer,
13
+ primary_key: %i(account_id id),
14
+ query_constraints: %i(account_id customer_id)
15
+ end
16
+
17
+ end
8
18
  end
data/test/models/order.rb CHANGED
@@ -1,8 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Order < ActiveRecord::Base
4
- belongs_to :customer,
5
- inverse_of: :orders,
6
- primary_key: %i(account_id id),
7
- foreign_key: %i(account_id customer_id)
4
+ unless ENV["SKIP_COMPOSITE_PK"]
5
+ if ENV['AR_VERSION'].to_f <= 7.0
6
+ belongs_to :customer,
7
+ inverse_of: :orders,
8
+ primary_key: %i(account_id id),
9
+ foreign_key: %i(account_id customer_id)
10
+ else
11
+ belongs_to :customer,
12
+ inverse_of: :orders,
13
+ primary_key: %i(account_id id),
14
+ query_constraints: %i(account_id customer_id)
15
+ end
16
+ end
8
17
  end
data/test/models/tag.rb CHANGED
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Tag < ActiveRecord::Base
4
- self.primary_keys = :tag_id, :publisher_id unless ENV["SKIP_COMPOSITE_PK"]
4
+ if ENV['AR_VERSION'].to_f <= 7.0
5
+ self.primary_keys = :tag_id, :publisher_id unless ENV["SKIP_COMPOSITE_PK"]
6
+ else
7
+ self.primary_key = [:tag_id, :publisher_id] unless ENV["SKIP_COMPOSITE_PK"]
8
+ end
9
+ self.primary_key = [:tag_id, :publisher_id] unless ENV["SKIP_COMPOSITE_PK"]
5
10
  has_many :books, inverse_of: :tag
6
11
  has_many :tag_aliases, inverse_of: :tag
7
12
  end
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class TagAlias < ActiveRecord::Base
4
- belongs_to :tag, foreign_key: [:tag_id, :parent_id], required: true
4
+ unless ENV["SKIP_COMPOSITE_PK"]
5
+ if ENV['AR_VERSION'].to_f <= 7.0
6
+ belongs_to :tag, foreign_key: [:tag_id, :parent_id], required: true
7
+ else
8
+ belongs_to :tag, query_constraints: [:tag_id, :parent_id], required: true
9
+ end
10
+ end
5
11
  end
data/test/models/topic.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Topic < ActiveRecord::Base
4
- if ENV['AR_VERSION'].to_i >= 6.0
4
+ if ENV['AR_VERSION'].to_f >= 6.0
5
5
  self.ignored_columns = [:priority]
6
6
  end
7
7
  alias_attribute :name, :title
@@ -19,8 +19,15 @@ class Widget < ActiveRecord::Base
19
19
 
20
20
  default_scope -> { where(active: true) }
21
21
 
22
- serialize :data, Hash
23
- serialize :json_data, JSON
22
+ if ENV['AR_VERSION'].to_f >= 7.1
23
+ serialize :data, coder: YAML
24
+ serialize :json_data, coder: JSON
25
+ serialize :custom_data, coder: CustomCoder.new
26
+ else
27
+ serialize :data, Hash
28
+ serialize :json_data, JSON
29
+ serialize :custom_data, CustomCoder.new
30
+ end
31
+
24
32
  serialize :unspecified_data
25
- serialize :custom_data, CustomCoder.new
26
33
  end
@@ -2,8 +2,10 @@
2
2
 
3
3
  ActiveRecord::Schema.define do
4
4
  create_table :schema_info, force: :cascade do |t|
5
- t.integer :version, unique: true
5
+ t.integer :version
6
6
  end
7
+ add_index :schema_info, :version, unique: true
8
+
7
9
  SchemaInfo.create version: SchemaInfo::VERSION
8
10
 
9
11
  create_table :group, force: :cascade do |t|
@@ -8,10 +8,7 @@ ActiveRecord::Schema.define do
8
8
  # create ENUM if it does not exist yet
9
9
  begin
10
10
  execute('CREATE TYPE vendor_type AS ENUM (\'wholesaler\', \'retailer\');')
11
- rescue ActiveRecord::StatementInvalid => e
12
- # since PostgreSQL does not support IF NOT EXISTS when creating a TYPE,
13
- # rescue the error and check the error class
14
- raise unless e.cause.is_a? PG::DuplicateObject
11
+ rescue ActiveRecord::StatementInvalid
15
12
  execute('ALTER TYPE vendor_type ADD VALUE IF NOT EXISTS \'wholesaler\';')
16
13
  execute('ALTER TYPE vendor_type ADD VALUE IF NOT EXISTS \'retailer\';')
17
14
  end
@@ -60,4 +57,38 @@ ActiveRecord::Schema.define do
60
57
  end
61
58
 
62
59
  add_index :alarms, [:device_id, :alarm_type], unique: true, where: 'status <> 0'
60
+
61
+ unless ENV["SKIP_COMPOSITE_PK"]
62
+ create_table :authors, force: :cascade do |t|
63
+ t.string :name
64
+ end
65
+
66
+ execute %(
67
+ DROP SEQUENCE IF EXISTS composite_book_id_seq CASCADE;
68
+ CREATE SEQUENCE composite_book_id_seq
69
+ AS integer
70
+ START WITH 1
71
+ INCREMENT BY 1
72
+ NO MINVALUE
73
+ NO MAXVALUE
74
+ CACHE 1;
75
+
76
+ DROP TABLE IF EXISTS composite_books;
77
+ CREATE TABLE composite_books (
78
+ id bigint DEFAULT nextval('composite_book_id_seq'::regclass) NOT NULL,
79
+ title character varying,
80
+ author_id bigint
81
+ );
82
+
83
+ ALTER TABLE ONLY composite_books ADD CONSTRAINT fk_rails_040a418131 FOREIGN KEY (author_id) REFERENCES authors(id);
84
+ ).split.join(' ').strip
85
+ end
86
+
87
+ create_table :composite_chapters, force: :cascade do |t|
88
+ t.string :title
89
+ t.integer :composite_book_id, null: false
90
+ t.integer :author_id, null: false
91
+ t.datetime :created_at
92
+ t.datetime :updated_at
93
+ end
63
94
  end
@@ -351,6 +351,18 @@ def should_support_postgresql_import_functionality
351
351
  assert_equal db_customer.orders.last, db_order
352
352
  assert_not_equal db_order.customer_id, nil
353
353
  end
354
+
355
+ it "should import models with auto-incrementing ID successfully" do
356
+ author = Author.create!(name: "Foo Barson")
357
+
358
+ books = []
359
+ 2.times do |i|
360
+ books << CompositeBook.new(author_id: author.id, title: "book #{i}")
361
+ end
362
+ assert_difference "CompositeBook.count", +2 do
363
+ CompositeBook.import books
364
+ end
365
+ end
354
366
  end
355
367
  end
356
368
  end
@@ -223,6 +223,34 @@ def should_support_basic_on_duplicate_key_update
223
223
  end
224
224
  end
225
225
  end
226
+
227
+ context 'with locking disabled' do
228
+ it 'does not update the lock_version' do
229
+ users = [
230
+ User.new(name: 'Salomon'),
231
+ User.new(name: 'Nathan')
232
+ ]
233
+ User.import(users)
234
+ assert User.count == users.length
235
+ User.all.each do |user|
236
+ assert_equal 0, user.lock_version
237
+ end
238
+ updated_users = User.all.map do |user|
239
+ user.name += ' Rothschild'
240
+ user
241
+ end
242
+
243
+ ActiveRecord::Base.lock_optimistically = false # Disable locking
244
+ User.import(updated_users, on_duplicate_key_update: [:name])
245
+ ActiveRecord::Base.lock_optimistically = true # Enable locking
246
+
247
+ assert User.count == updated_users.length
248
+ User.all.each_with_index do |user, i|
249
+ assert_equal user.name, "#{users[i].name} Rothschild"
250
+ assert_equal 0, user.lock_version
251
+ end
252
+ end
253
+ end
226
254
  end
227
255
 
228
256
  context "with :on_duplicate_key_update" do
@@ -147,7 +147,7 @@ def should_support_recursive_import
147
147
  end
148
148
 
149
149
  books.each do |book|
150
- assert_equal book.topic_id, nil
150
+ assert_nil book.topic_id, nil
151
151
  end
152
152
  end
153
153
 
@@ -165,6 +165,24 @@ def should_support_recursive_import
165
165
  assert_equal 1, tags[0].tag_id
166
166
  assert_equal 2, tags[1].tag_id
167
167
  end
168
+
169
+ if ENV['AR_VERSION'].to_f >= 7.1
170
+ it "should import models with auto-incrementing ID successfully with recursive set to true" do
171
+ author = Author.create!(name: "Foo Barson")
172
+ books = []
173
+ 2.times do |i|
174
+ books << CompositeBook.new(author_id: author.id, title: "Book #{i}", composite_chapters: [
175
+ CompositeChapter.new(title: "Book #{i} composite chapter 1"),
176
+ CompositeChapter.new(title: "Book #{i} composite chapter 2"),
177
+ ])
178
+ end
179
+ assert_difference "CompositeBook.count", +2 do
180
+ assert_difference "CompositeChapter.count", +4 do
181
+ CompositeBook.import books, recursive: true
182
+ end
183
+ end
184
+ end
185
+ end
168
186
  end
169
187
  end
170
188
 
@@ -213,6 +231,54 @@ def should_support_recursive_import
213
231
  end
214
232
  end
215
233
  end
234
+
235
+ describe "recursive_on_duplicate_key_update" do
236
+ let(:new_topics) { Build(1, :topic_with_book) }
237
+
238
+ setup do
239
+ Topic.import new_topics, recursive: true
240
+ end
241
+
242
+ it "updates associated objects" do
243
+ new_author_name = 'Richard Bachman'
244
+ topic = new_topics.first
245
+ topic.books.each do |book|
246
+ book.author_name = new_author_name
247
+ end
248
+
249
+ assert_nothing_raised do
250
+ Topic.import new_topics,
251
+ recursive: true,
252
+ on_duplicate_key_update: [:id],
253
+ recursive_on_duplicate_key_update: {
254
+ books: { conflict_target: [:id], columns: [:author_name] }
255
+ }
256
+ end
257
+ Topic.find(topic.id).books.each do |book|
258
+ assert_equal new_author_name, book.author_name
259
+ end
260
+ end
261
+
262
+ it "updates nested associated objects" do
263
+ new_chapter_title = 'The Final Chapter'
264
+ book = new_topics.first.books.first
265
+ book.author_name = 'Richard Bachman'
266
+
267
+ example_chapter = book.chapters.first
268
+ example_chapter.title = new_chapter_title
269
+
270
+ assert_nothing_raised do
271
+ Topic.import new_topics,
272
+ recursive: true,
273
+ on_duplicate_key_update: [:id],
274
+ recursive_on_duplicate_key_update: {
275
+ books: { conflict_target: [:id], columns: [:author_name] },
276
+ chapters: { conflict_target: [:id], columns: [:title] }
277
+ }
278
+ end
279
+ assert_equal new_chapter_title, Chapter.find(example_chapter.id).title
280
+ end
281
+ end
216
282
  end
217
283
 
218
284
  # If returning option is provided, it is only applied to top level models so that SQL with invalid
data/test/test_helper.rb CHANGED
@@ -33,7 +33,9 @@ require 'chronic'
33
33
  begin
34
34
  require 'composite_primary_keys'
35
35
  rescue LoadError
36
- ENV["SKIP_COMPOSITE_PK"] = "true"
36
+ if ENV['AR_VERSION'].to_f <= 7.1
37
+ ENV['SKIP_COMPOSITE_PK'] = 'true'
38
+ end
37
39
  end
38
40
 
39
41
  # Support MySQL 5.7
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("#{File.dirname(__FILE__)}/../test_helper")
4
+ require File.expand_path("#{File.dirname(__FILE__)}/../support/assertions")
5
+ require File.expand_path("#{File.dirname(__FILE__)}/../support/mysql/import_examples")
6
+
7
+ should_support_mysql_import_functionality
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-import
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zach Dennis
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-21 00:00:00.000000000 Z
11
+ date: 2024-08-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -51,6 +51,7 @@ files:
51
51
  - ".rubocop_todo.yml"
52
52
  - Brewfile
53
53
  - CHANGELOG.md
54
+ - Dockerfile
54
55
  - Gemfile
55
56
  - LICENSE
56
57
  - README.markdown
@@ -68,6 +69,7 @@ files:
68
69
  - benchmarks/models/test_memory.rb
69
70
  - benchmarks/models/test_myisam.rb
70
71
  - benchmarks/schema/mysql2_schema.rb
72
+ - docker-compose.yml
71
73
  - gemfiles/4.2.gemfile
72
74
  - gemfiles/5.0.gemfile
73
75
  - gemfiles/5.1.gemfile
@@ -75,6 +77,8 @@ files:
75
77
  - gemfiles/6.0.gemfile
76
78
  - gemfiles/6.1.gemfile
77
79
  - gemfiles/7.0.gemfile
80
+ - gemfiles/7.1.gemfile
81
+ - gemfiles/7.2.gemfile
78
82
  - lib/activerecord-import.rb
79
83
  - lib/activerecord-import/active_record/adapters/abstract_adapter.rb
80
84
  - lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb
@@ -84,12 +88,14 @@ files:
84
88
  - lib/activerecord-import/active_record/adapters/postgresql_adapter.rb
85
89
  - lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb
86
90
  - lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb
91
+ - lib/activerecord-import/active_record/adapters/trilogy_adapter.rb
87
92
  - lib/activerecord-import/adapters/abstract_adapter.rb
88
93
  - lib/activerecord-import/adapters/em_mysql2_adapter.rb
89
94
  - lib/activerecord-import/adapters/mysql2_adapter.rb
90
95
  - lib/activerecord-import/adapters/mysql_adapter.rb
91
96
  - lib/activerecord-import/adapters/postgresql_adapter.rb
92
97
  - lib/activerecord-import/adapters/sqlite3_adapter.rb
98
+ - lib/activerecord-import/adapters/trilogy_adapter.rb
93
99
  - lib/activerecord-import/base.rb
94
100
  - lib/activerecord-import/import.rb
95
101
  - lib/activerecord-import/mysql2.rb
@@ -111,6 +117,7 @@ files:
111
117
  - test/adapters/seamless_database_pool.rb
112
118
  - test/adapters/spatialite.rb
113
119
  - test/adapters/sqlite3.rb
120
+ - test/adapters/trilogy.rb
114
121
  - test/database.yml.sample
115
122
  - test/github/database.yml
116
123
  - test/import_test.rb
@@ -121,11 +128,14 @@ files:
121
128
  - test/models/account.rb
122
129
  - test/models/alarm.rb
123
130
  - test/models/animal.rb
131
+ - test/models/author.rb
124
132
  - test/models/bike_maker.rb
125
133
  - test/models/book.rb
126
134
  - test/models/car.rb
127
135
  - test/models/card.rb
128
136
  - test/models/chapter.rb
137
+ - test/models/composite_book.rb
138
+ - test/models/composite_chapter.rb
129
139
  - test/models/customer.rb
130
140
  - test/models/deck.rb
131
141
  - test/models/dictionary.rb
@@ -169,13 +179,15 @@ files:
169
179
  - test/support/sqlite3/import_examples.rb
170
180
  - test/synchronize_test.rb
171
181
  - test/test_helper.rb
182
+ - test/trilogy/import_test.rb
172
183
  - test/value_sets_bytes_parser_test.rb
173
184
  - test/value_sets_records_parser_test.rb
174
185
  homepage: https://github.com/zdennis/activerecord-import
175
186
  licenses:
176
187
  - MIT
177
- metadata: {}
178
- post_install_message:
188
+ metadata:
189
+ changelog_uri: https://github.com/zdennis/activerecord-import/blob/master/CHANGELOG.md
190
+ post_install_message:
179
191
  rdoc_options: []
180
192
  require_paths:
181
193
  - lib
@@ -190,8 +202,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
190
202
  - !ruby/object:Gem::Version
191
203
  version: '0'
192
204
  requirements: []
193
- rubygems_version: 3.0.3.1
194
- signing_key:
205
+ rubygems_version: 3.5.11
206
+ signing_key:
195
207
  specification_version: 4
196
208
  summary: Bulk insert extension for ActiveRecord
197
209
  test_files:
@@ -208,6 +220,7 @@ test_files:
208
220
  - test/adapters/seamless_database_pool.rb
209
221
  - test/adapters/spatialite.rb
210
222
  - test/adapters/sqlite3.rb
223
+ - test/adapters/trilogy.rb
211
224
  - test/database.yml.sample
212
225
  - test/github/database.yml
213
226
  - test/import_test.rb
@@ -218,11 +231,14 @@ test_files:
218
231
  - test/models/account.rb
219
232
  - test/models/alarm.rb
220
233
  - test/models/animal.rb
234
+ - test/models/author.rb
221
235
  - test/models/bike_maker.rb
222
236
  - test/models/book.rb
223
237
  - test/models/car.rb
224
238
  - test/models/card.rb
225
239
  - test/models/chapter.rb
240
+ - test/models/composite_book.rb
241
+ - test/models/composite_chapter.rb
226
242
  - test/models/customer.rb
227
243
  - test/models/deck.rb
228
244
  - test/models/dictionary.rb
@@ -266,5 +282,6 @@ test_files:
266
282
  - test/support/sqlite3/import_examples.rb
267
283
  - test/synchronize_test.rb
268
284
  - test/test_helper.rb
285
+ - test/trilogy/import_test.rb
269
286
  - test/value_sets_bytes_parser_test.rb
270
287
  - test/value_sets_records_parser_test.rb