torque-postgresql 2.0.1 → 2.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/lib/torque/postgresql/adapter.rb +7 -0
  3. data/lib/torque/postgresql/adapter/database_statements.rb +2 -0
  4. data/lib/torque/postgresql/adapter/oid.rb +3 -1
  5. data/lib/torque/postgresql/adapter/oid/box.rb +2 -0
  6. data/lib/torque/postgresql/adapter/oid/circle.rb +2 -0
  7. data/lib/torque/postgresql/adapter/oid/enum.rb +2 -0
  8. data/lib/torque/postgresql/adapter/oid/enum_set.rb +2 -0
  9. data/lib/torque/postgresql/adapter/oid/interval.rb +2 -0
  10. data/lib/torque/postgresql/adapter/oid/line.rb +2 -0
  11. data/lib/torque/postgresql/adapter/oid/range.rb +2 -0
  12. data/lib/torque/postgresql/adapter/oid/segment.rb +2 -0
  13. data/lib/torque/postgresql/adapter/quoting.rb +2 -0
  14. data/lib/torque/postgresql/adapter/schema_creation.rb +8 -1
  15. data/lib/torque/postgresql/adapter/schema_definitions.rb +2 -0
  16. data/lib/torque/postgresql/adapter/schema_dumper.rb +11 -2
  17. data/lib/torque/postgresql/adapter/schema_statements.rb +2 -0
  18. data/lib/torque/postgresql/arel/infix_operation.rb +5 -1
  19. data/lib/torque/postgresql/arel/join_source.rb +2 -0
  20. data/lib/torque/postgresql/arel/nodes.rb +2 -0
  21. data/lib/torque/postgresql/arel/operations.rb +2 -0
  22. data/lib/torque/postgresql/arel/select_manager.rb +2 -0
  23. data/lib/torque/postgresql/arel/visitors.rb +6 -3
  24. data/lib/torque/postgresql/associations/association.rb +14 -3
  25. data/lib/torque/postgresql/associations/association_scope.rb +2 -0
  26. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +164 -47
  27. data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +8 -5
  28. data/lib/torque/postgresql/associations/builder/has_many.rb +2 -0
  29. data/lib/torque/postgresql/associations/preloader/association.rb +30 -1
  30. data/lib/torque/postgresql/attributes/builder.rb +3 -1
  31. data/lib/torque/postgresql/attributes/builder/enum.rb +5 -3
  32. data/lib/torque/postgresql/attributes/builder/period.rb +6 -4
  33. data/lib/torque/postgresql/attributes/enum.rb +5 -10
  34. data/lib/torque/postgresql/attributes/enum_set.rb +2 -0
  35. data/lib/torque/postgresql/attributes/lazy.rb +3 -1
  36. data/lib/torque/postgresql/attributes/period.rb +2 -0
  37. data/lib/torque/postgresql/autosave_association.rb +19 -16
  38. data/lib/torque/postgresql/auxiliary_statement.rb +2 -0
  39. data/lib/torque/postgresql/auxiliary_statement/settings.rb +2 -0
  40. data/lib/torque/postgresql/base.rb +5 -2
  41. data/lib/torque/postgresql/coder.rb +5 -3
  42. data/lib/torque/postgresql/collector.rb +2 -0
  43. data/lib/torque/postgresql/config.rb +5 -0
  44. data/lib/torque/postgresql/geometry_builder.rb +2 -0
  45. data/lib/torque/postgresql/i18n.rb +2 -0
  46. data/lib/torque/postgresql/inheritance.rb +2 -0
  47. data/lib/torque/postgresql/migration/command_recorder.rb +2 -0
  48. data/lib/torque/postgresql/railtie.rb +2 -0
  49. data/lib/torque/postgresql/reflection.rb +2 -0
  50. data/lib/torque/postgresql/reflection/abstract_reflection.rb +13 -28
  51. data/lib/torque/postgresql/reflection/association_reflection.rb +24 -0
  52. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +18 -4
  53. data/lib/torque/postgresql/reflection/has_many_reflection.rb +2 -0
  54. data/lib/torque/postgresql/reflection/runtime_reflection.rb +2 -0
  55. data/lib/torque/postgresql/reflection/through_reflection.rb +2 -0
  56. data/lib/torque/postgresql/relation.rb +15 -11
  57. data/lib/torque/postgresql/relation/auxiliary_statement.rb +6 -1
  58. data/lib/torque/postgresql/relation/distinct_on.rb +2 -0
  59. data/lib/torque/postgresql/relation/inheritance.rb +2 -0
  60. data/lib/torque/postgresql/relation/merger.rb +2 -0
  61. data/lib/torque/postgresql/schema_cache.rb +2 -0
  62. data/lib/torque/postgresql/version.rb +3 -1
  63. data/spec/schema.rb +3 -2
  64. data/spec/tests/arel_spec.rb +3 -1
  65. data/spec/tests/belongs_to_many_spec.rb +104 -12
  66. data/spec/tests/enum_set_spec.rb +1 -1
  67. data/spec/tests/has_many_spec.rb +25 -1
  68. data/spec/tests/table_inheritance_spec.rb +1 -1
  69. metadata +7 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4164165e64b027d11997dde286684b27bd77c33634e3eb84c6f1c757b5d814a0
