activerecord-import 0.7.0 → 0.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
  SHA1:
3
- metadata.gz: 7aec7bd524a95df0244ff6fd857ca4b534fffe7e
4
- data.tar.gz: 3680f99953ff777985422c2a349eacd88b7df0d5
3
+ metadata.gz: aad783ef61c17fc4254414ec61467e9f3a2feb87
4
+ data.tar.gz: 6f6724d8165495a08b09621024ddaa7178af3049
5
5
  SHA512:
6
- metadata.gz: 302282ad1f0db1ce53f9961a46de68dea66b6f332e25742b35cf086ce16a458c40c502aa8c3b3a93a220b3fc4307c5ee33e731d786c2365f82a294dac214c17b
7
- data.tar.gz: edbc30d0d1e4ecdeeabacfed9042b02ae89c79ea644d8683dd6a9048847ee1f40f6bea357fe702628bbe8f694a20e2d76db20d1ea7102b2dfe31e3068e7930bc
6
+ metadata.gz: f757f8b390437bace4de12912a69e2638ee6bf3dd9a0d1092134337325052a1e00c31e8e3eb59e3fd7c1064d0fae8e8c935f521dfcdebff540334f8290095dec
7
+ data.tar.gz: aebcf07390a107d36309e34dcf121d2cae7f0264e7b39d403afc7fa3fad19fe9c1d0575e36ff0590c95546bdaf0e2480b5efe4fb303b2a02b134c3a0dff06fef
data/Gemfile CHANGED
@@ -4,7 +4,7 @@ gemspec
4
4
 
5
5
  # Database Adapters
6
6
  platforms :ruby do
7
- gem "em-synchrony", "~> 1.0.3"
7
+ gem "em-synchrony", "1.0.3"
8
8
  gem "mysql2", "~> 0.3.0"
9
9
  gem "pg", "~> 0.9"
10
10
  gem "sqlite3-ruby", "~> 1.3.1"
@@ -2,6 +2,25 @@
2
2
 
3
3
  activerecord-import is a library for bulk inserting data using ActiveRecord.
4
4
 
5
+ One of its major features is following activerecord associations and generating the minimal
6
+ number of SQL insert statements required, avoiding the N+1 insert problem. An example probably
7
+ explains it best. Say you had a schema like this:
8
+
9
+ Publishers have Books
10
+ Books have Reviews
11
+
12
+ and you wanted to bulk insert 100 new publishers with 10K books and 3 reviews per book. This library will follow the associations
13
+ down and generate only 3 SQL insert statements - one for the publishers, one for the books, and one for the reviews.
14
+
15
+ In contrast, the standard ActiveRecord save would generate
16
+ 100 insert statements for the publishers, then it would visit each publisher and save all the books:
17
+ 100 * 10,000 = 1,000,000 SQL insert statements
18
+ and then the reviews:
19
+ 100 * 10,000 * 3 = 3M SQL insert statements,
20
+
21
+ That would be about 4M SQL insert statements vs 3, which results in vastly improved performance. In our case, it converted
22
+ an 18 hour batch process to <2 hrs.
23
+
5
24
  ### Rails 4.0
6
25
 
7
26
  Use activerecord-import 0.4.0 or higher.
@@ -1,4 +1,4 @@
1
1
  platforms :ruby do
2
- gem 'mysql', '~> 2.8.1'
2
+ gem 'mysql', '>= 2.8.1'
3
3
  gem 'activerecord', '~> 3.1.0'
4
4
  end
@@ -1,4 +1,4 @@
1
1
  platforms :ruby do
2
- gem 'mysql', '~> 2.8.1'
2
+ gem 'mysql', '>= 2.8.1'
3
3
  gem 'activerecord', '~> 3.2.0'
4
4
  end
@@ -4,4 +4,3 @@ require "activerecord-import/adapters/postgresql_adapter"
4
4
  class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
5
5
  include ActiveRecord::Import::PostgreSQLAdapter
6
6
  end
7
-
@@ -16,7 +16,7 @@ module ActiveRecord::Import::AbstractAdapter
16
16
  sql2insert = base_sql + values.join( ',' ) + post_sql
