1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Return a Point object from the PG Point type

This introduces a deprecation cycle to change the behavior of the
default point type in the PostgreSQL adapter. The old behavior will
continue to be available for the immediate future as `:legacy_point`.

The current behavior of returning an `Array` causes several problems,
the most significant of which is that we cannot differentiate between an
array of points, and a point itself in the case of a column with the
`point[]` type.

The attributes API gives us a reasonable way to have a proper
deprecation cycle for this change, so let's take advantage of it. If we
like this change, we can also add proper support for the other geometric
types (line, lseg, box, path, polygon, and circle), all of which are
just aliases for string today.

Fixes #20441
This commit is contained in:
Sean Griffin 2015-06-05 09:02:25 -06:00
parent 3673955506
commit 9f4a3fd738
6 changed files with 189 additions and 11 deletions

View file

@ -1,3 +1,8 @@
* Deprecate the PG `:point` type in favor of a new one which will return
`Point` objects instead of an `Array`
*Sean Griffin*
* Ensure symbols passed to `ActiveRecord::Relation#select` are always treated * Ensure symbols passed to `ActiveRecord::Relation#select` are always treated
as columns. as columns.

View file

@ -12,6 +12,7 @@ require 'active_record/connection_adapters/postgresql/oid/json'
require 'active_record/connection_adapters/postgresql/oid/jsonb' require 'active_record/connection_adapters/postgresql/oid/jsonb'
require 'active_record/connection_adapters/postgresql/oid/money' require 'active_record/connection_adapters/postgresql/oid/money'
require 'active_record/connection_adapters/postgresql/oid/point' require 'active_record/connection_adapters/postgresql/oid/point'
require 'active_record/connection_adapters/postgresql/oid/rails_5_1_point'
require 'active_record/connection_adapters/postgresql/oid/range' require 'active_record/connection_adapters/postgresql/oid/range'
require 'active_record/connection_adapters/postgresql/oid/specialized_string' require 'active_record/connection_adapters/postgresql/oid/specialized_string'
require 'active_record/connection_adapters/postgresql/oid/uuid' require 'active_record/connection_adapters/postgresql/oid/uuid'

View file

@ -0,0 +1,50 @@
module ActiveRecord
Point = Struct.new(:x, :y)
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class Rails51Point < Type::Value # :nodoc:
include Type::Helpers::Mutable
def type
:point
end
def cast(value)
case value
when ::String
if value[0] == '(' && value[-1] == ')'
value = value[1...-1]
end
x, y = value.split(",")
build_point(x, y)
when ::Array
build_point(*value)
else
value
end
end
def serialize(value)
if value.is_a?(ActiveRecord::Point)
"(#{number_for_point(value.x)},#{number_for_point(value.y)})"
else
super
end
end
private
def number_for_point(number)
number.to_s.gsub(/\.0$/, '')
end
def build_point(x, y)
ActiveRecord::Point.new(Float(x), Float(y))
end
end
end
end
end
end

View file

@ -838,6 +838,8 @@ module ActiveRecord
ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql)
ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql)
ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql)
ActiveRecord::Type.register(:legacy_point, OID::Point, adapter: :postgresql)
ActiveRecord::Type.register(:rails_5_1_point, OID::Rails51Point, adapter: :postgresql)
ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql) ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql)
ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql) ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql)
ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql) ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql)

View file

