diff --git a/acceptance/cases/models/binary_identifiers.rb b/acceptance/cases/models/binary_identifiers.rb index 7c1920c0..4db9d74a 100644 --- a/acceptance/cases/models/binary_identifiers.rb +++ b/acceptance/cases/models/binary_identifiers.rb @@ -66,6 +66,11 @@ def test_includes_works user = User.all.includes(:projects).first assert user + project_count = 0 + user.projects.each do |_| + project_count += 1 + end + assert_equal 3, project_count assert_equal 3, user.projects.count end diff --git a/acceptance/models/binary_project.rb b/acceptance/models/binary_project.rb index 8e817b37..e09967a9 100644 --- a/acceptance/models/binary_project.rb +++ b/acceptance/models/binary_project.rb @@ -6,6 +6,8 @@ # frozen_string_literal: true +require_relative 'string_io' + class BinaryProject < ActiveRecord::Base belongs_to :owner, class_name: 'User' diff --git a/acceptance/models/string_io.rb b/acceptance/models/string_io.rb new file mode 100644 index 00000000..9de8c1e2 --- /dev/null +++ b/acceptance/models/string_io.rb @@ -0,0 +1,28 @@ +# Copyright 2025 Google LLC +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. + +# frozen_string_literal: true + +# Add equality and hash functions to StringIO to use it for sets. +class StringIO + def ==(o) + o.class == self.class && self.to_base64 == o.to_base64 + end + + def eql?(o) + self == o + end + + def hash + to_base64.hash + end + + def to_base64 + self.rewind + value = self.read + Base64.strict_encode64 value.force_encoding("ASCII-8BIT") + end +end diff --git a/acceptance/models/user.rb b/acceptance/models/user.rb index f5fbd330..54608d4e 100644 --- a/acceptance/models/user.rb +++ b/acceptance/models/user.rb @@ -6,6 +6,8 @@ # frozen_string_literal: true +require_relative 'string_io' + class User < ActiveRecord::Base has_many :binary_projects, foreign_key: :owner_id diff --git a/test/activerecord_spanner_mock_server/models/binary_project.rb b/test/activerecord_spanner_mock_server/models/binary_project.rb index 8e817b37..d34ca894 100644 --- a/test/activerecord_spanner_mock_server/models/binary_project.rb +++ b/test/activerecord_spanner_mock_server/models/binary_project.rb @@ -6,6 +6,8 @@ # frozen_string_literal: true +require_relative "string_io" + class BinaryProject < ActiveRecord::Base belongs_to :owner, class_name: 'User' diff --git a/test/activerecord_spanner_mock_server/models/string_io.rb b/test/activerecord_spanner_mock_server/models/string_io.rb new file mode 100644 index 00000000..22e9cfa4 --- /dev/null +++ b/test/activerecord_spanner_mock_server/models/string_io.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class StringIO + def ==(o) + o.class == self.class && self.to_base64 == o.to_base64 + end + + def eql?(o) + self == o + end + + def hash + to_base64.hash + end + + def to_base64 + self.rewind + value = self.read + Base64.strict_encode64 value.force_encoding("ASCII-8BIT") + end +end diff --git a/test/activerecord_spanner_mock_server/models/user.rb b/test/activerecord_spanner_mock_server/models/user.rb index f5fbd330..0f5b6e8f 100644 --- a/test/activerecord_spanner_mock_server/models/user.rb +++ b/test/activerecord_spanner_mock_server/models/user.rb @@ -6,6 +6,8 @@ # frozen_string_literal: true +require_relative "string_io" + class User < ActiveRecord::Base has_many :binary_projects, foreign_key: :owner_id diff --git a/test/activerecord_spanner_mock_server/spanner_active_record_with_mock_server_test.rb b/test/activerecord_spanner_mock_server/spanner_active_record_with_mock_server_test.rb index 2c03f099..4beff9ae 100644 --- a/test/activerecord_spanner_mock_server/spanner_active_record_with_mock_server_test.rb +++ b/test/activerecord_spanner_mock_server/spanner_active_record_with_mock_server_test.rb @@ -1317,6 +1317,61 @@ def test_binary_id_association assert_equal to_base64(user.id), mutation.insert.values[0][3] end + def test_binary_id_association_includes + col_id = Field.new name: "id", type: Type.new(code: TypeCode::BYTES) + col_email = Field.new name: "email", type: Type.new(code: TypeCode::STRING) + col_full_name = Field.new name: "full_name", type: Type.new(code: TypeCode::STRING) + + metadata = ResultSetMetadata.new row_type: StructType.new + metadata.row_type.fields.push col_id, col_email, col_full_name + result_set = ResultSet.new metadata: metadata + + user_id = to_base64(StringIO.new(SecureRandom.random_bytes(16))) + row = ListValue.new + row.values.push( + Value.new(string_value: user_id), + Value.new(string_value: "test_user@example.com"), + Value.new(string_value: "Test User") + ) + result_set.rows.push row + statement_result = StatementResult.new(result_set) + + sql = "SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT @p1" + @mock.put_statement_result sql, statement_result + + col_id = Field.new name: "id", type: Type.new(code: TypeCode::BYTES) + col_name = Field.new name: "name", type: Type.new(code: TypeCode::STRING) + col_description = Field.new name: "description", type: Type.new(code: TypeCode::STRING) + col_owner = Field.new name: "owner_id", type: Type.new(code: TypeCode::BYTES) + + metadata = ResultSetMetadata.new row_type: StructType.new + metadata.row_type.fields.push col_id, col_name, col_description, col_owner + result_set = ResultSet.new metadata: metadata + + project_count = 3 + (1..project_count).each { |i| + row = ListValue.new + row.values.push( + Value.new(string_value: to_base64(StringIO.new(SecureRandom.random_bytes(16)))), + Value.new(string_value: "Test Project #{i}"), + Value.new(string_value: "Test Project Description #{i}"), + Value.new(string_value: user_id) + ) + result_set.rows.push row + } + statement_result = StatementResult.new(result_set) + projects_sql = "SELECT `binary_projects`.* FROM `binary_projects` WHERE `binary_projects`.`owner_id` = @p1" + @mock.put_statement_result projects_sql, statement_result + + users = User.all.includes(:binary_projects) + u1 = users.first + found = 0 + u1.binary_projects.each do |_| + found += 1 + end + assert_equal project_count, found + end + def test_skip_binary_deserialization ENV["SPANNER_BYTES_DESERIALIZE_DISABLED"] = "true" begin