torque-postgresql 2.0.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/lib/torque/postgresql.rb +1 -0
  3. data/lib/torque/postgresql/adapter.rb +23 -0
  4. data/lib/torque/postgresql/adapter/database_statements.rb +2 -0
  5. data/lib/torque/postgresql/adapter/oid.rb +3 -1
  6. data/lib/torque/postgresql/adapter/oid/box.rb +2 -0
  7. data/lib/torque/postgresql/adapter/oid/circle.rb +2 -0
  8. data/lib/torque/postgresql/adapter/oid/enum.rb +2 -0
  9. data/lib/torque/postgresql/adapter/oid/enum_set.rb +2 -0
  10. data/lib/torque/postgresql/adapter/oid/interval.rb +2 -0
  11. data/lib/torque/postgresql/adapter/oid/line.rb +2 -0
  12. data/lib/torque/postgresql/adapter/oid/range.rb +2 -0
  13. data/lib/torque/postgresql/adapter/oid/segment.rb +2 -0
  14. data/lib/torque/postgresql/adapter/quoting.rb +2 -0
  15. data/lib/torque/postgresql/adapter/schema_creation.rb +8 -1
  16. data/lib/torque/postgresql/adapter/schema_definitions.rb +2 -0
  17. data/lib/torque/postgresql/adapter/schema_dumper.rb +7 -1
  18. data/lib/torque/postgresql/adapter/schema_statements.rb +2 -0
  19. data/lib/torque/postgresql/arel/infix_operation.rb +5 -1
  20. data/lib/torque/postgresql/arel/join_source.rb +2 -0
  21. data/lib/torque/postgresql/arel/nodes.rb +2 -0
  22. data/lib/torque/postgresql/arel/operations.rb +2 -0
  23. data/lib/torque/postgresql/arel/select_manager.rb +2 -0
  24. data/lib/torque/postgresql/arel/visitors.rb +6 -3
  25. data/lib/torque/postgresql/associations/association.rb +14 -3
  26. data/lib/torque/postgresql/associations/association_scope.rb +2 -0
  27. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +169 -47
  28. data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +8 -5
  29. data/lib/torque/postgresql/associations/builder/has_many.rb +2 -0
  30. data/lib/torque/postgresql/associations/preloader/association.rb +30 -1
  31. data/lib/torque/postgresql/attributes/builder.rb +3 -1
  32. data/lib/torque/postgresql/attributes/builder/enum.rb +5 -3
  33. data/lib/torque/postgresql/attributes/builder/period.rb +6 -4
  34. data/lib/torque/postgresql/attributes/enum.rb +5 -10
  35. data/lib/torque/postgresql/attributes/enum_set.rb +2 -0
  36. data/lib/torque/postgresql/attributes/lazy.rb +3 -1
  37. data/lib/torque/postgresql/attributes/period.rb +2 -0
  38. data/lib/torque/postgresql/autosave_association.rb +19 -16
  39. data/lib/torque/postgresql/auxiliary_statement.rb +2 -0
  40. data/lib/torque/postgresql/auxiliary_statement/settings.rb +2 -0
  41. data/lib/torque/postgresql/base.rb +11 -2
  42. data/lib/torque/postgresql/coder.rb +5 -3
  43. data/lib/torque/postgresql/collector.rb +2 -0
  44. data/lib/torque/postgresql/config.rb +5 -0
  45. data/lib/torque/postgresql/geometry_builder.rb +2 -0
  46. data/lib/torque/postgresql/i18n.rb +2 -0
  47. data/lib/torque/postgresql/inheritance.rb +2 -0
  48. data/lib/torque/postgresql/insert_all.rb +26 -0
  49. data/lib/torque/postgresql/migration/command_recorder.rb +2 -0
  50. data/lib/torque/postgresql/railtie.rb +2 -0
  51. data/lib/torque/postgresql/reflection.rb +2 -0
  52. data/lib/torque/postgresql/reflection/abstract_reflection.rb +13 -28
  53. data/lib/torque/postgresql/reflection/association_reflection.rb +24 -0
  54. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +18 -4
  55. data/lib/torque/postgresql/reflection/has_many_reflection.rb +2 -0
  56. data/lib/torque/postgresql/reflection/runtime_reflection.rb +2 -0
  57. data/lib/torque/postgresql/reflection/through_reflection.rb +2 -0
  58. data/lib/torque/postgresql/relation.rb +15 -11
  59. data/lib/torque/postgresql/relation/auxiliary_statement.rb +6 -1
  60. data/lib/torque/postgresql/relation/distinct_on.rb +2 -0
  61. data/lib/torque/postgresql/relation/inheritance.rb +2 -0
  62. data/lib/torque/postgresql/relation/merger.rb +2 -0
  63. data/lib/torque/postgresql/schema_cache.rb +2 -0
  64. data/lib/torque/postgresql/version.rb +3 -1
  65. data/spec/schema.rb +3 -2
  66. data/spec/tests/arel_spec.rb +3 -1
  67. data/spec/tests/belongs_to_many_spec.rb +134 -13
  68. data/spec/tests/enum_set_spec.rb +1 -1
  69. data/spec/tests/has_many_spec.rb +25 -1
  70. data/spec/tests/insert_all_spec.rb +89 -0
  71. data/spec/tests/table_inheritance_spec.rb +1 -1
  72. metadata +9 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9085a88f0ef5d87a1cb95bdaf82fac432c7857ef97f485df3d20b27dab2a814
4
- data.tar.gz: 0252a645b4c6f3422c296f621155f7c78d82d10a4d296a8f46616abfade3e481
3
+ metadata.gz: cd2096d27cb0b11fd4cd2048627bf0b12dcd905d4240ea0251366f580a0afcf2
4
+ data.tar.gz: c6d948dd77768feff3d85284bc838b8fb03c91233cc33ecdc8b3c22e463038c3
5
5
  SHA512:
6
- metadata.gz: 54593a0e4e7e25de241904f72ed3c016447f24ab93668d80f2c2b84d6f8e10689d3dc8749012cd6b20c1b45c33d3d1b5dd3124ee8d7c9bef659507ef3c85e2ea
7
- data.tar.gz: a58ae7a09eeaf9afdef0c3f1fbff5c7a7a96d671d913c33d57d286230b3c939888d1c985fe09ab8991896698a98f2b63d8f977ed251670ad4753dd26e73fa124
6
+ metadata.gz: 3f61ba4bf68f2519090bb23b259cf9d8fa2c59f5a9935c867fe0dd2629eeeddd21e7a9f68a0e168550f696564d9613a4617ed6619ac4fe1f3d7660bf3fd1fc9c
7
+ data.tar.gz: dbb298a6df589aa9368fb70f933b5ece9984bfc3661d667c7e42b989e2bdda6159aafbf78a303f9926f3b17dbc762473454f4646e91e976a21a0fd439a57c9f9
@@ -22,6 +22,7 @@ require 'torque/postgresql/autosave_association'
22
22
  require 'torque/postgresql/auxiliary_statement'
23
23
  require 'torque/postgresql/base'
24
24
  require 'torque/postgresql/inheritance'
25
+ require 'torque/postgresql/insert_all'
25
26
  require 'torque/postgresql/coder'
26
27
  require 'torque/postgresql/migration'
27
28
  require 'torque/postgresql/relation'
@@ -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'
@@ -13,12 +15,33 @@ module Torque
13
15
  include DatabaseStatements
14
16
  include SchemaStatements
15
17
 
18
+ INJECT_WHERE_REGEX = /(DO UPDATE SET.*excluded\.[^ ]+) RETURNING/.freeze
19
+
16
20
  # Get the current PostgreSQL version as a Gem Version.
17
21
  def version
