diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 054b14f1..b5c97c00 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,14 +39,14 @@ jobs: --health-timeout 5s --health-retries 5 env: - AR_VERSION: 8.0.0.1 + AR_VERSION: 8.1.1 strategy: fail-fast: false matrix: # https://ruby-lang.org/en/downloads/branches ruby: ["3.4", "3.3", "3.2"] # https://www.postgresql.org/support/versioning/ - pg: [12-master, 13-master, 14-master, 15-master, 16-master] + pg: [14-master, 15-master, 16-master, 17-master, 18-3.6-alpine] steps: - name: Set Up Actions uses: actions/checkout@v4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b6ff06d..a150be69 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,8 +40,8 @@ Make sure the tests pass: Run tests with a specific ActiveRecord version: ```sh -AR_VERSION=7.0.1 bundle install -AR_VERSION=7.0.1 bundle exec rake test +AR_VERSION=8.1.0 bundle install +AR_VERSION=8.1.0 bundle exec rake test ``` To run a specific test, use the `POSTGIS_TEST_FILES` environment variable: diff --git a/activerecord-postgis-adapter.gemspec b/activerecord-postgis-adapter.gemspec index 53cf6952..6174e12f 100644 --- a/activerecord-postgis-adapter.gemspec +++ b/activerecord-postgis-adapter.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.summary = "ActiveRecord adapter for PostGIS, based on RGeo." spec.description = "ActiveRecord connection adapter for PostGIS. It is based on the stock " \ - "PostgreSQL adapter, and adds built-in support for the spatial extensions "\ + "PostgreSQL adapter, and adds built-in support for the spatial extensions " \ "provided by PostGIS. It uses the RGeo library to represent spatial data in Ruby." spec.version = ActiveRecord::ConnectionAdapters::PostGIS::VERSION @@ -20,8 +20,8 @@ Gem::Specification.new do |spec| # ruby-lang.org/en/downloads/branches spec.required_ruby_version = ">= 3.2.0" - spec.add_dependency "activerecord", "~> 8.0.0" - spec.add_dependency "rgeo-activerecord", "~> 8.0.0" + spec.add_dependency "activerecord", "~> 8.1.0" + spec.add_dependency "rgeo-activerecord", "~> 8.1.0" spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "minitest", "~> 5.4" diff --git a/lib/active_record/connection_adapters/postgis/spatial_column.rb b/lib/active_record/connection_adapters/postgis/column.rb similarity index 67% rename from lib/active_record/connection_adapters/postgis/spatial_column.rb rename to lib/active_record/connection_adapters/postgis/column.rb index 9457e390..bb4d64d0 100644 --- a/lib/active_record/connection_adapters/postgis/spatial_column.rb +++ b/lib/active_record/connection_adapters/postgis/column.rb @@ -3,14 +3,15 @@ module ActiveRecord # :nodoc: module ConnectionAdapters # :nodoc: module PostGIS # :nodoc: - class SpatialColumn < ConnectionAdapters::PostgreSQLColumn # :nodoc: + class Column < PostgreSQL::Column # :nodoc: # sql_type examples: # "Geometry(Point,4326)" # "Geography(Point,4326)" - def initialize(name, default, sql_type_metadata = nil, null = true, + def initialize(name, cast_type, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, serial: nil, generated: nil, spatial: nil, identity: nil) - @sql_type_metadata = sql_type_metadata + super(name, cast_type, default, sql_type_metadata, null, default_function, + collation: collation, comment: comment, serial: serial, generated: generated, identity: identity) @geographic = !!(sql_type_metadata.sql_type =~ /geography\(/i) if spatial # This case comes from an entry in the geometry_columns table @@ -30,8 +31,6 @@ def initialize(name, default, sql_type_metadata = nil, null = true, # @geometric_type = geo_type_from_sql_type(sql_type) build_from_sql_type(sql_type_metadata.sql_type) end - super(name, default, sql_type_metadata, null, default_function, - collation: collation, comment: comment, serial: serial, generated: generated, identity: identity) if spatial? && @srid @limit = { srid: @srid, type: to_type_name(geometric_type) } @limit[:has_z] = true if @has_z @@ -58,6 +57,49 @@ def spatial? %i[geometry geography].include?(@sql_type_metadata.type) end + def init_with(coder) + @geographic = coder["geographic"] + @geometric_type = coder["geometric_type"] + @has_m = coder["has_m"] + @has_z = coder["has_z"] + @srid = coder["srid"] + @limit = coder["limit"] + super + end + + def encode_with(coder) + coder["geographic"] = @geographic + coder["geometric_type"] = @geometric_type + coder["has_m"] = @has_m + coder["has_z"] = @has_z + coder["srid"] = @srid + coder["limit"] = @limit + super + end + + def ==(other) + other.is_a?(Column) && + super && + other.geographic == geographic && + other.geometric_type == geometric_type && + other.has_m == has_m && + other.has_z == has_z && + other.srid == srid && + other.limit == limit + end + alias :eql? :== + + def hash + Column.hash ^ + super.hash ^ + geographic.hash ^ + geometric_type.hash ^ + has_m.hash ^ + has_z.hash ^ + srid.hash ^ + limit.hash + end + private def set_geometric_type_from_name(name) diff --git a/lib/active_record/connection_adapters/postgis/oid/spatial.rb b/lib/active_record/connection_adapters/postgis/oid/spatial.rb index 46ed619f..d44c933b 100644 --- a/lib/active_record/connection_adapters/postgis/oid/spatial.rb +++ b/lib/active_record/connection_adapters/postgis/oid/spatial.rb @@ -10,12 +10,17 @@ module OID # Responsible for parsing sql_types returned from the database and WKT features. class Spatial < Type::Value 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 + super() + @geographic = geographic.freeze + @factory_attrs = { + geo_type: geo_type.underscore.freeze, + has_m: has_m.freeze, + has_z: has_z.freeze, + srid: srid.freeze, + sql_type: type.to_s.freeze + }.freeze end + protected attr_reader :geographic, :factory_attrs # sql_type: geometry, geometry(Point), geometry(Point,4326), ... # @@ -53,10 +58,9 @@ def self.parse_sql_type(sql_type) end def spatial_factory - @spatial_factory ||= - RGeo::ActiveRecord::SpatialFactoryStore.instance.factory( - factory_attrs - ) + RGeo::ActiveRecord::SpatialFactoryStore.instance.factory( + factory_attrs + ) end def spatial? @@ -80,6 +84,17 @@ def serialize(value) .generate(geo_value) end + def ==(other) + super && + @geographic == other.geographic && + @factory_attrs == other.factory_attrs + end + alias eql? == + + def hash + super ^ [@geographic, @factory_attrs].hash + end + private def cast_value(value) @@ -105,16 +120,6 @@ def wkt_parser(string) RGeo::WKRep::WKTParser.new(spatial_factory, support_ewkt: true, default_srid: @srid) end end - - def factory_attrs - { - geo_type: @geo_type.underscore, - has_m: @has_m, - has_z: @has_z, - srid: @srid, - sql_type: type.to_s - } - end end end end diff --git a/lib/active_record/connection_adapters/postgis/quoting.rb b/lib/active_record/connection_adapters/postgis/quoting.rb index 1d7c8b98..87b1d5c3 100644 --- a/lib/active_record/connection_adapters/postgis/quoting.rb +++ b/lib/active_record/connection_adapters/postgis/quoting.rb @@ -12,6 +12,21 @@ def type_cast(value) super end end + + # NOTE: This method should be private in future rails versions. + # Hence we should also make it private then. + # + # See https://github.com/rails/rails/blob/v8.1.1/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb#L190 + def lookup_cast_type(sql_type) + type_map.lookup( + # oid + query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i, + # fmod, not needed. + nil, + # details needed for `..::PostGIS::OID::Spatial` (e.g. `geometry(point,3857)`) + sql_type + ) + end end end end diff --git a/lib/active_record/connection_adapters/postgis/schema_statements.rb b/lib/active_record/connection_adapters/postgis/schema_statements.rb index 62f3f754..6a021797 100644 --- a/lib/active_record/connection_adapters/postgis/schema_statements.rb +++ b/lib/active_record/connection_adapters/postgis/schema_statements.rb @@ -12,11 +12,11 @@ def new_column_from_field(table_name, field, _definitions) type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i) default_value = extract_value_from_default(default) - if attgenerated.present? - default_function = default - else - default_function = extract_default_function(default_value, default) - end + default_function = if attgenerated.present? + default + else + extract_default_function(default_value, default) + end if (match = default_function&.match(/\Anextval\('"?(?.+_(?seq\d*))"?'::regclass\)\z/)) serial = sequence_name_from_parts(table_name, column_name, match[:suffix]) == match[:sequence_name] @@ -25,8 +25,9 @@ def new_column_from_field(table_name, field, _definitions) # {:dimension=>2, :has_m=>false, :has_z=>false, :name=>"latlon", :srid=>0, :type=>"GEOMETRY"} spatial = spatial_column_info(table_name).get(column_name, type_metadata.sql_type) - SpatialColumn.new( + Column.new( column_name, + get_oid_type(oid.to_i, fmod.to_i, column_name, type), default_value, type_metadata, !notnull, diff --git a/lib/active_record/connection_adapters/postgis_adapter.rb b/lib/active_record/connection_adapters/postgis_adapter.rb index 1a823e6e..1a749e4b 100644 --- a/lib/active_record/connection_adapters/postgis_adapter.rb +++ b/lib/active_record/connection_adapters/postgis_adapter.rb @@ -12,7 +12,7 @@ require_relative "postgis/database_statements" require_relative "postgis/spatial_column_info" require_relative "postgis/spatial_table_definition" -require_relative "postgis/spatial_column" +require_relative "postgis/column" require_relative "postgis/arel_tosql" require_relative "postgis/oid/spatial" require_relative "postgis/oid/date_time" @@ -84,7 +84,7 @@ def initialize_type_map(map = type_map) # "geometry(Polygon,4326) NOT NULL" # "geometry(Geography,4326)" geo_type, srid, has_z, has_m, geographic = PostGIS::OID::Spatial.parse_sql_type(sql_type) - PostGIS::OID::Spatial.new(geo_type: geo_type, srid: srid, has_z: has_z, has_m: has_m, geographic: geographic) + PostGIS::OID::Spatial.new(geo_type: geo_type, srid: srid, has_z: has_z, has_m: has_m, geographic: geographic).freeze end end @@ -92,9 +92,7 @@ def initialize_type_map(map = type_map) end def native_database_types - @native_database_types ||= begin - default_types = PostgreSQLAdapter.native_database_types - default_types.merge({ + @native_database_types ||= super.merge({ geography: { name: "geography" }, geometry: { name: "geometry" }, geometry_collection: { name: "geometry_collection" }, @@ -106,7 +104,6 @@ def native_database_types st_point: { name: "st_point" }, st_polygon: { name: "st_polygon" } }) - end end end diff --git a/test/excludes/AssociationDeprecationTest/NotifyModeTest.rb b/test/excludes/AssociationDeprecationTest/NotifyModeTest.rb new file mode 100644 index 00000000..ed1f6040 --- /dev/null +++ b/test/excludes/AssociationDeprecationTest/NotifyModeTest.rb @@ -0,0 +1,2 @@ +require_relative "fix_backtrace_cleaner" +include(FixBacktraceCleaner) diff --git a/test/excludes/AssociationDeprecationTest/RaiseBacktraceModeTest.rb b/test/excludes/AssociationDeprecationTest/RaiseBacktraceModeTest.rb new file mode 100644 index 00000000..ed1f6040 --- /dev/null +++ b/test/excludes/AssociationDeprecationTest/RaiseBacktraceModeTest.rb @@ -0,0 +1,2 @@ +require_relative "fix_backtrace_cleaner" +include(FixBacktraceCleaner) diff --git a/test/excludes/AssociationDeprecationTest/RaiseModeTest.rb b/test/excludes/AssociationDeprecationTest/RaiseModeTest.rb new file mode 100644 index 00000000..ed1f6040 --- /dev/null +++ b/test/excludes/AssociationDeprecationTest/RaiseModeTest.rb @@ -0,0 +1,2 @@ +require_relative "fix_backtrace_cleaner" +include(FixBacktraceCleaner) diff --git a/test/excludes/AssociationDeprecationTest/WarnBacktraceModeTest.rb b/test/excludes/AssociationDeprecationTest/WarnBacktraceModeTest.rb new file mode 100644 index 00000000..ed1f6040 --- /dev/null +++ b/test/excludes/AssociationDeprecationTest/WarnBacktraceModeTest.rb @@ -0,0 +1,2 @@ +require_relative "fix_backtrace_cleaner" +include(FixBacktraceCleaner) diff --git a/test/excludes/AssociationDeprecationTest/WarnModeTest.rb b/test/excludes/AssociationDeprecationTest/WarnModeTest.rb new file mode 100644 index 00000000..ed1f6040 --- /dev/null +++ b/test/excludes/AssociationDeprecationTest/WarnModeTest.rb @@ -0,0 +1,2 @@ +require_relative "fix_backtrace_cleaner" +include(FixBacktraceCleaner) diff --git a/test/excludes/AssociationDeprecationTest/fix_backtrace_cleaner.rb b/test/excludes/AssociationDeprecationTest/fix_backtrace_cleaner.rb new file mode 100644 index 00000000..9ed08029 --- /dev/null +++ b/test/excludes/AssociationDeprecationTest/fix_backtrace_cleaner.rb @@ -0,0 +1,10 @@ +module FixBacktraceCleaner + def setup + super + bc = ActiveSupport::BacktraceCleaner.new + bc.remove_silencers! + bc.remove_filters! + bc.add_silencer { !_1.include?(::AssociationDeprecationTest::TestCase::THIS_FILE) } + ActiveRecord::LogSubscriber.backtrace_cleaner = bc + end +end