17
17
  insert( sql2insert, *args )
18
18
 
19
- number_of_inserts
19
+ [number_of_inserts,[]]
20
20
  end
21
21
 
22
22
  def pre_sql_statements(options)
@@ -5,7 +5,7 @@ module ActiveRecord::Import::MysqlAdapter
5
5
  NO_MAX_PACKET = 0
6
6
  QUERY_OVERHEAD = 8 #This was shown to be true for MySQL, but it's not clear where the overhead is from.
7
7
 
8
- # +sql+ can be a single string or an array. If it is an array all
8
+ # +sql+ can be a single string or an array. If it is an array all
9
9
  # elements that are in position >= 1 will be appended to the final SQL.
10
10
  def insert_many( sql, values, *args ) # :nodoc:
11
11
  # the number of inserts default
@@ -46,7 +46,7 @@ module ActiveRecord::Import::MysqlAdapter
46
46
  end
47
47
  end
48
48
 
49
- number_of_inserts
49
+ [number_of_inserts,[]]
50
50
  end
51
51
 
52
52
  # Returns the maximum number of bytes that the server will allow
@@ -1,7 +1,34 @@
1
1
  module ActiveRecord::Import::PostgreSQLAdapter
2
2
  include ActiveRecord::Import::ImportSupport
3
3
 
4
+ def insert_many( sql, values, *args ) # :nodoc:
5
+ number_of_inserts = 1
6
+
7
+ base_sql,post_sql = if sql.is_a?( String )
8
+ [ sql, '' ]
9
+ elsif sql.is_a?( Array )
10
+ [ sql.shift, sql.join( ' ' ) ]
11
+ end
12
+
13
+ sql2insert = base_sql + values.join( ',' ) + post_sql
14
+ ids = select_values( sql2insert, *args )
15
+
16
+ [number_of_inserts,ids]
17
+ end
18
+
4
19
  def next_value_for_sequence(sequence_name)
5
20
  %{nextval('#{sequence_name}')}
6
21
  end
22
+
23
+ def post_sql_statements( table_name, options ) # :nodoc:
24
+ unless options[:primary_key].blank?
25
+ super(table_name, options) << (" RETURNING #{options[:primary_key]}")
26
+ else
27
+ super(table_name, options)
28
+ end
29
+ end
30
+
31
+ def support_setting_primary_key_of_imported_objects?
32
+ true
33
+ end
7
34
  end
@@ -34,7 +34,7 @@ module ActiveRecord::Import::SQLite3Adapter
34
34
  insert( sql2insert, *args )
35
35
  end
36
36
 
37
- number_of_inserts
37
+ [number_of_inserts,[]]
38
38
  end
39
39
 
40
40
  def next_value_for_sequence(sequence_name)
@@ -3,7 +3,7 @@ require "ostruct"
3
3
  module ActiveRecord::Import::ConnectionAdapters ; end
4
4
 
5
5
  module ActiveRecord::Import #:nodoc:
6
- class Result < Struct.new(:failed_instances, :num_inserts)
6
+ class Result < Struct.new(:failed_instances, :num_inserts, :ids)
7
7
  end
8
8
 
9
9
  module ImportSupport #:nodoc:
@@ -68,7 +68,7 @@ class ActiveRecord::Associations::CollectionAssociation
68
68
 
69
69
  # supports empty array
70
70
  elsif args.last.is_a?( Array ) and args.last.empty?
71
- return ActiveRecord::Import::Result.new([], 0) if args.last.empty?
71
+ return ActiveRecord::Import::Result.new([], 0, []) if args.last.empty?
72
72
 
73
73
  # supports 2-element array and array
74
74
  elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
@@ -109,18 +109,21 @@ class ActiveRecord::Base
109
109
  # Returns true if the current database connection adapter
110
110
  # supports import functionality, otherwise returns false.
111
111
  def supports_import?(*args)
112
- connection.supports_import?(*args)
113
- rescue NoMethodError
114
- false
112
+ connection.respond_to?(:supports_import?) && connection.supports_import?(*args)
115
113
  end
116
114
 
