activerecord-cockroachdb-adapter 6.0.0beta2 → 6.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +298 -0
- data/activerecord-cockroachdb-adapter.gemspec +2 -1
- data/build/config.teamcity.yml +3 -0
- data/build/teamcity-test.sh +1 -1
- data/lib/active_record/connection_adapters/cockroachdb/arel_tosql.rb +27 -0
- data/lib/active_record/connection_adapters/cockroachdb/column.rb +78 -1
- data/lib/active_record/connection_adapters/cockroachdb/column_methods.rb +53 -0
- data/lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb +121 -0
- data/lib/active_record/connection_adapters/cockroachdb/oid/type_map_initializer.rb +26 -0
- data/lib/active_record/connection_adapters/cockroachdb/quoting.rb +10 -2
- data/lib/active_record/connection_adapters/cockroachdb/schema_statements.rb +65 -0
- data/lib/active_record/connection_adapters/cockroachdb/setup.rb +19 -0
- data/lib/active_record/connection_adapters/cockroachdb/spatial_column_info.rb +44 -0
- data/lib/active_record/connection_adapters/cockroachdb/table_definition.rb +56 -0
- data/lib/active_record/connection_adapters/cockroachdb_adapter.rb +248 -2
- metadata +30 -8
@@ -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
|
@@ -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
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module CockroachDB
|
4
|
+
module OID
|
5
|
+
module TypeMapInitializer
|
6
|
+
# override
|
7
|
+
# Replaces the query with a faster version that doesn't rely on the
|
8
|
+
# use of 'array_in(cstring,oid,integer)'::regprocedure.
|
9
|
+
def query_conditions_for_initial_load
|
10
|
+
known_type_names = @store.keys.map { |n| "'#{n}'" }
|
11
|
+
known_type_types = %w('r' 'e' 'd')
|
12
|
+
<<~SQL % [known_type_names.join(", "), known_type_types.join(", ")]
|
13
|
+
WHERE
|
14
|
+
t.typname IN (%s)
|
15
|
+
OR t.typtype IN (%s)
|
16
|
+
OR (t.typarray = 0 AND t.typcategory='A')
|
17
|
+
OR t.typelem != 0
|
18
|
+
SQL
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
PostgreSQL::OID::TypeMapInitializer.prepend(TypeMapInitializer)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -14,10 +14,18 @@ module ActiveRecord
|
|
14
14
|
# always be strings. Then, we won't have to make any additional changes
|
15
15
|
# to ActiveRecord to support inserting integer values into string
|
16
16
|
# 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.
|
17
22
|
def _quote(value)
|
18
|
-
|
19
|
-
when Numeric
|
23
|
+
if value.is_a?(Numeric)
|
20
24
|
"'#{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"
|
21
29
|
else
|
22
30
|
super
|
23
31
|
end
|
@@ -39,8 +39,43 @@ module ActiveRecord
|
|
39
39
|
nil
|
40
40
|
end
|
41
41
|
|
42
|
+
# override
|
43
|
+
# https://github.com/rails/rails/blob/6-0-stable/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L624
|
44
|
+
def new_column_from_field(table_name, field)
|
45
|
+
column_name, type, default, notnull, oid, fmod, collation, comment = field
|
46
|
+
type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i)
|
47
|
+
default_value = extract_value_from_default(default)
|
48
|
+
default_function = extract_default_function(default_value, default)
|
49
|
+
|
50
|
+
serial =
|
51
|
+
if (match = default_function&.match(/\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z/))
|
52
|
+
sequence_name_from_parts(table_name, column_name, match[:suffix]) == match[:sequence_name]
|
53
|
+
end
|
54
|
+
|
55
|
+
# {:dimension=>2, :has_m=>false, :has_z=>false, :name=>"latlon", :srid=>0, :type=>"GEOMETRY"}
|
56
|
+
spatial = spatial_column_info(table_name).get(column_name, type_metadata.sql_type)
|
57
|
+
|
58
|
+
PostgreSQL::Column.new(
|
59
|
+
column_name,
|
60
|
+
default_value,
|
61
|
+
type_metadata,
|
62
|
+
!notnull,
|
63
|
+
default_function,
|
64
|
+
collation: collation,
|
65
|
+
comment: comment.presence,
|
66
|
+
serial: serial,
|
67
|
+
spatial: spatial
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
42
71
|
# CockroachDB will use INT8 if the SQL type is INTEGER, so we make it use
|
43
72
|
# INT4 explicitly when needed.
|
73
|
+
#
|
74
|
+
# For spatial columns, include the limit to properly format the column name
|
75
|
+
# since type alone is not enough to format the column.
|
76
|
+
# Ex. type_to_sql(:geography, limit: "Point,4326")
|
77
|
+
# => "geography(Point,4326)"
|
78
|
+
#
|
44
79
|
def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc:
|
45
80
|
sql = \
|
46
81
|
case type.to_s
|
@@ -52,6 +87,8 @@ module ActiveRecord
|
|
52
87
|
when 5..8; "int8"
|
53
88
|
else super
|
54
89
|
end
|
90
|
+
when "geometry", "geography"
|
91
|
+
"#{type}(#{limit})"
|
55
92
|
else
|
56
93
|
super
|
57
94
|
end
|
@@ -86,6 +123,34 @@ module ActiveRecord
|
|
86
123
|
query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false})", "SCHEMA")
|
87
124
|
end
|
88
125
|
end
|
126
|
+
|
127
|
+
# override
|
128
|
+
def native_database_types
|
129
|
+
# Add spatial types
|
130
|
+
super.merge(
|
131
|
+
geography: { name: "geography" },
|
132
|
+
geometry: { name: "geometry" },
|
133
|
+
geometry_collection: { name: "geometry_collection" },
|
134
|
+
line_string: { name: "line_string" },
|
135
|
+
multi_line_string: { name: "multi_line_string" },
|
136
|
+
multi_point: { name: "multi_point" },
|
137
|
+
multi_polygon: { name: "multi_polygon" },
|
138
|
+
spatial: { name: "geometry" },
|
139
|
+
st_point: { name: "st_point" },
|
140
|
+
st_polygon: { name: "st_polygon" }
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
# override
|
145
|
+
def create_table_definition(*args, **kwargs)
|
146
|
+
CockroachDB::TableDefinition.new(self, *args, **kwargs)
|
147
|
+
end
|
148
|
+
|
149
|
+
# memoize hash of column infos for tables
|
150
|
+
def spatial_column_info(table_name)
|
151
|
+
@spatial_column_info ||= {}
|
152
|
+
@spatial_column_info[table_name.to_sym] ||= SpatialColumnInfo.new(self, table_name.to_s)
|
153
|
+
end
|
89
154
|
end
|
90
155
|
end
|
91
156
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord # :nodoc:
|
4
|
+
module ConnectionAdapters # :nodoc:
|
5
|
+
module CockroachDB # :nodoc:
|
6
|
+
def self.initial_setup
|
7
|
+
::ActiveRecord::SchemaDumper.ignore_tables |= %w[
|
8
|
+
geography_columns
|
9
|
+
geometry_columns
|
10
|
+
layer
|
11
|
+
raster_columns
|
12
|
+
raster_overviews
|
13
|
+
spatial_ref_sys
|
14
|
+
topology
|
15
|
+
]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module CockroachDB
|
4
|
+
class SpatialColumnInfo
|
5
|
+
def initialize(adapter, table_name)
|
6
|
+
@adapter = adapter
|
7
|
+
@table_name = table_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def all
|
11
|
+
info = @adapter.query(
|
12
|
+
"SELECT f_geometry_column,coord_dimension,srid,type FROM geometry_columns WHERE f_table_name='#{@table_name}'"
|
13
|
+
)
|
14
|
+
result = {}
|
15
|
+
info.each do |row|
|
16
|
+
name = row[0]
|
17
|
+
type = row[3]
|
18
|
+
dimension = row[1].to_i
|
19
|
+
has_m = !!(type =~ /m$/i)
|
20
|
+
type.sub!(/m$/, '')
|
21
|
+
has_z = dimension > 3 || dimension == 3 && !has_m
|
22
|
+
result[name] = {
|
23
|
+
dimension: dimension,
|
24
|
+
has_m: has_m,
|
25
|
+
has_z: has_z,
|
26
|
+
name: name,
|
27
|
+
srid: row[2].to_i,
|
28
|
+
type: type
|
29
|
+
}
|
30
|
+
end
|
31
|
+
result
|
32
|
+
end
|
33
|
+
|
34
|
+
# do not query the database for non-spatial columns/tables
|
35
|
+
def get(column_name, type)
|
36
|
+
return unless CockroachDBAdapter.spatial_column_options(type.to_sym)
|
37
|
+
|
38
|
+
@spatial_column_info ||= all
|
39
|
+
@spatial_column_info[column_name]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord # :nodoc:
|
4
|
+
module ConnectionAdapters # :nodoc:
|
5
|
+
module CockroachDB # :nodoc:
|
6
|
+
class TableDefinition < PostgreSQL::TableDefinition # :nodoc:
|
7
|
+
include ColumnMethods
|
8
|
+
|
9
|
+
# Support for spatial columns in tables
|
10
|
+
# super: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
|
11
|
+
def new_column_definition(name, type, **options)
|
12
|
+
if (info = CockroachDBAdapter.spatial_column_options(type.to_sym))
|
13
|
+
if (limit = options.delete(:limit)) && limit.is_a?(::Hash)
|
14
|
+
options.merge!(limit)
|
15
|
+
end
|
16
|
+
|
17
|
+
geo_type = ColumnDefinitionUtils.geo_type(options[:type] || type || info[:type])
|
18
|
+
base_type = info[:type] || (options[:geographic] ? :geography : :geometry)
|
19
|
+
|
20
|
+
options[:limit] = ColumnDefinitionUtils.limit_from_options(geo_type, options)
|
21
|
+
options[:spatial_type] = geo_type
|
22
|
+
column = super(name, base_type, **options)
|
23
|
+
else
|
24
|
+
column = super(name, type, **options)
|
25
|
+
end
|
26
|
+
|
27
|
+
column
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ColumnDefinitionUtils
|
32
|
+
class << self
|
33
|
+
def geo_type(type = 'GEOMETRY')
|
34
|
+
g_type = type.to_s.delete('_').upcase
|
35
|
+
return 'POINT' if g_type == 'STPOINT'
|
36
|
+
return 'POLYGON' if g_type == 'STPOLYGON'
|
37
|
+
|
38
|
+
g_type
|
39
|
+
end
|
40
|
+
|
41
|
+
def limit_from_options(type, options = {})
|
42
|
+
spatial_type = geo_type(type)
|
43
|
+
spatial_type << 'Z' if options[:has_z]
|
44
|
+
spatial_type << 'M' if options[:has_m]
|
45
|
+
spatial_type << ",#{options[:srid] || default_srid(options)}"
|
46
|
+
spatial_type
|
47
|
+
end
|
48
|
+
|
49
|
+
def default_srid(options)
|
50
|
+
options[:geographic] ? 4326 : CockroachDBAdapter::DEFAULT_SRID
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -1,12 +1,25 @@
|
|
1
|
+
require "rgeo/active_record"
|
2
|
+
|
1
3
|
require 'active_record/connection_adapters/postgresql_adapter'
|
4
|
+
require "active_record/connection_adapters/cockroachdb/column_methods"
|
2
5
|
require "active_record/connection_adapters/cockroachdb/schema_statements"
|
3
6
|
require "active_record/connection_adapters/cockroachdb/referential_integrity"
|
4
7
|
require "active_record/connection_adapters/cockroachdb/transaction_manager"
|
5
|
-
require "active_record/connection_adapters/cockroachdb/column"
|
6
8
|
require "active_record/connection_adapters/cockroachdb/database_statements"
|
9
|
+
require "active_record/connection_adapters/cockroachdb/table_definition"
|
7
10
|
require "active_record/connection_adapters/cockroachdb/quoting"
|
8
11
|
require "active_record/connection_adapters/cockroachdb/type"
|
9
12
|
require "active_record/connection_adapters/cockroachdb/attribute_methods"
|
13
|
+
require "active_record/connection_adapters/cockroachdb/column"
|
14
|
+
require "active_record/connection_adapters/cockroachdb/spatial_column_info"
|
15
|
+
require "active_record/connection_adapters/cockroachdb/setup"
|
16
|
+
require "active_record/connection_adapters/cockroachdb/oid/type_map_initializer"
|
17
|
+
require "active_record/connection_adapters/cockroachdb/oid/spatial"
|
18
|
+
require "active_record/connection_adapters/cockroachdb/arel_tosql"
|
19
|
+
|
20
|
+
# Run to ignore spatial tables that will break schemna dumper.
|
21
|
+
# Defined in ./setup.rb
|
22
|
+
ActiveRecord::ConnectionAdapters::CockroachDB.initial_setup
|
10
23
|
|
11
24
|
module ActiveRecord
|
12
25
|
module ConnectionHandling
|
@@ -38,15 +51,132 @@ end
|
|
38
51
|
|
39
52
|
module ActiveRecord
|
40
53
|
module ConnectionAdapters
|
54
|
+
module CockroachDBConnectionPool
|
55
|
+
def initialize(spec)
|
56
|
+
super(spec)
|
57
|
+
disable_telemetry = spec.config[:disable_cockroachdb_telemetry]
|
58
|
+
adapter = spec.config[:adapter]
|
59
|
+
return if disable_telemetry || adapter != "cockroachdb"
|
60
|
+
|
61
|
+
|
62
|
+
begin
|
63
|
+
with_connection do |conn|
|
64
|
+
if conn.active?
|
65
|
+
begin
|
66
|
+
query = "SELECT crdb_internal.increment_feature_counter('ActiveRecord %d.%d')"
|
67
|
+
conn.execute(query % [ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR])
|
68
|
+
rescue ActiveRecord::StatementInvalid
|
69
|
+
# The increment_feature_counter built-in is not supported on this
|
70
|
+
# CockroachDB version. Ignore.
|
71
|
+
rescue StandardError => e
|
72
|
+
conn.logger.warn "Unexpected error when incrementing feature counter: #{e}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
rescue ActiveRecord::NoDatabaseError
|
77
|
+
# Prevent failures on db creation and parallel testing.
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
ConnectionPool.prepend(CockroachDBConnectionPool)
|
82
|
+
|
41
83
|
class CockroachDBAdapter < PostgreSQLAdapter
|
42
84
|
ADAPTER_NAME = "CockroachDB".freeze
|
43
85
|
DEFAULT_PRIMARY_KEY = "rowid"
|
44
86
|
|
87
|
+
SPATIAL_COLUMN_OPTIONS =
|
88
|
+
{
|
89
|
+
geography: { geographic: true },
|
90
|
+
geometry: {},
|
91
|
+
geometry_collection: {},
|
92
|
+
line_string: {},
|
93
|
+
multi_line_string: {},
|
94
|
+
multi_point: {},
|
95
|
+
multi_polygon: {},
|
96
|
+
spatial: {},
|
97
|
+
st_point: {},
|
98
|
+
st_polygon: {},
|
99
|
+
}
|
100
|
+
|
101
|
+
# http://postgis.17.x6.nabble.com/Default-SRID-td5001115.html
|
102
|
+
DEFAULT_SRID = 0
|
103
|
+
|
45
104
|
include CockroachDB::SchemaStatements
|
46
105
|
include CockroachDB::ReferentialIntegrity
|
47
106
|
include CockroachDB::DatabaseStatements
|
48
107
|
include CockroachDB::Quoting
|
49
108
|
|
109
|
+
# override
|
110
|
+
# This method makes a sql query to gather information about columns
|
111
|
+
# in a table. It returns an array of arrays (one for each col) and
|
112
|
+
# passes each to the SchemaStatements#new_column_from_field method
|
113
|
+
# as the field parameter. This data is then used to format the column
|
114
|
+
# objects for the model and sent to the OID for data casting.
|
115
|
+
#
|
116
|
+
# The issue with the default method is that the sql_type field is
|
117
|
+
# retrieved with the `format_type` function, but this is implemented
|
118
|
+
# differently in CockroachDB than PostGIS, so geometry/geography
|
119
|
+
# types are missing information which makes parsing them impossible.
|
120
|
+
# Below is an example of what `format_type` returns for a geometry
|
121
|
+
# column.
|
122
|
+
#
|
123
|
+
# column_type: geometry(POINT, 4326)
|
124
|
+
# Expected: geometry(POINT, 4326)
|
125
|
+
# Actual: geometry
|
126
|
+
#
|
127
|
+
# The solution is to make the default query with super, then
|
128
|
+
# iterate through the columns and if it is a spatial type,
|
129
|
+
# access the proper column_type with the information_schema.columns
|
130
|
+
# table.
|
131
|
+
#
|
132
|
+
# @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
|
133
|
+
def column_definitions(table_name)
|
134
|
+
fields = super
|
135
|
+
# iterate through and identify all spatial fields based on format_type
|
136
|
+
# being geometry or geography, then query for the information_schema.column
|
137
|
+
# column_type because that contains the necessary information.
|
138
|
+
fields.map do |field|
|
139
|
+
dtype = field[1]
|
140
|
+
if dtype == 'geometry' || dtype == 'geography'
|
141
|
+
col_name = field[0]
|
142
|
+
data_type = \
|
143
|
+
query(<<~SQL, "SCHEMA")
|
144
|
+
SELECT c.data_type
|
145
|
+
FROM information_schema.columns c
|
146
|
+
WHERE c.table_name = #{quote(table_name)}
|
147
|
+
AND c.column_name = #{quote(col_name)}
|
148
|
+
SQL
|
149
|
+
field[1] = data_type[0][0]
|
150
|
+
end
|
151
|
+
field
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def arel_visitor
|
156
|
+
Arel::Visitors::CockroachDB.new(self)
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.spatial_column_options(key)
|
160
|
+
SPATIAL_COLUMN_OPTIONS[key]
|
161
|
+
end
|
162
|
+
|
163
|
+
def postgis_lib_version
|
164
|
+
@postgis_lib_version ||= select_value("SELECT PostGIS_Lib_Version()")
|
165
|
+
end
|
166
|
+
|
167
|
+
def default_srid
|
168
|
+
DEFAULT_SRID
|
169
|
+
end
|
170
|
+
|
171
|
+
def srs_database_columns
|
172
|
+
{
|
173
|
+
auth_name_column: "auth_name",
|
174
|
+
auth_srid_column: "auth_srid",
|
175
|
+
proj4text_column: "proj4text",
|
176
|
+
srtext_column: "srtext",
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
50
180
|
def debugging?
|
51
181
|
!!ENV["DEBUG_COCKROACHDB_ADAPTER"]
|
52
182
|
end
|
@@ -117,6 +247,10 @@ module ActiveRecord
|
|
117
247
|
@crdb_version >= 202
|
118
248
|
end
|
119
249
|
|
250
|
+
def supports_partitioned_indexes?
|
251
|
+
false
|
252
|
+
end
|
253
|
+
|
120
254
|
# This is hardcoded to 63 (as previously was in ActiveRecord 5.0) to aid in
|
121
255
|
# migration from PostgreSQL to CockroachDB. In practice, this limitation
|
122
256
|
# is arbitrary since CockroachDB supports index name lengths and table alias
|
@@ -134,6 +268,7 @@ module ActiveRecord
|
|
134
268
|
|
135
269
|
def initialize(connection, logger, conn_params, config)
|
136
270
|
super(connection, logger, conn_params, config)
|
271
|
+
|
137
272
|
crdb_version_string = query_value("SHOW crdb_version")
|
138
273
|
if crdb_version_string.include? "v1."
|
139
274
|
version_num = 1
|
@@ -160,7 +295,22 @@ module ActiveRecord
|
|
160
295
|
private
|
161
296
|
|
162
297
|
def initialize_type_map(m = type_map)
|
163
|
-
|
298
|
+
%w(
|
299
|
+
geography
|
300
|
+
geometry
|
301
|
+
geometry_collection
|
302
|
+
line_string
|
303
|
+
multi_line_string
|
304
|
+
multi_point
|
305
|
+
multi_polygon
|
306
|
+
st_point
|
307
|
+
st_polygon
|
308
|
+
).each do |geo_type|
|
309
|
+
m.register_type(geo_type) do |oid, _, sql_type|
|
310
|
+
CockroachDB::OID::Spatial.new(oid, sql_type)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
164
314
|
# NOTE(joey): PostgreSQL intervals have a precision.
|
165
315
|
# CockroachDB intervals do not, so overide the type
|
166
316
|
# definition. Returning a ArgumentError may not be correct.
|
@@ -172,6 +322,8 @@ module ActiveRecord
|
|
172
322
|
end
|
173
323
|
OID::SpecializedString.new(:interval, precision: precision)
|
174
324
|
end
|
325
|
+
|
326
|
+
super(m)
|
175
327
|
end
|
176
328
|
|
177
329
|
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
|
@@ -281,6 +433,100 @@ module ActiveRecord
|
|
281
433
|
return "{}"
|
282
434
|
end
|
283
435
|
|
436
|
+
# override
|
437
|
+
# This method loads info about data types from the database to
|
438
|
+
# populate the TypeMap.
|
439
|
+
#
|
440
|
+
# Currently, querying from the pg_type catalog can be slow due to geo-partitioning
|
441
|
+
# so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
|
442
|
+
def load_additional_types(oids = nil)
|
443
|
+
if @config[:use_follower_reads_for_type_introspection]
|
444
|
+
initializer = OID::TypeMapInitializer.new(type_map)
|
445
|
+
|
446
|
+
query = <<~SQL
|
447
|
+
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
|
448
|
+
FROM pg_type as t
|
449
|
+
LEFT JOIN pg_range as r ON oid = rngtypid AS OF SYSTEM TIME '-10s'
|
450
|
+
SQL
|
451
|
+
|
452
|
+
if oids
|
453
|
+
query += "WHERE t.oid IN (%s)" % oids.join(", ")
|
454
|
+
else
|
455
|
+
query += initializer.query_conditions_for_initial_load
|
456
|
+
end
|
457
|
+
|
458
|
+
execute_and_clear(query, "SCHEMA", []) do |records|
|
459
|
+
initializer.run(records)
|
460
|
+
end
|
461
|
+
else
|
462
|
+
super
|
463
|
+
end
|
464
|
+
rescue ActiveRecord::StatementInvalid => e
|
465
|
+
raise e unless e.cause.is_a? PG::InvalidCatalogName
|
466
|
+
# use original if database is younger than 10s
|
467
|
+
super
|
468
|
+
end
|
469
|
+
|
470
|
+
# override
|
471
|
+
# This method maps data types to their proper decoder.
|
472
|
+
#
|
473
|
+
# Currently, querying from the pg_type catalog can be slow due to geo-partitioning
|
474
|
+
# so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
|
475
|
+
def add_pg_decoders
|
476
|
+
if @config[:use_follower_reads_for_type_introspection]
|
477
|
+
@default_timezone = nil
|
478
|
+
@timestamp_decoder = nil
|
479
|
+
|
480
|
+
coders_by_name = {
|
481
|
+
"int2" => PG::TextDecoder::Integer,
|
482
|
+
"int4" => PG::TextDecoder::Integer,
|
483
|
+
"int8" => PG::TextDecoder::Integer,
|
484
|
+
"oid" => PG::TextDecoder::Integer,
|
485
|
+
"float4" => PG::TextDecoder::Float,
|
486
|
+
"float8" => PG::TextDecoder::Float,
|
487
|
+
"numeric" => PG::TextDecoder::Numeric,
|
488
|
+
"bool" => PG::TextDecoder::Boolean,
|
489
|
+
"timestamp" => PG::TextDecoder::TimestampUtc,
|
490
|
+
"timestamptz" => PG::TextDecoder::TimestampWithTimeZone,
|
491
|
+
}
|
492
|
+
|
493
|
+
known_coder_types = coders_by_name.keys.map { |n| quote(n) }
|
494
|
+
query = <<~SQL % known_coder_types.join(", ")
|
495
|
+
SELECT t.oid, t.typname
|
496
|
+
FROM pg_type as t AS OF SYSTEM TIME '-10s'
|
497
|
+
WHERE t.typname IN (%s)
|
498
|
+
SQL
|
499
|
+
|
500
|
+
coders = execute_and_clear(query, "SCHEMA", []) do |result|
|
501
|
+
result
|
502
|
+
.map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
|
503
|
+
.compact
|
504
|
+
end
|
505
|
+
|
506
|
+
map = PG::TypeMapByOid.new
|
507
|
+
coders.each { |coder| map.add_coder(coder) }
|
508
|
+
@connection.type_map_for_results = map
|
509
|
+
|
510
|
+
@type_map_for_results = PG::TypeMapByOid.new
|
511
|
+
@type_map_for_results.default_type_map = map
|
512
|
+
@type_map_for_results.add_coder(PG::TextDecoder::Bytea.new(oid: 17, name: "bytea"))
|
513
|
+
|
514
|
+
# extract timestamp decoder for use in update_typemap_for_default_timezone
|
515
|
+
@timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
|
516
|
+
update_typemap_for_default_timezone
|
517
|
+
else
|
518
|
+
super
|
519
|
+
end
|
520
|
+
rescue ActiveRecord::StatementInvalid => e
|
521
|
+
raise e unless e.cause.is_a? PG::InvalidCatalogName
|
522
|
+
# use original if database is younger than 10s
|
523
|
+
super
|
524
|
+
end
|
525
|
+
|
526
|
+
def arel_visitor
|
527
|
+
Arel::Visitors::CockroachDB.new(self)
|
528
|
+
end
|
529
|
+
|
284
530
|
# end private
|
285
531
|
end
|
286
532
|
end
|