Skip to content

Commit 9337e23

Browse files
committed
Add support for spatial features.
1 parent ee9f667 commit 9337e23

23 files changed

+1386
-5
lines changed

activerecord-cockroachdb-adapter.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Gem::Specification.new do |spec|
1515

1616
spec.add_dependency "activerecord", "~> 6.0.3"
1717
spec.add_dependency "pg", ">= 0.20"
18+
spec.add_dependency "rgeo-activerecord", "~> 7.0.0"
1819

1920
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
2021
# to allow pushing to a single host or delete this section to allow pushing to any host.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
module RGeo
4+
module ActiveRecord
5+
##
6+
# Extend rgeo-activerecord visitors to use PostGIS specific functionality
7+
module SpatialToPostGISSql
8+
def visit_in_spatial_context(node, collector)
9+
# Use ST_GeomFromEWKT for EWKT geometries
10+
if node.is_a?(String) && node =~ /SRID=[\d+]{0,};/
11+
collector << "#{st_func('ST_GeomFromEWKT')}(#{quote(node)})"
12+
else
13+
super(node, collector)
14+
end
15+
end
16+
end
17+
end
18+
end
19+
RGeo::ActiveRecord::SpatialToSql.prepend RGeo::ActiveRecord::SpatialToPostGISSql
20+
21+
module Arel # :nodoc:
22+
module Visitors # :nodoc:
23+
class CockroachDB < PostgreSQL # :nodoc:
24+
include RGeo::ActiveRecord::SpatialToSql
25+
end
26+
end
27+
end