117
115
  # Returns true if the current database connection adapter
118
116
  # supports on duplicate key update functionality, otherwise
119
117
  # returns false.
120
118
  def supports_on_duplicate_key_update?
121
- connection.supports_on_duplicate_key_update?
122
- rescue NoMethodError
123
- false
119
+ connection.respond_to?(:supports_on_duplicate_key_update?) && connection.supports_on_duplicate_key_update?
120
+ end
121
+
122
+ # returns true if the current database connection adapter
123
+ # supports setting the primary key of bulk imported models, otherwise
124
+ # returns false
125
+ def support_setting_primary_key_of_imported_objects?
126
+ connection.respond_to?(:support_setting_primary_key_of_imported_objects?) && connection.support_setting_primary_key_of_imported_objects?
124
127
  end
125
128
 
126
129
  # Imports a collection of values to the database.
@@ -172,6 +175,9 @@ class ActiveRecord::Base
172
175
  # existing model instances in memory with updates from the import.
173
176
  # * +timestamps+ - true|false, tells import to not add timestamps \
174
177
  # (if false) even if record timestamps is disabled in ActiveRecord::Base
178
+ # * +recursive - true|false, tells import to import all autosave association
179
+ # if the adapter supports setting the primary keys of the newly imported
180
+ # objects.
175
181
  #
176
182
  # == Examples
177
183
  # class BlogPost < ActiveRecord::Base ; end
@@ -230,11 +236,24 @@ class ActiveRecord::Base
230
236
  # This returns an object which responds to +failed_instances+ and +num_inserts+.
231
237
  # * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
232
238
  # * num_inserts - the number of insert statements it took to import the data
233
- def import( *args )
234
- options = { :validate=>true, :timestamps=>true }
239
+ # * ids - the priamry keys of the imported ids, if the adpater supports it, otherwise and empty array.
240
+ def import(*args)
241
+ if args.first.is_a?( Array ) and args.first.first.is_a? ActiveRecord::Base
242
+ options = {}
243
+ options.merge!( args.pop ) if args.last.is_a?(Hash)
244
+
245
+ models = args.first
246
+ import_helper(models, options)
247
+ else
248
+ import_helper(*args)
249
+ end
250
+ end
251
+
252
+ def import_helper( *args )
253
+ options = { :validate=>true, :timestamps=>true, :primary_key=>primary_key }
235
254
  options.merge!( args.pop ) if args.last.is_a? Hash
236
255
 
237
- is_validating = options.delete( :validate )
256
+ is_validating = options[:validate]
238
257
  is_validating = true unless options[:validate_with_context].nil?
239
258
 
240
259
  # assume array of model objects
@@ -257,7 +276,7 @@ class ActiveRecord::Base
257
276
  end
258
277
  # supports empty array
259
278
  elsif args.last.is_a?( Array ) and args.last.empty?
260
- return ActiveRecord::Import::Result.new([], 0) if args.last.empty?
279
+ return ActiveRecord::Import::Result.new([], 0, []) if args.last.empty?
261
280
  # supports 2-element array and array
262
281
  elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
263
282
  column_names, array_of_attributes = args
@@ -284,16 +303,26 @@ class ActiveRecord::Base
284
303
  return_obj = if is_validating
285
304
  import_with_validations( column_names, array_of_attributes, options )
286
305
  else
287
- num_inserts = import_without_validations_or_callbacks( column_names, array_of_attributes, options )
288
- ActiveRecord::Import::Result.new([], num_inserts)
306
+ (num_inserts, ids) = import_without_validations_or_callbacks( column_names, array_of_attributes, options )
307
+ ActiveRecord::Import::Result.new([], num_inserts, ids)
289
308
  end
290
309
 
291
310
  if options[:synchronize]
292
311
  sync_keys = options[:synchronize_keys] || [self.primary_key]
293
312
  synchronize( options[:synchronize], sync_keys)
294
313
  end
295
-
296
314
  return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
