aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/test/cases/adapters/postgresql/composite_test.rb
blob: 1da2a9e2ac07b7bd29b94e7d5b2e68280e714ded (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
require "cases/helper"
require "support/connection_helper"

module PostgresqlCompositeBehavior
  include ConnectionHelper

  class PostgresqlComposite < ActiveRecord::Base
    self.table_name = "postgresql_composites"
  end

  def setup
    super

    @connection = ActiveRecord::Base.connection
    @connection.transaction do
      @connection.execute <<-SQL
         CREATE TYPE full_address AS
         (
             city VARCHAR(90),
             street VARCHAR(90)
         );
        SQL
      @connection.create_table("postgresql_composites") do |t|
        t.column :address, :full_address
      end
    end
  end

  def teardown
    super

    @connection.drop_table "postgresql_composites", if_exists: true
    @connection.execute "DROP TYPE IF EXISTS full_address"
    reset_connection
    PostgresqlComposite.reset_column_information
  end
end

# Composites are mapped to `OID::Identity` by default. The user is informed by a warning like:
#   "unknown OID 5653508: failed to recognize type of 'address'. It will be treated as String."
# To take full advantage of composite types, we suggest you register your own +OID::Type+.
# See PostgresqlCompositeWithCustomOIDTest
class PostgresqlCompositeTest < ActiveRecord::PostgreSQLTestCase
  include PostgresqlCompositeBehavior

  def test_column
    ensure_warning_is_issued

    column = PostgresqlComposite.columns_hash["address"]
    assert_nil column.type
    assert_equal "full_address", column.sql_type
    assert_not column.array?

    type = PostgresqlComposite.type_for_attribute("address")
    assert_not type.binary?
  end

  def test_composite_mapping
    ensure_warning_is_issued

    @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));"
    composite = PostgresqlComposite.first
    assert_equal "(Paris,Champs-Élysées)", composite.address

    composite.address = "(Paris,Rue Basse)"
    composite.save!

    assert_equal '(Paris,"Rue Basse")', composite.reload.address
  end

  private
    def ensure_warning_is_issued
      warning = capture(:stderr) do
        PostgresqlComposite.columns_hash
      end
      assert_match(/unknown OID \d+: failed to recognize type of 'address'\. It will be treated as String\./, warning)
    end
end

class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase
  include PostgresqlCompositeBehavior

  class FullAddressType < ActiveRecord::Type::Value
    def type; :full_address end

    def deserialize(value)
      if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/
        FullAddress.new($1, $2)
      end
    end

    def cast(value)
      value
    end

    def serialize(value)
      return if value.nil?
      "(#{value.city},#{value.street})"
    end
  end

  FullAddress = Struct.new(:city, :street)

  def setup
    super

    @connection.type_map.register_type "full_address", FullAddressType.new
  end

  def test_column
    column = PostgresqlComposite.columns_hash["address"]
    assert_equal :full_address, column.type
    assert_equal "full_address", column.sql_type
    assert_not column.array?

    type = PostgresqlComposite.type_for_attribute("address")
    assert_not type.binary?
  end

  def test_composite_mapping
    @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));"
    composite = PostgresqlComposite.first
    assert_equal "Paris", composite.address.city
    assert_equal "Champs-Élysées", composite.address.street

    composite.address = FullAddress.new("Paris", "Rue Basse")
    composite.save!

    assert_equal "Paris", composite.reload.address.city
    assert_equal "Rue Basse", composite.reload.address.street
  end
end