torque-postgresql 2.0.1 → 2.0.6

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.
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)