activerecord-postgis 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b452469d6861f42a259853b4f6b3f7e5a694d98c325141d5a43ac09eaf40e967
4
- data.tar.gz: ada960ed4d93542ae2f2f805412d8b39dc58ae52cfa1612b8007e53e2bf2952f
3
+ metadata.gz: b80f70c84d88643edef39e5d702779a4248b05c03ed692c09139bad00f7f5009
4
+ data.tar.gz: 9f923717999ac80231119e353356f8f9570c04e17ef560e84aa0656132ec0618
5
5
  SHA512:
6
- metadata.gz: d3c0f7fa79a22ba014655f18198bc4c048f3287fa6ade5e5c1442e8f0099c910394bc7592939b3a8c911670d9c4a54b5bbb8d0d29b568dddaf0406cba310c453
7
- data.tar.gz: 14c0462864a39e967b1c2c684a0dda6a6e0d6b1d902c87561398dc214f4934238594f8847b26ba33e52df669f00d998828df0c2ca29305fc56f94209e45019bc
6
+ metadata.gz: aff8a1b78d4b16f498c013652b5fd0d1b9b268fe9292197e7d799e12c8cfa562be6f6ba9a118fd3bab8cef19f028439db1befa6da02528e81c7aede7669560d6
7
+ data.tar.gz: 8cb2c7bfe70dfdab95a8c8d535f5b7397be7b03978808b01a3169979cc6c417eae1f4a9c8e24043b5196aa5bfb678bd50c1778393ed40429f10b58d82f3666fd
data/README.md CHANGED
@@ -93,22 +93,42 @@ parks_in_city = Park.where(
93
93
  )
94
94
  ```
95
95
 
96
- **With Arel Spatial Methods**:
96
+ **With Arel Spatial Methods** (Now with expanded arsenal!):
97
97
 
98
98
  ```ruby
99
- # Find locations within distance
99
+ # Basic spatial queries
100
100
  Location.where(
101
101
  Location.arel_table[:coordinates].st_distance(point).lt(1000)
102
102
  )
103
103
 