lib/active_record/connection_adapters/cockroachdb/column.rb

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,85 @@ module ActiveRecord
22
module ConnectionAdapters
33
module CockroachDB
44
module PostgreSQLColumnMonkeyPatch
5+
# most functions taken from activerecord-postgis-adapter spatial_column
6+
# https://github.com/rgeo/activerecord-postgis-adapter/blob/master/lib/active_record/connection_adapters/postgis/spatial_column.rb
7+
def initialize(name, default, sql_type_metadata = nil, null = true,
8+
default_function = nil, collation: nil, comment: nil,
9+
serial: nil, spatial: nil)
10+
@sql_type_metadata = sql_type_metadata
11+
@geographic = !!(sql_type_metadata.sql_type =~ /geography\(/i)
12+
13+
if spatial
14+
# This case comes from an entry in the geometry_columns table
15+
set_geometric_type_from_name(spatial[:type])
16+
@srid = spatial[:srid].to_i
17+
@has_z = !!spatial[:has_z]
18+
@has_m = !!spatial[:has_m]
19+
elsif @geographic
20+
# Geographic type information is embedded in the SQL type
21+
@srid = 4326
22+
@has_z = @has_m = false
23+
build_from_sql_type(sql_type_metadata.sql_type)
24+
elsif sql_type =~ /geography|geometry|point|linestring|polygon/i
25+
build_from_sql_type(sql_type_metadata.sql_type)
26+
elsif sql_type_metadata.sql_type =~ /geography|geometry|point|linestring|polygon/i
27+
# A geometry column with no geometry_columns entry.
28+
# @geometric_type = geo_type_from_sql_type(sql_type)
29+
build_from_sql_type(sql_type_metadata.sql_type)
30+
end
31+
super(name, default, sql_type_metadata, null, default_function,
32+
collation: collation, comment: comment, serial: serial)
33+
if spatial? && @srid
34+
@limit = { srid: @srid, type: to_type_name(geometric_type) }
35+
@limit[:has_z] = true if @has_z
36+
@limit[:has_m] = true if @has_m
37+
@limit[:geographic] = true if @geographic
38+
end
39+
end
40+
41+
attr_reader :geographic,
42+
:geometric_type,
43+
:has_m,
44+
:has_z,
45+
:srid
46+
47+
alias geographic? geographic
48+
alias has_z? has_z
49+
alias has_m? has_m
50+
51+
def limit
52+
spatial? ? @limit : super
53+
end
54+
55+
def spatial?
56+
%i[geometry geography].include?(@sql_type_metadata.type)
57+
end
58+
559
def serial?
6-
default_function == "unique_rowid()"
60+
default_function == 'unique_rowid()'
61+
end
62+
63+
private
64+
65+
def set_geometric_type_from_name(name)
66+
@geometric_type = RGeo::ActiveRecord.geometric_type_from_name(name) || RGeo::Feature::Geometry
67+
end
68+
69+
def build_from_sql_type(sql_type)
70+
geo_type, @srid, @has_z, @has_m = OID::Spatial.parse_sql_type(sql_type)
71+
set_geometric_type_from_name(geo_type)
72+
end
73+
74+
def to_type_name(geometric_type)
75+
name = geometric_type.type_name.underscore
76+
case name
77+
when 'point'
78+
'st_point'
79+
when 'polygon'
80+
'st_polygon'
81+
else
82+
name
83+
end
784
end
885
end
986
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveRecord
4+
module ConnectionAdapters
5+
module CockroachDB
6+
module ColumnMethods
7+
def spatial(name, options = {})
8+
raise "You must set a type. For example: 't.spatial type: :st_point'" unless options[:type]
9+
10+
column(name, options[:type], **options)
11+
end
12+
13+
def geography(name, options = {})
14+
column(name, :geography, **options)
15+
end
16+
17+
def geometry(name, options = {})
18+
column(name, :geometry, **options)
19+
end
20+
21+
def geometry_collection(name, options = {})
22+
column(name, :geometry_collection, **options)
23+
end
24+
25+
def line_string(name, options = {})
26+
column(name, :line_string, **options)
27+
end
28+
29+
def multi_line_string(name, options = {})
30+
column(name, :multi_line_string, **options)
31+
end
32+
33+
def multi_point(name, options = {})
34+
column(name, :multi_point, **options)
35+
end
36+
37+
def multi_polygon(name, options = {})
38+
column(name, :multi_polygon, **options)
39+
end
40+
41+
def st_point(name, options = {})
42+
column(name, :st_point, **options)
43+
end
44+
45+
def st_polygon(name, options = {})
46+
column(name, :st_polygon, **options)
47+
end
48+
end
49+
end
50+
51+
PostgreSQL::Table.include CockroachDB::ColumnMethods
52+
end
53+
end
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveRecord
4+
module ConnectionAdapters
5+
module CockroachDB
6+
module OID
7+
class Spatial < Type::Value
8+
# sql_type is a string that comes from the database definition
9+
# examples:
10+
# "geometry(Point,4326)"
11+
# "geography(Point,4326)"
12+
# "geometry(Polygon,4326) NOT NULL"
13+
# "geometry(Geography,4326)"
14+
def initialize(oid, sql_type)
15+
@sql_type = sql_type
16+
@geo_type, @srid, @has_z, @has_m = self.class.parse_sql_type(sql_type)
17+
end
18+
19+
# sql_type: geometry, geometry(Point), geometry(Point,4326), ...
20+
#
21+
# returns [geo_type, srid, has_z, has_m]
22+
# geo_type: geography, geometry, point, line_string, polygon, ...
23+
# srid: 1234
24+
# has_z: false
25+
# has_m: false
26+
def self.parse_sql_type(sql_type)
27+
geo_type = nil
28+
srid = 0
29+
has_z = false
30+
has_m = false
31+
32+
if sql_type =~ /(geography|geometry)\((.*)\)$/i
33+
# geometry(Point)
34+
# geometry(Point,4326)
35+
params = Regexp.last_match(2).split(',')
36+
if params.first =~ /([a-z]+[^zm])(z?)(m?)/i
37+
has_z = Regexp.last_match(2).length > 0
38+
has_m = Regexp.last_match(3).length > 0
39+
geo_type = Regexp.last_match(1)
40+
end
41+
srid = Regexp.last_match(1).to_i if params.last =~ /(\d+)/
42+
else
43+
geo_type = sql_type
44+
end
45+
[geo_type, srid, has_z, has_m]
46+
end
47+
48+
def spatial_factory
49+
@spatial_factory ||=
50+
RGeo::ActiveRecord::SpatialFactoryStore.instance.factory(
51+
factory_attrs
52+
)
53+
end
54+
55+
def geographic?
56+
@sql_type =~ /geography/
57+
end
58+
59+
def spatial?
60+
true
61+
end
62+
63+
def type
64+
geographic? ? :geography : :geometry
65+
end
66+
67+
# support setting an RGeo object or a WKT string
68+
def serialize(value)
69+
return if value.nil?
70+
71+
geo_value = cast_value(value)
72+
73+
# TODO: - only valid types should be allowed
74+
# e.g. linestring is not valid for point column
75+
# raise "maybe should raise" unless RGeo::Feature::Geometry.check_type(geo_value)
76+
77+
RGeo::WKRep::WKBGenerator.new(hex_format: true, type_format: :ewkb, emit_ewkb_srid: true)
78+
.generate(geo_value)
79+
end
80+
81+
private
82+
83+
def cast_value(value)
84+
return if value.nil?
85+
86+
value.is_a?(String) ? parse_wkt(value) : value
87+
end
88+
89+
# convert WKT string into RGeo object
90+
def parse_wkt(string)
91+
wkt_parser(string).parse(string)
92+
rescue RGeo::Error::ParseError
93+
nil
94+
end
95+
96+
def binary_string?(string)
97+
string[0] == "\x00" || string[0] == "\x01" || string[0, 4] =~ /[0-9a-fA-F]{4}/
98+
end
99+
100+
def wkt_parser(string)
101+
if binary_string?(string)
102+
RGeo::WKRep::WKBParser.new(spatial_factory, support_ewkb: true, default_srid: @srid)
103+
else
104+
RGeo::WKRep::WKTParser.new(spatial_factory, support_ewkt: true, default_srid: @srid)
105+
end
106+
end
107+
108+
def factory_attrs
109+
{
110+
geo_type: @geo_type.underscore,
111+
has_m: @has_m,
112+
has_z: @has_z,
113+
srid: @srid,
114+
sql_type: type.to_s
115+
}
116+
end
117+
end
118+
end
119+
end
120+
end
121+
end

lib/active_record/connection_adapters/cockroachdb/quoting.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,18 @@ module Quoting
1414
# always be strings. Then, we won't have to make any additional changes
1515
# to ActiveRecord to support inserting integer values into string
1616
# columns.
17+
#
18+
# For spatial types, data is stored as Well-known Binary (WKB) strings
19+
# (https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well-known_binary)
20+
# but when creating objects, using RGeo features is more convenient than
21+
# converting to WKB, so this does it automatically.
1722
def _quote(value)
18-
case value
19-
when Numeric
23+
if value.is_a?(Numeric)
2024
"'#{quote_string(value.to_s)}'"
25+
elsif RGeo::Feature::Geometry.check_type(value)
26+
"'#{RGeo::WKRep::WKBGenerator.new(hex_format: true, type_format: :ewkb, emit_ewkb_srid: true).generate(value)}'"
27+
elsif value.is_a?(RGeo::Cartesian::BoundingBox)
28+
"'#{value.min_x},#{value.min_y},#{value.max_x},#{value.max_y}'::box"
2129
else
2230
super
2331
end

0 commit comments

Comments
 (0)