activerecord-import 1.4.1 → 1.8.1
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 +4 -4
- data/.github/workflows/test.yaml +53 -13
- data/.gitignore +4 -0
- data/.rubocop.yml +7 -4
- data/.rubocop_todo.yml +10 -16
- data/CHANGELOG.md +48 -1
- data/Dockerfile +23 -0
- data/Gemfile +15 -7
- data/README.markdown +44 -5
- data/Rakefile +1 -0
- data/activerecord-import.gemspec +4 -0
- data/benchmarks/benchmark.rb +3 -3
- data/benchmarks/lib/base.rb +2 -2
- data/benchmarks/lib/cli_parser.rb +2 -2
- data/docker-compose.yml +34 -0
- data/gemfiles/7.1.gemfile +3 -0
- data/gemfiles/7.2.gemfile +3 -0
- data/lib/activerecord-import/active_record/adapters/trilogy_adapter.rb +8 -0
- data/lib/activerecord-import/adapters/abstract_adapter.rb +6 -5
- data/lib/activerecord-import/adapters/mysql_adapter.rb +24 -18
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +26 -18
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +29 -23
- data/lib/activerecord-import/adapters/trilogy_adapter.rb +7 -0
- data/lib/activerecord-import/import.rb +63 -28
- data/lib/activerecord-import/value_sets_parser.rb +1 -0
- data/lib/activerecord-import/version.rb +1 -1
- data/lib/activerecord-import.rb +0 -1
- data/test/adapters/trilogy.rb +9 -0
- data/test/database.yml.sample +7 -0
- data/test/github/database.yml +4 -0
- data/test/jdbcmysql/import_test.rb +3 -3
- data/test/jdbcpostgresql/import_test.rb +2 -2
- data/test/jdbcsqlite3/import_test.rb +2 -2
- data/test/makara_postgis/import_test.rb +2 -2
- data/test/models/author.rb +7 -0
- data/test/models/bike_maker.rb +1 -0
- data/test/models/book.rb +5 -2
- data/test/models/composite_book.rb +19 -0
- data/test/models/composite_chapter.rb +9 -0
- data/test/models/customer.rb +14 -4
- data/test/models/order.rb +13 -4
- data/test/models/tag.rb +6 -1
- data/test/models/tag_alias.rb +7 -1
- data/test/models/topic.rb +5 -0
- data/test/models/widget.rb +10 -3
- data/test/mysql2/import_test.rb +3 -3
- data/test/mysql2_makara/import_test.rb +3 -3
- data/test/mysqlspatial2/import_test.rb +3 -3
- data/test/postgis/import_test.rb +2 -2
- data/test/postgresql/import_test.rb +2 -2
- data/test/schema/generic_schema.rb +4 -1
- data/test/schema/jdbcpostgresql_schema.rb +1 -1
- data/test/schema/postgis_schema.rb +1 -1
- data/test/schema/postgresql_schema.rb +35 -4
- data/test/sqlite3/import_test.rb +2 -2
- data/test/support/postgresql/import_examples.rb +12 -0
- data/test/support/shared_examples/on_duplicate_key_update.rb +67 -10
- data/test/support/shared_examples/recursive_import.rb +67 -1
- data/test/test_helper.rb +6 -4
- data/test/trilogy/import_test.rb +7 -0
- data/test/value_sets_bytes_parser_test.rb +1 -1
- data/test/value_sets_records_parser_test.rb +1 -1
- metadata +24 -7
| @@ -13,13 +13,14 @@ module ActiveRecord::Import::MysqlAdapter | |
| 13 13 | 
             
                # the number of inserts default
         | 
| 14 14 | 
             
                number_of_inserts = 0
         | 
| 15 15 |  | 
| 16 | 
            -
                base_sql, post_sql =  | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 16 | 
            +
                base_sql, post_sql = case sql
         | 
| 17 | 
            +
                                     when String
         | 
| 18 | 
            +
                                       [sql, '']
         | 
| 19 | 
            +
                                     when Array
         | 
| 20 | 
            +
                                       [sql.shift, sql.join( ' ' )]
         | 
| 20 21 | 
             
                end
         | 
| 21 22 |  | 
| 22 | 
            -
                sql_size = QUERY_OVERHEAD + base_sql. | 
| 23 | 
            +
                sql_size = QUERY_OVERHEAD + base_sql.bytesize + post_sql.bytesize
         | 
| 23 24 |  | 
| 24 25 | 
             
                # the number of bytes the requested insert statement values will take up
         | 
| 25 26 | 
             
                values_in_bytes = values.sum(&:bytesize)
         | 
| @@ -33,7 +34,7 @@ module ActiveRecord::Import::MysqlAdapter | |
| 33 34 | 
             
                max = max_allowed_packet
         | 
| 34 35 |  | 
| 35 36 | 
             
                # if we can insert it all as one statement
         | 
| 36 | 
            -
                if  | 
| 37 | 
            +
                if max == NO_MAX_PACKET || total_bytes <= max || options[:force_single_insert]
         | 
| 37 38 | 
             
                  number_of_inserts += 1
         | 