315
+
316
+ # if we have ids, then set the id on the models and mark the models as clean.
317
+ if support_setting_primary_key_of_imported_objects?
318
+ set_ids_and_mark_clean(models, return_obj)
319
+
320
+ # if there are auto-save associations on the models we imported that are new, import them as well
321
+ if options[:recursive]
322
+ import_associations(models, options)
323
+ end
324
+ end
325
+
297
326
  return_obj
298
327
  end
299
328
 
@@ -327,12 +356,12 @@ class ActiveRecord::Base
327
356
  end
328
357
  array_of_attributes.compact!
329
358
 
330
- num_inserts = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any?
331
- 0
359
+ (num_inserts, ids) = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any?
360
+ [0,[]]
332
361
  else
333
362
  import_without_validations_or_callbacks( column_names, array_of_attributes, options )
334
363
  end
335
- ActiveRecord::Import::Result.new(failed_instances, num_inserts)
364
+ ActiveRecord::Import::Result.new(failed_instances, num_inserts, ids)
336
365
  end
337
366
 
338
367
  # Imports the passed in +column_names+ and +array_of_attributes+
@@ -364,6 +393,7 @@ class ActiveRecord::Base
364
393
  columns_sql = "(#{column_names.map{|name| connection.quote_column_name(name) }.join(',')})"
365
394
  insert_sql = "INSERT #{options[:ignore] ? 'IGNORE ':''}INTO #{quoted_table_name} #{columns_sql} VALUES "
366
395
  values_sql = values_sql_for_columns_and_attributes(columns, array_of_attributes)
396
+ ids = []
367
397
  if not supports_import?
368
398
  number_inserted = 0
369
399
  values_sql.each do |values|
@@ -375,15 +405,60 @@ class ActiveRecord::Base
375
405
  post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
376
406
 
377
407
  # perform the inserts
