Skip to content

Commit 1d5393c

Browse files
author
Carlos Silva
committed
Add support to any kind of type in array associations
1 parent ce7c862 commit 1d5393c

File tree

5 files changed

+108
-39
lines changed

5 files changed

+108
-39
lines changed

lib/torque/postgresql/reflection/abstract_reflection.rb

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -40,34 +40,29 @@ def build_join_constraint(table, foreign_table)
4040
result
4141
end
4242

43-
# Build the id constraint checking if both types are perfect matching
43+
# Build the id constraint checking if both types are perfect matching.
44+
# The klass attribute (left side) will always be a column attribute
4445
def build_id_constraint(klass_attr, source_attr)
4546
return klass_attr.eq(source_attr) unless connected_through_array?
4647

4748
# Klass and key are associated with the reflection Class
48-
klass_type = klass.columns_hash[join_primary_key.to_s]
49-
# active_record and foreign_key are associated with the source Class
50-
source_type = active_record.columns_hash[join_foreign_key.to_s]
51-
52-
# If both are attributes but the left side is not an array, and the
53-
# right side is, use the ANY operation
54-
any_operation = arel_array_to_any(klass_attr, source_attr, klass_type, source_type)
55-
return klass_attr.eq(any_operation) if any_operation
49+
klass_type = klass.columns_hash[join_keys.key.to_s]
50+
51+
# Apply an ANY operation which checks if the single value on the left
52+
# side exists in the array on the right side
53+
if source_attr.is_a?(AREL_ATTR)
54+
any_value = [klass_attr, source_attr]
55+
any_value.reverse! if klass_type.try(:array?)
56+
return any_value.shift.eq(::Arel::Nodes::NamedFunction.new('ANY', any_value))
57+
end
5658

5759
# If the left side is not an array, just use the IN condition
5860
return klass_attr.in(source_attr) unless klass_type.try(:array)
5961

60-
# Decide if should apply a cast to ensure same type comparision
61-
should_cast = klass_type.type.eql?(:integer) && source_type.type.eql?(:integer)
62-
should_cast &= !klass_type.sql_type.eql?(source_type.sql_type)
63-
should_cast |= !(klass_attr.is_a?(AREL_ATTR) && source_attr.is_a?(AREL_ATTR))
64-
65-
# Apply necessary transformations to values
66-
klass_attr = cast_constraint_to_array(klass_type, klass_attr, should_cast)
67-
source_attr = cast_constraint_to_array(source_type, source_attr, should_cast)
68-
69-
# Return the overlap condition
70-
klass_attr.overlaps(source_attr)
62+
# Build the overlap condition (array && array) ensuring that the right
63+
# side has the same type as the left side
64+
source_attr = ::Arel::Nodes.build_quoted(Array.wrap(source_attr))
65+
klass_attr.overlaps(source_attr.cast(klass_type.sql_type_metadata.sql_type))
7166
end
7267

7368
# TODO: Deprecate this method
@@ -83,24 +78,6 @@ def build_id_constraint_between(table, foreign_table)
8378

8479
build_id_constraint(klass_attr, source_attr)
8580
end
86-
87-
# Prepare a value for an array constraint overlap condition
88-
def cast_constraint_to_array(type, value, should_cast)
89-
base_ready = type.try(:array) && value.is_a?(AREL_ATTR)
90-
return value if base_ready && (type.sql_type.eql?(ARR_NO_CAST) || !should_cast)
91-
92-
value = ::Arel::Nodes.build_quoted(Array.wrap(value)) unless base_ready
93-
value = value.cast(ARR_CAST) if should_cast
94-
value
95-
end
96-
97-
# Check if it's possible to turn both attributes into an ANY condition
98-
def arel_array_to_any(klass_attr, source_attr, klass_type, source_type)
99-
return unless !klass_type.try(:array) && source_type.try(:array) &&
100-
klass_attr.is_a?(AREL_ATTR) && source_attr.is_a?(AREL_ATTR)
101-
102-
::Arel::Nodes::NamedFunction.new('ANY', [source_attr])
103-
end
10481
end
10582

10683
::ActiveRecord::Reflection::AbstractReflection.prepend(AbstractReflection)

