ocean-dynamo 0.5.0 → 0.5.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.
@@ -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