torque-postgresql 2.0.3 → 2.1.1

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 +5 -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 +9 -3
  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 +168 -48
  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 +10 -8
  33. data/lib/torque/postgresql/attributes/builder/period.rb +6 -4
  34. data/lib/torque/postgresql/attributes/enum.rb +6 -11
  35. data/lib/torque/postgresql/attributes/enum_set.rb +3 -1
  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 +2 -0
  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 +139 -13
  68. data/spec/tests/enum_set_spec.rb +1 -1
  69. data/spec/tests/has_many_spec.rb +15 -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: 9e426686cf04fcf990d84b945abe3b3435168237f1ff24c6f23ac3b4941bbc4e
4
- data.tar.gz: 2f936f5676f80804e4766bc9bf64b81d70ddbd08e9409b373b0d59d7d20e8bf1
3
+ metadata.gz: 1f5815c2fe0a3682db3ccb907a29aa171382bfa6d70ffcb58ef7e7bdbb840f65
4
+ data.tar.gz: 32ae2c431e089c2c83d6c73be202ae86e79c129f1a79a08ca596ff87ffb231f6
5
5
  SHA512:
6
- metadata.gz: 373d085090e02e76eb2523f7c2d5a32434c002f712cf892aa38f81bc9935a51f2426054c97bb5df534c47e23fd1800d84b670b9443266908e2f076f20928e7a2
7
- data.tar.gz: f0441cc66de2ed015d0b8dbb13ae88dc4074a54c8477fc7a31fc1d51e2bfd7cb57ab0e3de529dcbd4feb3fa72dd1fc8894738868e8bfa36ad9490badc3d1c990
6
+ metadata.gz: 78c8d75e7b3534570caa48fe0a607417431813b04d842fb60b292d252d7ab0cb73ac16df488ba71242238f160ad746fe21ea162a988da0e11809c962e988b2d7
7
+ data.tar.gz: a9450b4271ddf00be01ef4fb1a45365970310887d0179c457928f8ab36d0be9378e9738befe3797380ace0cbb66181f973f4d966cd39cd3fff7eceef7afb8082
@@ -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
@@ -6,6 +8,9 @@ module Torque
6
8
 
7
9
  attr_reader :name, :klass, :set_klass, :enum_klass
8
10
 
11
+ # Delegate all Hash-like methods to the enum class
12
+ delegate *(Array.public_instance_methods - Object.public_methods), to: :@klass
13
+
9
14
  def self.create(row, type_map)
10
15
  name = row['typname']
11
16
  oid = row['oid'].to_i
@@ -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,15 +41,15 @@ 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
- stream.puts " # These are the common tables managed"
46
+ stream.puts " # These are the common tables"
45
47
  (sorted_tables - inherited_tables.keys).each do |table_name|
46
48
  table(table_name, stream) unless ignored?(table_name)
47
49
  end
48
50
 
49
51
  if inherited_tables.present?
50
- stream.puts " # These are tables that has inheritance"
52
+ stream.puts " # These are tables that have inheritance"
51
53
  inherited_tables.each do |table_name, inherits|
52
54
  next if ignored?(table_name)
53
55
 
@@ -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,71 @@ 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(from_target = false)
64
+ return yield if defined?(@_building_changes) && @_building_changes
65
+
66
+ @_building_changes = true
67
+ yield.tap { ids_writer(from_target ? ids_reader : stale_state) }
68
+ ensure
69
+ @_building_changes = nil
70
+ end
71
+
72
+ ## HAS MANY
9
73
  def handle_dependency
10
74
  case options[:dependent]
11
75
  when :restrict_with_exception
12
- raise ::ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
76
+ raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
13
77
 
14
78
  when :restrict_with_error
15
79
  unless empty?
@@ -19,86 +83,89 @@ module Torque
19
83
  end
20
84
 
21
85
  when :destroy
22
- # No point in executing the counter update since we're going to destroy the parent anyway
23
86
  load_target.each { |t| t.destroyed_by_association = reflection }
24
87
  destroy_all
88
+ when :destroy_async
89
+ load_target.each do |t|
90
+ t.destroyed_by_association = reflection
91
+ end
92
+
93
+ unless target.empty?
94
+ association_class = target.first.class
95
+ primary_key_column = association_class.primary_key.to_sym
96
+
97
+ ids = target.collect do |assoc|
98
+ assoc.public_send(primary_key_column)
99
+ end
100
+
101
+ enqueue_destroy_association(
102
+ owner_model_name: owner.class.to_s,
103
+ owner_id: owner.id,
104
+ association_class: association_class.to_s,
105
+ association_ids: ids,
106
+ association_primary_key_column: primary_key_column,
107
+ ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
108
+ )
109
+ end
25
110
  else
26
111
  delete_all
27
112
  end
28
113
  end
29
114
 
30
- def ids_reader
31
- owner[source_attr]
32
- end
33
-
34
- def ids_writer(new_ids)
35
- column = source_attr
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
115
  def insert_record(record, *)
42
- super
43
-
44
- attribute = (ids_reader || owner[source_attr] = [])
45
- attribute.push(record[klass_attr])
46
- record
47
- end
48
-
49
- def empty?
50
- size.zero?
116
+ super.tap do |saved|
117
+ ids_rewriter(record.read_attribute(klass_attr), :<<) if saved
118
+ end
51
119
  end
52
120
 
53
- def include?(record)
54
- list = owner[source_attr]
55
- ids_reader && ids_reader.include?(record[klass_attr])
121
+ ## BELONGS TO
122
+ def default(&block)
123
+ writer(owner.instance_exec(&block)) if reader.nil?
56
124
  end
57
125
 
58
126
  private
59
127
 
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
128
+ ## CUSTOM
129
+ def _create_record(attributes, raises = false, &block)
130
+ if attributes.is_a?(Array)
131
+ attributes.collect { |attr| _create_record(attr, raises, &block) }
132
+ else
133
+ build_record(attributes, &block).tap do |record|
134
+ transaction do
135
+ result = nil
136
+ add_to_target(record) do
137
+ result = insert_record(record, true, raises) { @_was_loaded = loaded? }
138
+ end
139
+ raise ActiveRecord::Rollback unless result
140
+ end
141
+ end
142
+ end
64
143
  end
65
144
 
66
- # When the idea is to nulligy the association, then just set the owner
145
+ # When the idea is to nullify the association, then just set the owner
67
146
  # +primary_key+ as empty
68
- def delete_count(method, scope, ids = nil)
69
- ids ||= scope.pluck(klass_attr)
70
- scope.delete_all if method == :delete_all
71
- remove_stash_records(ids)
147
+ def delete_count(method, scope, ids)
148
+ size_cache = scope.delete_all if method == :delete_all
149
+ (size_cache || ids.size).tap { ids_rewriter(ids, :-) }
72
150
  end
73
151
 
74
152
  def delete_or_nullify_all_records(method)
75
- delete_count(method, scope)
153
+ delete_count(method, scope, ids_reader)
76
154
  end
77
155
 
78
156
  # Deletes the records according to the <tt>:dependent</tt> option.
79
157
  def delete_records(records, method)
80
- ids = Array.wrap(records).each_with_object(klass_attr).map(&:[])
158
+ ids = read_records_ids(records)
81
159
 
82
160
  if method == :destroy
83
161
  records.each(&:destroy!)
84
- remove_stash_records(ids)
162
+ ids_rewriter(ids, :-)
85
163
  else
86
164
  scope = self.scope.where(klass_attr => records)
87
165
  delete_count(method, scope, ids)
88
166
  end
89
167
  end
90
168
 
91
- def concat_records(*)
92
- result = super
93
- ids_writer(ids_reader)
94
- result
95
- end
96
-
97
- def remove_stash_records(ids)
98
- return if ids_reader.nil?
99
- ids_writer(ids_reader - Array.wrap(ids))
100
- end
101
-
102
169
  def source_attr
103
170
  reflection.foreign_key
104
171
  end
@@ -107,6 +174,37 @@ module Torque
107
174
  reflection.active_record_primary_key
108
175
  end
109
176
 
177
+ def read_records_ids(records)
178
+ return unless records.present?
179
+ Array.wrap(records).each_with_object(klass_attr).map(&:read_attribute).presence
180
+ end
181
+
182
+ def ids_rewriter(ids, operator)
183
+ list = owner[source_attr] ||= []
184
+ list = list.public_send(operator, ids)
185
+ owner[source_attr] = list.uniq.compact.presence || column_default_value
186
+
187
+ return if @_building_changes || !owner.persisted?
188
+ owner.update_attribute(source_attr, list)
189
+ end
190
+
191
+ def column_default_value
192
+ owner.class.columns_hash[source_attr].default
193
+ end
194
+
195
+ ## HAS MANY
196
+ def replace_records(*)
197
+ build_changes(true) { super }
198
+ end
199
+
200
+ def concat_records(*)
201
+ build_changes(true) { super }
202
+ end
203
+
204
+ def delete_or_destroy(*)
205
+ build_changes(true) { super }
206
+ end
207
+
110
208
  def difference(a, b)
111
209
  a - b
112
210
  end
@@ -114,6 +212,28 @@ module Torque
114
212
  def intersection(a, b)
115
213
  a & b
116
214
  end
215
+
216
+ ## BELONGS TO
217
+ def scope_for_create
218
+ super.except!(klass.primary_key)
219
+ end
220
+
221
+ def find_target?
222
+ !loaded? && foreign_key_present? && klass
223
+ end
224
+
225
+ def foreign_key_present?
226
+ stale_state.present?
227
+ end
228
+
229
+ def invertible_for?(record)
230
+ inverse = inverse_reflection_for(record)
231
+ inverse && (inverse.has_many? && inverse.connected_through_array?)
232
+ end
233
+
234
+ def stale_state
235
+ owner.read_attribute(source_attr)
236
+ end
117
237
  end
118
238
 
119
239
  ::ActiveRecord::Associations.const_set(:BelongsToManyAssociation, BelongsToManyAssociation)