activerecord-cockroachdb-adapter 5.2.1 → 6.1.0beta1
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/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
|