4
- data.tar.gz: 8bf196d92b261661631ceb02f2ab7600a8a6cec5cacc43fa69b0c300afb65582
3
+ metadata.gz: bd314ab034c3d38a227829faf2d706b13625a5e4cc6393004f6dbe2890e784a8
4
+ data.tar.gz: 035b0fc92e1ea2aedb507c18094e66025c0fe98838f8553f3abc64764a0b2f03
5
5
  SHA512:
6
- metadata.gz: 7e44fd4215ba1ef9d2aebf8d6a5c285266f0bb64544a2a74d36773aaaa8e57c5776c3260183352accf76f81e94edcba27ffa898145e4d0bb467f5097ce7e7467
7
- data.tar.gz: 9c8acb15a0e78f33e8da372dcffc0e1a3e238631d01932056e5e48392801ec3706c3168a138944e5ebe7b80222f2f962e32af043a91c29235b6e824e02b70bbc
6
+ metadata.gz: 6eeaffde8209891fee13de7518acea135cfe5f35ecbc5aa59124faad208ed0545a24c2ffb993f10eb5e306c2ccb96036e1dbc812a11177ecdda5e3ead62e0dd7
7
+ data.tar.gz: ad55cf8b8a5e9de71575c12e8452d440439529258752ceceb95da17111991b8a9ad90101806ab328d5993d328bec69a71feb4cc6b85431c9802d0c225557dbb5
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'adapter/database_statements'
2
4
  require_relative 'adapter/oid'
3
5
  require_relative 'adapter/quoting'
@@ -19,6 +21,11 @@ module Torque
19
21
  select_value('SELECT version()').match(/#{Adapter::ADAPTER_NAME} ([\d\.]+)/)[1]
20
22
  )
21
23
  end
24
+
25
+ # Add `inherits` to the list of extracted table options
26
+ def extract_table_options!(options)
27
+ super.merge(options.extract!(:inherits))
28
+ end
22
29
  end
23
30
 
24
31
  ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend Adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Adapter
@@ -17,9 +17,11 @@ module Torque
17
17
  ActiveRecord::Type.register(:circle, OID::Circle, adapter: :postgresql)
18
18
  ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql)
19
19
  ActiveRecord::Type.register(:enum_set, OID::EnumSet, adapter: :postgresql)
20
- ActiveRecord::Type.register(:interval, OID::Interval, adapter: :postgresql)
21
20
  ActiveRecord::Type.register(:line, OID::Line, adapter: :postgresql)
22
21
  ActiveRecord::Type.register(:segment, OID::Segment, adapter: :postgresql)
22
+
23
+ ActiveRecord::Type.register(:interval, OID::Interval, adapter: :postgresql) \
24
+ unless PostgreSQL::AR610
23
25
  end
24
26
  end
25
27
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  class Box < Struct.new(:x1, :y1, :x2, :y2)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  class Circle < Struct.new(:x, :y, :r)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  class Line < Struct.new(:slope, :intercept)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  class Segment < Struct.new(:point0, :point1)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Adapter
@@ -24,10 +24,17 @@ module Torque
24
24
  end)
25
25
  end
26
26
 
27
+ if respond_to?(:supports_check_constraints?) && supports_check_constraints?
28
+ statements.concat(o.check_constraints.map do |expression, options|
29
+ check_constraint_in_create(o.name, expression, options)
30
+ end)
31
+ end
32
+
27
33
  create_sql << "(#{statements.join(', ')})" \
28
34
  if statements.present? || o.inherits.present?
29
35
 
30
- add_table_options!(create_sql, table_options(o))
36
+ options = PostgreSQL::AR610 ? o : table_options(o)
37
+ add_table_options!(create_sql, options)
31
38
 
32
39
  if o.inherits.present?
