activerecord-import 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  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: