ocean-dynamo 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,11 +1,5 @@
1
- module OceanDynamo
2
- module Associations
3
- #
4
- # This entire file shamelessly lifted from ActiveRecord, for compatibility
5
- # reasons. OceanDynamo must implement the same query interface as ActiveRecord.
6
- #
7
-
8
-
1
+ module OceanDynamo #:nodoc:
2
+ module Associations #:nodoc:
9
3
  #
10
4
  # Association proxies in OceanDynamo are middlemen between the object that
11
5
  # holds the association, known as the <tt>@owner</tt>, and the actual associated
@@ -0,0 +1,57 @@
1
+ module ActiveRecord #:nodoc:
2
+ # = Active Record Has And Belongs To Many Association
3
+ module Associations #:nodoc:
4
+ class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
5
+ attr_reader :join_table
6
+
7
+ def initialize(owner, reflection)
8
+ @join_table = Arel::Table.new(reflection.join_table)
9
+ super
10
+ end
11
+
12
+ def insert_record(record, validate = true, raise = false)
13
+ if record.new_record?
14
+ if raise
15
+ record.save!(:validate => validate)
16
+ else
17
+ return unless record.save(:validate => validate)
18
+ end
19
+ end
20
+
21
+ stmt = join_table.compile_insert(
22
+ join_table[reflection.foreign_key] => own
23
+ er.id,
24
+ join_table[reflection.association_foreign_key] => record.id
25
+ )
26
+
27
+ owner.class.connection.insert stmt
28
+
29
+ record
30
+ end
31
+
32
+ private
33
+
34
+ def count_records
35
+ load_target.size
36
+ end
37
+
38
+ def delete_records(records, method)
39
+ relation = join_table
40
+ condition = relation[reflection.foreign_key].eq(owner.id)
41
+
42
+ unless records == :all
43
+ condition = condition.and(
44
+ relation[reflection.association_foreign_key]
45
+ .in(records.map { |x| x.id }.compact)
46
+ )
47
+ end
48
+
49
+ owner.class.connection.delete(relation.where(condition).compile_delete)
50
+ end
51
+
52
+ def invertible_for?(record)
53
+ false
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,132 @@
1
+ module ActiveRecord #:nodoc:
2
+ # = Active Record Has Many Association
3
+ module Associations #:nodoc:
4
+ # This is the proxy that handles a has many association.
5
+ #
6
+ # If the association has a <tt>:through</tt> option further specialization
7
+ # is provided by its child HasManyThroughAssociation.
8
+ class HasManyAssociation < CollectionAssociation #:nodoc:
9
+
10
+ def handle_dependency
11
+ case options[:dependent]
12
+ when :restrict_with_exception
13
+ raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
14
+
15
+ when :restrict_with_error
16
+ unless empty?
17
+ record = klass.human_attribute_name(reflection.name).downcase
18
+ owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record)
19
+ false
20
+ end
21
+
22
+ else
23
+ if options[:dependent] == :destroy
24
+ # No point in executing the counter update since we're going to destroy the parent anyway
25
+ load_target.each { |t| t.destroyed_by_association = reflection }
26
+ destroy_all
27
+ else
28
+ delete_all
29
+ end
30
+ end
31
+ end
32
+
33
+ def insert_record(record, validate = true, raise = false)
34
+ set_owner_attributes(record)
35
+
36
+ if raise
37
+ record.save!(:validate => validate)
38
+ else
39
+ record.save(:validate => validate)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # Returns the number of records in this collection.
46
+ #
47
+ # If the association has a counter cache it gets that value. Otherwise
48
+ # it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
49
+ # there's one. Some configuration options like :group make it impossible
50
+ # to do an SQL count, in those cases the array count will be used.
51
+ #
52
+ # That does not depend on whether the collection has already been loaded
53
+ # or not. The +size+ method is the one that takes the loaded flag into
54
+ # account and delegates to +count_records+ if needed.
55
+ #
56
+ # If the collection is empty the target is set to an empty array and
57
+ # the loaded flag is set to true as well.
58
+ def count_records
59
+ count = if has_cached_counter?
60
+ owner.send(:read_attribute, cached_counter_attribute_name)
61
+ else
62
+ scope.count
63
+ end
64
+
65
+ # If there's nothing in the database and @target has no new records
66
+ # we are certain the current target is an empty array. This is a
67
+ # documented side-effect of the method that may avoid an extra SELECT.
68
+ @target ||= [] and loaded! if count == 0
69
+
70
+ [association_scope.limit_value, count].compact.min
71
+ end
72
+
73
+ def has_cached_counter?(reflection = reflection)
74
+ owner.attribute_present?(cached_counter_attribute_name(reflection))
75
+ end
76
+
77
+ def cached_counter_attribute_name(reflection = reflection)
78
+ options[:counter_cache] || "#{reflection.name}_count"
79
+ end
80
+
81
+ def update_counter(difference, reflection = reflection)
82
+ if has_cached_counter?(reflection)
83
+ counter = cached_counter_attribute_name(reflection)
84
+ owner.class.update_counters(owner.id, counter => difference)
85
+ owner[counter] += difference
86
+ owner.changed_attributes.delete(counter) # eww
87
+ end
88
+ end
89
+
90
+ # This shit is nasty. We need to avoid the following situation:
91
+ #
92
+ # * An associated record is deleted via record.destroy
93
+ # * Hence the callbacks run, and they find a belongs_to on the record with a
94
+ # :counter_cache options which points back at our owner. So they update the
95
+ # counter cache.
96
+ # * In which case, we must make sure to *not* update the counter cache, or else
97
+ # it will be decremented twice.
98
+ #
99
+ # Hence this method.
100
+ def inverse_updates_counter_cache?(reflection = reflection)
101
+ counter_name = cached_counter_attribute_name(reflection)
102
+ reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
103
+ inverse_reflection.counter_cache_column == counter_name
104
+ }
105
+ end
106
+
107
+ # Deletes the records according to the <tt>:dependent</tt> option.
108
+ def delete_records(records, method)
109
+ if method == :destroy
110
+ records.each { |r| r.destroy }
111
+ update_counter(-records.length) unless inverse_updates_counter_cache?
112
+ else
113
+ if records == :all
114
+ scope = self.scope
115
+ else
116
+ scope = self.scope.where(reflection.klass.primary_key => records)
117
+ end
118
+
119
+ if method == :delete_all
120
+ update_counter(-scope.delete_all)
121
+ else
122
+ update_counter(-scope.update_all(reflection.foreign_key => nil))
123
+ end
124
+ end
125
+ end
126
+
127
+ def foreign_key_present?
128
+ owner.attribute_present?(reflection.association_primary_key)
129
+ end
130
+ end
131
+ end
132
+ end
@@ -1,4 +1,4 @@
1
- module ActiveRecord
1
+ module ActiveRecord #:nodoc:
2
2
  # = Active Record Reflection
3
3
  module Reflection # :nodoc:
4
4
  extend ActiveSupport::Concern
@@ -16,7 +16,7 @@ module ActiveRecord
16
16
  #
17
17
  # MacroReflection class has info for AggregateReflection and AssociationReflection
18
18
  # classes.
19
- module ClassMethods
19
+ module ClassMethods #:nodoc:
20
20
  def create_reflection(macro, name, scope, options, active_record)
21
21
  case macro
22
22
  when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
@@ -1,6 +1,6 @@
1
- module ActiveRecord
1
+ module ActiveRecord #:nodoc:
2
2
  # = Active Record Relation
3
- class Relation
3
+ class Relation #:nodoc:
4
4
  JoinOperation = Struct.new(:relation, :join_class, :on)
5
5
 
6
6
  MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
@@ -0,0 +1,158 @@
1
+ module OceanDynamo
2
+ module Associations
3
+ #
4
+ # This is the root class of all Associations.
5
+ # The class structure is exactly like in ActiveRecord:
6
+ #
7
+ # Association
8
+ # CollectionAssociation
9
+ # HasAndBelongsToManyAssociation
10
+ # HasManyAssociation
11
+ #
12
+ # It should be noted, however, that the ActiveRecord documentation
13
+ # is misleading: belongs_to and has_one no longer are implemented using
14
+ # proxies, even though the documentation and the source itself says it is.
15
+ # Furthermore, method_missing is no longer used at all, despite what the
16
+ # documentation and source comments say.
17
+ #
18
+ # In OceanDynamo, we have removed the unused classes and stripped away
19
+ # the SQL-specific features such as scopes. Neither do we implement counter
20
+ # caches. We have kept the same module and class structure for compatibility,
21
+ # though.
22
+ #
23
+ class Association #:nodoc:
24
+
25
+ attr_reader :owner
26
+ attr_reader :reflection
27
+ attr_reader :target
28
+
29
+
30
+ #
31
+ #
32
+ #
33
+ def initialize(owner, reflection)
34
+ @owner, @reflection = owner, reflection
35
+ reset
36
+ end
37
+
38
+ #
39
+ # Resets the \loaded flag to +false+ and sets the \target to +nil+.
40
+ #
41
+ def reset
42
+ @loaded = false
43
+ @target = nil
44
+ @stale_state = nil
45
+ end
46
+
47
+ #
48
+ # Has the \target been already \loaded?
49
+ #
50
+ def loaded?
51
+ @loaded
52
+ end
53
+
54
+ #
55
+ # Asserts the \target has been loaded setting the \loaded flag to +true+.
56
+ #
57
+ def loaded!
58
+ @loaded = true
59
+ @stale_state = stale_state
60
+ end
61
+
62
+ #
63
+ # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
64
+ #
65
+ def target=(target)
66
+ @target = target
67
+ loaded!
68
+ end
69
+
70
+ #
71
+ # The target is stale if the target no longer points to the record(s) that the
72
+ # relevant foreign_key(s) refers to. If stale, the association accessor method
73
+ # on the owner will reload the target. It's up to subclasses to implement the
74
+ # stale_state method if relevant.
75
+ #
76
+ # Note that if the target has not been loaded, it is not considered stale.
77
+ #
78
+ def stale_target?
79
+ loaded? && @stale_state != stale_state
80
+ end
81
+
82
+ #
83
+ # Loads the \target if needed and returns it.
84
+ #
85
+ # This method is abstract in the sense that it relies on +find_target+,
86
+ # which is expected to be provided by descendants.
87
+ #
88
+ # If the \target is already \loaded it is just returned. Thus, you can call
89
+ # +load_target+ unconditionally to get the \target.
90
+ #
91
+ # ActiveRecord::RecordNotFound is rescued within the method, and it is
92
+ # not reraised. The proxy is \reset and +nil+ is the return value.
93
+ #
94
+ def load_target
95
+ @target = find_target if (@stale_state && stale_target?) || find_target?
96
+ loaded! unless loaded?
97
+ target
98
+ rescue OceanDynamo::RecordNotFound
99
+ reset
100
+ end
101
+
102
+ #
103
+ # Returns the class of the target. belongs_to polymorphic used to override this
104
+ # to look at the polymorphic_type field on the owner. However, belongs_to is no
105
+ # longer implemented in AR using an Assocation, so we keep this only for structural
106
+ # compatibility.
107
+ #
108
+ def klass
109
+ reflection.klass
110
+ end
111
+
112
+ #
113
+ # Reloads the \target and returns +self+ on success.
114
+ #
115
+ def reload
116
+ reset
117
+ load_target
118
+ self unless target.nil?
119
+ end
120
+
121
+
122
+ private
123
+
124
+ def find_target?
125
+ !loaded? && (!owner.new_record? || foreign_key_present?) && klass
126
+ end
127
+
128
+ #
129
+ # Should be true if there is a foreign key present on the owner which
130
+ # references the target. This is used to determine whether we can load
131
+ # the target if the owner is currently a new record (and therefore
132
+ # without a key).
133
+ #
134
+ # Currently implemented by belongs_to (vanilla and polymorphic) and
135
+ # has_one/has_many :through associations which go through a belongs_to
136
+ #
137
+ # This method is, nowadays, merely an archaeological artifact since
138
+ # +belongs_to+ no longer uses Associations, meaning this method will
139
+ # never be overridden.
140
+ #
141
+ def foreign_key_present?
142
+ false
143
+ end
144
+
145
+ #
146
+ # This should be implemented to return the values of the relevant key(s) on the owner,
147
+ # so that when stale_state is different from the value stored on the last find_target,
148
+ # the target is stale.
149
+ #
150
+ # This is only relevant to certain associations, which is why it returns nil by default.
151
+ #
152
+ def stale_state
153
+ nil
154
+ end
155
+
156
+ end
157
+ end
158
+ end
@@ -4,6 +4,10 @@ module OceanDynamo
4
4
  def self.included(base)
5
5
  base.extend(ClassMethods)
6
6
  end
7
+
8
+ # This class really does some of the stuff that in ActiveRecord is
9
+ # handled by the Reflection class. We could switch to the AR paradigm
10
+ # at some point.
7
11
 
8
12
 
9
13
  # ---------------------------------------------------------
@@ -24,7 +24,7 @@ module OceanDynamo
24
24
  # end
25
25
  # has_many :topics, dependent: :destroy
26
26
  # end
27
- #
27
+ #
28
28
  # class Topic < OceanDynamo::Table
29
29
  # dynamo_schema(:uuid) do
30
30
  # attribute :title
@@ -32,14 +32,14 @@ module OceanDynamo
32
32
  # belongs_to :forum
33
33
  # has_many :posts, dependent: :destroy
34
34
  # end
35
- #
35
+ #
36
36
  # class Post < OceanDynamo::Table
37
37
  # dynamo_schema(:uuid) do
38
38
  # attribute :body
39
39
  # end
40
40
  # belongs_to :topic, composite_key: true
41
41
  # end
42
- #
42
+ #
43
43
  # The only non-standard aspect of the above is <tt>composite_key: true</tt>,
44
44
  # which is required as the Topic class itself has a +belongs_to+ relation and
45
45
  # thus has a composite key. This must be declared in the child class as it
@@ -57,6 +57,8 @@ module OceanDynamo
57
57
  target_class = class_name.constantize # Master
58
58
 
59
59
  assert_only_one_belongs_to!
60
+ assert_range_key_not_specified!
61
+ assert_hash_key_is_not_id!
60
62
 
61
63
  self.table_range_key = table_hash_key # The RANGE KEY is variable
62
64
  self.table_hash_key = target_attr_id.to_sym # The HASH KEY is the parent UUID
@@ -72,7 +74,6 @@ module OceanDynamo
72
74
  define_attribute_accessors(table_range_key) # define uuid, uuid=, uuid?
73
75
 
74
76
 
75
-
76
77
  # Define the parent id attribute
77
78
  attribute target_attr_id, :reference, default: nil, target_class: target_class,
78
79
  association: :belongs_to
@@ -101,6 +102,15 @@ module OceanDynamo
101
102
  @#{target_attr} = nil
102
103
  value
103
104
  end"
105
+
106
+ # Define parent builders
107
+ self.class_eval "def self.build_#{target_attr}(**opts)
108
+ #{target_class}.new(opts)
109
+ end"
110
+
111
+ self.class_eval "def self.create_#{target_attr}(**opts)
112
+ #{target_class}.create(opts)
113
+ end"
104
114
  end
105
115
 
106
116
 
@@ -144,6 +154,24 @@ module OceanDynamo
144
154
  end
145
155
  false
146
156
  end
157
+
158
+
159
+ #
160
+ # Make sure the range key isn't specified.
161
+ #
162
+ def assert_range_key_not_specified! # :nodoc:
163
+ raise RangeKeyMustNotBeSpecified,
164
+ "Tables with belongs_to relations may not specify the range key" if table_range_key
165
+ end
166
+
167
+
168
+ #
169
+ # Make sure the hash key isn't called :id.
170
+ #
171
+ def assert_hash_key_is_not_id! # :nodoc:
172
+ raise HashKeyMayNotBeNamedId,
173
+ "Tables with belongs_to relations may not name their hash key :id" if table_hash_key.to_s == "id"
174
+ end
147
175
  end
148
176
 
149
177