18
22
  @version ||= Gem::Version.new(
19
23
  select_value('SELECT version()').match(/#{Adapter::ADAPTER_NAME} ([\d\.]+)/)[1]
20
24
  )
21
25
  end
26
+
27
+ # Add `inherits` to the list of extracted table options
28
+ def extract_table_options!(options)
29
+ super.merge(options.extract!(:inherits))
30
+ end
31
+
32
+ # Allow filtered bulk insert by adding the where clause. This method is only used by
33
+ # +InsertAll+, so it somewhat safe to override it
34
+ def build_insert_sql(insert)
35
+ super.tap do |sql|
36
+ if insert.update_duplicates? && insert.where_condition?
37
+ if insert.returning
38
+ sql.gsub!(INJECT_WHERE_REGEX, "\\1 WHERE #{insert.where} RETURNING")
39
+ else
40
+ sql << " WHERE #{insert.where}"
41
+ end
42
+ end
43
+ end
44
+ end
22
45
  end
23
46
 
24
47
  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|
@@ -76,6 +78,10 @@ module Torque
76
78
 
77
79
  # Scenic integration
78
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)
79
85
  end
80
86
 
81
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,69 @@ 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 || column_default_value
20
+ end
21
+ end
22
+
23
+ def ids_writer(ids)
24
+ ids = ids.presence || column_default_value
25
+ owner.write_attribute(source_attr, ids)
26
+ return unless owner.persisted? && owner.attribute_changed?(source_attr)
27
+
28
+ owner.update_attribute(source_attr, ids)
29
+ end
30
+
31
+ def size
32
+ if loaded?
33
+ target.size
34
+ elsif !target.empty?
35
+ unsaved_records = target.select(&:new_record?)
36
+ unsaved_records.size + stale_state.size
37
+ else
38
+ stale_state&.size || 0
39
+ end
40
+ end
41
+
42
+ def empty?
43
+ size.zero?
44
+ end
45
+
46
+ def include?(record)
47
+ return false unless record.is_a?(reflection.klass)
48
+ return include_in_memory?(record) if record.new_record?
49
+
50
+ (!target.empty? && target.include?(record)) ||
51
+ stale_state&.include?(record.read_attribute(klass_attr))
52
+ end
53
+
54
+ def load_target
55
+ if stale_target? || find_target?
56
+ @target = merge_target_lists(find_target, target)
57
+ end
58
+
59
+ loaded!
60
+ target
61
+ end
62
+
63
+ def build_changes
64
+ @_building_changes = true
65
+ yield.tap { ids_writer(ids_reader) }
66
+ ensure
67
+ @_building_changes = nil
68
+ end
69
+
70
+ ## HAS MANY
9
71
  def handle_dependency
10
72
  case options[:dependent]
11
73
  when :restrict_with_exception
12
- raise ::ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
74
+ raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
13
75
 
14
76
  when :restrict_with_error
15
77
  unless empty?
@@ -19,88 +81,126 @@ module Torque
19
81
  end
20
82
 
21
83
  when :destroy
22
- # No point in executing the counter update since we're going to destroy the parent anyway
23
84
  load_target.each { |t| t.destroyed_by_association = reflection }
24
85
  destroy_all
86
+ when :destroy_async
87
+ load_target.each do |t|
88
+ t.destroyed_by_association = reflection
89
+ end
90
+
91
+ unless target.empty?
92
+ association_class = target.first.class
93
+ primary_key_column = association_class.primary_key.to_sym
94
+
95
+ ids = target.collect do |assoc|
96
+ assoc.public_send(primary_key_column)
97
+ end
98
+
99
+ enqueue_destroy_association(
100
+ owner_model_name: owner.class.to_s,
101
+ owner_id: owner.id,
102
+ association_class: association_class.to_s,
103
+ association_ids: ids,
104
+ association_primary_key_column: primary_key_column,
105
+ ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
106
+ )
107
+ end
25
108
  else
26
109
  delete_all
27
110
  end
28
111
  end
29
112
 
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
113
  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?
114
+ super.tap do |saved|
115
+ ids_rewriter(record.read_attribute(klass_attr), :<<) if saved
116
+ end
51
117
  end
52
118
 
53
- def include?(record)
54
- list = owner[reflection.active_record_primary_key]
55
- ids_reader && ids_reader.include?(record[klass_fk])
119
+ ## BELONGS TO
120
+ def default(&block)
121
+ writer(owner.instance_exec(&block)) if reader.nil?
56
122
  end
57
123
 
58
124
  private
59
125
 
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
126
+ ## CUSTOM
127
+ def _create_record(attributes, raises = false, &block)
128
+ if attributes.is_a?(Array)
129
+ attributes.collect { |attr| _create_record(attr, raises, &block) }
130
+ else
131
+ build_record(attributes, &block).tap do |record|
132
+ transaction do
133
+ result = nil
134
+ add_to_target(record) do
135
+ result = insert_record(record, true, raises) { @_was_loaded = loaded? }
136
+ end
137
+ raise ActiveRecord::Rollback unless result
138
+ end
139
+ end
140
+ end
64
141
  end
65
142
 
66
- # When the idea is to nulligy the association, then just set the owner
143
+ # When the idea is to nullify the association, then just set the owner
67
144
  # +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)
145
+ def delete_count(method, scope, ids)
146
+ size_cache = scope.delete_all if method == :delete_all
147
+ (size_cache || ids.size).tap { ids_rewriter(ids, :-) }
72
148
  end
73
149
 
74
150
  def delete_or_nullify_all_records(method)
75
- delete_count(method, scope)
151
+ delete_count(method, scope, ids_reader)
76
152
  end
77
153
 
78
154
  # Deletes the records according to the <tt>:dependent</tt> option.
79
155
  def delete_records(records, method)
80
- ids = Array.wrap(records).each_with_object(klass_fk).map(&:[])
156
+ ids = read_records_ids(records)
81
157
 
82
158
  if method == :destroy
83
159
  records.each(&:destroy!)
84
- remove_stash_records(ids)
160
+ ids_rewriter(ids, :-)
85
161
  else
86
- scope = self.scope.where(klass_fk => records)
162
+ scope = self.scope.where(klass_attr => records)
87
163
  delete_count(method, scope, ids)
88
164
  end
89
165
  end
90
166
 
91
- def concat_records(*)
92
- result = super
93
- ids_writer(ids_reader)
94
- result
167
+ def source_attr
168
+ reflection.foreign_key
95
169
  end
96
170
 
97
- def remove_stash_records(ids)
98
- return if ids_reader.nil?
99
- ids_writer(ids_reader - Array.wrap(ids))
171
+ def klass_attr
172
+ reflection.active_record_primary_key
100
173
  end
101
174
 
102
- def klass_fk
103
- reflection.foreign_key
175
+ def read_records_ids(records)
176
+ return unless records.present?
177
+ Array.wrap(records).each_with_object(klass_attr).map(&:read_attribute).presence
178
+ end
179
+
180
+ def ids_rewriter(ids, operator)
181
+ list = owner[source_attr] ||= []
182
+ list = list.public_send(operator, ids)
183
+ owner[source_attr] = list.uniq.compact.presence || column_default_value
184
+
185
+ return if @_building_changes || !owner.persisted?
186
+ owner.update_attribute(source_attr, list)
187
+ end
188
+
189
+ def column_default_value
190
+ owner.class.columns_hash[source_attr].default
191
+ end
192
+
193
+ ## HAS MANY
194
+ def replace_records(*)
195
+ build_changes { super }
196
+ end
197
+
198
+ def concat_records(*)
199
+ build_changes { super }
200
+ end
201
+
202
+ def delete_or_destroy(*)
203
+ build_changes { super }
104
204
  end
105
205
 
106
206
  def difference(a, b)
@@ -110,6 +210,28 @@ module Torque
110
210
  def intersection(a, b)
111
211
  a & b
112
212
  end
213
+
214
+ ## BELONGS TO
215
+ def scope_for_create
216
+ super.except!(klass.primary_key)
217
+ end
218
+
219
+ def find_target?
220
+ !loaded? && foreign_key_present? && klass
221
+ end
222
+
223
+ def foreign_key_present?
224
+ stale_state.present?
225
+ end
226
+
227
+ def invertible_for?(record)
228
+ inverse = inverse_reflection_for(record)
229
+ inverse && (inverse.has_many? && inverse.connected_through_array?)
230
+ end
231
+
232
+ def stale_state
233
+ owner.read_attribute(source_attr)
234
+ end
113
235
  end
114
236
 
115
237
  ::ActiveRecord::Associations.const_set(:BelongsToManyAssociation, BelongsToManyAssociation)