33
40
  tables = o.inherits.map(&method(:quote_table_name))
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Adapter
@@ -39,7 +41,7 @@ module Torque
39
41
 
40
42
  def tables(stream) # :nodoc:
41
43
  inherited_tables = @connection.inherited_tables
42
- sorted_tables = @connection.data_sources.sort - @connection.views
44
+ sorted_tables = @connection.tables.sort - @connection.views
43
45
 
44
46
  stream.puts " # These are the common tables managed"
45
47
  (sorted_tables - inherited_tables.keys).each do |table_name|
@@ -67,12 +69,19 @@ module Torque
67
69
  end
68
70
  end
69
71
 
70
- # dump foreign keys at the end to make sure all dependent tables exist.
72
+ # Dump foreign keys at the end to make sure all dependent tables exist.
71
73
  if @connection.supports_foreign_keys?
72
74
  sorted_tables.each do |tbl|
73
75
  foreign_keys(tbl, stream) unless ignored?(tbl)
74
76
  end
75
77
  end
78
+
79
+ # Scenic integration
80
+ views(stream) if defined?(::Scenic)
81
+
82
+ # FX integration
83
+ functions(stream) if defined?(::Fx::SchemaDumper::Function)
84
+ triggers(stream) if defined?(::Fx::SchemaDumper::Trigger)
76
85
  end
77
86
 
78
87
  # Dump user defined types like enum
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Adapter
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Arel
@@ -22,6 +24,8 @@ module Torque
22
24
  }.freeze
23
25
 
24
26
  INFLIX_OPERATION.each do |operator_name, operator|
27
+ next if nodes.const_defined?(operator_name)
28
+
25
29
  klass = Class.new(inflix)
26
30
  klass.send(:define_method, :initialize) { |*args| super(operator, *args) }
27
31
 
@@ -31,7 +35,7 @@ module Torque
31
35
  # Don't worry about quoting here, if the right side is something that
32
36
  # doesn't need quoting, it will leave it as it is
33
37
  Math.send(:define_method, operator_name.underscore) do |other|
34
- klass.new(self, nodes.build_quoted(other, self))
38
+ klass.new(self, other)
35
39
  end
36
40
  end
37
41
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Arel
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Arel
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Arel
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Arel
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Arel
4
6
  module Visitors
5
7
  # Enclose select manager with parenthesis
6
8
  # :TODO: Remove when checking the new version of Arel
7
- def visit_Arel_SelectManager o, collector
9
+ def visit_Arel_SelectManager(o, collector)
8
10
  collector << '('
9
11
  visit(o.ast, collector) << ')'
10
12
  end
@@ -23,8 +25,9 @@ module Torque
23
25
 
24
26
  # Allow quoted arrays to get here
25
27
  def visit_Arel_Nodes_Casted(o, collector)
26
- return super unless o.val.is_a?(::Enumerable)
27
- quote_array(o.val, collector)
28
+ value = o.respond_to?(:val) ? o.val : o.value
29
+ return super unless value.is_a?(::Enumerable)
30
+ quote_array(value, collector)
28
31
  end
29
32
 
30
33
  ## TORQUE VISITORS
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Associations
4
6
  module Association
5
7
 
8
+ # There is no problem of adding temporary items on target because
9
+ # CollectionProxy will handle memory and persisted relationship
6
10
  def inversed_from(record)
7
11
  return super unless reflection.connected_through_array?
8
12
 
@@ -11,20 +15,27 @@ module Torque
11
15
  @inversed = self.target.present?
12
16
  end
13
17
 
18
+ # The binds and the cache are getting mixed and caching the wrong query
19
+ def skip_statement_cache?(*)
20
+ super || reflection.connected_through_array?
21
+ end
22
+
14
23
  private
15
24
 
25
+ # This is mainly for the has many when connect through an array to add
26
+ # its id to the list of the inverse belongs to many association
16
27
  def set_owner_attributes(record)
17
28
  return super unless reflection.connected_through_array?
18
29
 
19
30
  add_id = owner[reflection.active_record_primary_key]
20
- record_fk = reflection.foreign_key
21
-
22
- record[record_fk].push(add_id) unless (record[record_fk] ||= []).include?(add_id)
31
+ list = record[reflection.foreign_key] ||= []
32
+ list.push(add_id) unless list.include?(add_id)
23
33
  end
24
34
 
25
35
  end
26
36
 
27
37
  ::ActiveRecord::Associations::Association.prepend(Association)
38
+ ::ActiveRecord::Associations::HasManyAssociation.prepend(Association)
28
39
  end
29
40
  end
30
41
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Torque
2
4
  module PostgreSQL