378
- number_inserted = connection.insert_many( [ insert_sql, post_sql_statements ].flatten,
408
+ (number_inserted,ids) = connection.insert_many( [ insert_sql, post_sql_statements ].flatten,
379
409
  values_sql,
380
410
  "#{self.class.name} Create Many Without Validations Or Callbacks" )
381
411
  end
382
- number_inserted
412
+ [number_inserted, ids]
383
413
  end
384
414
 
385
415
  private
386
416
 
417
+ def set_ids_and_mark_clean(models, import_result)
418
+ unless models.nil?
419
+ import_result.ids.each_with_index do |id, index|
420
+ models[index].id = id.to_i
421
+ models[index].instance_variable_get(:@changed_attributes).clear # mark the model as saved
422
+ end
423
+ end
424
+ end
425
+
426
+ def import_associations(models, options)
427
+ # now, for all the dirty associations, collect them into a new set of models, then recurse.
428
+ # notes:
429
+ # does not handle associations that reference themselves
430
+ # assumes that the only associations to be saved are marked with :autosave
431
+ # should probably take a hash to associations to follow.
432
+ associated_objects_by_class={}
433
+ models.each {|model| find_associated_objects_for_import(associated_objects_by_class, model) }
434
+
435
+ associated_objects_by_class.each_pair do |class_name, associations|
436
+ associations.each_pair do |association_name, associated_records|
437
+ associated_records.first.class.import(associated_records, options) unless associated_records.empty?
438
+ end
439
+ end
440
+ end
441
+
442
+ # We are eventually going to call Class.import <objects> so we build up a hash
443
+ # of class => objects to import.
444
+ def find_associated_objects_for_import(associated_objects_by_class, model)
445
+ associated_objects_by_class[model.class.name]||={}
446
+
447
+ model.class.reflect_on_all_autosave_associations.each do |association_reflection|
448
+ associated_objects_by_class[model.class.name][association_reflection.name]||=[]
449
+
450
+ association = model.association(association_reflection.name)
451
+ association.loaded!
452
+
453
+ changed_objects = association.select {|a| a.new_record? || a.changed?}
454
+ changed_objects.each do |child|
455
+ child.send("#{association_reflection.foreign_key}=", model.id)
456
+ end
457
+ associated_objects_by_class[model.class.name][association_reflection.name].concat changed_objects
458
+ end
459
+ associated_objects_by_class
460
+ end
461
+
387
462
  # Returns SQL the VALUES for an INSERT statement given the passed in +columns+
388
463
  # and +array_of_attributes+.
389
464
  def values_sql_for_columns_and_attributes(columns, array_of_attributes) # :nodoc:
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Import
3
- VERSION = "0.7.0"
3
+ VERSION = "0.8.0"
4
4
  end
5
5
  end
@@ -1,3 +1,5 @@
1
1
  class Book < ActiveRecord::Base
2
- belongs_to :topic
2
+ belongs_to :topic, :inverse_of=>:books
3
+ has_many :chapters, :autosave => true, :inverse_of => :book
4
+ has_many :end_notes, :autosave => true, :inverse_of => :book
3
5
  end
@@ -0,0 +1,4 @@
1
+ class Chapter < ActiveRecord::Base
2
+ belongs_to :book, :inverse_of=>:chapters
3
+ validates :title, :presence => true
4
+ end
@@ -0,0 +1,4 @@
1
+ class EndNote < ActiveRecord::Base
2
+ belongs_to :book, :inverse_of=>:end_notes
3
+ validates :note, :presence => true
4
+ end
@@ -2,7 +2,7 @@ class Topic < ActiveRecord::Base
2
2
  validates_presence_of :author_name
3
3
  validates :title, numericality: { only_integer: true }, on: :context_test
4
4
 
5
- has_many :books
5
+ has_many :books, :autosave=>true, :inverse_of=>:topic
6
6
  belongs_to :parent, :class_name => "Topic"
7
7
 
8
8
  composed_of :description, :mapping => [ %w(title title), %w(author_name author_name)], :allow_nil => true, :class_name => "TopicDescription"
@@ -1,4 +1,4 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
2
  require File.expand_path(File.dirname(__FILE__) + '/../support/postgresql/import_examples')
3
3
 
4
- should_support_postgresql_import_functionality
4
+ should_support_postgresql_import_functionality
@@ -66,6 +66,21 @@ ActiveRecord::Schema.define do
66
66
  t.column :for_sale, :boolean, :default => true
67
67
  end
68
68
 
69
+ create_table :chapters, :force => true do |t|
70
+ t.column :title, :string
71
+ t.column :book_id, :integer, :null => false
72
+ t.column :created_at, :datetime
73
+ t.column :updated_at, :datetime
74
+ end
75
+
76
+ create_table :end_notes, :force => true do |t|
77
+ t.column :note, :string
78
+ t.column :book_id, :integer, :null => false
79
+ t.column :created_at, :datetime
80
+ t.column :updated_at, :datetime
81
+ end
82
+
83
+
69
84
  create_table :languages, :force=>true do |t|
70
85
  t.column :name, :string
71
86
  t.column :developer_id, :integer
@@ -1,4 +1,8 @@
1
1
  FactoryGirl.define do
2
+ sequence(:book_title) {|n| "Book #{n}"}
3
+ sequence(:chapter_title) {|n| "Chapter #{n}"}
4
+ sequence(:end_note) {|n| "Endnote #{n}"}
5
+
2
6
  factory :group do
3
7
  sequence(:order) { |n| "Order #{n}" }
4
8
  end
@@ -16,4 +20,20 @@ FactoryGirl.define do
16
20
  factory :widget do
17
21
  sequence(:w_id){ |n| n}
18
22
  end
23
+
24
+ factory :topic_with_book, :parent=>:topic do |m|
25
+ after(:build) do |topic|
26
+ 2.times do
27
+ book = topic.books.build(:title=>FactoryGirl.generate(:book_title), :author_name=>'Stephen King')
28
+ 3.times do
29
+ book.chapters.build(:title => FactoryGirl.generate(:chapter_title))
30
+ end
31
+
32
+ 4.times do
33
+ book.end_notes.build(:note => FactoryGirl.generate(:end_note))
34
+ end
35
+ end
36
+ end
37
+ end
38
+
19
39
  end
@@ -17,5 +17,89 @@ def should_support_postgresql_import_functionality
17
17
  assert_equal 1, result.num_inserts
18
18
  end
19
19
  end
20
+
21
+ describe "importing objects with associations" do
22
+
23
+ let(:new_topics) { Build(num_topics, :topic_with_book) }
24
+ let(:new_topics_with_invalid_chapter) {
25
+ chapter = new_topics.first.books.first.chapters.first
26
+ chapter.title = nil
27
+ new_topics
28
+ }
29
+ let(:num_topics) {3}
30
+ let(:num_books) {6}
31
+ let(:num_chapters) {18}
32
+ let(:num_endnotes) {24}
33
+
34
+ it 'imports top level' do
35
+ assert_difference "Topic.count", +num_topics do
36
+ Topic.import new_topics, :recursive => true
37
+ new_topics.each do |topic|
38
+ assert_not_nil topic.id
39
+ end
40
+ end
41
+ end
42
+
43
+ it 'imports first level associations' do
44
+ assert_difference "Book.count", +num_books do
45
+ Topic.import new_topics, :recursive => true
46
+ new_topics.each do |topic|
47
+ topic.books.each do |book|
48
+ assert_equal topic.id, book.topic_id
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ [{:recursive => false}, {}].each do |import_options|
55
+ it "skips recursion for #{import_options.to_s}" do
56
+ assert_difference "Book.count", 0 do
57
+ Topic.import new_topics, import_options
58
+ end
59
+ end
60
+ end
61
+
62
+ it 'imports deeper nested associations' do
63
+ assert_difference "Chapter.count", +num_chapters do
64
+ assert_difference "EndNote.count", +num_endnotes do
65
+ Topic.import new_topics, :recursive => true
66
+ new_topics.each do |topic|
67
+ topic.books.each do |book|
68
+ book.chapters.each do |chapter|
69
+ assert_equal book.id, chapter.book_id
70
+ end
71
+ book.end_notes.each do |endnote|
72
+ assert_equal book.id, endnote.book_id
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ it "skips validation of the associations if requested" do
81
+ assert_difference "Chapter.count", +num_chapters do
82
+ Topic.import new_topics_with_invalid_chapter, :validate => false, :recursive => true
83
+ end
84
+ end
85
+
86
+ # These models dont validate associated. So we expect that books and topics get inserted, but not chapters
87
+ # Putting a transaction around everything wouldn't work, so if you want your chapters to prevent topics from
88
+ # being created, you would need to have validates_associated in your models and insert with validation
89
+ describe "all_or_none" do
90
+ [Book, Topic, EndNote].each do |type|
91
+ it "creates #{type.to_s}" do
92
+ assert_difference "#{type.to_s}.count", send("num_#{type.to_s.downcase}s") do
93
+ Topic.import new_topics_with_invalid_chapter, :all_or_none => true, :recursive => true
94
+ end
95
+ end
96
+ end
97
+ it "doesn't create chapters" do
98
+ assert_difference "Chapter.count", 0 do
99
+ Topic.import new_topics_with_invalid_chapter, :all_or_none => true, :recursive => true
100
+ end
101
+ end
102
+ end
103
+ end
20
104
  end
21
105
  end
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: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zach Dennis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-12-22 00:00:00.000000000 Z
11
+ date: 2015-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -116,6 +116,8 @@ files:
116
116
  - test/jdbcmysql/import_test.rb
117
117
  - test/jdbcpostgresql/import_test.rb
118
118
  - test/models/book.rb
119
+ - test/models/chapter.rb
120
+ - test/models/end_note.rb
119
121
  - test/models/group.rb
120
122
  - test/models/topic.rb
121
123
  - test/models/widget.rb
@@ -184,6 +186,8 @@ test_files:
184
186
  - test/jdbcmysql/import_test.rb
185
187
  - test/jdbcpostgresql/import_test.rb
186
188
  - test/models/book.rb
189
+ - test/models/chapter.rb
190
+ - test/models/end_note.rb
187
191
  - test/models/group.rb
188
192
  - test/models/topic.rb
189
193
  - test/models/widget.rb
@@ -209,3 +213,4 @@ test_files:
209
213
  - test/travis/database.yml
210
214
  - test/value_sets_bytes_parser_test.rb
211
215
  - test/value_sets_records_parser_test.rb
216
+ has_rdoc: