torque-postgresql 2.0.3 → 2.1.1

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