activerecord-postgis 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +194 -0
- data/activerecord-postgis.gemspec +28 -0
- data/lib/active_record/connection_adapters/postgis/adapter_extensions.rb +85 -0
- data/lib/active_record/connection_adapters/postgis/column_extensions.rb +90 -0
- data/lib/active_record/connection_adapters/postgis/column_methods.rb +61 -0
- data/lib/active_record/connection_adapters/postgis/constants.rb +36 -0
- data/lib/active_record/connection_adapters/postgis/oid/spatial.rb +122 -0
- data/lib/active_record/connection_adapters/postgis/oid/spatial_types.rb +26 -0
- data/lib/active_record/connection_adapters/postgis/quoting.rb +35 -0
- data/lib/active_record/connection_adapters/postgis/schema_dumper.rb +94 -0
- data/lib/active_record/connection_adapters/postgis/schema_statements.rb +136 -0
- data/lib/active_record/connection_adapters/postgis/spatial_column_methods.rb +40 -0
- data/lib/active_record/connection_adapters/postgis/spatial_column_type.rb +173 -0
- data/lib/active_record/connection_adapters/postgis/table_definition.rb +144 -0
- data/lib/active_record/connection_adapters/postgis/type/geography.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/geometry.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/geometry_collection.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/line_string.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/multi_line_string.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/multi_point.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/multi_polygon.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/point.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/polygon.rb +21 -0
- data/lib/active_record/connection_adapters/postgis/type/spatial.rb +86 -0
- data/lib/active_record/connection_adapters/postgis/version.rb +9 -0
- data/lib/active_record/connection_adapters/postgis.rb +196 -0
- data/lib/activerecord-postgis.rb +12 -0
- data/lib/arel/visitors/postgis.rb +147 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d2022a537c5f4ac8fcdff4b7e9fb5a8801167da139a23df3095c66c5da4cad86
|
4
|
+
data.tar.gz: 174ed2a80856954bc39fae2bd479c47d3508f50ae5ed5ba2dfea458b2741a955
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a5c66a366cf6e101c9adb412abc8f5714299f9d38c005125a9fff1ba54d0621190acdd391b7fcdf99f16bbf36a59b45a6504d99ef6fffa4d0165f6acff5db8e4
|
7
|
+
data.tar.gz: 2ba4c06a28dbef90a5aff91db38732e6df62cb9d19f65d363b883d5b49e8895aba4622c9b6f81f21892812b201c88a8c8310e6f2c027404d339ab00a29c237e3
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Abdelkader Boudih
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
# ActiveRecord::PostGIS
|
2
|
+
|
3
|
+
**The next-generation PostGIS adapter for Rails** - clean, modern, and built for the future.
|
4
|
+
|
5
|
+
## Why This Gem?
|
6
|
+
|
7
|
+
This is the **next-generation PostGIS adapter** that brings PostGIS support to Rails the right way:
|
8
|
+
|
9
|
+
✅ **Use standard `postgres://` URLs** - No custom adapter names, no special configuration
|
10
|
+
✅ **No monkey patching** - Clean extensions using Rails 8 patterns
|
11
|
+
✅ **No obscure hacks** - Transparent, well-documented implementation
|
12
|
+
✅ **Latest APIs** - Built for Rails 8+ and Ruby 3.3+
|
13
|
+
✅ **Zero configuration** - Just add the gem and it works
|
14
|
+
|
15
|
+
Unlike legacy PostGIS adapters that require custom database URLs, special configurations, and complex setup, this gem **extends the existing PostgreSQL adapter** seamlessly. Your database configuration stays clean and standard.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'activerecord-postgis'
|
23
|
+
```
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
```
|
28
|
+
$ bundle install
|
29
|
+
```
|
30
|
+
|
31
|
+
Or install it yourself as:
|
32
|
+
|
33
|
+
```
|
34
|
+
$ gem install activerecord-postgis
|
35
|
+
```
|
36
|
+
|
37
|
+
## Configuration
|
38
|
+
|
39
|
+
**Zero configuration required!** Just use your standard PostgreSQL database configuration:
|
40
|
+
|
41
|
+
```yaml
|
42
|
+
# config/database.yml
|
43
|
+
development:
|
44
|
+
adapter: postgresql
|
45
|
+
url: postgres://user:password@localhost/myapp_development
|
46
|
+
# That's it! No special adapter, no custom configuration
|
47
|
+
```
|
48
|
+
|
49
|
+
## Usage
|
50
|
+
|
51
|
+
### Migrations
|
52
|
+
|
53
|
+
Create spatial columns using PostGIS types:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class CreateLocations < ActiveRecord::Migration[8.0]
|
57
|
+
def change
|
58
|
+
create_table :locations do |t|
|
59
|
+
t.st_point :coordinates, srid: 4326
|
60
|
+
t.st_polygon :boundary, geographic: true
|
61
|
+
t.st_line_string :route, has_z: true
|
62
|
+
t.timestamps
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
### Spatial Queries
|
69
|
+
|
70
|
+
**With RGeo Objects** (Recommended):
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
# Create RGeo geometries
|
74
|
+
factory = RGeo::Geographic.spherical_factory(srid: 4326)
|
75
|
+
point = factory.point(-5.923647, 35.790897) # Cap Spartel, Tangier, Morocco
|
76
|
+
polygon = factory.polygon(...)
|
77
|
+
|
78
|
+
# Direct queries with RGeo objects
|
79
|
+
Location.where(coordinates: point)
|
80
|
+
Location.where("ST_Distance(coordinates, ?) < ?", point, 1000)
|
81
|
+
|
82
|
+
# Using parameterized queries (automatically quoted)
|
83
|
+
locations_nearby = Location.where(
|
84
|
+
"ST_DWithin(coordinates, ?, ?)",
|
85
|
+
point,
|
86
|
+
1000 # meters
|
87
|
+
)
|
88
|
+
|
89
|
+
# Complex spatial queries
|
90
|
+
parks_in_city = Park.where(
|
91
|
+
"ST_Within(boundary, ?)",
|
92
|
+
city_polygon
|
93
|
+
)
|
94
|
+
```
|
95
|
+
|
96
|
+
**With Arel Spatial Methods**:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
# Find locations within distance
|
100
|
+
Location.where(
|
101
|
+
Location.arel_table[:coordinates].st_distance(point).lt(1000)
|
102
|
+
)
|
103
|
+
|
104
|
+
# Find polygons that contain a point
|
105
|
+
Boundary.where(
|
106
|
+
Boundary.arel_table[:area].st_contains(Arel.spatial(point))
|
107
|
+
)
|
108
|
+
|
109
|
+
# Calculate lengths and areas
|
110
|
+
Route.select(
|
111
|
+
Route.arel_table[:path].st_length.as('distance')
|
112
|
+
)
|
113
|
+
```
|
114
|
+
|
115
|
+
**With WKT Strings**:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
# Using Well-Known Text format
|
119
|
+
Location.where(
|
120
|
+
"ST_Distance(coordinates, ST_GeomFromText(?)) < ?",
|
121
|
+
"POINT(-5.923647 35.790897)",
|
122
|
+
1000
|
123
|
+
)
|
124
|
+
```
|
125
|
+
|
126
|
+
### Model Integration
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
class Location < ApplicationRecord
|
130
|
+
# Works automatically - no configuration needed
|
131
|
+
# Spatial attributes are automatically parsed and serialized
|
132
|
+
end
|
133
|
+
|
134
|
+
location = Location.create!(
|
135
|
+
coordinates: "POINT(-5.923647 35.790897)" # Tangier, Morocco
|
136
|
+
)
|
137
|
+
|
138
|
+
puts location.coordinates.x # -5.923647
|
139
|
+
puts location.coordinates.y # 35.790897
|
140
|
+
```
|
141
|
+
|
142
|
+
## Features
|
143
|
+
|
144
|
+
🌍 **Complete PostGIS Type Support**
|
145
|
+
- `st_point`, `st_line_string`, `st_polygon`
|
146
|
+
- `st_multi_point`, `st_multi_line_string`, `st_multi_polygon`
|
147
|
+
- `st_geometry_collection`, `st_geography`
|
148
|
+
- Support for SRID, Z/M dimensions
|
149
|
+
|
150
|
+
🔍 **Spatial Query Methods**
|
151
|
+
- `st_distance`, `st_contains`, `st_within`, `st_length`
|
152
|
+
- Custom Arel visitor for PostGIS SQL generation
|
153
|
+
- Seamless integration with ActiveRecord queries
|
154
|
+
|
155
|
+
⚡ **Modern Architecture**
|
156
|
+
- Built on Rails 8 patterns
|
157
|
+
- Clean module extensions (no inheritance)
|
158
|
+
- Proper type registration and schema dumping
|
159
|
+
- Compatible with multi-database setups
|
160
|
+
|
161
|
+
🛠️ **Developer Experience**
|
162
|
+
- Standard `postgres://` URLs
|
163
|
+
- Works with existing PostgreSQL tools
|
164
|
+
- Clear error messages and debugging
|
165
|
+
- Full RGeo integration
|
166
|
+
|
167
|
+
## Acknowledgments
|
168
|
+
|
169
|
+
This gem builds upon the incredible work of many contributors to the Ruby geospatial ecosystem:
|
170
|
+
|
171
|
+
🙏 **RGeo Ecosystem** - The foundation that makes Ruby geospatial possible:
|
172
|
+
- [RGeo](https://github.com/rgeo/rgeo) originally by Daniel Azuma, currently maintained by Keith Doggett (@keithdoggett) and Ulysse Buonomo (@BuonOmo)
|
173
|
+
- [RGeo::ActiveRecord](https://github.com/rgeo/rgeo-activerecord) for ActiveRecord integration
|
174
|
+
- [RGeo::Proj4](https://github.com/rgeo/rgeo-proj4) for coordinate system transformations
|
175
|
+
- Former maintainer Tee Parham and all contributors who built this ecosystem
|
176
|
+
|
177
|
+
🗺️ **PostGIS Pioneers** - Previous PostGIS adapters that paved the way:
|
178
|
+
- [activerecord-postgis-adapter](https://github.com/rgeo/activerecord-postgis-adapter) by Daniel Azuma and the RGeo team
|
179
|
+
- All the maintainers and contributors who solved spatial data challenges in Rails
|
180
|
+
|
181
|
+
🌍 **PostGIS & GEOS** - The underlying spatial powerhouses:
|
182
|
+
- PostGIS developers for the amazing spatial database extension
|
183
|
+
- GEOS contributors for computational geometry
|
184
|
+
- PostgreSQL team for the solid foundation
|
185
|
+
|
186
|
+
This gem exists because of their pioneering work. I'm simply bringing it into the modern Rails era with cleaner patterns and zero configuration.
|
187
|
+
|
188
|
+
## Contributing
|
189
|
+
|
190
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/seuros/activerecord-postgis.
|
191
|
+
|
192
|
+
## License
|
193
|
+
|
194
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/active_record/connection_adapters/postgis/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'activerecord-postgis'
|
7
|
+
spec.version = ActiveRecord::ConnectionAdapters::PostGIS::VERSION
|
8
|
+
spec.authors = [ 'Abdelkader Boudih' ]
|
9
|
+
spec.email = [ 'terminale@gmail.com' ]
|
10
|
+
|
11
|
+
spec.summary = 'PostGIS Type support for ActiveRecord'
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = 'https://github.com/seuros/activerecord-postgis'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = '>= 3.3.0'
|
16
|
+
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
18
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
19
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/master/CHANGELOG.md"
|
20
|
+
|
21
|
+
gemspec = File.basename(__FILE__)
|
22
|
+
spec.files = Dir.glob('{lib}/**/*') + [ gemspec, 'LICENSE.txt', 'README.md' ]
|
23
|
+
spec.require_paths = [ 'lib' ]
|
24
|
+
|
25
|
+
spec.add_dependency 'activerecord', '>= 8.0', '< 8.1'
|
26
|
+
spec.add_dependency 'pg'
|
27
|
+
spec.add_dependency 'rgeo-activerecord', '>= 8.0'
|
28
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module PostGIS
|
6
|
+
module AdapterExtensions
|
7
|
+
# Override type_to_sql to handle PostGIS spatial types
|
8
|
+
def type_to_sql(type, limit: nil, precision: nil, scale: nil, geographic: false, srid: nil, has_z: false, has_m: false, **options)
|
9
|
+
if type.to_s =~ /^st_/ || type.to_s == "geography"
|
10
|
+
geometric_type = type.to_s.sub(/^st_/, "")
|
11
|
+
|
12
|
+
# If limit contains our custom format, parse it
|
13
|
+
if limit.is_a?(String) && limit.include?(",")
|
14
|
+
geo_part, srid_part = limit.split(",", 2)
|
15
|
+
# Extract geometry type from the limit if provided
|
16
|
+
if geo_part && !geo_part.empty? && geo_part != geometric_type.upcase
|
17
|
+
geometric_type = geo_part.downcase
|
18
|
+
end
|
19
|
+
# Extract SRID from limit
|
20
|
+
if srid_part && !srid_part.empty?
|
21
|
+
srid = srid_part.to_i
|
22
|
+
end
|
23
|
+
# Extract Z/M modifiers
|
24
|
+
if geo_part && geo_part.include?("Z")
|
25
|
+
has_z = true
|
26
|
+
geometric_type = geo_part.gsub(/[ZM]/, "").downcase
|
27
|
+
end
|
28
|
+
if geo_part && geo_part.include?("M")
|
29
|
+
has_m = true
|
30
|
+
geometric_type = geo_part.gsub(/[ZM]/, "").downcase
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
PostGIS::SpatialColumnType.new(
|
35
|
+
geometric_type,
|
36
|
+
srid,
|
37
|
+
has_z: has_z,
|
38
|
+
has_m: has_m,
|
39
|
+
geography: geographic
|
40
|
+
).to_sql
|
41
|
+
else
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Override to handle PostGIS types
|
47
|
+
def lookup_cast_type(sql_type)
|
48
|
+
# Handle PostGIS types
|
49
|
+
if sql_type.to_s =~ /^(geometry|geography)/i
|
50
|
+
type_name = case sql_type.to_s
|
51
|
+
when /geography\(Point/i, /geometry\(Point/i then :st_point
|
52
|
+
when /geography\(LineString/i, /geometry\(LineString/i then :st_line_string
|
53
|
+
when /geography\(Polygon/i, /geometry\(Polygon/i then :st_polygon
|
54
|
+
when /geography\(MultiPoint/i, /geometry\(MultiPoint/i then :st_multi_point
|
55
|
+
when /geography\(MultiLineString/i, /geometry\(MultiLineString/i then :st_multi_line_string
|
56
|
+
when /geography\(MultiPolygon/i, /geometry\(MultiPolygon/i then :st_multi_polygon
|
57
|
+
when /geography\(GeometryCollection/i, /geometry\(GeometryCollection/i then :st_geometry_collection
|
58
|
+
when /geography/i then :st_geography
|
59
|
+
when /geometry/i then :st_geometry
|
60
|
+
else
|
61
|
+
:st_geometry
|
62
|
+
end
|
63
|
+
|
64
|
+
ActiveRecord::Type.lookup(type_name, adapter: adapter_name.downcase.to_sym)
|
65
|
+
else
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Override create_table_definition to use our custom table definition
|
71
|
+
def create_table_definition(*args, **kwargs)
|
72
|
+
table_def = super(*args, **kwargs)
|
73
|
+
table_def.extend(PostGIS::TableDefinition)
|
74
|
+
table_def
|
75
|
+
end
|
76
|
+
|
77
|
+
# Use PostGIS Arel visitor for spatial queries
|
78
|
+
def arel_visitor
|
79
|
+
require_relative "../../../arel/visitors/postgis"
|
80
|
+
@arel_visitor ||= Arel::Visitors::PostGIS.new(self)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module PostGIS
|
6
|
+
module ColumnExtensions
|
7
|
+
def spatial?
|
8
|
+
type.to_s.start_with?("st_") ||
|
9
|
+
[ :geography, :geometry, :geometry_collection, :line_string,
|
10
|
+
:multi_line_string, :multi_point, :multi_polygon, :polygon ].include?(type.to_sym)
|
11
|
+
end
|
12
|
+
|
13
|
+
def geographic?
|
14
|
+
sql_type.start_with?("geography") || (spatial? && limit.is_a?(Hash) && limit[:geographic])
|
15
|
+
end
|
16
|
+
|
17
|
+
def srid
|
18
|
+
if spatial?
|
19
|
+
if limit.is_a?(Hash) && limit[:srid]
|
20
|
+
limit[:srid]
|
21
|
+
elsif sql_type =~ /,(\d+)\)/
|
22
|
+
$1.to_i
|
23
|
+
else
|
24
|
+
geographic? ? 4326 : 0
|
25
|
+
end
|
26
|
+
else
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def geometric_type
|
33
|
+
if spatial?
|
34
|
+
case type.to_sym
|
35
|
+
when :st_point then RGeo::Feature::Point
|
36
|
+
when :st_line_string then RGeo::Feature::LineString
|
37
|
+
when :st_polygon then RGeo::Feature::Polygon
|
38
|
+
when :st_multi_point then RGeo::Feature::MultiPoint
|
39
|
+
when :st_multi_line_string then RGeo::Feature::MultiLineString
|
40
|
+
when :st_multi_polygon then RGeo::Feature::MultiPolygon
|
41
|
+
when :st_geometry_collection then RGeo::Feature::GeometryCollection
|
42
|
+
when :st_geometry then RGeo::Feature::Geometry
|
43
|
+
when :st_geography then RGeo::Feature::Geometry
|
44
|
+
# Legacy types
|
45
|
+
when :geometry then RGeo::Feature::Geometry
|
46
|
+
when :geography then RGeo::Feature::Geometry
|
47
|
+
when :line_string then RGeo::Feature::LineString
|
48
|
+
when :polygon then RGeo::Feature::Polygon
|
49
|
+
when :multi_point then RGeo::Feature::MultiPoint
|
50
|
+
when :multi_line_string then RGeo::Feature::MultiLineString
|
51
|
+
when :multi_polygon then RGeo::Feature::MultiPolygon
|
52
|
+
when :geometry_collection then RGeo::Feature::GeometryCollection
|
53
|
+
else RGeo::Feature::Geometry
|
54
|
+
end
|
55
|
+
else
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def has_z?
|
61
|
+
if spatial?
|
62
|
+
if limit.is_a?(Hash) && limit.key?(:has_z)
|
63
|
+
limit[:has_z]
|
64
|
+
elsif sql_type =~ /\b\w+Z\b|\b\w+ZM\b/
|
65
|
+
true
|
66
|
+
else
|
67
|
+
false
|
68
|
+
end
|
69
|
+
else
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def has_m?
|
75
|
+
if spatial?
|
76
|
+
if limit.is_a?(Hash) && limit.key?(:has_m)
|
77
|
+
limit[:has_m]
|
78
|
+
elsif sql_type =~ /\b\w+M\b|\b\w+ZM\b/
|
79
|
+
true
|
80
|
+
else
|
81
|
+
false
|
82
|
+
end
|
83
|
+
else
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module PostGIS
|
6
|
+
module ColumnMethods
|
7
|
+
# Override to handle PostGIS types in column definitions
|
8
|
+
def new_column_from_field(table_name, field, _definitions)
|
9
|
+
column_name = field["column_name"]
|
10
|
+
type_metadata = fetch_type_metadata(column_name, field["data_type"], field["sql_type"], field)
|
11
|
+
|
12
|
+
# Handle PostGIS types
|
13
|
+
if field["sql_type"] =~ /geometry|geography/i
|
14
|
+
type_metadata = handle_spatial_type_metadata(field["sql_type"], type_metadata)
|
15
|
+
end
|
16
|
+
|
17
|
+
default_value = extract_value_from_default(field["column_default"])
|
18
|
+
default_function = extract_default_function(field["column_default"], default_value)
|
19
|
+
|
20
|
+
PostgreSQL::Column.new(
|
21
|
+
column_name,
|
22
|
+
default_value,
|
23
|
+
type_metadata,
|
24
|
+
field["is_nullable"] == "YES",
|
25
|
+
default_function,
|
26
|
+
comment: field["comment"].presence
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def handle_spatial_type_metadata(sql_type, original_metadata)
|
33
|
+
# Parse spatial type info
|
34
|
+
type_name = case sql_type
|
35
|
+
when /geography\(Point/i, /geometry\(Point/i then "point"
|
36
|
+
when /geography\(LineString/i, /geometry\(LineString/i then "line_string"
|
37
|
+
when /geography\(Polygon/i, /geometry\(Polygon/i then "polygon"
|
38
|
+
when /geography\(MultiPoint/i, /geometry\(MultiPoint/i then "multi_point"
|
39
|
+
when /geography\(MultiLineString/i, /geometry\(MultiLineString/i then "multi_line_string"
|
40
|
+
when /geography\(MultiPolygon/i, /geometry\(MultiPolygon/i then "multi_polygon"
|
41
|
+
when /geography\(GeometryCollection/i, /geometry\(GeometryCollection/i then "geometry_collection"
|
42
|
+
when /geography/i then "geography"
|
43
|
+
when /geometry/i then "geometry"
|
44
|
+
else
|
45
|
+
return original_metadata
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create new type metadata with spatial type
|
49
|
+
type = ActiveRecord::Type.lookup(type_name.to_sym, adapter: adapter_name.downcase.to_sym)
|
50
|
+
ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
|
51
|
+
sql_type: sql_type,
|
52
|
+
type: type,
|
53
|
+
limit: original_metadata.limit,
|
54
|
+
precision: original_metadata.precision,
|
55
|
+
scale: original_metadata.scale
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# lib/active_record/connection_adapters/postgis/constants.rb
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module PostGIS
|
6
|
+
SPATIAL_TYPES = %i[
|
7
|
+
geometry
|
8
|
+
geography
|
9
|
+
point
|
10
|
+
line_string
|
11
|
+
polygon
|
12
|
+
multi_point
|
13
|
+
multi_line_string
|
14
|
+
multi_polygon
|
15
|
+
geometry_collection
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
GEOMETRIC_TYPES = %i[
|
19
|
+
st_geography
|
20
|
+
st_geometry
|
21
|
+
st_geometry_collection
|
22
|
+
st_line_string
|
23
|
+
st_multi_line_string
|
24
|
+
st_multi_point
|
25
|
+
st_multi_polygon
|
26
|
+
st_point
|
27
|
+
st_polygon
|
28
|
+
].freeze
|
29
|
+
|
30
|
+
VALID_TYPES = %w[
|
31
|
+
point line_string polygon multi_point
|
32
|
+
multi_line_string multi_polygon geometry_collection geography
|
33
|
+
].freeze
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module PostGIS
|
6
|
+
module OID
|
7
|
+
# OID used to represent geometry/geography database types and attributes.
|
8
|
+
#
|
9
|
+
# Accepts `geo_type`, `srid`, `has_z`, `has_m`, and `geographic` as parameters.
|
10
|
+
# Responsible for parsing sql_types returned from the database and WKT features.
|
11
|
+
class Spatial < Type::Value
|
12
|
+
def initialize(geo_type: "geometry", srid: 0, has_z: false, has_m: false, geographic: false)
|
13
|
+
@geo_type = geo_type
|
14
|
+
@srid = srid
|
15
|
+
@has_z = has_z
|
16
|
+
@has_m = has_m
|
17
|
+
@geographic = geographic
|
18
|
+
end
|
19
|
+
|
20
|
+
# sql_type: geometry, geometry(Point), geometry(Point,4326), ...
|
21
|
+
#
|
22
|
+
# returns [geo_type, srid, has_z, has_m]
|
23
|
+
# geo_type: geography, geometry, point, line_string, polygon, ...
|
24
|
+
# srid: 1234
|
25
|
+
# has_z: false
|
26
|
+
# has_m: false
|
27
|
+
def self.parse_sql_type(sql_type)
|
28
|
+
geo_type = nil
|
29
|
+
srid = 0
|
30
|
+
has_z = false
|
31
|
+
has_m = false
|
32
|
+
|
33
|
+
if sql_type =~ /(geography|geometry)\((.*)\)$/i
|
34
|
+
# geometry(Point)
|
35
|
+
# geometry(Point,4326)
|
36
|
+
params = Regexp.last_match(2).split(",")
|
37
|
+
if params.first =~ /([a-z]+[^zm])(z?)(m?)/i
|
38
|
+
has_z = Regexp.last_match(2).length.positive?
|
39
|
+
has_m = Regexp.last_match(3).length.positive?
|
40
|
+
geo_type = Regexp.last_match(1)
|
41
|
+
end
|
42
|
+
srid = Regexp.last_match(1).to_i if params.last =~ /(\d+)/
|
43
|
+
else
|
44
|
+
# geometry
|
45
|
+
# otherType(a,b)
|
46
|
+
geo_type = sql_type
|
47
|
+
end
|
48
|
+
geographic = sql_type.match?(/geography/)
|
49
|
+
|
50
|
+
[ geo_type, srid, has_z, has_m, geographic ]
|
51
|
+
end
|
52
|
+
|
53
|
+
def spatial_factory
|
54
|
+
@spatial_factory ||=
|
55
|
+
RGeo::ActiveRecord::SpatialFactoryStore.instance.factory(
|
56
|
+
factory_attrs
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def spatial?
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
def type
|
65
|
+
@geographic ? :geography : :geometry
|
66
|
+
end
|
67
|
+
|
68
|
+
# support setting an RGeo object or a WKT string
|
69
|
+
def serialize(value)
|
70
|
+
return if value.nil?
|
71
|
+
|
72
|
+
geo_value = cast_value(value)
|
73
|
+
|
74
|
+
# TODO: - only valid types should be allowed
|
75
|
+
# e.g. linestring is not valid for point column
|
76
|
+
# raise "maybe should raise" unless RGeo::Feature::Geometry.check_type(geo_value)
|
77
|
+
|
78
|
+
RGeo::WKRep::WKBGenerator.new(hex_format: true, type_format: :ewkb, emit_ewkb_srid: true)
|
79
|
+
.generate(geo_value)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def cast_value(value)
|
85
|
+
return if value.nil?
|
86
|
+
|
87
|
+
value.is_a?(String) ? parse_wkt(value) : value
|
88
|
+
end
|
89
|
+
|
90
|
+
# convert WKT string into RGeo object
|
91
|
+
def parse_wkt(string)
|
92
|
+
wkt_parser(string).parse(string)
|
93
|
+
rescue RGeo::Error::ParseError
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def binary_string?(string)
|
98
|
+
string[0] == "\x00" || string[0] == "\x01" || string[0, 4] =~ /[0-9a-fA-F]{4}/
|
99
|
+
end
|
100
|
+
|
101
|
+
def wkt_parser(string)
|
102
|
+
if binary_string?(string)
|
103
|
+
RGeo::WKRep::WKBParser.new(spatial_factory, support_ewkb: true, default_srid: @srid)
|
104
|
+
else
|
105
|
+
RGeo::WKRep::WKTParser.new(spatial_factory, support_ewkt: true, default_srid: @srid)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def factory_attrs
|
110
|
+
{
|
111
|
+
geo_type: @geo_type.underscore,
|
112
|
+
has_m: @has_m,
|
113
|
+
has_z: @has_z,
|
114
|
+
srid: @srid,
|
115
|
+
sql_type: type.to_s
|
116
|
+
}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module PostGIS
|
4
|
+
module OID
|
5
|
+
SPATIAL_TYPES = %i[
|
6
|
+
geometry
|
7
|
+
geography
|
8
|
+
point
|
9
|
+
line_string
|
10
|
+
polygon
|
11
|
+
multi_point
|
12
|
+
multi_line_string
|
13
|
+
multi_polygon
|
14
|
+
geometry_collection
|
15
|
+
]
|
16
|
+
|
17
|
+
SPATIAL_TYPES.each do |type|
|
18
|
+
class_name = "St#{type.to_s.camelize}"
|
19
|
+
const_set(class_name, Class.new(Spatial) do
|
20
|
+
define_method(:type) { :"st_#{type}" }
|
21
|
+
end)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|