torque-postgresql 2.0.2 → 2.1.0

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