activerecord-cockroachdb-adapter 0.2.3 → 5.2.2
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 +5 -5
- data/.gitignore +1 -0
- data/.gitmodules +0 -3
- data/CONTRIBUTING.md +25 -53
- data/Gemfile +58 -6
- data/README.md +293 -2
- data/Rakefile +17 -5
- data/activerecord-cockroachdb-adapter.gemspec +3 -6
- data/build/Dockerfile +1 -1
- data/build/teamcity-test.sh +17 -37
- data/docker.sh +1 -1
- data/lib/active_record/connection_adapters/cockroachdb/arel_tosql.rb +27 -0
- data/lib/active_record/connection_adapters/cockroachdb/attribute_methods.rb +28 -0
- data/lib/active_record/connection_adapters/cockroachdb/column.rb +94 -0
- data/lib/active_record/connection_adapters/cockroachdb/column_methods.rb +53 -0
- data/lib/active_record/connection_adapters/cockroachdb/database_statements.rb +102 -0
- data/lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb +121 -0
- data/lib/active_record/connection_adapters/cockroachdb/quoting.rb +37 -0
- data/lib/active_record/connection_adapters/cockroachdb/referential_integrity.rb +23 -38
- data/lib/active_record/connection_adapters/cockroachdb/schema_statements.rb +123 -40
- 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 +14 -16
- data/lib/active_record/connection_adapters/cockroachdb/type.rb +14 -0
- data/lib/active_record/connection_adapters/cockroachdb_adapter.rb +218 -123
- metadata +18 -42
@@ -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,37 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module CockroachDB
|
4
|
+
module Quoting
|
5
|
+
private
|
6
|
+
|
7
|
+
# CockroachDB does not allow inserting integer values into string
|
8
|
+
# columns, but ActiveRecord expects this to work. CockroachDB will
|
9
|
+
# however allow inserting string values into integer columns. It will
|
10
|
+
# try to parse string values and convert them to integers so they can be
|
11
|
+
# inserted in integer columns.
|
12
|
+
#
|
13
|
+
# We take advantage of this behavior here by forcing numeric values to
|
14
|
+
# always be strings. Then, we won't have to make any additional changes
|
15
|
+
# to ActiveRecord to support inserting integer values into string
|
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.
|
22
|
+
def _quote(value)
|
23
|
+
case value
|
24
|
+
when Numeric
|
25
|
+
"'#{quote_string(value.to_s)}'"
|
26
|
+
when RGeo::Feature::Geometry
|
27
|
+
"'#{RGeo::WKRep::WKBGenerator.new(hex_format: true, type_format: :ewkb, emit_ewkb_srid: true).generate(value)}'"
|
28
|
+
when RGeo::Cartesian::BoundingBox
|
29
|
+
"'#{value.min_x},#{value.min_y},#{value.max_x},#{value.max_y}'::box"
|
30
|
+
else
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,51 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
#
|
2
|
+
|
3
|
+
# The PostgresSQL Adapter's ReferentialIntegrity module can disable and
|
4
|
+
# re-enable foreign key constraints by disabling all table triggers. Since
|
5
|
+
# triggers are not available in CockroachDB, we have to remove foreign keys and
|
6
|
+
# re-add them via the ActiveRecord API.
|
7
|
+
#
|
8
|
+
# This module is commonly used to load test fixture data without having to worry
|
9
|
+
# about the order in which that data is loaded.
|
4
10
|
module ActiveRecord
|
5
11
|
module ConnectionAdapters
|
6
12
|
module CockroachDB
|
7
|
-
module ReferentialIntegrity
|
8
|
-
def disable_referential_integrity
|
9
|
-
|
10
|
-
fkeys = nil
|
13
|
+
module ReferentialIntegrity
|
14
|
+
def disable_referential_integrity
|
15
|
+
foreign_keys = tables.map { |table| foreign_keys(table) }.flatten
|
11
16
|
|
12
|
-
|
13
|
-
|
14
|
-
tables.each do |table_name|
|
15
|
-
fkeys = foreign_keys(table_name)
|
16
|
-
fkeys.each do |fkey|
|
17
|
-
remove_foreign_key table_name, name: fkey.options[:name]
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
rescue ActiveRecord::ActiveRecordError => e
|
22
|
-
original_exception = e
|
17
|
+
foreign_keys.each do |foreign_key|
|
18
|
+
remove_foreign_key(foreign_key.from_table, name: foreign_key.options[:name])
|
23
19
|
end
|
24
20
|
|
25
|
-
|
26
|
-
yield
|
27
|
-
rescue ActiveRecord::InvalidForeignKey => e
|
28
|
-
warn <<-WARNING
|
29
|
-
WARNING: Rails was not able to disable referential integrity.
|
30
|
-
|
31
|
-
Please go to https://github.com/cockroachdb/activerecord-cockroachdb-adapter
|
32
|
-
and report this issue.
|
33
|
-
|
34
|
-
cause: #{original_exception.try(:message)}
|
35
|
-
|
36
|
-
WARNING
|
37
|
-
raise e
|
38
|
-
end
|
21
|
+
yield
|
39
22
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
23
|
+
foreign_keys.each do |foreign_key|
|
24
|
+
begin
|
25
|
+
add_foreign_key(foreign_key.from_table, foreign_key.to_table, foreign_key.options)
|
26
|
+
rescue ActiveRecord::StatementInvalid => error
|
27
|
+
if error.cause.class == PG::DuplicateObject
|
28
|
+
# This error is safe to ignore because the yielded caller
|
29
|
+
# already re-added the foreign key constraint.
|
30
|
+
else
|
31
|
+
raise error
|
46
32
|
end
|
47
33
|
end
|
48
|
-
rescue ActiveRecord::ActiveRecordError
|
49
34
|
end
|
50
35
|
end
|
51
36
|
end
|
@@ -1,56 +1,139 @@
|
|
1
|
-
require 'active_record/connection_adapters/postgresql/schema_statements'
|
2
|
-
|
3
1
|
module ActiveRecord
|
4
2
|
module ConnectionAdapters
|
5
3
|
module CockroachDB
|
6
4
|
module SchemaStatements
|
7
5
|
include ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements
|
8
|
-
# NOTE(joey): This was ripped from PostgresSQL::SchemaStatements, with a
|
9
|
-
# slight modification to change setval(string, int, bool) to just
|
10
|
-
# setval(string, int) for CockroachDB compatbility.
|
11
|
-
# See https://github.com/cockroachdb/cockroach/issues/19723
|
12
|
-
#
|
13
|
-
# Resets the sequence of a table's primary key to the maximum value.
|
14
|
-
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
|
15
|
-
unless pk && sequence
|
16
|
-
default_pk, default_sequence = pk_and_sequence_for(table)
|
17
6
|
|
18
|
-
|
19
|
-
|
7
|
+
def add_index(table_name, column_name, options = {})
|
8
|
+
super
|
9
|
+
rescue ActiveRecord::StatementInvalid => error
|
10
|
+
if debugging? && error.cause.class == PG::FeatureNotSupported
|
11
|
+
warn "#{error}\n\nThis error will be ignored and the index will not be created.\n\n"
|
12
|
+
else
|
13
|
+
raise error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# ActiveRecord allows for tables to exist without primary keys.
|
18
|
+
# Databases like PostgreSQL support this behavior, but CockroachDB does
|
19
|
+
# not. If a table is created without a primary key, CockroachDB will add
|
20
|
+
# a rowid column to serve as its primary key. This breaks a lot of
|
21
|
+
# ActiveRecord's assumptions so we'll treat tables with rowid primary
|
22
|
+
# keys as if they didn't have primary keys at all.
|
23
|
+
# https://www.cockroachlabs.com/docs/v19.2/create-table.html#create-a-table
|
24
|
+
# https://api.rubyonrails.org/v5.2.4/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-create_table
|
25
|
+
def primary_key(table_name)
|
26
|
+
pk = super
|
27
|
+
|
28
|
+
if pk == CockroachDBAdapter::DEFAULT_PRIMARY_KEY
|
29
|
+
nil
|
30
|
+
else
|
31
|
+
pk
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# CockroachDB uses unique_rowid() for primary keys, not sequences. It's
|
36
|
+
# possible to force a table to use sequences, but since it's not the
|
37
|
+
# default behavior we'll always return nil for default_sequence_name.
|
38
|
+
def default_sequence_name(table_name, pk = "id")
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def columns(table_name)
|
43
|
+
# Limit, precision, and scale are all handled by the superclass.
|
44
|
+
column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation, comment|
|
45
|
+
oid = oid.to_i
|
46
|
+
fmod = fmod.to_i
|
47
|
+
type_metadata = fetch_type_metadata(column_name, type, oid, fmod)
|
48
|
+
cast_type = get_oid_type(oid.to_i, fmod.to_i, column_name, type)
|
49
|
+
default_value = extract_value_from_default(default)
|
50
|
+
|
51
|
+
default_function = extract_default_function(default_value, default)
|
52
|
+
new_column(table_name, column_name, default_value, cast_type, type_metadata, !notnull,
|
53
|
+
default_function, collation, comment)
|
20
54
|
end
|
55
|
+
end
|
21
56
|
|
22
|
-
|
23
|
-
|
57
|
+
def new_column(table_name, column_name, default, cast_type, sql_type_metadata = nil,
|
58
|
+
null = true, default_function = nil, collation = nil, comment = nil)
|
59
|
+
# JDBC gets true/false in Rails 4, where other platforms get 't'/'f' strings.
|
60
|
+
if null.is_a?(String)
|
61
|
+
null = (null == "t")
|
24
62
|
end
|
25
63
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
64
|
+
column_info = spatial_column_info(table_name).get(column_name, sql_type_metadata.sql_type)
|
65
|
+
|
66
|
+
PostgreSQLColumn.new(
|
67
|
+
column_name,
|
68
|
+
default,
|
69
|
+
sql_type_metadata,
|
70
|
+
null,
|
71
|
+
table_name,
|
72
|
+
default_function,
|
73
|
+
collation,
|
74
|
+
comment,
|
75
|
+
cast_type,
|
76
|
+
column_info
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
# CockroachDB will use INT8 if the SQL type is INTEGER, so we make it use
|
81
|
+
# INT4 explicitly when needed.
|
82
|
+
#
|
83
|
+
# For spatial columns, include the limit to properly format the column name
|
84
|
+
# since type alone is not enough to format the column.
|
85
|
+
# Ex. type_to_sql(:geography, limit: "Point,4326")
|
86
|
+
# => "geography(Point,4326)"
|
87
|
+
#
|
88
|
+
def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc:
|
89
|
+
sql = \
|
90
|
+
case type.to_s
|
91
|
+
when "integer"
|
92
|
+
case limit
|
93
|
+
when nil; "int"
|
94
|
+
when 1, 2; "int2"
|
95
|
+
when 3, 4; "int4"
|
96
|
+
when 5..8; "int8"
|
97
|
+
else super
|
34
98
|
end
|
99
|
+
when "geometry", "geography"
|
100
|
+
"#{type}(#{limit})"
|
101
|
+
else
|
102
|
+
super
|
35
103
|
end
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# SELECT setval(..., max_pk, false)
|
40
|
-
#
|
41
|
-
# with
|
42
|
-
#
|
43
|
-
# SELECT setval(..., max_pk-1)
|
44
|
-
#
|
45
|
-
# These two statements are semantically equivilant, but
|
46
|
-
# setval(string, int, bool) is not supported by CockroachDB.
|
47
|
-
#
|
48
|
-
# FIXME(joey): This is incorrect if the sequence is not 1
|
49
|
-
# incremented. We would need to pull out the custom increment value.
|
50
|
-
max_pk - 1
|
51
|
-
end
|
52
|
-
query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue})", "SCHEMA")
|
104
|
+
# The call to super might have appeneded [] already.
|
105
|
+
if array && type != :primary_key && !sql.end_with?("[]")
|
106
|
+
sql = "#{sql}[]"
|
53
107
|
end
|
108
|
+
sql
|
109
|
+
end
|
110
|
+
|
111
|
+
# override
|
112
|
+
def native_database_types
|
113
|
+
# Add spatial types
|
114
|
+
super.merge(
|
115
|
+
geography: { name: "geography" },
|
116
|
+
geometry: { name: "geometry" },
|
117
|
+
geometry_collection: { name: "geometry_collection" },
|
118
|
+
line_string: { name: "line_string" },
|
119
|
+
multi_line_string: { name: "multi_line_string" },
|
120
|
+
multi_point: { name: "multi_point" },
|
121
|
+
multi_polygon: { name: "multi_polygon" },
|
122
|
+
spatial: { name: "geometry" },
|
123
|
+
st_point: { name: "st_point" },
|
124
|
+
st_polygon: { name: "st_polygon" }
|
125
|
+
)
|
126
|
+
end
|
127
|
+
|
128
|
+
# override
|
129
|
+
def create_table_definition(*args, **kwargs)
|
130
|
+
CockroachDB::TableDefinition.new(*args, **kwargs)
|
131
|
+
end
|
132
|
+
|
133
|
+
# memoize hash of column infos for tables
|
134
|
+
def spatial_column_info(table_name)
|
135
|
+
@spatial_column_info ||= {}
|
136
|
+
@spatial_column_info[table_name.to_sym] ||= SpatialColumnInfo.new(self, table_name.to_s)
|
54
137
|
end
|
55
138
|
end
|
56
139
|
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
|