vorpal 0.0.6.rc2 → 0.0.6.rc3

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.
@@ -7,11 +7,15 @@ module Configuration
7
7
 
8
8
  # Configures and creates a {Vorpal::AggregateRepository} instance.
9
9
  #
10
+ # @param options [Hash] Global configuration options for the repository instance.
11
+ # @option options [Object] :db_driver (Object that will be used to interact with the DB.)
12
+ # Must be duck-type compatible with Vorpal::DbDriver.
13
+ #
10
14
  # @return [Vorpal::AggregateRepository] Repository instance.
11
- def define(&block)
12
- @class_configs = []
13
- self.instance_exec(&block)
14
- AggregateRepository.new(@class_configs)
15
+ def define(options={}, &block)
16
+ master_config = build_config(&block)
17
+ db_driver = options.fetch(:db_driver, DbDriver.new)
18
+ AggregateRepository.new(db_driver, master_config)
15
19
  end
16
20
 
17
21
  # Maps a domain class to a relational table.
@@ -35,5 +39,12 @@ module Configuration
35
39
 
36
40
  @class_configs << builder.build
37
41
  end
42
+
43
+ # @private
44
+ def build_config(&block)
45
+ @class_configs = []
46
+ self.instance_exec(&block)
47
+ MasterConfig.new(@class_configs)
48
+ end
38
49
  end
39
50
  end
@@ -1,50 +1,48 @@
1
1
  module Vorpal
2
- module DbDriver
3
- extend self
2
+ class DbDriver
3
+ def insert(db_class, db_objects)
4
+ if defined? ActiveRecord::Import
5
+ db_class.import(db_objects, validate: false)
6
+ else
7
+ db_objects.each do |db_object|
8
+ db_object.save!(validate: false)
9
+ end
10
+ end
11
+ end
4
12
 
5
- def insert(config, db_objects)
6
- if defined? ActiveRecord::Import
7
- config.db_class.import db_objects
8
- else
13
+ def update(db_class, db_objects)
9
14
  db_objects.each do |db_object|
10
- db_object.save!
15
+ db_object.save!(validate: false)
11
16
  end
12
17
  end
13
- end
14
18
 
15
- def update(config, db_objects)
16
- db_objects.each do |db_object|
17
- db_object.save!
19
+ def destroy(db_class, db_objects)
20
+ db_class.delete_all(id: db_objects.map(&:id))
18
21
  end
19
- end
20
22
 
21
- def destroy(config, db_objects)
22
- config.db_class.delete_all(id: db_objects.map(&:id))
23
- end
24
-
25
- def load_by_id(config, ids)
26
- config.db_class.where(id: ids)
27
- end
23
+ def load_by_id(db_class, ids)
24
+ db_class.where(id: ids)
25
+ end
28
26
 
29
- def load_by_foreign_key(config, id, foreign_key_info)
30
- arel = config.db_class.where(foreign_key_info.fk_column => id)
31
- arel = arel.where(foreign_key_info.fk_type_column => foreign_key_info.fk_type) if foreign_key_info.polymorphic?
32
- arel.order(:id).all
33
- end
27
+ def load_by_foreign_key(db_class, id, foreign_key_info)
28
+ arel = db_class.where(foreign_key_info.fk_column => id)
29
+ arel = arel.where(foreign_key_info.fk_type_column => foreign_key_info.fk_type) if foreign_key_info.polymorphic?
30
+ arel.order(:id).all
31
+ end
34
32
 
35
- def get_primary_keys(config, count)
36
- result = execute("select nextval('#{sequence_name(config)}') from generate_series(1,#{count});")
37
- result.column_values(0).map(&:to_i)
38
- end
33
+ def get_primary_keys(db_class, count)
34
+ result = execute("select nextval('#{sequence_name(db_class)}') from generate_series(1,#{count});")
35
+ result.column_values(0).map(&:to_i)
36
+ end
39
37
 
40
- private
38
+ private
41
39
 
42
- def execute(sql)
43
- ActiveRecord::Base.connection.execute(sql)
44
- end
40
+ def execute(sql)
41
+ ActiveRecord::Base.connection.execute(sql)
42
+ end
45
43
 
46
- def sequence_name(config)
47
- "#{config.table_name}_id_seq"
44
+ def sequence_name(db_class)
45
+ "#{db_class.table_name}_id_seq"
46
+ end
48
47
  end
49
- end
50
48
  end
@@ -4,11 +4,14 @@ require 'vorpal/db_driver'
4
4
 
5
5
  module Vorpal
6
6
 
7
+ # Handles loading of objects from the database.
8
+ #
7
9
  # @private
8
10
  class DbLoader
9
- def initialize(configs, only_owned)
11
+ def initialize(configs, only_owned, driver)
10
12
  @configs = configs
11
13
  @only_owned = only_owned
14
+ @driver = driver
12
15
  end
13
16
 
14
17
  def load_from_db(ids, domain_class)
@@ -19,7 +22,7 @@ class DbLoader
19
22
 
20
23
  until @lookup_instructions.empty?
21
24
  lookup = @lookup_instructions.next_lookup
22
- new_objects = lookup.load_all
25
+ new_objects = lookup.load_all(@driver)
23
26
  @loaded_objects.add(lookup.config, new_objects)
24
27
  explore_objects(new_objects)
25
28
  end
@@ -31,7 +34,7 @@ class DbLoader
31
34
 
32
35
  def explore_objects(objects_to_explore)
33
36
  objects_to_explore.each do |db_object|
34
- config = @configs.config_for_db(db_object.class)
37
+ config = @configs.config_for_db_object(db_object)
35
38
  config.has_manys.each do |has_many_config|
36
39
  lookup_by_fk(db_object, has_many_config) if explore_association?(has_many_config)
37
40
  end
@@ -57,11 +60,10 @@ class DbLoader
57
60
  @lookup_instructions.lookup_by_id(child_config, id)
58
61
  end
59
62
 
60
- def lookup_by_fk(db_object, has_many_config)
61
- child_config = has_many_config.child_config
62
- fk_info = has_many_config.foreign_key_info
63
+ def lookup_by_fk(db_object, has_some_config)
64
+ child_config = has_some_config.child_config
65
+ fk_info = has_some_config.foreign_key_info
63
66
  fk_value = db_object.id
64
- return if @loaded_objects.fk_lookup_done?(child_config, fk_info, fk_value)
65
67
  @lookup_instructions.lookup_by_fk(child_config, fk_info, fk_value)
66
68
  end
67
69
  end
@@ -84,19 +86,29 @@ class LookupInstructions
84
86
 
85
87
  def next_lookup
86
88
  if @lookup_by_id.empty?
87
- config, fk_info = @lookup_by_fk.first.first
88
- fk_values = @lookup_by_fk.delete([config, fk_info])
89
- LookupByFk.new(config, fk_info, fk_values)
89
+ pop_fk_lookup
90
90
  else
91
- config = @lookup_by_id.first.first
92
- ids = @lookup_by_id.delete(config)
93
- LookupById.new(config, ids)
91
+ pop_id_lookup
94
92
  end
95
93
  end
96
94
 
97
95
  def empty?
98
96
  @lookup_by_id.empty? && @lookup_by_fk.empty?
99
97
  end
98
+
99
+ private
100
+
101
+ def pop_id_lookup
102
+ config, ids = pop(@lookup_by_id)
103
+ LookupById.new(config, ids)
104
+ end
105
+
106
+ def pop_fk_lookup
107
+ key, fk_values = pop(@lookup_by_fk)
108
+ config = key.first
109
+ fk_info = key.last
110
+ LookupByFk.new(config, fk_info, fk_values)
111
+ end
100
112
  end
101
113
 
102
114
  # @private
@@ -107,9 +119,9 @@ class LookupById
107
119
  @ids = ids
108
120
  end
109
121
 
110
- def load_all
122
+ def load_all(driver)
111
123
  return [] if @ids.empty?
112
- DbDriver.load_by_id(@config, @ids)
124
+ driver.load_by_id(@config.db_class, @ids)
113
125
  end
114
126
  end
115
127
 
@@ -122,9 +134,9 @@ class LookupByFk
122
134
  @fk_values = fk_values
123
135
  end
124
136
 
125
- def load_all
137
+ def load_all(driver)
126
138
  return [] if @fk_values.empty?
127
- DbDriver.load_by_foreign_key(@config, @fk_values, @fk_info)
139
+ driver.load_by_foreign_key(@config.db_class, @fk_values, @fk_info)
128
140
  end
129
141
  end
130
142
 
@@ -23,10 +23,6 @@ class IdentityMap
23
23
  key_objects.map { |k| @entities[key(k)] }
24
24
  end
25
25
 
26
- def map_raw(key_ids, key_class)
27
- key_ids.map { |key_id| @entities[[key_id, key_class.name]] }
28
- end
29
-
30
26
  private
31
27
 
32
28
  def key(key_object)
@@ -14,40 +14,27 @@ class LoadedObjects
14
14
 
15
15
  def initialize
16
16
  @objects = Hash.new([])
17
+ @objects_by_id = Hash.new
17
18
  end
18
19
 
19
20
  def add(config, objects)
20
21
  add_to_hash(@objects, config, objects)
21
- end
22
22
 
23
- def find_by_id(object, config)
24
- @objects[config].detect { |obj| obj.id == object.id }
23
+ objects.each do |object|
24
+ @objects_by_id[[config.domain_class.name, object.id]] = object
25
+ end
25
26
  end
26
27
 
27
- def loaded_ids(config)
28
- @objects[config].map(&:id)
29
- end
30
-
31
- def loaded_fk_values(config, fk_info)
32
- if fk_info.polymorphic?
33
- @objects[config].
34
- find_all { |db_object| fk_info.matches_polymorphic_type?(db_object) }.
35
- map(&(fk_info.fk_column.to_sym))
36
- else
37
- @objects[config].map(&(fk_info.fk_column.to_sym))
38
- end
28
+ def find_by_id(config, id)
29
+ @objects_by_id[[config.domain_class.name, id]]
39
30
  end
40
31
 
41
32
  def all_objects
42
- @objects.values.flatten
33
+ @objects_by_id.values
43
34
  end
44
35
 
45
36
  def id_lookup_done?(config, id)
46
- loaded_ids(config).include?(id)
47
- end
48
-
49
- def fk_lookup_done?(config, fk_info, fk_value)
50
- loaded_fk_values(config, fk_info).include?(fk_value)
37
+ !find_by_id(config, id).nil?
51
38
  end
52
39
  end
53
40
  end
@@ -2,11 +2,17 @@ module Vorpal
2
2
 
3
3
  # @private
4
4
  module ArrayHash
5
- def add_to_hash(h, key, values)
6
- if h[key].nil? || h[key].empty?
7
- h[key] = []
5
+ def add_to_hash(array_hash, key, values)
6
+ if array_hash[key].nil? || array_hash[key].empty?
7
+ array_hash[key] = []
8
8
  end
9
- h[key].concat(Array(values))
9
+ array_hash[key].concat(Array(values))
10
+ end
11
+
12
+ def pop(array_hash)
13
+ key = array_hash.first.first
14
+ values = array_hash.delete(key)
15
+ [key, values]
10
16
  end
11
17
  end
12
18
 
@@ -1,3 +1,3 @@
1
1
  module Vorpal
2
- VERSION = "0.0.6.rc2"
2
+ VERSION = "0.0.6.rc3"
3
3
  end
@@ -0,0 +1,18 @@
1
+ module DbHelpers
2
+ # when you change a table's columns, set force to true to re-generate the table in the DB
3
+ def define_table(table_name, columns, force)
4
+ if !ActiveRecord::Base.connection.table_exists?(table_name) || force
5
+ ActiveRecord::Base.connection.create_table(table_name, force: true) do |t|
6
+ columns.each do |name, type|
7
+ t.send(type, name)
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ def defineAr(table_name)
14
+ Class.new(ActiveRecord::Base) do
15
+ self.table_name = table_name
16
+ end
17
+ end
18
+ end
@@ -12,7 +12,11 @@ ActiveRecord::Base.establish_connection(
12
12
  min_messages: 'error'
13
13
  )
14
14
 
15
+ require 'helpers/db_helpers'
16
+
15
17
  RSpec.configure do |config|
18
+ config.include DbHelpers
19
+
16
20
  # implements `use_transactional_fixtures = true`
17
21
  # from lib/active_record/fixtures.rb
18
22
  # works with Rails 3.2. Probably not with Rails 4
@@ -104,11 +104,12 @@ describe 'Aggregate Repository' do
104
104
 
105
105
  describe 'on error' do
106
106
  it 'nils ids of new objects' do
107
- test_repository = configure
107
+ db_driver = Vorpal::DbDriver.new
108
+ test_repository = configure(db_driver: db_driver)
108
109
 
109
110
  tree_db = TreeDB.create!
110
111
 
111
- expect(Vorpal::DbDriver).to receive(:update).and_raise('not so good')
112
+ expect(db_driver).to receive(:update).and_raise('not so good')
112
113
 
113
114
  fissure = Fissure.new
114
115
  tree = Tree.new(id: tree_db.id, fissures: [fissure])
@@ -580,72 +581,6 @@ describe 'Aggregate Repository' do
580
581
  end
581
582
  end
582
583
 
583
- describe 'lots of data' do
584
- it 'avoids N+1s on load' do
585
- test_repository = Vorpal.define do
586
- map Tree do
587
- fields :name
588
- belongs_to :trunk
589
- # has_many :fissures
590
- has_many :branches
591
- end
592
-
593
- map Trunk do
594
- fields :length
595
- has_one :tree
596
- has_many :bugs, fk: :lives_on_id, fk_type: :lives_on_type
597
- end
598
-
599
- map Branch do
600
- fields :length
601
- belongs_to :tree
602
- has_many :bugs, fk: :lives_on_id, fk_type: :lives_on_type
603
- has_many :branches
604
- end
605
-
606
- map Bug do
607
- fields :name
608
- belongs_to :lives_on, fk: :lives_on_id, fk_type: :lives_on_type, child_classes: [Trunk, Branch]
609
- end
610
- # map Fissure, to: Fissure
611
- end
612
-
613
- ids = (1..3).map do
614
- trunk_db = TrunkDB.create!
615
- tree_db = TreeDB.create!(trunk_id: trunk_db.id)
616
- branch_db1 = BranchDB.create!(tree_id: tree_db.id)
617
- branch_db2 = BranchDB.create!(tree_id: tree_db.id)
618
- branch_db3 = BranchDB.create!(branch_id: branch_db2.id)
619
- BugDB.create!(name: 'trunk bug', lives_on_id: trunk_db.id, lives_on_type: Trunk.name)
620
- BugDB.create!(name: 'branch bug!', lives_on_id: branch_db1.id, lives_on_type: Branch.name)
621
- tree_db.id
622
- end
623
-
624
- puts '*************************'
625
- puts '*************************'
626
- puts '*************************'
627
- puts '*************************'
628
- test_repository.load_all(ids, Tree)
629
- end
630
- end
631
-
632
- # when you change a table's columns, set force to true to re-generate the table in the DB
633
- def define_table(table_name, columns, force)
634
- if !ActiveRecord::Base.connection.table_exists?(table_name) || force
635
- ActiveRecord::Base.connection.create_table(table_name, force: true) do |t|
636
- columns.each do |name, type|
637
- t.send(type, name)
638
- end
639
- end
640
- end
641
- end
642
-
643
- def defineAr(table_name)
644
- Class.new(ActiveRecord::Base) do
645
- self.table_name = table_name
646
- end
647
- end
648
-
649
584
  private
650
585
 
651
586
  def configure_polymorphic_has_many
@@ -763,8 +698,8 @@ private
763
698
  end
764
699
  end
765
700
 
766
- def configure
767
- Vorpal.define do
701
+ def configure(options={})
702
+ Vorpal.define(options) do
768
703
  map Tree do
769
704
  fields :name
770
705
  belongs_to :trunk