@ -310,6 +310,7 @@ module ActiveRecord
def load_schema! def load_schema!
@columns_hash = connection.schema_cache.columns_hash(table_name) @columns_hash = connection.schema_cache.columns_hash(table_name)
@columns_hash.each do |name, column| @columns_hash.each do |name, column|
warn_if_deprecated_type(column)
define_attribute( define_attribute(
name, name,
connection.lookup_cast_type_from_column(column), connection.lookup_cast_type_from_column(column),
@ -356,6 +357,28 @@ module ActiveRecord
base.table_name base.table_name
end end
end end
def warn_if_deprecated_type(column)
return if attributes_to_define_after_schema_loads.key?(column.name)
if column.respond_to?(:oid) && column.sql_type.start_with?("point")
if column.array?
array_arguments = ", array: true"
else
array_arguments = ""
end
ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc)
The behavior of the `:point` type will be changing in Rails 5.1 to
return a `Point` object, rather than an `Array`. If you'd like to
keep the old behavior, you can add this line to #{self.name}:
attribute :#{column.name}, :legacy_point#{array_arguments}
If you'd like the new behavior today, you can add this line:
attribute :#{column.name}, :rails_5_1_point#{array_arguments}
WARNING
end
end
end end
end end
end end

View file

@ -6,7 +6,15 @@ class PostgresqlPointTest < ActiveRecord::TestCase
include ConnectionHelper include ConnectionHelper
include SchemaDumpingHelper include SchemaDumpingHelper
class PostgresqlPoint < ActiveRecord::Base; end class PostgresqlPoint < ActiveRecord::Base
attribute :x, :rails_5_1_point
attribute :y, :rails_5_1_point
attribute :z, :rails_5_1_point
attribute :array_of_points, :rails_5_1_point, array: true
attribute :legacy_x, :legacy_point
attribute :legacy_y, :legacy_point
attribute :legacy_z, :legacy_point
end
def setup def setup
@connection = ActiveRecord::Base.connection @connection = ActiveRecord::Base.connection
@ -14,11 +22,27 @@ class PostgresqlPointTest < ActiveRecord::TestCase
t.point :x t.point :x
t.point :y, default: [12.2, 13.3] t.point :y, default: [12.2, 13.3]
t.point :z, default: "(14.4,15.5)" t.point :z, default: "(14.4,15.5)"
t.point :array_of_points, array: true
t.point :legacy_x
t.point :legacy_y, default: [12.2, 13.3]
t.point :legacy_z, default: "(14.4,15.5)"
end
@connection.create_table('deprecated_points') do |t|
t.point :x
end end
end end
teardown do teardown do
@connection.drop_table 'postgresql_points', if_exists: true @connection.drop_table 'postgresql_points', if_exists: true
@connection.drop_table 'deprecated_points', if_exists: true
end
class DeprecatedPoint < ActiveRecord::Base; end
def test_deprecated_legacy_type
assert_deprecated do
DeprecatedPoint.new
end
end end
def test_column def test_column
@ -32,11 +56,11 @@ class PostgresqlPointTest < ActiveRecord::TestCase
end end
def test_default def test_default
assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y'] assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.column_defaults['y']
assert_equal [12.2, 13.3], PostgresqlPoint.new.y assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.new.y
assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z'] assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.column_defaults['z']
assert_equal [14.4, 15.5], PostgresqlPoint.new.z assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.new.z
end end
def test_schema_dumping def test_schema_dumping
@ -49,22 +73,95 @@ class PostgresqlPointTest < ActiveRecord::TestCase
def test_roundtrip def test_roundtrip
PostgresqlPoint.create! x: [10, 25.2] PostgresqlPoint.create! x: [10, 25.2]
record = PostgresqlPoint.first record = PostgresqlPoint.first
assert_equal [10, 25.2], record.x assert_equal ActiveRecord::Point.new(10, 25.2), record.x
record.x = [1.1, 2.2] record.x = ActiveRecord::Point.new(1.1, 2.2)
record.save! record.save!
assert record.reload assert record.reload
assert_equal [1.1, 2.2], record.x assert_equal ActiveRecord::Point.new(1.1, 2.2), record.x
end end
def test_mutation def test_mutation
p = PostgresqlPoint.create! x: [10, 20] p = PostgresqlPoint.create! x: ActiveRecord::Point.new(10, 20)
p.x[1] = 25 p.x.y = 25
p.save! p.save!
p.reload p.reload
assert_equal [10.0, 25.0], p.x assert_equal ActiveRecord::Point.new(10.0, 25.0), p.x
assert_not p.changed?
end
def test_array_assignment
p = PostgresqlPoint.new(x: [1, 2])
assert_equal ActiveRecord::Point.new(1, 2), p.x
end
def test_string_assignment
p = PostgresqlPoint.new(x: "(1, 2)")
assert_equal ActiveRecord::Point.new(1, 2), p.x
end
def test_array_of_points_round_trip
expected_value = [
ActiveRecord::Point.new(1, 2),
ActiveRecord::Point.new(2, 3),
ActiveRecord::Point.new(3, 4),
]
p = PostgresqlPoint.new(array_of_points: expected_value)
assert_equal expected_value, p.array_of_points
p.save!
p.reload
assert_equal expected_value, p.array_of_points
end
def test_legacy_column
column = PostgresqlPoint.columns_hash["legacy_x"]
assert_equal :point, column.type
assert_equal "point", column.sql_type
assert_not column.array?
type = PostgresqlPoint.type_for_attribute("legacy_x")
assert_not type.binary?
end
def test_legacy_default
assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['legacy_y']
assert_equal [12.2, 13.3], PostgresqlPoint.new.legacy_y
assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['legacy_z']
assert_equal [14.4, 15.5], PostgresqlPoint.new.legacy_z
end
def test_legacy_schema_dumping
output = dump_table_schema("postgresql_points")
assert_match %r{t\.point\s+"legacy_x"$}, output
assert_match %r{t\.point\s+"legacy_y",\s+default: \[12\.2, 13\.3\]$}, output
assert_match %r{t\.point\s+"legacy_z",\s+default: \[14\.4, 15\.5\]$}, output
end
def test_legacy_roundtrip
PostgresqlPoint.create! legacy_x: [10, 25.2]
record = PostgresqlPoint.first
assert_equal [10, 25.2], record.legacy_x
record.legacy_x = [1.1, 2.2]
record.save!
assert record.reload
assert_equal [1.1, 2.2], record.legacy_x
end
def test_legacy_mutation
p = PostgresqlPoint.create! legacy_x: [10, 20]
p.legacy_x[1] = 25
p.save!
p.reload
assert_equal [10.0, 25.0], p.legacy_x
assert_not p.changed? assert_not p.changed?
end end
end end