3
5
  module Associations
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record/associations/collection_association'
4
+
2
5
  # FIXME: build, create
3
6
  module Torque
4
7
  module PostgreSQL
@@ -6,10 +9,68 @@ module Torque
6
9
  class BelongsToManyAssociation < ::ActiveRecord::Associations::CollectionAssociation
7
10
  include ::ActiveRecord::Associations::ForeignAssociation
8
11
 
12
+ ## CUSTOM
13
+ def ids_reader
14
+ if loaded?
15
+ target.pluck(reflection.association_primary_key)
16
+ elsif !target.empty?
17
+ load_target.pluck(reflection.association_primary_key)
18
+ else
19
+ stale_state
20
+ end
21
+ end
22
+
23
+ def ids_writer(ids)
24
+ owner.write_attribute(source_attr, ids.presence)
25
+ return unless owner.persisted? && owner.attribute_changed?(source_attr)
26
+
27
+ owner.update_attribute(source_attr, ids.presence)
28
+ end
29
+
30
+ def size
31
+ if loaded?
32
+ target.size
33
+ elsif !target.empty?
34
+ unsaved_records = target.select(&:new_record?)
35
+ unsaved_records.size + stale_state.size
36
+ else
37
+ stale_state&.size || 0
38
+ end
39
+ end
40
+
41
+ def empty?
42
+ size.zero?
43
+ end
44
+
45
+ def include?(record)
46
+ return false unless record.is_a?(reflection.klass)
47
+ return include_in_memory?(record) if record.new_record?
48
+
49
+ (!target.empty? && target.include?(record)) ||
50
+ stale_state&.include?(record.read_attribute(klass_attr))
51
+ end
52
+
53
+ def load_target
54
+ if stale_target? || find_target?
55
+ @target = merge_target_lists(find_target, target)
56
+ end
57
+
58
+ loaded!
59
+ target
60
+ end
61
+
62
+ def build_changes
63
+ @_building_changes = true
64
+ yield.tap { ids_writer(ids_reader) }
65
+ ensure
66
+ @_building_changes = nil
67
+ end
68
+
69
+ ## HAS MANY
9
70
  def handle_dependency
10
71
  case options[:dependent]
11
72
  when :restrict_with_exception
12
- raise ::ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
73
+ raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
13
74
 
14
75
  when :restrict_with_error
15
76
  unless empty?
@@ -19,88 +80,122 @@ module Torque
19
80
  end
20
81
 
21
82
  when :destroy
22
- # No point in executing the counter update since we're going to destroy the parent anyway
23
83
  load_target.each { |t| t.destroyed_by_association = reflection }
24
84
  destroy_all
85
+ when :destroy_async
86
+ load_target.each do |t|
87
+ t.destroyed_by_association = reflection
88
+ end
89
+
90
+ unless target.empty?
91
+ association_class = target.first.class
92
+ primary_key_column = association_class.primary_key.to_sym
93
+
94
+ ids = target.collect do |assoc|
95
+ assoc.public_send(primary_key_column)
96
+ end
97
+
98
+ enqueue_destroy_association(
99
+ owner_model_name: owner.class.to_s,
100
+ owner_id: owner.id,
101
+ association_class: association_class.to_s,
102
+ association_ids: ids,
103
+ association_primary_key_column: primary_key_column,
104
+ ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
105
+ )
106
+ end
25
107
  else
26
108
  delete_all
27
109
  end
28
110
  end
29
111
 
30
- def ids_reader
31
- owner[reflection.active_record_primary_key]
32
- end
33
-
34
- def ids_writer(new_ids)
35
- column = reflection.active_record_primary_key
36
- command = owner.persisted? ? :update_column : :write_attribute
37
- owner.public_send(command, column, new_ids.presence)
38
- @association_scope = nil
39
- end
40
-
41
112
  def insert_record(record, *)
42
- super
43
-
44
- attribute = (ids_reader || owner[reflection.active_record_primary_key] = [])
45
- attribute.push(record[klass_fk])
46
- record
47
- end
48
-
49
- def empty?
50
- size.zero?
113
+ super.tap do |saved|
114
+ ids_rewriter(record.read_attribute(klass_attr), :<<) if saved
115
+ end
51
116
  end
52
117
 
53
- def include?(record)
54
- list = owner[reflection.active_record_primary_key]
55
- ids_reader && ids_reader.include?(record[klass_fk])
118
+ ## BELONGS TO
119
+ def default(&block)
120
+ writer(owner.instance_exec(&block)) if reader.nil?
56
121
  end
57
122
 
58
123
  private
59
124
 
60
- # Returns the number of records in this collection, which basically
61
- # means count the number of entries in the +primary_key+
62
- def count_records
63
- ids_reader&.size || (@target ||= []).size
125
+ ## CUSTOM
126
+ def _create_record(attributes, raises = false, &block)
127
+ if attributes.is_a?(Array)
128
+ attributes.collect { |attr| _create_record(attr, raises, &block) }
129
+ else
130
+ build_record(attributes, &block).tap do |record|
131
+ transaction do
132
+ result = nil
133
+ add_to_target(record) do
134
+ result = insert_record(record, true, raises) { @_was_loaded = loaded? }
135
+ end
136
+ raise ActiveRecord::Rollback unless result
137
+ end
138
+ end
139
+ end
64
140
  end
65
141
 
66
- # When the idea is to nulligy the association, then just set the owner
142
+ # When the idea is to nullify the association, then just set the owner
67
143
  # +primary_key+ as empty
68
- def delete_count(method, scope, ids = nil)
69
- ids ||= scope.pluck(klass_fk)
70
- scope.delete_all if method == :delete_all
71
- remove_stash_records(ids)
144
+ def delete_count(method, scope, ids)
145
+ size_cache = scope.delete_all if method == :delete_all
146
+ (size_cache || ids.size).tap { ids_rewriter(ids, :-) }
72
147
  end
73
148
 
74
149
  def delete_or_nullify_all_records(method)
75
- delete_count(method, scope)
150
+ delete_count(method, scope, ids_reader)
76
151
  end
77
152
 
78
153
  # Deletes the records according to the <tt>:dependent</tt> option.
79
154
  def delete_records(records, method)
80
- ids = Array.wrap(records).each_with_object(klass_fk).map(&:[])
155
+ ids = read_records_ids(records)
81
156
 
82
157
  if method == :destroy
83
158
  records.each(&:destroy!)
84
- remove_stash_records(ids)
159
+ ids_rewriter(ids, :-)
85
160
  else
86
- scope = self.scope.where(klass_fk => records)
161
+ scope = self.scope.where(klass_attr => records)
87
162
  delete_count(method, scope, ids)
88
163
  end
89
164
  end
90
165
 
91
- def concat_records(*)
92
- result = super
93
- ids_writer(ids_reader)
94
- result
166
+ def source_attr
167
+ reflection.foreign_key
95
168
  end
96
169
 
97
- def remove_stash_records(ids)
98
- return if ids_reader.nil?
99
- ids_writer(ids_reader - Array.wrap(ids))
170
+ def klass_attr
171
+ reflection.active_record_primary_key
100
172
  end
101
173
 
102
- def klass_fk
103
- reflection.foreign_key
174
+ def read_records_ids(records)
175
+ return unless records.present?
176
+ Array.wrap(records).each_with_object(klass_attr).map(&:read_attribute).presence
177
+ end
178
+
179
+ def ids_rewriter(ids, operator)
180
+ list = owner[source_attr] ||= []
181
+ list = list.public_send(operator, ids)
182
+ owner[source_attr] = list.uniq.compact.presence
183
+
184
+ return if @_building_changes || !owner.persisted?
185
+ owner.update_attribute(source_attr, list)
186
+ end
187
+
188
+ ## HAS MANY
189
+ def replace_records(*)
190
+ build_changes { super }
191
+ end
192
+
193
+ def concat_records(*)
194
+ build_changes { super }
195
+ end
196
+
197
+ def delete_or_destroy(*)
198
+ build_changes { super }
104
199
  end
105
200
 
106
201
  def difference(a, b)
@@ -110,6 +205,28 @@ module Torque
110
205
  def intersection(a, b)
111
206
  a & b
112
207
  end
208
+
209
+ ## BELONGS TO
210
+ def scope_for_create
211
+ super.except!(klass.primary_key)
212
+ end
213
+
214
+ def find_target?
215
+ !loaded? && foreign_key_present? && klass
216
+ end
217
+
218
+ def foreign_key_present?
219
+ stale_state.present?
220
+ end
221
+
222
+ def invertible_for?(record)
223
+ inverse = inverse_reflection_for(record)
224
+ inverse && (inverse.has_many? && inverse.connected_through_array?)
225
+ end
226
+
227
+ def stale_state
228
+ owner.read_attribute(source_attr)
229
+ end
113
230
  end
114
231
 
115
232
  ::ActiveRecord::Associations.const_set(:BelongsToManyAssociation, BelongsToManyAssociation)