activerecord-cockroachdb-adapter 6.0.0beta2 → 6.0.1
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 +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
|