From 883e33ce51b41216362c130114593cb15a460e97 Mon Sep 17 00:00:00 2001 From: Keith Doggett Date: Thu, 11 Feb 2021 09:52:48 -0500 Subject: [PATCH 1/9] Add support for postgresql attributes and spatial types as attributes. --- .github/workflows/tests.yml | 4 +- .../postgis/oid/spatial.rb | 19 ++-- .../postgis/schema_statements.rb | 5 +- .../postgis/spatial_column.rb | 2 +- .../connection_adapters/postgis/type.rb | 29 +++++ .../connection_adapters/postgis_adapter.rb | 15 +++ test/attributes_test.rb | 100 ++++++++++++++++++ test/basic_test.rb | 9 +- test/test_helper.rb | 5 + test/type_test.rb | 36 +++---- 10 files changed, 188 insertions(+), 36 deletions(-) create mode 100644 lib/active_record/connection_adapters/postgis/type.rb create mode 100644 test/attributes_test.rb diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f9847839..b083b656 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,10 +18,12 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + env: + BUNDLE_GEMFILE: ${{matrix.gemfile}} strategy: matrix: ruby: [3.0, 2.7, 2.6, 2.5] - BUNDLE_GEMFILE: + gemfile: - gemfiles/ar61.gemfile pg: [9.6-3.0, 10-2.5, 11-3.0, 12-master, 13-master] steps: diff --git a/lib/active_record/connection_adapters/postgis/oid/spatial.rb b/lib/active_record/connection_adapters/postgis/oid/spatial.rb index 50eb2aa2..700fae1d 100644 --- a/lib/active_record/connection_adapters/postgis/oid/spatial.rb +++ b/lib/active_record/connection_adapters/postgis/oid/spatial.rb @@ -11,9 +11,12 @@ class Spatial < Type::Value # "geography(Point,4326)" # "geometry(Polygon,4326) NOT NULL" # "geometry(Geography,4326)" - def initialize(oid, sql_type) - @sql_type = sql_type - @geo_type, @srid, @has_z, @has_m = self.class.parse_sql_type(sql_type) + def initialize(geo_type: 'geometry', srid: 0, has_z: false, has_m: false, geographic: false) + @geo_type = geo_type + @srid = srid + @has_z = has_z + @has_m = has_m + @geographic = geographic end # sql_type: geometry, geometry(Point), geometry(Point,4326), ... @@ -43,7 +46,9 @@ def self.parse_sql_type(sql_type) # otherType(a,b) geo_type = sql_type end - [geo_type, srid, has_z, has_m] + geographic = !(sql_type =~ /geography/).nil? + + [geo_type, srid, has_z, has_m, geographic] end def spatial_factory @@ -53,16 +58,12 @@ def spatial_factory ) end - def geographic? - @sql_type =~ /geography/ - end - def spatial? true end def type - geographic? ? :geography : :geometry + @geographic ? :geography : :geometry end # support setting an RGeo object or a WKT string diff --git a/lib/active_record/connection_adapters/postgis/schema_statements.rb b/lib/active_record/connection_adapters/postgis/schema_statements.rb index 1a1be6a4..510aa1dd 100644 --- a/lib/active_record/connection_adapters/postgis/schema_statements.rb +++ b/lib/active_record/connection_adapters/postgis/schema_statements.rb @@ -96,8 +96,9 @@ def initialize_type_map(map = type_map) st_point st_polygon ).each do |geo_type| - map.register_type(geo_type) do |oid, _, sql_type| - OID::Spatial.new(oid, sql_type) + map.register_type(geo_type) do |_, _, sql_type| + geo_type, srid, has_z, has_m, geographic = OID::Spatial.parse_sql_type(sql_type) + OID::Spatial.new(geo_type: geo_type, srid: srid, has_z: has_z, has_m: has_m, geographic: geographic) end end diff --git a/lib/active_record/connection_adapters/postgis/spatial_column.rb b/lib/active_record/connection_adapters/postgis/spatial_column.rb index 528f72f9..75c1e7d4 100644 --- a/lib/active_record/connection_adapters/postgis/spatial_column.rb +++ b/lib/active_record/connection_adapters/postgis/spatial_column.rb @@ -67,7 +67,7 @@ def set_geometric_type_from_name(name) end def build_from_sql_type(sql_type) - geo_type, @srid, @has_z, @has_m = OID::Spatial.parse_sql_type(sql_type) + geo_type, @srid, @has_z, @has_m, @geographic = OID::Spatial.parse_sql_type(sql_type) set_geometric_type_from_name(geo_type) end diff --git a/lib/active_record/connection_adapters/postgis/type.rb b/lib/active_record/connection_adapters/postgis/type.rb new file mode 100644 index 00000000..f2402202 --- /dev/null +++ b/lib/active_record/connection_adapters/postgis/type.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module ActiveRecord + module Type + ## + # Attributes are looked up based on the type and the current adapter. + # For example, attribute :foo, :string has a type :string. + # + # The issue is that our adapter is :postgis, but all of the + # attributes are registered under :postgresql. This overwrite + # just forces us to always return :postgresql. + # + # The caveat is that when registering spatial types, we have + # to specify :postgesql as the adapter, not :postgis. + # + # ex. ActiveRecord::Type.register(:st_point, OID::Spatial, adapter: :postgresql) + class << self + def adapter_name_from(_model) + :postgresql + end + + private + + def current_adapter_name + :postgresql + end + end + end +end diff --git a/lib/active_record/connection_adapters/postgis_adapter.rb b/lib/active_record/connection_adapters/postgis_adapter.rb index c60ee076..d2f8de33 100644 --- a/lib/active_record/connection_adapters/postgis_adapter.rb +++ b/lib/active_record/connection_adapters/postgis_adapter.rb @@ -9,6 +9,7 @@ require "active_record/connection_adapters" require "active_record/connection_adapters/postgresql_adapter" +require "active_record/connection_adapters/postgis/type" require "active_record/connection_adapters/postgis/version" require "active_record/connection_adapters/postgis/column_methods" require "active_record/connection_adapters/postgis/schema_statements" @@ -87,6 +88,20 @@ def quote(value) super end end + + [ + :geography, + :geometry, + :geometry_collection, + :line_string, + :multi_line_string, + :multi_point, + :multi_polygon, + :st_point, + :st_polygon, + ].each do |geo_type| + ActiveRecord::Type.register(geo_type, PostGIS::OID::Spatial, adapter: :postgresql) + end end end end diff --git a/test/attributes_test.rb b/test/attributes_test.rb new file mode 100644 index 00000000..0e44494b --- /dev/null +++ b/test/attributes_test.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "test_helper" + +class Foo < ActiveRecord::Base + establish_test_connection + has_one :spatial_foo + attribute :bar, :string, array: true + attribute :baz, :string, range: true +end + +class SpatialFoo < ActiveRecord::Base + establish_test_connection + attribute :point, :st_point, srid: 3857 + attribute :pointz, :st_point, has_z: true, srid: 3509 + attribute :pointm, :st_point, has_m: true, srid: 3509 + attribute :polygon, :st_polygon, srid: 3857 + attribute :path, :line_string, srid: 3857 + attribute :geo_path, :line_string, geographic: true, srid: 4326 +end + +class AttributesTest < ActiveSupport::TestCase + def setup + reset_spatial_store + create_foo + create_spatial_foo + end + + def test_postgresql_attributes_registered + assert Foo.attribute_names.include?("bar") + assert Foo.attribute_names.include?("baz") + + data = Foo.new + data.bar = %w[a b c] + data.baz = "1".."3" + + assert_equal data.bar, %w[a b c] + assert_equal data.baz, "1".."3" + end + + def test_spatial_attributes + data = SpatialFoo.new + data.point = "POINT(0 0)" + data.pointz = "POINT(0 0 1)" + data.pointm = "POINT(0 0 2)" + data.polygon = "POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))" + data.path = "LINESTRING(0 0, 0 1, 1 1, 1 0, 0 0)" + data.geo_path = "LINESTRING(-75.165222 39.952583,-73.561668 45.508888)" + + assert_equal 3857, data.point.srid + assert_equal 0, data.point.x + assert_equal 0, data.point.y + + assert_equal 3509, data.pointz.srid + assert_equal 1, data.pointz.z + + assert_equal 3509, data.pointm.srid + assert_equal 2, data.pointm.m + + assert_equal 3857, data.polygon.srid + assert_equal 3857, data.path.srid + assert_equal data.path, data.polygon.exterior_ring + + assert_equal 4326, data.geo_path.srid + assert_equal RGeo::Geographic::Factory, data.geo_path.factory.class + end + + def test_joined_spatial_attribute + # TODO: The attributes that will be joined have to be defined on the + # model we make the query with. Ideally, it would "just work" but + # at least this workaround makes joining functional. + Foo.attribute :geo_point, :st_point, srid: 4326, geographic: true + Foo.attribute :cart_point, :st_point, srid: 3509 + + foo = Foo.create + SpatialFoo.create(foo_id: foo.id, geo_point: "POINT(10 10)", cart_point: "POINT(2 2)") + + # query foo and join child spatial foo on it + foo = Foo.joins(:spatial_foo).select("foos.id, spatial_foos.geo_point, spatial_foos.cart_point").first + assert_equal 4326, foo.geo_point.srid + assert_equal 3509, foo.cart_point.srid + assert_equal foo.geo_point, SpatialFoo.first.geo_point + assert_equal foo.cart_point, SpatialFoo.first.cart_point + end + + private + + def create_foo + Foo.connection.create_table :foos, force: true do |t| + end + end + + def create_spatial_foo + SpatialFoo.connection.create_table :spatial_foos, force: true do |t| + t.references :foo + t.st_point :geo_point, geographic: true, srid: 4326 + t.st_point :cart_point, srid: 3509 + end + end +end diff --git a/test/basic_test.rb b/test/basic_test.rb index 1bb6e932..2e36ab8a 100644 --- a/test/basic_test.rb +++ b/test/basic_test.rb @@ -3,6 +3,10 @@ require "test_helper" class BasicTest < ActiveSupport::TestCase + def setup + reset_spatial_store + end + def test_version refute_nil ActiveRecord::ConnectionAdapters::PostGIS::VERSION end @@ -112,7 +116,6 @@ def test_custom_factory object.save! object.reload assert_equal area.to_s, object.area.to_s - spatial_factory_store.clear end def test_spatial_factory_attrs_parsing @@ -135,8 +138,6 @@ def test_spatial_factory_attrs_parsing object.save! object.reload assert_equal(factory, object.areas.factory) - - spatial_factory_store.clear end def test_readme_example @@ -165,8 +166,6 @@ def test_readme_example object.save! object.reload refute_equal geo_factory, object.shape.factory - - spatial_factory_store.clear end def test_point_to_json diff --git a/test/test_helper.rb b/test/test_helper.rb index c1dace16..a372d5ab 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -48,5 +48,10 @@ def geographic_factory def spatial_factory_store RGeo::ActiveRecord::SpatialFactoryStore.instance end + + def reset_spatial_store + spatial_factory_store.clear + spatial_factory_store.default = nil + end end end diff --git a/test/type_test.rb b/test/type_test.rb index a69a4a0b..a6c1b873 100644 --- a/test/type_test.rb +++ b/test/type_test.rb @@ -4,33 +4,33 @@ class TypeTest < ActiveSupport::TestCase def test_parse_simple_type - assert_equal ["geometry", 0, false, false], spatial.parse_sql_type("geometry") - assert_equal ["geography", 0, false, false], spatial.parse_sql_type("geography") + assert_equal ["geometry", 0, false, false, false], spatial.parse_sql_type("geometry") + assert_equal ["geography", 0, false, false, true], spatial.parse_sql_type("geography") end def test_parse_geo_type - assert_equal ["Point", 0, false, false], spatial.parse_sql_type("geography(Point)") - assert_equal ["Point", 0, false, true], spatial.parse_sql_type("geography(PointM)") - assert_equal ["Point", 0, true, false], spatial.parse_sql_type("geography(PointZ)") - assert_equal ["Point", 0, true, true], spatial.parse_sql_type("geography(PointZM)") - assert_equal ["Polygon", 0, false, false], spatial.parse_sql_type("geography(Polygon)") - assert_equal ["Polygon", 0, true, false], spatial.parse_sql_type("geography(PolygonZ)") - assert_equal ["Polygon", 0, false, true], spatial.parse_sql_type("geography(PolygonM)") - assert_equal ["Polygon", 0, true, true], spatial.parse_sql_type("geography(PolygonZM)") + assert_equal ["Point", 0, false, false, true], spatial.parse_sql_type("geography(Point)") + assert_equal ["Point", 0, false, true, true], spatial.parse_sql_type("geography(PointM)") + assert_equal ["Point", 0, true, false, true], spatial.parse_sql_type("geography(PointZ)") + assert_equal ["Point", 0, true, true, true], spatial.parse_sql_type("geography(PointZM)") + assert_equal ["Polygon", 0, false, false, true], spatial.parse_sql_type("geography(Polygon)") + assert_equal ["Polygon", 0, true, false, true], spatial.parse_sql_type("geography(PolygonZ)") + assert_equal ["Polygon", 0, false, true, true], spatial.parse_sql_type("geography(PolygonM)") + assert_equal ["Polygon", 0, true, true, true], spatial.parse_sql_type("geography(PolygonZM)") end def test_parse_type_with_srid - assert_equal ["Point", 4326, false, false], spatial.parse_sql_type("geography(Point,4326)") - assert_equal ["Polygon", 4327, true, false], spatial.parse_sql_type("geography(PolygonZ,4327)") - assert_equal ["Point", 4328, false, true], spatial.parse_sql_type("geography(PointM,4328)") - assert_equal ["Point", 4329, true, true], spatial.parse_sql_type("geography(PointZM,4329)") - assert_equal ["MultiPolygon", 4326, false, false], spatial.parse_sql_type("geometry(MultiPolygon,4326)") + assert_equal ["Point", 4326, false, false, true], spatial.parse_sql_type("geography(Point,4326)") + assert_equal ["Polygon", 4327, true, false, true], spatial.parse_sql_type("geography(PolygonZ,4327)") + assert_equal ["Point", 4328, false, true, true], spatial.parse_sql_type("geography(PointM,4328)") + assert_equal ["Point", 4329, true, true, true], spatial.parse_sql_type("geography(PointZM,4329)") + assert_equal ["MultiPolygon", 4326, false, false, false], spatial.parse_sql_type("geometry(MultiPolygon,4326)") end def test_parse_non_geo_types - assert_equal ["x", 0, false, false], spatial.parse_sql_type("x") - assert_equal ["foo", 0, false, false], spatial.parse_sql_type("foo") - assert_equal ["foo(A,1234)", 0, false, false], spatial.parse_sql_type("foo(A,1234)") + assert_equal ["x", 0, false, false, false], spatial.parse_sql_type("x") + assert_equal ["foo", 0, false, false, false], spatial.parse_sql_type("foo") + assert_equal ["foo(A,1234)", 0, false, false, false], spatial.parse_sql_type("foo(A,1234)") end private From 3d9e77d7b7fa00821be52aa826c15673f13eef0a Mon Sep 17 00:00:00 2001 From: Keith Doggett Date: Mon, 22 Feb 2021 17:24:38 -0500 Subject: [PATCH 2/9] Remove type module override in favor of explicitly registering all postgres types under postgis --- .../postgis/oid/spatial.rb | 12 ++++---- .../postgis/schema_statements.rb | 6 ++++ .../connection_adapters/postgis/type.rb | 29 ------------------- .../connection_adapters/postgis_adapter.rb | 26 +++++++++++++++-- 4 files changed, 35 insertions(+), 38 deletions(-) delete mode 100644 lib/active_record/connection_adapters/postgis/type.rb diff --git a/lib/active_record/connection_adapters/postgis/oid/spatial.rb b/lib/active_record/connection_adapters/postgis/oid/spatial.rb index 700fae1d..b97cb155 100644 --- a/lib/active_record/connection_adapters/postgis/oid/spatial.rb +++ b/lib/active_record/connection_adapters/postgis/oid/spatial.rb @@ -4,13 +4,11 @@ module ActiveRecord module ConnectionAdapters module PostGIS module OID + # OID used to represent geometry/geography database types and attributes. + # + # Accepts `geo_type`, `srid`, `has_z`, `has_m`, and `geographic` as parameters. + # Responsible for parsing sql_types returned from the database and WKT features. class Spatial < Type::Value - # sql_type is a string that comes from the database definition - # examples: - # "geometry(Point,4326)" - # "geography(Point,4326)" - # "geometry(Polygon,4326) NOT NULL" - # "geometry(Geography,4326)" def initialize(geo_type: 'geometry', srid: 0, has_z: false, has_m: false, geographic: false) @geo_type = geo_type @srid = srid @@ -46,7 +44,7 @@ def self.parse_sql_type(sql_type) # otherType(a,b) geo_type = sql_type end - geographic = !(sql_type =~ /geography/).nil? + geographic = sql_type.match?(/geography/) [geo_type, srid, has_z, has_m, geographic] end diff --git a/lib/active_record/connection_adapters/postgis/schema_statements.rb b/lib/active_record/connection_adapters/postgis/schema_statements.rb index 510aa1dd..cf88ea1b 100644 --- a/lib/active_record/connection_adapters/postgis/schema_statements.rb +++ b/lib/active_record/connection_adapters/postgis/schema_statements.rb @@ -97,6 +97,12 @@ def initialize_type_map(map = type_map) st_polygon ).each do |geo_type| map.register_type(geo_type) do |_, _, sql_type| + # sql_type is a string that comes from the database definition + # examples: + # "geometry(Point,4326)" + # "geography(Point,4326)" + # "geometry(Polygon,4326) NOT NULL" + # "geometry(Geography,4326)" geo_type, srid, has_z, has_m, geographic = OID::Spatial.parse_sql_type(sql_type) OID::Spatial.new(geo_type: geo_type, srid: srid, has_z: has_z, has_m: has_m, geographic: geographic) end diff --git a/lib/active_record/connection_adapters/postgis/type.rb b/lib/active_record/connection_adapters/postgis/type.rb deleted file mode 100644 index f2402202..00000000 --- a/lib/active_record/connection_adapters/postgis/type.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module ActiveRecord - module Type - ## - # Attributes are looked up based on the type and the current adapter. - # For example, attribute :foo, :string has a type :string. - # - # The issue is that our adapter is :postgis, but all of the - # attributes are registered under :postgresql. This overwrite - # just forces us to always return :postgresql. - # - # The caveat is that when registering spatial types, we have - # to specify :postgesql as the adapter, not :postgis. - # - # ex. ActiveRecord::Type.register(:st_point, OID::Spatial, adapter: :postgresql) - class << self - def adapter_name_from(_model) - :postgresql - end - - private - - def current_adapter_name - :postgresql - end - end - end -end diff --git a/lib/active_record/connection_adapters/postgis_adapter.rb b/lib/active_record/connection_adapters/postgis_adapter.rb index d2f8de33..240b13c4 100644 --- a/lib/active_record/connection_adapters/postgis_adapter.rb +++ b/lib/active_record/connection_adapters/postgis_adapter.rb @@ -9,7 +9,6 @@ require "active_record/connection_adapters" require "active_record/connection_adapters/postgresql_adapter" -require "active_record/connection_adapters/postgis/type" require "active_record/connection_adapters/postgis/version" require "active_record/connection_adapters/postgis/column_methods" require "active_record/connection_adapters/postgis/schema_statements" @@ -89,6 +88,29 @@ def quote(value) end end + # Copied from https://github.com/rails/rails/blob/ee7cf8cf7569ef87079c48ee81c867eae5e24ed4/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L948-L967 + ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgis) + ActiveRecord::Type.add_modifier({ range: true }, OID::Range, adapter: :postgis) + ActiveRecord::Type.register(:bit, OID::Bit, adapter: :postgis, override: false) + ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgis, override: false) + ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgis, override: false) + ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgis, override: false) + ActiveRecord::Type.register(:date, OID::Date, adapter: :postgis, override: false) + ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgis, override: false) + ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgis, override: false) + ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgis, override: false) + ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgis, override: false) + ActiveRecord::Type.register(:inet, OID::Inet, adapter: :postgis, override: false) + ActiveRecord::Type.register(:interval, OID::Interval, adapter: :postgis, override: false) + ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgis, override: false) + ActiveRecord::Type.register(:money, OID::Money, adapter: :postgis, override: false) + ActiveRecord::Type.register(:point, OID::Point, adapter: :postgis, override: false) + ActiveRecord::Type.register(:legacy_point, OID::LegacyPoint, adapter: :postgis, override: false) + ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgis, override: false) + ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgis, override: false) + ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgis, override: false) + + # RGeo specific types [ :geography, :geometry, @@ -100,7 +122,7 @@ def quote(value) :st_point, :st_polygon, ].each do |geo_type| - ActiveRecord::Type.register(geo_type, PostGIS::OID::Spatial, adapter: :postgresql) + ActiveRecord::Type.register(geo_type, PostGIS::OID::Spatial, adapter: :postgis) end end end From f0f9b5039595c6bf3a934c4ea7f1080b4c2d2214 Mon Sep 17 00:00:00 2001 From: Keith Doggett Date: Thu, 25 Feb 2021 09:15:12 -0500 Subject: [PATCH 3/9] Monkeypatch ActiveRecord::Type with PostGIS specific version that performs type lookups on :postgis, then falls back to :postgresql --- .../connection_adapters/postgis/type.rb | 20 +++++++++++++++ .../connection_adapters/postgis_adapter.rb | 25 ++----------------- test/attributes_test.rb | 17 +++++++++++++ 3 files changed, 39 insertions(+), 23 deletions(-) create mode 100644 lib/active_record/connection_adapters/postgis/type.rb diff --git a/lib/active_record/connection_adapters/postgis/type.rb b/lib/active_record/connection_adapters/postgis/type.rb new file mode 100644 index 00000000..1a974abc --- /dev/null +++ b/lib/active_record/connection_adapters/postgis/type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module ActiveRecord + module ConnectionAdapters + module PostGIS + module Type + # Look for :postgis types first, then check for :postgresql + # types to simulate a kind of Type inheritance. + def lookup(*args, adapter: current_adapter_name, **kwargs) + registry.lookup(*args, adapter: adapter, **kwargs) + rescue ArgumentError + super(*args, adapter: :postgresql, **kwargs) + end + end + end + end + + # Type uses `class << self` syntax so we have to prepend to the singleton_class + Type.singleton_class.prepend(ActiveRecord::ConnectionAdapters::PostGIS::Type) +end diff --git a/lib/active_record/connection_adapters/postgis_adapter.rb b/lib/active_record/connection_adapters/postgis_adapter.rb index 240b13c4..65a81205 100644 --- a/lib/active_record/connection_adapters/postgis_adapter.rb +++ b/lib/active_record/connection_adapters/postgis_adapter.rb @@ -18,6 +18,7 @@ require "active_record/connection_adapters/postgis/arel_tosql" require "active_record/connection_adapters/postgis/setup" require "active_record/connection_adapters/postgis/oid/spatial" +require "active_record/connection_adapters/postgis/type" # has to be after oid/* require "active_record/connection_adapters/postgis/create_connection" require "active_record/connection_adapters/postgis/postgis_database_tasks" @@ -88,29 +89,7 @@ def quote(value) end end - # Copied from https://github.com/rails/rails/blob/ee7cf8cf7569ef87079c48ee81c867eae5e24ed4/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L948-L967 - ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgis) - ActiveRecord::Type.add_modifier({ range: true }, OID::Range, adapter: :postgis) - ActiveRecord::Type.register(:bit, OID::Bit, adapter: :postgis, override: false) - ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgis, override: false) - ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgis, override: false) - ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgis, override: false) - ActiveRecord::Type.register(:date, OID::Date, adapter: :postgis, override: false) - ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgis, override: false) - ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgis, override: false) - ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgis, override: false) - ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgis, override: false) - ActiveRecord::Type.register(:inet, OID::Inet, adapter: :postgis, override: false) - ActiveRecord::Type.register(:interval, OID::Interval, adapter: :postgis, override: false) - ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgis, override: false) - ActiveRecord::Type.register(:money, OID::Money, adapter: :postgis, override: false) - ActiveRecord::Type.register(:point, OID::Point, adapter: :postgis, override: false) - ActiveRecord::Type.register(:legacy_point, OID::LegacyPoint, adapter: :postgis, override: false) - ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgis, override: false) - ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgis, override: false) - ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgis, override: false) - - # RGeo specific types + # PostGIS specific types [ :geography, :geometry, diff --git a/test/attributes_test.rb b/test/attributes_test.rb index 0e44494b..e53ad251 100644 --- a/test/attributes_test.rb +++ b/test/attributes_test.rb @@ -19,11 +19,16 @@ class SpatialFoo < ActiveRecord::Base attribute :geo_path, :line_string, geographic: true, srid: 4326 end +class InvalidAttribute < ActiveRecord::Base + establish_test_connection +end + class AttributesTest < ActiveSupport::TestCase def setup reset_spatial_store create_foo create_spatial_foo + create_invalid_attributes end def test_postgresql_attributes_registered @@ -38,6 +43,13 @@ def test_postgresql_attributes_registered assert_equal data.baz, "1".."3" end + def test_invalid_attribute + assert_raises(ArgumentError) do + InvalidAttribute.attribute(:attr, :invalid_attr) + InvalidAttribute.new + end + end + def test_spatial_attributes data = SpatialFoo.new data.point = "POINT(0 0)" @@ -97,4 +109,9 @@ def create_spatial_foo t.st_point :cart_point, srid: 3509 end end + + def create_invalid_attributes + InvalidAttribute.connection.create_table :invalid_attributes, force: true do |t| + end + end end From d2a8a2ad3b1be96c1ad69277ac28698d1d13106f Mon Sep 17 00:00:00 2001 From: Keith Doggett Date: Thu, 25 Feb 2021 09:23:04 -0500 Subject: [PATCH 4/9] Simplify lookup override --- lib/active_record/connection_adapters/postgis/type.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/postgis/type.rb b/lib/active_record/connection_adapters/postgis/type.rb index 1a974abc..663bd930 100644 --- a/lib/active_record/connection_adapters/postgis/type.rb +++ b/lib/active_record/connection_adapters/postgis/type.rb @@ -7,7 +7,7 @@ module Type # Look for :postgis types first, then check for :postgresql # types to simulate a kind of Type inheritance. def lookup(*args, adapter: current_adapter_name, **kwargs) - registry.lookup(*args, adapter: adapter, **kwargs) + super rescue ArgumentError super(*args, adapter: :postgresql, **kwargs) end From 21aa84024efbe8a07f9f54b843d3abafb4453967 Mon Sep 17 00:00:00 2001 From: Keith Doggett Date: Thu, 25 Feb 2021 09:27:17 -0500 Subject: [PATCH 5/9] fix lookup for Ruby 3.x syntax --- lib/active_record/connection_adapters/postgis/type.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/postgis/type.rb b/lib/active_record/connection_adapters/postgis/type.rb index 663bd930..6a2c6ef7 100644 --- a/lib/active_record/connection_adapters/postgis/type.rb +++ b/lib/active_record/connection_adapters/postgis/type.rb @@ -7,7 +7,7 @@ module Type # Look for :postgis types first, then check for :postgresql # types to simulate a kind of Type inheritance. def lookup(*args, adapter: current_adapter_name, **kwargs) - super + super(*args, adapter: adapter, **kwargs) rescue ArgumentError super(*args, adapter: :postgresql, **kwargs) end From 59e038c2d337a5be41b20dee8a53d6700572769d Mon Sep 17 00:00:00 2001 From: Keith Doggett Date: Fri, 26 Feb 2021 10:58:35 -0500 Subject: [PATCH 6/9] Only allow postgresql type lookup if current_adapter_name is postgis --- lib/active_record/connection_adapters/postgis/type.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/postgis/type.rb b/lib/active_record/connection_adapters/postgis/type.rb index 6a2c6ef7..babd22c5 100644 --- a/lib/active_record/connection_adapters/postgis/type.rb +++ b/lib/active_record/connection_adapters/postgis/type.rb @@ -8,7 +8,9 @@ module Type # types to simulate a kind of Type inheritance. def lookup(*args, adapter: current_adapter_name, **kwargs) super(*args, adapter: adapter, **kwargs) - rescue ArgumentError + rescue ArgumentError => e + raise e unless current_adapter_name == :postgis + super(*args, adapter: :postgresql, **kwargs) end end From d6bda33f22f8171e5b89a7bae7bcead24f02ba21 Mon Sep 17 00:00:00 2001 From: Keith Doggett Date: Fri, 26 Feb 2021 11:21:29 -0500 Subject: [PATCH 7/9] Establish connection with ActiveRecord::Base in tests --- test/test_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_helper.rb b/test/test_helper.rb index a372d5ab..93820347 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -21,6 +21,8 @@ def self.establish_test_connection end end +ActiveRecord::Base.establish_test_connection + class SpatialModel < ActiveRecord::Base establish_test_connection end From cdcbf3ef66527253c5d93b5e724add6d4c2c034f Mon Sep 17 00:00:00 2001 From: Keith Doggett Date: Tue, 2 Mar 2021 11:45:09 -0500 Subject: [PATCH 8/9] Update README to show attributes usage --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ff7a40e..700154e4 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,29 @@ RGeo objects can be embedded in where clauses. ## Install -The adapter requires PostgreSQL 9.0+. +The adapter requires PostgreSQL 9.0+ and PostGIS 2.4+. + +### Installing PostGIS + +Here are common methods for installing PostGIS, but more detailed methods can be found on the [installation guide](https://postgis.net/install/). + +#### MacOS + +```sh +brew install postgis +``` + +#### Ubuntu/Debian + +```sh +# The second package can be replaced depending on your postgresql version +# ex. postgresql-11-postgis-2 is valid as well +sudo apt-get install postgis postgresql-12-postgis-3 +``` + +#### Windows + +PostGIS is likely available as an optional package via your Postgresql installer. If not, refer to the installation guide. Gemfile: @@ -305,6 +327,18 @@ change_table :my_table do |t| end ``` +### Attributes + +Models may also define attributes using the above data types and options. + +```ruby +class SpatialModel < ActiveRecord::Base + attribute :centroid, :st_point, srid: 4326, geographic: true +end +``` + +`centroid` will not have an associated column in the `spatial_models` table, but any geometry object assigned to the `centroid` attribute will be cast to a geographic point. + ### Point and Polygon Types with ActiveRecord 4.2+ Prior to version 3, the `point` and `polygon` types were supported. In ActiveRecord 4.2, the Postgresql @@ -489,6 +523,32 @@ containing_buiildings = Building.where(buildings[:geom].st_contains(point)) See [rgeo-activerecord](https://github.com/rgeo/rgeo-activerecord) for more information about advanced spatial queries. +### Joining Spatial Columns + +If a spatial column is joined with another model, `srid` and `geographic` will not be automatically inferred and they will default to 0 and `false`, by default. In order to properly infer these options after a join, an `attribute` must be created on the target table. + +```ruby +class SpatialModel < ActiveRecord::Base + belongs_to :foo + + # has column geo_point (:st_point, srid: 4326, geographic: true) +end + +class Foo < ActiveRecord::Base + has_one :spatial_model + + # re-define geo_point here so join works + attribute :geo_point, :st_point, srid: 4326, geographic: true +end + +# perform a query where geo_point is joined to foo +foo = Foo.joins(:spatial_models).select("foos.id, spatial_models.geo_point").first +p foo.geo_point.class +# => RGeo::Geographic::SphericalPointImpl +p foo.geo_point.srid +# => 4326 +``` + ## Background: PostGIS A spatial database is one that includes a set of data types, functions, From 48e7b5611e8916be3e9cf910eb0b2d493ae99d38 Mon Sep 17 00:00:00 2001 From: Keith Doggett Date: Thu, 4 Mar 2021 10:26:31 -0500 Subject: [PATCH 9/9] Update History --- History.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/History.md b/History.md index ffa44c84..3aada46a 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,12 @@ +### HEAD + +* Support Attributes #334 +* Access `configuration_hash` using symbols #335 + ### 7.0.1 / 2021-01-13 * Fix db:gis:setup task #329 + ### 7.0.0 / 2020-12-22 * Add ActiveRecord 6.1 Compatability (tagliala) #324