lib/torque/postgresql/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
module Torque
44
module PostgreSQL
5-
VERSION = '3.0.0'
5+
VERSION = '3.0.1'
66
end
77
end

spec/spec_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
# Handles acton before rspec initialize
4141
config.before(:suite) do
42+
ActiveSupport::Deprecation.silenced = true
4243
DatabaseCleaner.clean_with(:truncation)
4344
end
4445

spec/tests/belongs_to_many_spec.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,4 +393,51 @@
393393
end
394394
end
395395
end
396+
397+
context 'using uuid' do
398+
let(:connection) { ActiveRecord::Base.connection }
399+
let(:game) { Class.new(ActiveRecord::Base) }
400+
let(:player) { Class.new(ActiveRecord::Base) }
401+
let(:other) { player.create }
402+
403+
# TODO: Set as a shred example
404+
before do
405+
connection.create_table(:players, id: :uuid) { |t| t.string :name }
406+
connection.create_table(:games, id: :uuid) { |t| t.uuid :player_ids, array: true }
407+
408+
game.table_name = 'games'
409+
player.table_name = 'players'
410+
game.belongs_to_many :players, anonymous_class: player,
411+
inverse_of: false, foreign_key: :player_ids
412+
end
413+
414+
subject { game.create }
415+
416+
it 'loads associated records' do
417+
subject.update(player_ids: [other.id])
418+
expect(subject.players.to_sql).to be_eql(<<-SQL.squish)
419+
SELECT "players".* FROM "players" WHERE "players"."id" IN ('#{other.id}')
420+
SQL
421+
422+
expect(subject.players.load).to be_a(ActiveRecord::Associations::CollectionProxy)
423+
expect(subject.players.to_a).to be_eql([other])
424+
end
425+
426+
it 'can preload records' do
427+
records = 5.times.map { player.create }
428+
subject.players.concat(records)
429+
430+
entries = game.all.includes(:players).load
431+
432+
expect(entries.size).to be_eql(1)
433+
expect(entries.first.players).to be_loaded
434+
expect(entries.first.players.size).to be_eql(5)
435+
end
436+
437+
it 'can joins records' do
438+
query = game.all.joins(:players)
439+
expect(query.to_sql).to match(/INNER JOIN "players"/)
440+
expect { query.load }.not_to raise_error
441+
end
442+
end
396443
end

spec/tests/has_many_spec.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,4 +411,48 @@
411411
expect { query.load }.not_to raise_error
412412
end
413413
end
414+
415+
context 'using uuid' do
416+
let(:connection) { ActiveRecord::Base.connection }
417+
let(:game) { Class.new(ActiveRecord::Base) }
418+
let(:player) { Class.new(ActiveRecord::Base) }
419+
420+
# TODO: Set as a shred example
421+
before do
422+
connection.create_table(:players, id: :uuid) { |t| t.string :name }
423+
connection.create_table(:games, id: :uuid) { |t| t.uuid :player_ids, array: true }
424+
425+
game.table_name = 'games'
426+
player.table_name = 'players'
427+
player.has_many :games, array: true, anonymous_class: game,
428+
inverse_of: false, foreign_key: :player_ids
429+
end
430+
431+
subject { player.create }
432+
433+
it 'loads associated records' do
434+
expect(subject.games.to_sql).to match(Regexp.new(<<-SQL.squish))
435+
SELECT "games"\\.\\* FROM "games"
436+
WHERE \\(?"games"\\."player_ids" && ARRAY\\['#{subject.id}'\\]::uuid\\[\\]\\)?
437+
SQL
438+
439+
expect(subject.games.load).to be_a(ActiveRecord::Associations::CollectionProxy)
440+
expect(subject.games.to_a).to be_eql([])
441+
end
442+
443+
it 'can preload records' do
444+
5.times { game.create(player_ids: [subject.id]) }
445+
entries = player.all.includes(:games).load
446+
447+
expect(entries.size).to be_eql(1)
448+
expect(entries.first.games).to be_loaded
449+
expect(entries.first.games.size).to be_eql(5)
450+
end
451+
452+
it 'can joins records' do
453+
query = player.all.joins(:games)
454+
expect(query.to_sql).to match(/INNER JOIN "games"/)
455+
expect { query.load }.not_to raise_error
456+
end
457+
end
414458
end

0 commit comments

Comments
 (0)