aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb3
-rw-r--r--actionview/test/template/translation_helper_test.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb71
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb68
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb101
5 files changed, 176 insertions, 73 deletions
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index 0bc40874d9..17ec6a40bf 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -7,7 +7,7 @@ module ActionView
module TranslationHelper
# Delegates to <tt>I18n#translate</tt> but also performs three additional functions.
#
- # First, it will ensure that any thrown +MissingTranslation+ messages will be turned
+ # First, it will ensure that any thrown +MissingTranslation+ messages will be turned
# into inline spans that:
#
# * have a "translation-missing" class set,
@@ -34,6 +34,7 @@ module ActionView
# naming convention helps to identify translations that include HTML tags so that
# you know what kind of output to expect when you call translate in a template.
def translate(key, options = {})
+ options = options.dup
options[:default] = wrap_translate_defaults(options[:default]) if options[:default]
# If the user has specified rescue_format then pass it all through, otherwise use
diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb
index c4770840fb..a9d5ea7345 100644
--- a/actionview/test/template/translation_helper_test.rb
+++ b/actionview/test/template/translation_helper_test.rb
@@ -151,4 +151,10 @@ class TranslationHelperTest < ActiveSupport::TestCase
translation = translate(:'translations.missing', default: ['A Generic String', 'Second generic string'])
assert_equal 'A Generic String', translation
end
+
+ def test_translate_does_not_change_options
+ options = {}
+ translate(:'translations.missing', options)
+ assert_equal({}, options)
+ end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index b173163e41..1e89f8cfd6 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -374,6 +374,77 @@ This is not reliable and will be removed in the future.
end
end
+ # This class uses the data from PostgreSQL pg_type table to build
+ # the OID -> Type mapping.
+ # - OID is and integer representing the type.
+ # - Type is an OID::Type object.
+ # This class has side effects on the +store+ passed during initialization.
+ class TypeMapInitializer # :nodoc:
+ def initialize(store)
+ @store = store
+ end
+
+ def run(records)
+ mapped, nodes = records.partition { |row| OID.registered_type? row['typname'] }
+ ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' }
+ enums, nodes = nodes.partition { |row| row['typtype'] == 'e' }
+ domains, nodes = nodes.partition { |row| row['typtype'] == 'd' }
+ arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
+ composites, nodes = nodes.partition { |row| row['typelem'] != '0' }
+
+ mapped.each { |row| register_mapped_type(row) }
+ enums.each { |row| register_enum_type(row) }
+ domains.each { |row| register_domain_type(row) }
+ arrays.each { |row| register_array_type(row) }
+ ranges.each { |row| register_range_type(row) }
+ composites.each { |row| register_composite_type(row) }
+ end
+
+ private
+ def register_mapped_type(row)
+ register row['oid'], OID::NAMES[row['typname']]
+ end
+
+ def register_enum_type(row)
+ register row['oid'], OID::Enum.new
+ end
+
+ def register_array_type(row)
+ if subtype = @store[row['typelem'].to_i]
+ register row['oid'], OID::Array.new(subtype)
+ end
+ end
+
+ def register_range_type(row)
+ if subtype = @store[row['rngsubtype'].to_i]
+ register row['oid'], OID::Range.new(subtype)
+ end
+ end
+
+ def register_domain_type(row)
+ if base_type = @store[row["typbasetype"].to_i]
+ register row['oid'], base_type
+ else
+ warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}."
+ end
+ end
+
+ def register_composite_type(row)
+ if subtype = @store[row['typelem'].to_i]
+ register row['oid'], OID::Vector.new(row['typdelim'], subtype)
+ end
+ end
+
+ def register(oid, oid_type)
+ oid = oid.to_i
+
+ raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil?
+ return if @store.key?(oid)
+
+ @store[oid] = oid_type
+ end
+ end
+
# When the PG adapter connects, the pg_type table is queried. The
# key of this hash maps to the `typname` column from the table.
# type_map is then dynamically built with oids as the key and type
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 764cb576d9..4908c5a47f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -571,25 +571,6 @@ module ActiveRecord
initialize_type_map(type_map)
end
- def add_oid(row, records_by_oid, type_map)
- return type_map if type_map.key? row['type_elem'].to_i
-
- if OID.registered_type? row['typname']
- # this composite type is explicitly registered
- vector = OID::NAMES[row['typname']]
- else
- # use the default for composite types
- unless type_map.key? row['typelem'].to_i
- add_oid records_by_oid[row['typelem']], records_by_oid, type_map
- end
-
- vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i]
- end
-
- type_map[row['oid'].to_i] = vector
- type_map
- end
-
def initialize_type_map(type_map, oids = nil)
if supports_ranges?
query = <<-SQL
@@ -608,52 +589,9 @@ module ActiveRecord
query += "WHERE t.oid::integer IN (%s)" % oids.join(", ")
end
- result = execute(query, 'SCHEMA')
- ranges, nodes = result.partition { |row| row['typtype'] == 'r' }
- enums, nodes = nodes.partition { |row| row['typtype'] == 'e' }
- domains, nodes = nodes.partition { |row| row['typtype'] == 'd' }
- arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
- leaves, nodes = nodes.partition { |row| row['typelem'] == '0' }
-
- # populate the enum types
- enums.each do |row|
- type_map[row['oid'].to_i] = OID::Enum.new
- end
-
- # populate the base types
- leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
- type_map[row['oid'].to_i] = OID::NAMES[row['typname']]
- end
-
- records_by_oid = result.group_by { |row| row['oid'] }
-
- # populate composite types
- nodes.each do |row|
- add_oid row, records_by_oid, type_map
- end
-
- # populate array types
- arrays.find_all { |row| type_map.key? row['typelem'].to_i }.each do |row|
- array = OID::Array.new type_map[row['typelem'].to_i]
- type_map[row['oid'].to_i] = array
- end
-
- # populate range types
- ranges.find_all { |row| type_map.key? row['rngsubtype'].to_i }.each do |row|
- subtype = type_map[row['rngsubtype'].to_i]
- range = OID::Range.new subtype
- type_map[row['oid'].to_i] = range
- end
-
- # populate domain types
- domains.each do |row|
- base_type_oid = row["typbasetype"].to_i
- if base_type = type_map[base_type_oid]
- type_map[row['oid'].to_i] = base_type
- else
- warn "unknown base type (OID: #{base_type_oid}) for domain #{row["typname"]}."
- end
- end
+ initializer = OID::TypeMapInitializer.new(type_map)
+ records = execute(query, 'SCHEMA')
+ initializer.run(records)
end
FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:
diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb
index 224b1b770b..68b9e6daf7 100644
--- a/activerecord/test/cases/adapters/postgresql/composite_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb
@@ -1,19 +1,19 @@
# -*- coding: utf-8 -*-
require "cases/helper"
+require 'support/connection_helper'
require 'active_record/base'
require 'active_record/connection_adapters/postgresql_adapter'
-class PostgresqlCompositeTest < ActiveRecord::TestCase
+module PostgresqlCompositeBehavior
+ include ConnectionHelper
+
class PostgresqlComposite < ActiveRecord::Base
self.table_name = "postgresql_composites"
end
- teardown do
- @connection.execute 'DROP TABLE IF EXISTS postgresql_composites'
- @connection.execute 'DROP TYPE IF EXISTS full_address'
- end
-
def setup
+ super
+
@connection = ActiveRecord::Base.connection
@connection.transaction do
@connection.execute <<-SQL
@@ -29,9 +29,27 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase
end
end
+ def teardown
+ super
+
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_composites'
+ @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::TestCase
+ include PostgresqlCompositeBehavior
+
def test_column
+ ensure_warning_is_issued
+
column = PostgresqlComposite.columns_hash["address"]
- # TODO: Composite columns should have a type
assert_nil column.type
assert_equal "full_address", column.sql_type
assert_not column.number?
@@ -41,6 +59,8 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase
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
@@ -50,4 +70,71 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase
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::TestCase
+ include PostgresqlCompositeBehavior
+
+ class FullAddressType
+ def type; :full_address end
+ def simplified_type(sql_type); type end
+
+ def type_cast(value)
+ if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/
+ FullAddress.new($1, $2)
+ end
+ end
+
+ def type_cast_for_write(value)
+ "(#{value.city},#{value.street})"
+ end
+ end
+
+ FullAddress = Struct.new(:city, :street)
+
+ def setup
+ super
+
+ @registration = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID
+ @registration.register_type "full_address", FullAddressType.new
+ end
+
+ def teardown
+ super
+
+ # there is currently no clean way to unregister a OID::Type
+ @registration::NAMES.delete("full_address")
+ 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.number?
+ assert_not column.text?
+ assert_not column.binary?
+ assert_not column.array
+ 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")
+ skip "Saving with custom OID type is currently not supported."
+ composite.save!
+
+ assert_equal 'Paris', composite.reload.address.city
+ assert_equal 'Rue Basse', composite.reload.address.street
+ end
end