@@ -0,0 +1,107 @@
1
+ require 'unit_spec_helper'
2
+ require 'vorpal/configs'
3
+
4
+ describe Vorpal::MasterConfig do
5
+ class Post
6
+ attr_accessor :comments
7
+ attr_accessor :best_comment
8
+ end
9
+
10
+ class Comment
11
+ attr_accessor :post
12
+ end
13
+
14
+ let(:post_config) { Vorpal::ClassConfig.new(domain_class: Post) }
15
+ let(:comment_config) { Vorpal::ClassConfig.new(domain_class: Comment) }
16
+ let(:post_has_many_comments_config) { Vorpal::HasManyConfig.new(name: 'comments', fk: 'post_id', child_class: Comment) }
17
+ let(:post_has_one_comment_config) { Vorpal::HasOneConfig.new(name: 'best_comment', fk: 'post_id', child_class: Comment) }
18
+ let(:comment_belongs_to_post_config) { Vorpal::BelongsToConfig.new(name: 'post', fk: 'post_id', child_classes: [Post]) }
19
+
20
+ describe 'local_association_configs' do
21
+ it 'builds an association_config for a belongs_to' do
22
+ comment_config.belongs_tos << comment_belongs_to_post_config
23
+
24
+ Vorpal::MasterConfig.new([post_config, comment_config])
25
+
26
+ expect(comment_config.local_association_configs.size).to eq(1)
27
+ expect(post_config.local_association_configs.size).to eq(0)
28
+ end
29
+
30
+ it 'sets the association end configs' do
31
+ comment_config.belongs_tos << comment_belongs_to_post_config
32
+ post_config.has_manys << post_has_many_comments_config
33
+
34
+ Vorpal::MasterConfig.new([post_config, comment_config])
35
+
36
+ association_config = comment_config.local_association_configs.first
37
+
38
+ expect(association_config.remote_end_config).to eq(post_has_many_comments_config)
39
+ expect(association_config.local_end_config).to eq(comment_belongs_to_post_config)
40
+ end
41
+
42
+ it 'builds an association_config for a has_many' do
43
+ post_config.has_manys << post_has_many_comments_config
44
+
45
+ Vorpal::MasterConfig.new([post_config, comment_config])
46
+
47
+ expect(comment_config.local_association_configs.size).to eq(1)
48
+ expect(post_config.local_association_configs.size).to eq(0)
49
+ end
50
+ end
51
+
52
+ describe Vorpal::AssociationConfig do
53
+ describe 'associate' do
54
+ let(:post) { Post.new }
55
+ let(:comment) { Comment.new }
56
+
57
+ it 'sets both ends of a one-to-one association' do
58
+ config = Vorpal::AssociationConfig.new(comment_config, 'post_id', nil)
59
+ config.add_remote_class_config(post_config)
60
+
61
+ config.local_end_config = comment_belongs_to_post_config
62
+ config.remote_end_config = post_has_one_comment_config
63
+
64
+ config.associate(comment, post)
65
+
66
+ expect(comment.post).to eq(post)
67
+ expect(post.best_comment).to eq(comment)
68
+ end
69
+
70
+ it 'sets both ends of a one-to-many association' do
71
+ config = Vorpal::AssociationConfig.new(comment_config, 'post_id', nil)
72
+ config.add_remote_class_config(post_config)
73
+
74
+ config.local_end_config = comment_belongs_to_post_config
75
+ config.remote_end_config = post_has_many_comments_config
76
+
77
+ config.associate(comment, post)
78
+
79
+ expect(comment.post).to eq(post)
80
+ expect(post.comments).to eq([comment])
81
+ end
82
+ end
83
+
84
+ describe 'remote_class_config' do
85
+ it 'works with non-polymorphic associations' do
86
+ config = Vorpal::AssociationConfig.new(comment_config, 'post_id', nil)
87
+ config.add_remote_class_config(post_config)
88
+
89
+ post = Post.new
90
+ class_config = config.remote_class_config(post)
91
+
92
+ expect(class_config).to eq(post_config)
93
+ end
94
+
95
+ it 'works with polymorphic associations' do
96
+ config = Vorpal::AssociationConfig.new(comment_config, 'commented_upon_id', 'commented_upon_type')
97
+ config.add_remote_class_config(post_config)
98
+ config.add_remote_class_config(comment_config)
99
+
100
+ comment = double('comment', commented_upon_type: 'Comment')
101
+ class_config = config.remote_class_config(comment)
102
+
103
+ expect(class_config).to eq(comment_config)
104
+ end
105
+ end
106
+ end
107
+ end