| 38 39 | 
             
                  sql2insert = base_sql + values.join( ',' ) + post_sql
         | 
| 39 40 | 
             
                  insert( sql2insert, *args )
         | 
| @@ -85,13 +86,13 @@ module ActiveRecord::Import::MysqlAdapter | |
| 85 86 | 
             
              # in +args+.
         | 
| 86 87 | 
             
              def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
         | 
| 87 88 | 
             
                sql = ' ON DUPLICATE KEY UPDATE '.dup
         | 
| 88 | 
            -
                arg = args | 
| 89 | 
            -
                 | 
| 90 | 
            -
                 | 
| 91 | 
            -
                  sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arg )
         | 
| 92 | 
            -
                 | 
| 93 | 
            -
                  sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, arg )
         | 
| 94 | 
            -
                 | 
| 89 | 
            +
                arg, model, _primary_key, locking_column = args
         | 
| 90 | 
            +
                case arg
         | 
| 91 | 
            +
                when Array
         | 
| 92 | 
            +
                  sql << sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, arg )
         | 
| 93 | 
            +
                when Hash
         | 
| 94 | 
            +
                  sql << sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, arg )
         | 
| 95 | 
            +
                when String
         | 
| 95 96 | 
             
                  sql << arg
         | 
| 96 97 | 
             
                else
         | 
| 97 98 | 
             
                  raise ArgumentError, "Expected Array or Hash"
         | 
| @@ -99,19 +100,24 @@ module ActiveRecord::Import::MysqlAdapter | |
| 99 100 | 
             
                sql
         | 
| 100 101 | 
             
              end
         | 
| 101 102 |  | 
| 102 | 
            -
              def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
         | 
| 103 | 
            +
              def sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, arr ) # :nodoc:
         | 
| 103 104 | 
             
                results = arr.map do |column|
         | 
| 104 | 
            -
                   | 
| 105 | 
            +
                  original_column_name = model.attribute_alias?( column ) ? model.attribute_alias( column ) : column
         | 
| 106 | 
            +
                  qc = quote_column_name( original_column_name )
         | 
| 105 107 | 
             
                  "#{table_name}.#{qc}=VALUES(#{qc})"
         | 
| 106 108 | 
             
                end
         | 
| 107 109 | 
             
                increment_locking_column!(table_name, results, locking_column)
         | 
| 108 110 | 
             
                results.join( ',' )
         | 
| 109 111 | 
             
              end
         | 
| 110 112 |  | 
| 111 | 
            -
              def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
         | 
| 113 | 
            +
              def sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, hsh ) # :nodoc:
         | 
| 112 114 | 
             
                results = hsh.map do |column1, column2|
         | 
| 113 | 
            -
                   | 
| 114 | 
            -
                   | 
| 115 | 
            +
                  original_column1_name = model.attribute_alias?( column1 ) ? model.attribute_alias( column1 ) : column1
         | 
| 116 | 
            +
                  qc1 = quote_column_name( original_column1_name )
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                  original_column2_name = model.attribute_alias?( column2 ) ? model.attribute_alias( column2 ) : column2
         | 
| 119 | 
            +
                  qc2 = quote_column_name( original_column2_name )
         | 
| 120 | 
            +
             | 
| 115 121 | 
             
                  "#{table_name}.#{qc1}=VALUES( #{qc2} )"
         | 
| 116 122 | 
             
                end
         | 
| 117 123 | 
             
                increment_locking_column!(table_name, results, locking_column)
         | 
| @@ -12,10 +12,11 @@ module ActiveRecord::Import::PostgreSQLAdapter | |
| 12 12 | 
             
                ids = []
         | 
| 13 13 | 
             
                results = []
         | 
| 14 14 |  | 
| 15 | 
            -
                base_sql, post_sql =  | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 15 | 
            +
                base_sql, post_sql = case sql
         | 
| 16 | 
            +
                                     when String
         | 
| 17 | 
            +
                                       [sql, '']
         | 
| 18 | 
            +
                                     when Array
         | 
| 19 | 
            +
                                       [sql.shift, sql.join( ' ' )]
         | 
| 19 20 | 
             
                end
         | 
| 20 21 |  | 
| 21 22 | 
             
                sql2insert = base_sql + values.join( ',' ) + post_sql
         | 
| @@ -110,13 +111,14 @@ module ActiveRecord::Import::PostgreSQLAdapter | |
| 110 111 | 
             
              # Add a column to be updated on duplicate key update
         | 
| 111 112 | 
             
              def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
         | 
| 112 113 | 
             
                arg = options[:on_duplicate_key_update]
         | 
| 113 | 
            -
                 | 
| 114 | 
            +
                case arg
         | 
| 115 | 
            +
                when Hash
         | 
| 114 116 | 
             
                  columns = arg.fetch( :columns ) { arg[:columns] = [] }
         | 
| 115 117 | 
             
                  case columns
         | 
| 116 118 | 
             
                  when Array then columns << column.to_sym unless columns.include?( column.to_sym )
         | 
| 117 119 | 
             
                  when Hash then columns[column.to_sym] = column.to_sym
         | 
| 118 120 | 
             
                  end
         | 
| 119 | 
            -
                 | 
| 121 | 
            +
                when Array
         | 
| 120 122 | 
             
                  arg << column.to_sym unless arg.include?( column.to_sym )
         | 
| 121 123 | 
             
                end
         | 
| 122 124 | 
             
              end
         | 
| @@ -132,7 +134,7 @@ module ActiveRecord::Import::PostgreSQLAdapter | |
| 132 134 | 
             
              # Returns a generated ON CONFLICT DO UPDATE statement given the passed
         | 
| 133 135 | 
             
              # in +args+.
         | 
| 134 136 | 
             
              def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
         | 
| 135 | 
            -
                arg, primary_key, locking_column = args
         | 
| 137 | 
            +
                arg, model, primary_key, locking_column = args
         | 
| 136 138 | 
             
                arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
         | 
| 137 139 | 
             
                return unless arg.is_a?( Hash )
         | 
| 138 140 |  | 
| @@ -151,11 +153,12 @@ module ActiveRecord::Import::PostgreSQLAdapter | |
| 151 153 | 
             
                end
         | 
| 152 154 |  | 
| 153 155 | 
             
                sql << "#{conflict_target}DO UPDATE SET "
         | 
| 154 | 
            -
                 | 
| 155 | 
            -
             | 
| 156 | 
            -
             | 
| 157 | 
            -
             | 
| 158 | 
            -
             | 
| 156 | 
            +
                case columns
         | 
| 157 | 
            +
                when Array
         | 
| 158 | 
            +
                  sql << sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, columns )
         | 
| 159 | 
            +
                when Hash
         | 
| 160 | 
            +
                  sql << sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, columns )
         | 
| 161 | 
            +
                when String
         | 
| 159 162 | 
             
                  sql << columns
         | 
| 160 163 | 
             
                else
         | 
| 161 164 | 
             
                  raise ArgumentError, 'Expected :columns to be an Array or Hash'
         | 
| @@ -166,19 +169,24 @@ module ActiveRecord::Import::PostgreSQLAdapter | |
| 166 169 | 
             
                sql
         | 
| 167 170 | 
             
              end
         | 
| 168 171 |  | 
| 169 | 
            -
              def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
         | 
| 172 | 
            +
              def sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, arr ) # :nodoc:
         | 
| 170 173 | 
             
                results = arr.map do |column|
         | 
| 171 | 
            -
                   | 
| 174 | 
            +
                  original_column_name = model.attribute_alias?( column ) ? model.attribute_alias( column ) : column
         | 
| 175 | 
            +
                  qc = quote_column_name( original_column_name )
         | 
| 172 176 | 
             
                  "#{qc}=EXCLUDED.#{qc}"
         | 
| 173 177 | 
             
                end
         | 
| 174 178 | 
             
                increment_locking_column!(table_name, results, locking_column)
         | 
| 175 179 | 
             
                results.join( ',' )
         | 
| 176 180 | 
             
              end
         | 
| 177 181 |  | 
| 178 | 
            -
              def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
         | 
| 182 | 
            +
              def sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, hsh ) # :nodoc:
         | 
| 179 183 | 
             
                results = hsh.map do |column1, column2|
         | 
| 180 | 
            -
                   | 
| 181 | 
            -
                   | 
| 184 | 
            +
                  original_column1_name = model.attribute_alias?( column1 ) ? model.attribute_alias( column1 ) : column1
         | 
| 185 | 
            +
                  qc1 = quote_column_name( original_column1_name )
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                  original_column2_name = model.attribute_alias?( column2 ) ? model.attribute_alias( column2 ) : column2
         | 
| 188 | 
            +
                  qc2 = quote_column_name( original_column2_name )
         | 
| 189 | 
            +
             | 
| 182 190 | 
             
                  "#{qc1}=EXCLUDED.#{qc2}"
         | 
| 183 191 | 
             
                end
         | 
| 184 192 | 
             
                increment_locking_column!(table_name, results, locking_column)
         | 
| @@ -192,7 +200,7 @@ module ActiveRecord::Import::PostgreSQLAdapter | |
| 192 200 | 
             
                if constraint_name.present?
         | 
| 193 201 | 
             
                  "ON CONSTRAINT #{constraint_name} "
         | 
| 194 202 | 
             
                elsif conflict_target.present?
         | 
| 195 | 
            -
                  sql =  | 
| 203 | 
            +
                  sql = "(#{Array( conflict_target ).reject( &:blank? ).join( ', ' )}) "
         | 
| 196 204 | 
             
                  sql += "WHERE #{index_predicate} " if index_predicate
         | 
| 197 205 | 
             
                  sql
         | 
| 198 206 | 
             
                end
         | 
| @@ -24,10 +24,11 @@ module ActiveRecord::Import::SQLite3Adapter | |
| 24 24 | 
             
              def insert_many( sql, values, _options = {}, *args ) # :nodoc:
         | 
| 25 25 | 
             
                number_of_inserts = 0
         | 
| 26 26 |  | 
| 27 | 
            -
                base_sql, post_sql =  | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 27 | 
            +
                base_sql, post_sql = case sql
         | 
| 28 | 
            +
                                     when String
         | 
| 29 | 
            +
                                       [sql, '']
         | 
| 30 | 
            +
                                     when Array
         | 
| 31 | 
            +
                                       [sql.shift, sql.join( ' ' )]
         | 
| 31 32 | 
             
                end
         | 
| 32 33 |  | 
| 33 34 | 
             
                value_sets = ::ActiveRecord::Import::ValueSetsRecordsParser.parse(values,
         | 
| @@ -56,11 +57,9 @@ module ActiveRecord::Import::SQLite3Adapter | |
| 56 57 | 
             
              def post_sql_statements( table_name, options ) # :nodoc:
         | 
| 57 58 | 
             
                sql = []
         | 
| 58 59 |  | 
| 59 | 
            -
                 | 
| 60 | 
            -
             | 
| 61 | 
            -
                   | 
| 62 | 
            -
                    sql << sql_for_on_duplicate_key_ignore( options[:on_duplicate_key_ignore] )
         | 
| 63 | 
            -
                  end
         | 
| 60 | 
            +
                # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
         | 
| 61 | 
            +
                if supports_on_duplicate_key_update? && ((options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update])
         | 
| 62 | 
            +
                  sql << sql_for_on_duplicate_key_ignore( options[:on_duplicate_key_ignore] )
         | 
| 64 63 | 
             
                end
         | 
| 65 64 |  | 
| 66 65 | 
             
                sql + super
         | 
| @@ -73,13 +72,14 @@ module ActiveRecord::Import::SQLite3Adapter | |
| 73 72 | 
             
              # Add a column to be updated on duplicate key update
         | 
| 74 73 | 
             
              def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
         | 
| 75 74 | 
             
                arg = options[:on_duplicate_key_update]
         | 
| 76 | 
            -
                 | 
| 75 | 
            +
                case arg
         | 
| 76 | 
            +
                when Hash
         | 
| 77 77 | 
             
                  columns = arg.fetch( :columns ) { arg[:columns] = [] }
         | 
| 78 78 | 
             
                  case columns
         | 
| 79 79 | 
             
                  when Array then columns << column.to_sym unless columns.include?( column.to_sym )
         | 
| 80 80 | 
             
                  when Hash then columns[column.to_sym] = column.to_sym
         | 
| 81 81 | 
             
                  end
         | 
| 82 | 
            -
                 | 
| 82 | 
            +
                when Array
         | 
| 83 83 | 
             
                  arg << column.to_sym unless arg.include?( column.to_sym )
         | 
| 84 84 | 
             
                end
         | 
| 85 85 | 
             
              end
         | 
| @@ -95,7 +95,7 @@ module ActiveRecord::Import::SQLite3Adapter | |
| 95 95 | 
             
              # Returns a generated ON CONFLICT DO UPDATE statement given the passed
         | 
| 96 96 | 
             
              # in +args+.
         | 
| 97 97 | 
             
              def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
         | 
| 98 | 
            -
                arg, primary_key, locking_column = args
         | 
| 98 | 
            +
                arg, model, primary_key, locking_column = args
         | 
| 99 99 | 
             
                arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
         | 
| 100 100 | 
             
                return unless arg.is_a?( Hash )
         | 
| 101 101 |  | 
| @@ -114,11 +114,12 @@ module ActiveRecord::Import::SQLite3Adapter | |
| 114 114 | 
             
                end
         | 
| 115 115 |  | 
| 116 116 | 
             
                sql << "#{conflict_target}DO UPDATE SET "
         | 
| 117 | 
            -
                 | 
| 118 | 
            -
             | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 117 | 
            +
                case columns
         | 
| 118 | 
            +
                when Array
         | 
| 119 | 
            +
                  sql << sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, columns )
         | 
| 120 | 
            +
                when Hash
         | 
| 121 | 
            +
                  sql << sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, columns )
         | 
| 122 | 
            +
                when String
         | 
| 122 123 | 
             
                  sql << columns
         | 
| 123 124 | 
             
                else
         | 
| 124 125 | 
             
                  raise ArgumentError, 'Expected :columns to be an Array or Hash'
         | 
| @@ -129,19 +130,24 @@ module ActiveRecord::Import::SQLite3Adapter | |
| 129 130 | 
             
                sql
         | 
| 130 131 | 
             
              end
         | 
| 131 132 |  | 
| 132 | 
            -
              def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
         | 
| 133 | 
            +
              def sql_for_on_duplicate_key_update_as_array( table_name, model, locking_column, arr ) # :nodoc:
         | 
| 133 134 | 
             
                results = arr.map do |column|
         | 
| 134 | 
            -
                   | 
| 135 | 
            +
                  original_column_name = model.attribute_alias?( column ) ? model.attribute_alias( column ) : column
         | 
| 136 | 
            +
                  qc = quote_column_name( original_column_name )
         | 
| 135 137 | 
             
                  "#{qc}=EXCLUDED.#{qc}"
         | 
| 136 138 | 
             
                end
         | 
| 137 139 | 
             
                increment_locking_column!(table_name, results, locking_column)
         | 
| 138 140 | 
             
                results.join( ',' )
         | 
| 139 141 | 
             
              end
         | 
| 140 142 |  | 
| 141 | 
            -
              def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
         | 
| 143 | 
            +
              def sql_for_on_duplicate_key_update_as_hash( table_name, model, locking_column, hsh ) # :nodoc:
         | 
| 142 144 | 
             
                results = hsh.map do |column1, column2|
         | 
| 143 | 
            -
                   | 
| 144 | 
            -
                   | 
| 145 | 
            +
                  original_column1_name = model.attribute_alias?( column1 ) ? model.attribute_alias( column1 ) : column1
         | 
| 146 | 
            +
                  qc1 = quote_column_name( original_column1_name )
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                  original_column2_name = model.attribute_alias?( column2 ) ? model.attribute_alias( column2 ) : column2
         | 
| 149 | 
            +
                  qc2 = quote_column_name( original_column2_name )
         | 
| 150 | 
            +
             | 
| 145 151 | 
             
                  "#{qc1}=EXCLUDED.#{qc2}"
         | 
| 146 152 | 
             
                end
         | 
| 147 153 | 
             
                increment_locking_column!(table_name, results, locking_column)
         | 
| @@ -152,7 +158,7 @@ module ActiveRecord::Import::SQLite3Adapter | |
| 152 158 | 
             
                conflict_target = args[:conflict_target]
         | 
| 153 159 | 
             
                index_predicate = args[:index_predicate]
         | 
| 154 160 | 
             
                if conflict_target.present?
         | 
| 155 | 
            -
                  sql =  | 
| 161 | 
            +
                  sql = "(#{Array( conflict_target ).reject( &:blank? ).join( ', ' )}) "
         | 
| 156 162 | 
             
                  sql += "WHERE #{index_predicate} " if index_predicate
         | 
| 157 163 | 
             
                  sql
         | 
| 158 164 | 
             
                end
         | 
| @@ -4,17 +4,17 @@ require "ostruct" | |
| 4 4 |  | 
| 5 5 | 
             
            module ActiveRecord::Import::ConnectionAdapters; end
         | 
| 6 6 |  | 
| 7 | 
            -
            module ActiveRecord::Import  | 
| 7 | 
            +
            module ActiveRecord::Import # :nodoc:
         | 
| 8 8 | 
             
              Result = Struct.new(:failed_instances, :num_inserts, :ids, :results)
         | 
| 9 9 |  | 
| 10 | 
            -
              module ImportSupport  | 
| 11 | 
            -
                def supports_import?  | 
| 10 | 
            +
              module ImportSupport # :nodoc:
         | 
| 11 | 
            +
                def supports_import? # :nodoc:
         | 
| 12 12 | 
             
                  true
         | 
| 13 13 | 
             
                end
         | 
| 14 14 | 
             
              end
         | 
| 15 15 |  | 
| 16 | 
            -
              module OnDuplicateKeyUpdateSupport  | 
| 17 | 
            -
                def supports_on_duplicate_key_update?  | 
| 16 | 
            +
              module OnDuplicateKeyUpdateSupport # :nodoc:
         | 
| 17 | 
            +
                def supports_on_duplicate_key_update? # :nodoc:
         | 
| 18 18 | 
             
                  true
         | 
| 19 19 | 
             
                end
         | 
| 20 20 | 
             
              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
         | 
| @@ -73,7 +74,7 @@ module ActiveRecord::Import #:nodoc: | |
| 73 74 | 
             
                end
         | 
| 74 75 |  | 
| 75 76 | 
             
                def valid_model?(model)
         | 
| 76 | 
            -
                  init_validations(model.class) unless model. | 
| 77 | 
            +
                  init_validations(model.class) unless model.instance_of?(@validator_class)
         | 
| 77 78 |  | 
| 78 79 | 
             
                  validation_context = @options[:validate_with_context]
         | 
| 79 80 | 
             
                  validation_context ||= (model.new_record? ? :create : :update)
         | 
| @@ -85,11 +86,15 @@ module ActiveRecord::Import #:nodoc: | |
| 85 86 |  | 
| 86 87 | 
             
                    model.run_callbacks(:validation) do
         | 
| 87 88 | 
             
                      if defined?(ActiveSupport::Callbacks::Filters::Environment) # ActiveRecord >= 4.1
         | 
| 88 | 
            -
                        runner = @validate_callbacks.compile
         | 
| 89 | 
            +
                        runner = if @validate_callbacks.method(:compile).arity == 0
         | 
| 90 | 
            +
                          @validate_callbacks.compile
         | 
| 91 | 
            +
                        else # ActiveRecord >= 7.1
         | 
| 92 | 
            +
                          @validate_callbacks.compile(nil)
         | 
| 93 | 
            +
                        end
         | 
| 89 94 | 
             
                        env = ActiveSupport::Callbacks::Filters::Environment.new(model, false, nil)
         | 
| 90 95 | 
             
                        if runner.respond_to?(:call) # ActiveRecord < 5.1
         | 
| 91 96 | 
             
                          runner.call(env)
         | 
| 92 | 
            -
                        else # ActiveRecord 5.1
         | 
| 97 | 
            +
                        else # ActiveRecord >= 5.1
         | 
| 93 98 | 
             
                          # Note that this is a gross simplification of ActiveSupport::Callbacks#run_callbacks.
         | 
| 94 99 | 
             
                          # It's technically possible for there to exist an "around" callback in the
         | 
| 95 100 | 
             
                          # :validate chain, but this would be an aberration, since Rails doesn't define
         | 
| @@ -102,7 +107,8 @@ module ActiveRecord::Import #:nodoc: | |
| 102 107 | 
             
                          # no real-world use case for it.
         | 
| 103 108 | 
             
                          raise "The :validate callback chain contains an 'around' callback, which is unsupported" unless runner.final?
         | 
| 104 109 | 
             
                          runner.invoke_before(env)
         | 
| 105 | 
            -
                           | 
| 110 | 
            +
                          # Ensure a truthy value is returned. ActiveRecord < 7.2 always returned an array.
         | 
| 111 | 
            +
                          runner.invoke_after(env) || []
         | 
| 106 112 | 
             
                        end
         | 
| 107 113 | 
             
                      elsif @validate_callbacks.method(:compile).arity == 0 # ActiveRecord = 4.0
         | 
| 108 114 | 
             
                        model.instance_eval @validate_callbacks.compile
         | 
| @@ -165,7 +171,7 @@ class ActiveRecord::Associations::CollectionAssociation | |
| 165 171 | 
             
                    m.public_send "#{reflection.type}=", owner.class.name if reflection.type
         | 
| 166 172 | 
             
                  end
         | 
| 167 173 |  | 
| 168 | 
            -
                   | 
| 174 | 
            +
                  model_klass.bulk_import column_names, models, options
         | 
| 169 175 |  | 
| 170 176 | 
             
                # supports array of hash objects
         | 
| 171 177 | 
             
                elsif args.last.is_a?( Array ) && args.last.first.is_a?(Hash)
         | 
| @@ -204,11 +210,11 @@ class ActiveRecord::Associations::CollectionAssociation | |
| 204 210 | 
             
                    end
         | 
| 205 211 | 
             
                  end
         | 
| 206 212 |  | 
| 207 | 
            -
                   | 
| 213 | 
            +
                  model_klass.bulk_import column_names, array_of_attributes, options
         | 
| 208 214 |  | 
| 209 215 | 
             
                # supports empty array
         | 
| 210 216 | 
             
                elsif args.last.is_a?( Array ) && args.last.empty?
         | 
| 211 | 
            -
                   | 
| 217 | 
            +
                  ActiveRecord::Import::Result.new([], 0, [])
         | 
| 212 218 |  | 
| 213 219 | 
             
                # supports 2-element array and array
         | 
| 214 220 | 
             
                elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
         | 
| @@ -239,7 +245,7 @@ class ActiveRecord::Associations::CollectionAssociation | |
| 239 245 | 
             
                    end
         | 
| 240 246 | 
             
                  end
         | 
| 241 247 |  | 
| 242 | 
            -
                   | 
| 248 | 
            +
                  model_klass.bulk_import column_names, array_of_attributes, options
         | 
| 243 249 | 
             
                else
         | 
| 244 250 | 
             
                  raise ArgumentError, "Invalid arguments!"
         | 
| 245 251 | 
             
                end
         | 
| @@ -553,7 +559,7 @@ class ActiveRecord::Base | |
| 553 559 | 
             
                  options.merge!( args.pop ) if args.last.is_a? Hash
         | 
| 554 560 | 
             
                  # making sure that current model's primary key is used
         | 
| 555 561 | 
             
                  options[:primary_key] = primary_key
         | 
| 556 | 
            -
                  options[:locking_column] = locking_column if  | 
| 562 | 
            +
                  options[:locking_column] = locking_column if locking_enabled?
         | 
| 557 563 |  | 
| 558 564 | 
             
                  is_validating = options[:validate_with_context].present? ? true : options[:validate]
         | 
| 559 565 | 
             
                  validator = ActiveRecord::Import::Validator.new(self, options)
         | 
| @@ -574,7 +580,7 @@ class ActiveRecord::Base | |
| 574 580 |  | 
| 575 581 | 
             
                    if models.first.id.nil?
         | 
| 576 582 | 
             
                      Array(primary_key).each do |c|
         | 
| 577 | 
            -
                        if column_names.include?(c) &&  | 
| 583 | 
            +
                        if column_names.include?(c) && schema_columns_hash[c].type == :uuid
         | 
| 578 584 | 
             
                          column_names.delete(c)
         | 
| 579 585 | 
             
                        end
         | 
| 580 586 | 
             
                      end
         | 
| @@ -697,7 +703,11 @@ class ActiveRecord::Base | |
| 697 703 | 
             
                  return_obj = if is_validating
         | 
| 698 704 | 
             
                    import_with_validations( column_names, array_of_attributes, options ) do |failed_instances|
         | 
| 699 705 | 
             
                      if models
         | 
| 700 | 
            -
                        models. | 
| 706 | 
            +
                        models.each_with_index do |m, i|
         | 
| 707 | 
            +
                          next unless m.errors.any?
         | 
| 708 | 
            +
             | 
| 709 | 
            +
                          failed_instances << (options[:track_validation_failures] ? [i, m] : m)
         | 
| 710 | 
            +
                        end
         | 
| 701 711 | 
             
                      else
         | 
| 702 712 | 
             
                        # create instances for each of our column/value sets
         | 
| 703 713 | 
             
                        arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
         | 
| @@ -774,7 +784,10 @@ class ActiveRecord::Base | |
| 774 784 | 
             
                def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
         | 
| 775 785 | 
             
                  return ActiveRecord::Import::Result.new([], 0, [], []) if array_of_attributes.empty?
         | 
| 776 786 |  | 
| 777 | 
            -
                  column_names = column_names.map | 
| 787 | 
            +
                  column_names = column_names.map do |name|
         | 
| 788 | 
            +
                    original_name = attribute_alias?(name) ? attribute_alias(name) : name
         | 
| 789 | 
            +
                    original_name.to_sym
         | 
| 790 | 
            +
                  end
         | 
| 778 791 | 
             
                  scope_columns, scope_values = scope_attributes.to_a.transpose
         | 
| 779 792 |  | 
| 780 793 | 
             
                  unless scope_columns.blank?
         | 
| @@ -786,15 +799,13 @@ class ActiveRecord::Base | |
| 786 799 | 
             
                    end
         | 
| 787 800 | 
             
                  end
         | 
| 788 801 |  | 
| 789 | 
            -
                  if finder_needs_type_condition?
         | 
| 790 | 
            -
                     | 
| 791 | 
            -
             | 
| 792 | 
            -
                      array_of_attributes.each { |attrs| attrs << sti_name }
         | 
| 793 | 
            -
                    end
         | 
| 802 | 
            +
                  if finder_needs_type_condition? && !column_names.include?(inheritance_column.to_sym)
         | 
| 803 | 
            +
                    column_names << inheritance_column.to_sym
         | 
| 804 | 
            +
                    array_of_attributes.each { |attrs| attrs << sti_name }
         | 
| 794 805 | 
             
                  end
         | 
| 795 806 |  | 
| 796 807 | 
             
                  columns = column_names.each_with_index.map do |name, i|
         | 
| 797 | 
            -
                    column =  | 
| 808 | 
            +
                    column = schema_columns_hash[name.to_s]
         | 
| 798 809 | 
             
                    raise ActiveRecord::Import::MissingColumnError.new(name.to_s, i) if column.nil?
         | 
| 799 810 | 
             
                    column
         | 
| 800 811 | 
             
                  end
         | 
| @@ -848,6 +859,15 @@ class ActiveRecord::Base | |
| 848 859 |  | 
| 849 860 | 
             
                private
         | 
| 850 861 |  | 
| 862 | 
            +
                def associated_options(options, associated_class)
         | 
| 863 | 
            +
                  return options unless options.key?(:recursive_on_duplicate_key_update)
         | 
| 864 | 
            +
             | 
| 865 | 
            +
                  table_name = associated_class.arel_table.name.to_sym
         | 
| 866 | 
            +
                  options.merge(
         | 
| 867 | 
            +
                    on_duplicate_key_update: options[:recursive_on_duplicate_key_update][table_name]
         | 
| 868 | 
            +
                  )
         | 
| 869 | 
            +
                end
         | 
| 870 | 
            +
             | 
| 851 871 | 
             
                def set_attributes_and_mark_clean(models, import_result, timestamps, options)
         | 
| 852 872 | 
             
                  return if models.nil?
         | 
| 853 873 | 
             
                  models -= import_result.failed_instances
         | 
| @@ -859,13 +879,13 @@ class ActiveRecord::Base | |
| 859 879 | 
             
                      model.id = id
         | 
| 860 880 |  | 
| 861 881 | 
             
                      timestamps.each do |attr, value|
         | 
| 862 | 
            -
                        model.send(attr | 
| 882 | 
            +
                        model.send("#{attr}=", value) if model.send(attr).nil?
         | 
| 863 883 | 
             
                      end
         | 
| 864 884 | 
             
                    end
         | 
| 865 885 | 
             
                  end
         | 
| 866 886 |  | 
| 867 887 | 
             
                  deserialize_value = lambda do |column, value|
         | 
| 868 | 
            -
                    column =  | 
| 888 | 
            +
                    column = schema_columns_hash[column]
         | 
| 869 889 | 
             
                    return value unless column
         | 
| 870 890 | 
             
                    if respond_to?(:type_caster)
         | 
| 871 891 | 
             
                      type = type_for_attribute(column.name)
         | 
| @@ -932,7 +952,7 @@ class ActiveRecord::Base | |
| 932 952 | 
             
                      association = association.target
         | 
| 933 953 | 
             
                      next if association.blank? || model.public_send(column_name).present?
         | 
| 934 954 |  | 
| 935 | 
            -
                      association_primary_key = Array(association_reflection.association_primary_key)[column_index]
         | 
| 955 | 
            +
                      association_primary_key = Array(association_reflection.association_primary_key.tr("[]:", "").split(", "))[column_index]
         | 
| 936 956 | 
             
                      model.public_send("#{column_name}=", association.send(association_primary_key))
         | 
| 937 957 | 
             
                    end
         | 
| 938 958 | 
             
                  end
         | 
| @@ -954,11 +974,23 @@ class ActiveRecord::Base | |
| 954 974 |  | 
| 955 975 | 
             
                  associated_objects_by_class.each_value do |associations|
         | 
| 956 976 | 
             
                    associations.each_value do |associated_records|
         | 
| 957 | 
            -
                       | 
| 977 | 
            +
                      next if associated_records.empty?
         | 
| 978 | 
            +
             | 
| 979 | 
            +
                      associated_class = associated_records.first.class
         | 
| 980 | 
            +
                      associated_class.bulk_import(associated_records,
         | 
| 981 | 
            +
                                                   associated_options(options, associated_class))
         | 
| 958 982 | 
             
                    end
         | 
| 959 983 | 
             
                  end
         | 
| 960 984 | 
             
                end
         | 
| 961 985 |  | 
| 986 | 
            +
                def schema_columns_hash
         | 
| 987 | 
            +
                  if respond_to?(:ignored_columns) && ignored_columns.any?
         | 
| 988 | 
            +
                    connection.schema_cache.columns_hash(table_name)
         | 
| 989 | 
            +
                  else
         | 
| 990 | 
            +
                    columns_hash
         | 
| 991 | 
            +
                  end
         | 
| 992 | 
            +
                end
         | 
| 993 | 
            +
             | 
| 962 994 | 
             
                # We are eventually going to call Class.import <objects> so we build up a hash
         | 
| 963 995 | 
             
                # of class => objects to import.
         | 
| 964 996 | 
             
                def find_associated_objects_for_import(associated_objects_by_class, model)
         | 
| @@ -979,7 +1011,10 @@ class ActiveRecord::Base | |
| 979 1011 |  | 
| 980 1012 | 
             
                    changed_objects = association.select { |a| a.new_record? || a.changed? }
         | 
| 981 1013 | 
             
                    changed_objects.each do |child|
         | 
| 982 | 
            -
                       | 
| 1014 | 
            +
                      Array(association_reflection.inverse_of&.foreign_key || association_reflection.foreign_key).each_with_index do |column, index|
         | 
| 1015 | 
            +
                        child.public_send("#{column}=", Array(model.id)[index])
         | 
| 1016 | 
            +
                      end
         | 
| 1017 | 
            +
             | 
| 983 1018 | 
             
                      # For polymorphic associations
         | 
| 984 1019 | 
             
                      association_name = if model.class.respond_to?(:polymorphic_name)
         | 
| 985 1020 | 
             
                        model.class.polymorphic_name
         | 
    
        data/lib/activerecord-import.rb
    CHANGED
    
    
    
        data/test/database.yml.sample
    CHANGED
    
    | @@ -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
         | 
    
        data/test/github/database.yml
    CHANGED
    
    
| @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require File.expand_path(File.dirname(__FILE__) | 
| 4 | 
            -
            require File.expand_path(File.dirname(__FILE__) | 
| 5 | 
            -
            require File.expand_path(File.dirname(__FILE__) | 
| 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 6 |  | 
| 7 7 | 
             
            should_support_mysql_import_functionality
         | 
| @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require File.expand_path(File.dirname(__FILE__) | 
| 4 | 
            -
            require File.expand_path(File.dirname(__FILE__) | 
| 3 | 
            +
            require File.expand_path("#{File.dirname(__FILE__)}/../test_helper")
         | 
| 4 | 
            +
            require File.expand_path("#{File.dirname(__FILE__)}/../support/postgresql/import_examples")
         | 
| 5 5 |  | 
| 6 6 | 
             
            should_support_postgresql_import_functionality
         | 
| @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require File.expand_path(File.dirname(__FILE__) | 
| 4 | 
            -
            require File.expand_path(File.dirname(__FILE__) | 
| 3 | 
            +
            require File.expand_path("#{File.dirname(__FILE__)}/../test_helper")
         | 
| 4 | 
            +
            require File.expand_path("#{File.dirname(__FILE__)}/../support/sqlite3/import_examples")
         | 
| 5 5 |  | 
| 6 6 | 
             
            should_support_sqlite3_import_functionality
         | 
| @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require File.expand_path(File.dirname(__FILE__) | 
| 4 | 
            -
            require File.expand_path(File.dirname(__FILE__) | 
| 3 | 
            +
            require File.expand_path("#{File.dirname(__FILE__)}/../test_helper")
         | 
| 4 | 
            +
            require File.expand_path("#{File.dirname(__FILE__)}/../support/postgresql/import_examples")
         | 
| 5 5 |  | 
| 6 6 | 
             
            should_support_postgresql_import_functionality
         | 
| 7 7 |  |