activerecord-cockroachdb-adapter 5.2.1 → 6.1.0beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +20 -0
- data/README.md +291 -0
- data/Rakefile +20 -0
- data/activerecord-cockroachdb-adapter.gemspec +4 -3
- data/build/teamcity-test.sh +2 -2
- 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/interval.rb +126 -0
- data/lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb +121 -0
- data/lib/active_record/connection_adapters/cockroachdb/quoting.rb +10 -2
- data/lib/active_record/connection_adapters/cockroachdb/schema_statements.rb +92 -2
- 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/transaction_manager.rb +11 -5
- data/lib/active_record/connection_adapters/cockroachdb/type.rb +1 -3
- data/lib/active_record/connection_adapters/cockroachdb_adapter.rb +191 -24
- metadata +35 -14
@@ -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,126 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require "active_support/duration"
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
module CockroachDB
|
8
|
+
module OID
|
9
|
+
module Interval # :nodoc:
|
10
|
+
DEFAULT_PRECISION = 6 # microseconds
|
11
|
+
|
12
|
+
def cast_value(value)
|
13
|
+
case value
|
14
|
+
when ::ActiveSupport::Duration
|
15
|
+
value
|
16
|
+
when ::String
|
17
|
+
begin
|
18
|
+
PostgresqlInterval::Parser.parse(value)
|
19
|
+
rescue PostgresqlInterval::ParseError
|
20
|
+
# Try ISO 8601
|
21
|
+
super
|
22
|
+
end
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def serialize(value)
|
29
|
+
precision = self.precision || DEFAULT_PRECISION
|
30
|
+
case value
|
31
|
+
when ::ActiveSupport::Duration
|
32
|
+
serialize_duration(value, precision)
|
33
|
+
when ::Numeric
|
34
|
+
serialize_duration(value.seconds, precision)
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def type_cast_for_schema(value)
|
41
|
+
serialize(value).inspect
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Convert an ActiveSupport::Duration to
|
47
|
+
# the postgres interval style
|
48
|
+
# ex. 1 year 2 mons 3 days 4 hours 5 minutes 6 seconds
|
49
|
+
def serialize_duration(value, precision)
|
50
|
+
yrs = value.parts.fetch(:years, 0)
|
51
|
+
mons = value.parts.fetch(:months, 0)
|
52
|
+
days = value.parts.fetch(:days, 0)
|
53
|
+
hrs = value.parts.fetch(:hours, 0)
|
54
|
+
mins = value.parts.fetch(:minutes, 0)
|
55
|
+
secs = value.parts.fetch(:seconds, 0).round(precision)
|
56
|
+
|
57
|
+
"#{yrs} years #{mons} mons #{days} days #{hrs} hours #{mins} minutes #{secs} seconds"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
PostgreSQL::OID::Interval.prepend(Interval)
|
62
|
+
end
|
63
|
+
|
64
|
+
module PostgresqlInterval
|
65
|
+
class Parser
|
66
|
+
PARTS = ActiveSupport::Duration::PARTS
|
67
|
+
PARTS_IN_SECONDS = ActiveSupport::Duration::PARTS_IN_SECONDS
|
68
|
+
|
69
|
+
# modified regex from https://github.com/jeremyevans/sequel/blob/master/lib/sequel/extensions/pg_interval.rb#L86
|
70
|
+
REGEX = /\A([+-]?\d+ years?\s?)?([+-]?\d+ mons?\s?)?([+-]?\d+ days?\s?)?(?:([+-])?(\d{2,10}):(\d\d):(\d\d(\.\d+)?))?\z/
|
71
|
+
|
72
|
+
def self.parse(string)
|
73
|
+
matches = REGEX.match(string)
|
74
|
+
raise(ParseError) unless matches
|
75
|
+
|
76
|
+
# 1 => years, 2 => months, 3 => days, 4 => nil, 5 => hours,
|
77
|
+
# 6 => minutes, 7 => seconds with fraction digits, 8 => fractional portion of 7
|
78
|
+
duration = 0
|
79
|
+
parts = {}
|
80
|
+
|
81
|
+
if matches[1]
|
82
|
+
val = matches[1].to_i
|
83
|
+
duration += val * PARTS_IN_SECONDS[:years]
|
84
|
+
parts[:years] = val
|
85
|
+
end
|
86
|
+
|
87
|
+
if matches[2]
|
88
|
+
val = matches[2].to_i
|
89
|
+
duration += val * PARTS_IN_SECONDS[:months]
|
90
|
+
parts[:months] = val
|
91
|
+
end
|
92
|
+
|
93
|
+
if matches[3]
|
94
|
+
val = matches[3].to_i
|
95
|
+
duration += val * PARTS_IN_SECONDS[:days]
|
96
|
+
parts[:days] = val
|
97
|
+
end
|
98
|
+
|
99
|
+
if matches[5]
|
100
|
+
val = matches[5].to_i
|
101
|
+
duration += val * PARTS_IN_SECONDS[:hours]
|
102
|
+
parts[:hours] = val
|
103
|
+
end
|
104
|
+
|
105
|
+
if matches[6]
|
106
|
+
val = matches[6].to_i
|
107
|
+
duration += val * PARTS_IN_SECONDS[:minutes]
|
108
|
+
parts[:minutes] = val
|
109
|
+
end
|
110
|
+
|
111
|
+
if matches[7]
|
112
|
+
val = matches[7].to_f
|
113
|
+
duration += val * PARTS_IN_SECONDS[:seconds]
|
114
|
+
parts[:seconds] = val
|
115
|
+
end
|
116
|
+
|
117
|
+
ActiveSupport::Duration.new(duration, parts)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class ParseError < StandardError
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
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
|
@@ -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
|
@@ -4,7 +4,7 @@ module ActiveRecord
|
|
4
4
|
module SchemaStatements
|
5
5
|
include ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements
|
6
6
|
|
7
|
-
def add_index(table_name, column_name, options
|
7
|
+
def add_index(table_name, column_name, **options)
|
8
8
|
super
|
9
9
|
rescue ActiveRecord::StatementInvalid => error
|
10
10
|
if debugging? && error.cause.class == PG::FeatureNotSupported
|
@@ -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,15 +87,70 @@ 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
|
58
95
|
# The call to super might have appeneded [] already.
|
59
96
|
if array && type != :primary_key && !sql.end_with?("[]")
|
60
|
-
sql = "#{sql}[]"
|
97
|
+
sql = "#{sql}[]"
|
61
98
|
end
|
62
99
|
sql
|
63
100
|
end
|
101
|
+
|
102
|
+
# This overrides the method from PostegreSQL adapter
|
103
|
+
# Resets the sequence of a table's primary key to the maximum value.
|
104
|
+
def reset_pk_sequence!(table, pk = nil, sequence = nil)
|
105
|
+
unless pk && sequence
|
106
|
+
default_pk, default_sequence = pk_and_sequence_for(table)
|
107
|
+
|
108
|
+
pk ||= default_pk
|
109
|
+
sequence ||= default_sequence
|
110
|
+
end
|
111
|
+
|
112
|
+
if @logger && pk && !sequence
|
113
|
+
@logger.warn "#{table} has primary key #{pk} with no default sequence."
|
114
|
+
end
|
115
|
+
|
116
|
+
if pk && sequence
|
117
|
+
quoted_sequence = quote_table_name(sequence)
|
118
|
+
max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
|
119
|
+
if max_pk.nil?
|
120
|
+
minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
|
121
|
+
end
|
122
|
+
|
123
|
+
query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false})", "SCHEMA")
|
124
|
+
end
|
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
|
64
154
|
end
|
65
155
|
end
|
66
156
|
end
|