104
- # Find polygons that contain a point
105
- Boundary.where(
106
- Boundary.arel_table[:area].st_contains(Arel.spatial(point))
104
+ # NEW: K-Nearest Neighbor - Lightning fast "find nearest" queries
105
+ # Uses spatial index for incredible performance!
106
+ nearest_locations = Location
107
+ .order(Location.arel_table[:coordinates].distance_operator(my_position))
108
+ .limit(10)
109
+
110
+ # Advanced spatial predicates
111
+ # Find intersecting routes
112
+ Route.where(Route.arel_table[:path].st_intersects(restricted_zone))
113
+
114
+ # Find points within efficient distance (uses spatial index!)
115
+ Location.where(
116
+ Location.arel_table[:coordinates].st_dwithin(headquarters, 5000)
117
+ )
118
+
119
+ # Create buffer zones
120
+ safe_zones = DangerZone.select(
121
+ DangerZone.arel_table[:area].st_buffer(100).as('safety_perimeter')
122
+ )
123
+
124
+ # Transform between coordinate systems
125
+ global_coords = Location.select(
126
+ Location.arel_table[:local_position].st_transform(4326).as('wgs84_position')
107
127
  )
108
128
 
109
- # Calculate lengths and areas
110
- Route.select(
111
- Route.arel_table[:path].st_length.as('distance')
129
+ # Calculate areas
130
+ territories = Region.select(
131
+ Region.arel_table[:boundary].st_area.as('territory_size')
112
132
  )
113
133
  ```
114
134
 
@@ -139,6 +159,97 @@ puts location.coordinates.x # -5.923647
139
159
  puts location.coordinates.y # 35.790897
140
160
  ```
141
161
 
162
+ ## Testing
163
+
164
+ When testing spatial functionality in your Rails application, this gem provides helpful test utilities:
165
+
166
+ ```ruby
167
+ # In your test_helper.rb or rails_helper.rb
168
+ require 'activerecord-postgis/test_helper'
169
+
170
+ class ActiveSupport::TestCase
171
+ include ActiveRecordPostgis::TestHelper
172
+ end
173
+
174
+ # Or for RSpec
175
+ RSpec.configure do |config|
176
+ config.include ActiveRecordPostgis::TestHelper
177
+ end
178
+ ```
179
+
180
+ ### Test Helper Methods
181
+
182
+ ```ruby
183
+ class LocationTest < ActiveSupport::TestCase
184
+ def test_spatial_operations
185
+ # Create test geometries
186
+ point1 = create_point(-5.9, 35.8)
187
+ point2 = create_point(-5.91, 35.81)
188
+ polygon = create_test_polygon
189
+
190
+ location = Location.create!(coordinates: point1, boundary: polygon)
191
+
192
+ # Traditional assertions
193
+ assert_spatial_equal point1, location.coordinates
194
+ assert_within_distance point1, point2, 200 # meters
195
+ assert_contains polygon, point1
196
+
197
+ # New chainable syntax (recommended)
198
+ assert_spatial_column(location.coordinates)
199
+ .has_srid(4326)
200
+ .is_type(:point)
201
+ .is_geographic
202
+
203
+ assert_spatial_column(location.boundary)
204
+ .is_type(:polygon)
205
+ .has_srid(4326)
206
+ end
207
+
208
+ def test_3d_geometry
209
+ point_3d = create_point(1.0, 2.0, srid: 4326, z: 10.0)
210
+
211
+ assert_spatial_column(point_3d)
212
+ .has_z
213
+ .has_srid(4326)
214
+ .is_type(:point)
215
+ .is_cartesian
216
+ end
217
+ end
218
+ ```
219
+
220
+ **Available Test Helpers:**
221
+
222
+ **Traditional Assertions:**
223
+ - `assert_spatial_equal(expected, actual)` - Assert spatial objects are equal
224
+ - `assert_within_distance(point1, point2, distance)` - Assert points within distance
225
+ - `assert_contains(container, contained)` - Assert geometry contains another
226
+ - `assert_within(inner, outer)` - Assert geometry is within another
227
+ - `assert_intersects(geom1, geom2)` - Assert geometries intersect
228
+ - `assert_disjoint(geom1, geom2)` - Assert geometries don't intersect
229
+
230
+ **Chainable Spatial Column Assertions:**
231
+ - `assert_spatial_column(geometry).has_z` - Assert has Z dimension
232
+ - `assert_spatial_column(geometry).has_m` - Assert has M dimension
233
+ - `assert_spatial_column(geometry).has_srid(srid)` - Assert SRID value
234
+ - `assert_spatial_column(geometry).is_type(type)` - Assert geometry type
235
+ - `assert_spatial_column(geometry).is_geographic` - Assert geographic factory
236
+ - `assert_spatial_column(geometry).is_cartesian` - Assert cartesian factory
237
+
238
+ **Geometry Factories:**
239
+ - `create_point(x, y, srid: 4326)` - Create test points
240
+ - `create_test_polygon(srid: 4326)` - Create test polygons
241
+ - `create_test_linestring(srid: 4326)` - Create test linestrings
242
+ - `factory(srid: 4326, geographic: false)` - Get geometry factory
243
+ - `geographic_factory(srid: 4326)` - Get geographic factory
244
+ - `cartesian_factory(srid: 0)` - Get cartesian factory
245
+
246
+ ## Documentation
247
+
248
+ 📚 **Learn Like You're Defending the Galaxy**
249
+
250
+ - [🚀 Spatial Warfare Manual](docs/SPATIAL_WARFARE.md) - Advanced PostGIS arsenal explained through space combat
251
+ - [🍳 The PostGIS Cookbook](docs/COOKBOOK.md) - Real-world recipes from delivery fleets to geofencing
252
+
142
253
  ## Features
143
254
 
144
255
  🌍 **Complete PostGIS Type Support**
@@ -148,7 +259,14 @@ puts location.coordinates.y # 35.790897
148
259
  - Support for SRID, Z/M dimensions
149
260
 
150
261
  🔍 **Spatial Query Methods**
151
- - `st_distance`, `st_contains`, `st_within`, `st_length`
262
+ - Core methods: `st_distance`, `st_contains`, `st_within`, `st_length`
263
+ - **NEW:** Advanced spatial operations:
264
+ - `<->` (distance_operator) - K-Nearest Neighbor search (blazing fast!)
265
+ - `st_intersects` - Detect geometry intersections
266
+ - `st_dwithin` - Efficient proximity queries (index-optimized!)
267
+ - `st_buffer` - Create buffer zones around geometries
268
+ - `st_transform` - Convert between coordinate systems
269
+ - `st_area` - Calculate polygon areas
152
270
  - Custom Arel visitor for PostGIS SQL generation
153
271
  - Seamless integration with ActiveRecord queries
154
272
 
@@ -163,6 +281,7 @@ puts location.coordinates.y # 35.790897
163
281
  - Works with existing PostgreSQL tools
164
282
  - Clear error messages and debugging
165
283
  - Full RGeo integration
284
+ - Comprehensive test helpers for spatial assertions
166
285
 
167
286
  ## Acknowledgments
168
287
 
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.files = Dir.glob('{lib}/**/*') + [ gemspec, 'LICENSE.txt', 'README.md' ]
23
23
  spec.require_paths = [ 'lib' ]
24
24
 
25
- spec.add_dependency 'activerecord', '>= 8.0', '< 8.1'
25
+ spec.add_dependency 'activerecord', '>= 8.1.0', '< 8.2'
26
26
  spec.add_dependency 'pg'
27
27
  spec.add_dependency 'rgeo-activerecord', '>= 8.0'
28
28
  end
@@ -19,6 +19,7 @@ module ActiveRecord
19
19
 
20
20
  PostgreSQL::Column.new(
21
21
  column_name,
22
+ type_metadata.type,
22
23
  default_value,
23
24
  type_metadata,
24
25
  field["is_nullable"] == "YES",
@@ -18,13 +18,15 @@ module ActiveRecord
18
18
  end
19
19
 
20
20
  def type_cast(value)
21
- if value.is_a?(RGeo::Feature::Instance)
22
- # Convert spatial objects to EWKT string for parameter binding
23
- if value.srid && value.srid != 0
24
- "SRID=#{value.srid};#{value.as_text}"
25
- else
26
- value.as_text
27
- end
21
+ if RGeo::Feature::Geometry.check_type(value)
22
+ # Use EWKB format to preserve SRID information
23
+ RGeo::WKRep::WKBGenerator.new(
24
+ hex_format: true,
25
+ type_format: :ewkb,
26
+ emit_ewkb_srid: true
27
+ ).generate(value)
28
+ elsif value.is_a?(RGeo::Cartesian::BoundingBox)
29
+ value.to_s
28
30
  else
29
31
  super
30
32
  end
@@ -91,6 +91,7 @@ module ActiveRecord
91
91
 
92
92
  PostgreSQL::Column.new(
93
93
  field["column_name"],
94
+ type_metadata.type,
94
95
  default_value,
95
96
  type_metadata,
96
97
  field["is_nullable"] == "YES",
@@ -120,10 +120,15 @@ module ActiveRecord
120
120
  return base_type if @type == "geography"
121
121
 
122
122
  type_with_dimensions = build_type_with_dimensions
123
- # Include SRID if specified and not the default for the type
124
- # Geography defaults to 4326, geometry defaults to 0
125
- should_include_srid = @srid &&
126
- ((@geography && @srid != 4326) || (!@geography && @srid != 0))
123
+ # Include SRID if specified and not the default for the column type
124
+ # Geography columns: only include SRID if it's not the default 4326
125
+ # Geometry columns: always include SRID when specified and not 0
126
+ if @geography
127
+ should_include_srid = @srid && @srid != 4326
128
+ else
129
+ # For geometry columns, always include SRID when explicitly specified and not 0
130
+ should_include_srid = @srid && @srid != 0
131
+ end
127
132
 
128
133
  if should_include_srid
129
134
  "#{base_type}(#{type_with_dimensions},#{@srid})"
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module PostGIS
6
+ module SpatialQueries
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ # Safe wrapper for ST_Distance queries
11
+ # Usage: Model.where_st_distance(:column, lon, lat, '<', distance)
12
+ # For geographic calculations (meters), cast to geography
13
+ def where_st_distance(column, lon, lat, operator, distance, srid: 4326, geographic: false)
14
+ if geographic
15
+ where("ST_Distance(#{column}::geography, ST_SetSRID(ST_MakePoint(?, ?), ?)::geography) #{operator} ?", lon, lat, srid, distance)
16
+ else
17
+ where("ST_Distance(#{column}, ST_SetSRID(ST_MakePoint(?, ?), ?)) #{operator} ?", lon, lat, srid, distance)
18
+ end
19
+ end
20
+
21
+ # Safe wrapper for ST_DWithin queries
22
+ # Usage: Model.where_st_dwithin(:column, lon, lat, distance)
23
+ # For geographic calculations (meters), cast to geography
24
+ def where_st_dwithin(column, lon, lat, distance, srid: 4326, geographic: false)
25
+ if geographic
26
+ where("ST_DWithin(#{column}::geography, ST_SetSRID(ST_MakePoint(?, ?), ?)::geography, ?)", lon, lat, srid, distance)
27
+ else
28
+ where("ST_DWithin(#{column}, ST_SetSRID(ST_MakePoint(?, ?), ?), ?)", lon, lat, srid, distance)
29
+ end
30
+ end
31
+
32
+ # Safe wrapper for ST_Contains with point
33
+ # Usage: Model.where_st_contains(:column, lon, lat)
34
+ def where_st_contains(column, lon, lat, srid: 4326)
35
+ where("ST_Contains(#{column}, ST_SetSRID(ST_MakePoint(?, ?), ?))", lon, lat, srid)
36
+ end
37
+
38
+ # Safe wrapper for ST_Within with point
39
+ # Usage: Model.where_st_within_point(:column, lon, lat)
40
+ def where_st_within_point(column, lon, lat, srid: 4326)
41
+ where("ST_Within(ST_SetSRID(ST_MakePoint(?, ?), ?), #{column})", lon, lat, srid)
42
+ end
43
+
44
+ # Safe wrapper for ST_Intersects with WKT geometry
45
+ # Usage: Model.where_st_intersects(:column, wkt_string)
46
+ def where_st_intersects(column, wkt, srid: 4326)
47
+ where("ST_Intersects(#{column}, ST_GeomFromText(?, ?))", wkt, srid)
48
+ end
49
+
50
+ # Generic safe wrapper for any PostGIS function with a point parameter
51
+ # Usage: Model.where_st_function('ST_Distance', :column, lon, lat, '<', value)
52
+ def where_st_function(function, column, lon, lat, operator = nil, value = nil, srid: 4326)
53
+ if operator && value
54
+ where("#{function}(#{column}, ST_SetSRID(ST_MakePoint(?, ?), ?)) #{operator} ?", lon, lat, srid, value)
55
+ else
56
+ where("#{function}(#{column}, ST_SetSRID(ST_MakePoint(?, ?), ?))", lon, lat, srid)
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ # Module to include in models for instance methods
63
+ module SpatialScopes
64
+ extend ActiveSupport::Concern
65
+
66
+ included do
67
+ # Define commonly used spatial scopes
68
+ scope :within_distance, ->(column, lon, lat, distance, srid: 4326, geographic: false) {
69
+ where_st_distance(column, lon, lat, "<", distance, srid: srid, geographic: geographic)
70
+ }
71
+
72
+ scope :beyond_distance, ->(column, lon, lat, distance, srid: 4326, geographic: false) {
73
+ where_st_distance(column, lon, lat, ">", distance, srid: srid, geographic: geographic)
74
+ }
75
+
76
+ scope :near, ->(column, lon, lat, distance, srid: 4326, geographic: false) {
77
+ where_st_dwithin(column, lon, lat, distance, srid: srid, geographic: geographic)
78
+ }
79
+
80
+ scope :containing_point, ->(column, lon, lat, srid: 4326) {
81
+ where_st_contains(column, lon, lat, srid: srid)
82
+ }
83
+
84
+ scope :intersecting, ->(column, wkt, srid: 4326) {
85
+ where_st_intersects(column, wkt, srid: srid)
86
+ }
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ # Automatically include in ActiveRecord::Base
94
+ ActiveSupport.on_load(:active_record) do
95
+ ActiveRecord::Base.include(ActiveRecord::ConnectionAdapters::PostGIS::SpatialQueries)
96
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module PostGIS
6
+ # Test helpers for spatial data assertions
7
+ module TestHelpers
8
+ # Assert that two spatial objects are equal
9
+ def assert_spatial_equal(expected, actual, msg = nil)
10
+ msg ||= "Expected spatial object #{expected.as_text} but got #{actual.as_text}"
11
+
12
+ if expected.respond_to?(:equals?) && actual.respond_to?(:equals?)
13
+ assert expected.equals?(actual), msg
14
+ else
15
+ assert_equal expected.to_s, actual.to_s, msg
16
+ end
17
+ end
18
+
19
+ # Assert that a point is within a specified distance of another point
20
+ def assert_within_distance(point1, point2, distance, msg = nil)
21
+ actual_distance = point1.distance(point2)
22
+ msg ||= "Distance #{actual_distance} exceeds maximum allowed distance of #{distance}"
23
+ assert actual_distance <= distance, msg
24
+ end
25
+
26
+ # Assert that a geometry contains another geometry
27
+ def assert_contains(container, contained, msg = nil)
28
+ msg ||= "Expected #{container.as_text} to contain #{contained.as_text}"
29
+ assert container.contains?(contained), msg
30
+ end
31
+
32
+ # Assert that a geometry is within another geometry
33
+ def assert_within(inner, outer, msg = nil)
34
+ msg ||= "Expected #{inner.as_text} to be within #{outer.as_text}"
35
+ assert inner.within?(outer), msg
36
+ end
37
+
38
+ # Assert that two geometries intersect
39
+ def assert_intersects(geom1, geom2, msg = nil)
40
+ msg ||= "Expected #{geom1.as_text} to intersect #{geom2.as_text}"
41
+ assert geom1.intersects?(geom2), msg
42
+ end
43
+
44
+ # Assert that two geometries do not intersect
45
+ def assert_disjoint(geom1, geom2, msg = nil)
46
+ msg ||= "Expected #{geom1.as_text} to be disjoint from #{geom2.as_text}"
47
+ assert geom1.disjoint?(geom2), msg
48
+ end
49
+
50
+ # Assert that a geometry has the expected SRID
51
+ def assert_srid(geometry, expected_srid, msg = nil)
52
+ actual_srid = geometry.srid
53
+ msg ||= "Expected SRID #{expected_srid} but got #{actual_srid}"
54
+ assert_equal expected_srid, actual_srid, msg
55
+ end
56
+
57
+ # Chainable spatial column assertion builder
58
+ def assert_spatial_column(geometry, msg_prefix = nil)
59
+ SpatialColumnAssertion.new(geometry, self, msg_prefix)
60
+ end
61
+
62
+ # Assert that a geometry is of the expected type
63
+ def assert_geometry_type(geometry, expected_type, msg = nil)
64
+ actual_type = geometry.geometry_type.type_name.downcase
65
+ expected_type = expected_type.to_s.downcase.gsub("_", "")
66
+ actual_type = actual_type.gsub("_", "")
67
+ msg ||= "Expected geometry type #{expected_type} but got #{actual_type}"
68
+ assert_equal expected_type, actual_type, msg
69
+ end
70
+
71
+ # Legacy methods for backward compatibility
72
+ def assert_has_z(geometry, msg = nil)
73
+ assert_spatial_column(geometry, msg).has_z
74
+ end
75
+
76
+ def assert_has_m(geometry, msg = nil)
77
+ assert_spatial_column(geometry, msg).has_m
78
+ end
79
+
80
+ # Create a point for testing
81
+ def create_point(x, y, srid: 4326, z: nil, m: nil)
82
+ if z || m
83
+ # Use cartesian factory for 3D/4D points
84
+ factory = RGeo::Cartesian.preferred_factory(srid: srid, has_z_coordinate: !!z, has_m_coordinate: !!m)
85
+ if z && m
86
+ factory.point(x, y, z, m)
87
+ elsif z
88
+ factory.point(x, y, z)
89
+ elsif m
90
+ factory.point(x, y, 0, m) # Default Z to 0 for M-only
91
+ else
92
+ factory.point(x, y)
93
+ end
94
+ else
95
+ factory = RGeo::Geographic.simple_mercator_factory(srid: srid)
96
+ factory.point(x, y)
97
+ end
98
+ end
99
+
100
+ # Create a test polygon for testing
101
+ def create_test_polygon(srid: 4326)
102
+ factory = RGeo::Geographic.simple_mercator_factory(srid: srid)
103
+ factory.polygon(
104
+ factory.linear_ring([
105
+ factory.point(0, 0),
106
+ factory.point(0, 1),
107
+ factory.point(1, 1),
108
+ factory.point(1, 0),
109
+ factory.point(0, 0)
110
+ ])
111
+ )
112
+ end
113
+
114
+ # Create a test linestring for testing
115
+ def create_test_linestring(srid: 4326)
116
+ factory = RGeo::Geographic.simple_mercator_factory(srid: srid)
117
+ factory.line_string([
118
+ factory.point(0, 0),
119
+ factory.point(1, 1),
120
+ factory.point(2, 0)
121
+ ])
122
+ end
123
+ end
124
+
125
+ # Chainable spatial column assertion class
126
+ class SpatialColumnAssertion
127
+ def initialize(geometry, test_case, msg_prefix = nil)
128
+ @geometry = geometry
129
+ @test_case = test_case
130
+ @msg_prefix = msg_prefix
131
+ end
132
+
133
+ def has_z
134
+ msg = build_message("to have Z dimension")
135
+ has_z = detect_has_z(@geometry)
136
+ @test_case.assert has_z, msg
137
+ self
138
+ end
139
+
140
+ def has_m
141
+ msg = build_message("to have M dimension")
142
+ has_m = detect_has_m(@geometry)
143
+ @test_case.assert has_m, msg
144
+ self
145
+ end
146
+
147
+ def has_srid(expected_srid)
148
+ msg = build_message("to have SRID #{expected_srid}")
149
+ actual_srid = @geometry.srid
150
+ @test_case.assert_equal expected_srid, actual_srid, msg
151
+ self
152
+ end
153
+
154
+ def is_type(expected_type)
155
+ msg = build_message("to be of type #{expected_type}")
156
+ actual_type = @geometry.geometry_type.type_name.downcase
157
+ expected_type = expected_type.to_s.downcase.gsub("_", "")
158
+ actual_type = actual_type.gsub("_", "")
159
+ @test_case.assert_equal expected_type, actual_type, msg
160
+ self
161
+ end
162
+
163
+ def is_geographic
164
+ msg = build_message("to be geographic")
165
+ # Check if factory is geographic
166
+ is_geo = @geometry.factory.respond_to?(:spherical?) && @geometry.factory.spherical?
167
+ @test_case.assert is_geo, msg
168
+ self
169
+ end
170
+
171
+ def is_cartesian
172
+ msg = build_message("to be cartesian")
173
+ # Check if factory is cartesian
174
+ is_cart = !(@geometry.factory.respond_to?(:spherical?) && @geometry.factory.spherical?)
175
+ @test_case.assert is_cart, msg
176
+ self
177
+ end
178
+
179
+ private
180
+
181
+ def build_message(expectation)
182
+ prefix = @msg_prefix ? "#{@msg_prefix}: " : ""
183
+ "#{prefix}Expected geometry #{expectation}"
184
+ end
185
+
186
+ def detect_has_z(geometry)
187
+ return geometry.has_z_coordinate? if geometry.respond_to?(:has_z_coordinate?)
188
+ return geometry.has_z? if geometry.respond_to?(:has_z?)
189
+ return !geometry.z.nil? if geometry.respond_to?(:z)
190
+ false
191
+ end
192
+
193
+ def detect_has_m(geometry)
194
+ return geometry.has_m_coordinate? if geometry.respond_to?(:has_m_coordinate?)
195
+ return geometry.has_m? if geometry.respond_to?(:has_m?)
196
+ return !geometry.m.nil? if geometry.respond_to?(:m)
197
+ false
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
@@ -3,7 +3,7 @@
3
3
  module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module PostGIS
6
- VERSION = "0.2.0"
6
+ VERSION = "0.5.0"
7
7
  end
8
8
  end
9
9
  end
@@ -19,6 +19,7 @@ require_relative "postgis/spatial_column_type"
19
19
  require_relative "postgis/adapter_extensions"
20
20
  require_relative "postgis/column_extensions"
21
21
  require_relative "postgis/quoting"
22
+ require_relative "postgis/spatial_queries"
22
23
 
23
24
  module ActiveRecord
24
25
  module ConnectionAdapters
@@ -142,6 +143,16 @@ module ActiveRecord
142
143
  private
143
144
 
144
145
  def create_spatial_type_from_sql(sql_type)
146
+ # Handle empty sql_type (common in joins) - this is an upstream Rails issue
147
+ # where sql_type comes back as empty string, making it impossible to determine
148
+ # the correct spatial type properties
149
+ if sql_type.nil? || sql_type.empty?
150
+ # Log warning about potential type mismatch due to upstream issue
151
+ # Users experiencing this should use explicit attribute registration as workaround
152
+ # See: https://github.com/rgeo/activerecord-postgis-adapter/pull/334
153
+ return Type::Geometry.new(srid: 0, has_z: false, has_m: false, geographic: false)
154
+ end
155
+
145
156
  # Extract SRID and dimensions from SQL type
146
157
  srid = extract_srid_from_sql(sql_type)
147
158
  # Check for dimension suffixes (e.g., PointZ, PointM, PointZM)
@@ -182,6 +193,7 @@ module ActiveRecord
182
193
  when /geometry/i
183
194
  Type::Geometry.new(srid: srid, has_z: has_z, has_m: has_m)
184
195
  else
196
+ # Fallback for unrecognized types
185
197
  Type::Geometry.new(srid: srid, has_z: has_z, has_m: has_m)
186
198
  end
187
199
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../active_record/connection_adapters/postgis/test_helpers"
4
+
5
+ module ActiveRecordPostgis
6
+ module TestHelper
7
+ include ActiveRecord::ConnectionAdapters::PostGIS::TestHelpers
8
+
9
+ # Additional convenience methods for PostGIS testing
10
+ def factory(srid: 4326, geographic: false)
11
+ if geographic
12
+ RGeo::Geographic.spherical_factory(srid: srid)
13
+ else
14
+ RGeo::Cartesian.preferred_factory(srid: srid)
15
+ end
16
+ end
17
+
18
+ def geographic_factory(srid: 4326)
19
+ RGeo::Geographic.spherical_factory(srid: srid)
20
+ end
21
+
22
+ def cartesian_factory(srid: 0)
23
+ RGeo::Cartesian.preferred_factory(srid: srid)
24
+ end
25
+
26
+ def spatial_factory_store
27
+ RGeo::ActiveRecord::SpatialFactoryStore.instance
28
+ end
29
+
30
+ def reset_spatial_store
31
+ spatial_factory_store.clear
32
+ spatial_factory_store.default = nil
33
+ end
34
+
35
+ # Create a test table with spatial columns
36
+ def create_spatial_table(table_name, connection = ActiveRecord::Base.connection)
37
+ connection.create_table table_name, force: true do |t|
38
+ t.st_point :coordinates, srid: 4326
39
+ t.st_point :location, srid: 4326, geographic: true
40
+ t.st_polygon :boundary, srid: 4326
41
+ t.st_line_string :path, srid: 4326
42
+ t.timestamps
43
+ end
44
+ end
45
+
46
+ # Clean up spatial tables after tests
47
+ def drop_spatial_table(table_name, connection = ActiveRecord::Base.connection)
48
+ connection.drop_table table_name if connection.table_exists?(table_name)
49
+ end
50
+ end
51
+ end
@@ -16,6 +16,33 @@ module Arel
16
16
  class SpatialLength < Unary; end
17
17
  class SpatialContains < SpatialNode; end
18
18
  class SpatialWithin < SpatialNode; end
19
+ class SpatialIntersects < SpatialNode; end
20
+ class SpatialDWithin < SpatialNode
21
+ attr_reader :distance
22
+
23
+ def initialize(left, right, distance)
24
+ super(left, right)
25
+ @distance = distance
26
+ end
27
+ end
28
+ class SpatialBuffer < SpatialNode
29
+ def st_area
30
+ SpatialArea.new(self)
31
+ end
32
+ end
33
+ class SpatialTransform < SpatialNode
34
+ def st_area
35
+ SpatialArea.new(self)
36
+ end
37
+ end
38
+ class SpatialArea < Unary; end
39
+
40
+ # K-Nearest Neighbor distance operator
41
+ class SpatialDistanceOperator < Binary
42
+ def initialize(left, right)
43
+ super
44
+ end
45
+ end
19
46
 
20
47
  # Wrapper for spatial values that need special handling
21
48
  class SpatialValue < Node
@@ -36,6 +63,32 @@ module Arel
36
63
  def st_within(other)
37
64
  SpatialWithin.new(self, other)
38
65
  end
66
+
67
+ def st_intersects(other)
68
+ SpatialIntersects.new(self, other)
69
+ end
70
+
71
+ def st_dwithin(other, distance)
72
+ SpatialDWithin.new(self, other, distance)
73
+ end
74
+
75
+ def st_buffer(distance)
76
+ SpatialBuffer.new(self, distance)
77
+ end
78
+
79
+ def st_transform(srid)
80
+ SpatialTransform.new(self, srid)
81
+ end
82
+
83
+ def st_area
84
+ SpatialArea.new(self)
85
+ end
86
+
87
+ def distance_operator(other)
88
+ SpatialDistanceOperator.new(self, other)
89
+ end
90
+
91
+ alias :'<->' :distance_operator
39
92
  end
40
93
  end
41
94
 
@@ -56,6 +109,32 @@ module Arel
56
109
  def st_within(other)
57
110
  Arel::Nodes::SpatialWithin.new(self, other)
58
111
  end
112
+
113
+ def st_intersects(other)
114
+ Arel::Nodes::SpatialIntersects.new(self, other)
115
+ end
116
+
117
+ def st_dwithin(other, distance)
118
+ Arel::Nodes::SpatialDWithin.new(self, other, distance)
119
+ end
120
+
121
+ def st_buffer(distance)
122
+ Arel::Nodes::SpatialBuffer.new(self, distance)
123
+ end
124
+
125
+ def st_transform(srid)
126
+ Arel::Nodes::SpatialTransform.new(self, srid)
127
+ end
128
+
129
+ def st_area
130
+ Arel::Nodes::SpatialArea.new(self)
131
+ end
132
+
133
+ def distance_operator(other)
134
+ Arel::Nodes::SpatialDistanceOperator.new(self, other)
135
+ end
136
+
137
+ alias :'<->' :distance_operator
59
138
  end
60
139
  end
61
140
 
@@ -64,6 +143,26 @@ module Arel
64
143
  Arel::Nodes::SpatialValue.new(value)
65
144
  end
66
145
 
146
+ # Add Arel.st_make_point() method that properly handles floats for Rails 8.1
147
+ def self.st_make_point(x, y, srid = nil)
148
+ # Wrap floats in SqlLiteral nodes to avoid Rails 8.1 Arel strictness
149
+ x_node = x.is_a?(Numeric) ? Arel::Nodes::SqlLiteral.new(x.to_s) : x
150
+ y_node = y.is_a?(Numeric) ? Arel::Nodes::SqlLiteral.new(y.to_s) : y
151
+
152
+ if srid
153
+ srid_node = srid.is_a?(Numeric) ? Arel::Nodes::SqlLiteral.new(srid.to_s) : srid
154
+ Arel::Nodes::NamedFunction.new(
155
+ "ST_SetSRID",
156
+ [
157
+ Arel::Nodes::NamedFunction.new("ST_MakePoint", [ x_node, y_node ]),
158
+ srid_node
159
+ ]
160
+ )
161
+ else
162
+ Arel::Nodes::NamedFunction.new("ST_MakePoint", [ x_node, y_node ])
163
+ end
164
+ end
165
+
67
166
  module Visitors
68
167
  class PostGIS < PostgreSQL
69
168
  include RGeo::ActiveRecord::SpatialToSql
@@ -112,6 +211,52 @@ module Arel
112
211
  visit_spatial_operand(node.value, collector)
113
212
  end
114
213
 
214
+ def visit_Arel_Nodes_SpatialIntersects(node, collector)
215
+ collector << "ST_Intersects("
216
+ visit(node.left, collector)
217
+ collector << ", "
218
+ visit_spatial_operand(node.right, collector)
219
+ collector << ")"
220
+ end
221
+
222
+ def visit_Arel_Nodes_SpatialDWithin(node, collector)
223
+ collector << "ST_DWithin("
224
+ visit(node.left, collector)
225
+ collector << ", "
226
+ visit_spatial_operand(node.right, collector)
227
+ collector << ", "
228
+ collector << node.distance.to_s
229
+ collector << ")"
230
+ end
231
+
232
+ def visit_Arel_Nodes_SpatialBuffer(node, collector)
233
+ collector << "ST_Buffer("
234
+ visit(node.left, collector)
235
+ collector << ", "
236
+ collector << node.right.to_s
237
+ collector << ")"
238
+ end
239
+
240
+ def visit_Arel_Nodes_SpatialTransform(node, collector)
241
+ collector << "ST_Transform("
242
+ visit(node.left, collector)
243
+ collector << ", "
244
+ collector << node.right.to_s
245
+ collector << ")"
246
+ end
247
+
248
+ def visit_Arel_Nodes_SpatialArea(node, collector)
249
+ collector << "ST_Area("
250
+ visit(node.expr, collector)
251
+ collector << ")"
252
+ end
253
+
254
+ def visit_Arel_Nodes_SpatialDistanceOperator(node, collector)
255
+ visit(node.left, collector)
256
+ collector << " <-> "
257
+ visit_spatial_operand(node.right, collector)
258
+ end
259
+
115
260
  private
116
261
 
117
262
  def visit_spatial_operand(operand, collector)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-postgis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -15,20 +15,20 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '8.0'
18
+ version: 8.1.0
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
- version: '8.1'
21
+ version: '8.2'
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: '8.0'
28
+ version: 8.1.0
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
- version: '8.1'
31
+ version: '8.2'
32
32
  - !ruby/object:Gem::Dependency
33
33
  name: pg
34
34
  requirement: !ruby/object:Gem::Requirement
@@ -79,7 +79,9 @@ files:
79
79
  - lib/active_record/connection_adapters/postgis/schema_statements.rb
80
80
  - lib/active_record/connection_adapters/postgis/spatial_column_methods.rb
81
81
  - lib/active_record/connection_adapters/postgis/spatial_column_type.rb
82
+ - lib/active_record/connection_adapters/postgis/spatial_queries.rb
82
83
  - lib/active_record/connection_adapters/postgis/table_definition.rb
84
+ - lib/active_record/connection_adapters/postgis/test_helpers.rb
83
85
  - lib/active_record/connection_adapters/postgis/type/geography.rb
84
86
  - lib/active_record/connection_adapters/postgis/type/geometry.rb
85
87
  - lib/active_record/connection_adapters/postgis/type/geometry_collection.rb
@@ -92,6 +94,7 @@ files:
92
94
  - lib/active_record/connection_adapters/postgis/type/spatial.rb
93
95
  - lib/active_record/connection_adapters/postgis/version.rb
94
96
  - lib/activerecord-postgis.rb
97
+ - lib/activerecord-postgis/test_helper.rb
95
98
  - lib/arel/visitors/postgis.rb
96
99
  homepage: https://github.com/seuros/activerecord-postgis
97
100
  licenses:
@@ -114,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
117
  - !ruby/object:Gem::Version
115
118
  version: '0'
116
119
  requirements: []
117
- rubygems_version: 3.6.7
120
+ rubygems_version: 3.6.9
118
121
  specification_version: 4
119
122
  summary: PostGIS Type support for ActiveRecord
120
123
  test_files: []