activerecord-cockroachdb-adapter 6.0.0beta1 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -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 +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/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/transaction_manager.rb +10 -4
- data/lib/active_record/connection_adapters/cockroachdb_adapter.rb +250 -6
- metadata +26 -4
@@ -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
|
@@ -24,12 +37,9 @@ module ActiveRecord
|
|
24
37
|
valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:sslmode, :application_name]
|
25
38
|
conn_params.slice!(*valid_conn_param_keys)
|
26
39
|
|
27
|
-
# The postgres drivers don't allow the creation of an unconnected
|
28
|
-
# PG::Connection object, so just pass a nil connection object for the
|
29
|
-
# time being.
|
30
40
|
conn = PG.connect(conn_params)
|
31
41
|
ConnectionAdapters::CockroachDBAdapter.new(conn, logger, conn_params, config)
|
32
|
-
rescue ::PG::Error => error
|
42
|
+
rescue ::PG::Error, ActiveRecord::ActiveRecordError => error
|
33
43
|
if error.message.include?("does not exist")
|
34
44
|
raise ActiveRecord::NoDatabaseError
|
35
45
|
else
|
@@ -41,15 +51,127 @@ end
|
|
41
51
|
|
42
52
|
module ActiveRecord
|
43
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
|
+
with_connection do |conn|
|
62
|
+
if conn.active?
|
63
|
+
begin
|
64
|
+
query = "SELECT crdb_internal.increment_feature_counter('ActiveRecord %d.%d')"
|
65
|
+
conn.execute(query % [ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR])
|
66
|
+
rescue ActiveRecord::StatementInvalid
|
67
|
+
# The increment_feature_counter built-in is not supported on this
|
68
|
+
# CockroachDB version. Ignore.
|
69
|
+
rescue StandardError => e
|
70
|
+
conn.logger.warn "Unexpected error when incrementing feature counter: #{e}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
ConnectionPool.prepend(CockroachDBConnectionPool)
|
77
|
+
|
44
78
|
class CockroachDBAdapter < PostgreSQLAdapter
|
45
79
|
ADAPTER_NAME = "CockroachDB".freeze
|
46
80
|
DEFAULT_PRIMARY_KEY = "rowid"
|
47
81
|
|
82
|
+
SPATIAL_COLUMN_OPTIONS =
|
83
|
+
{
|
84
|
+
geography: { geographic: true },
|
85
|
+
geometry: {},
|
86
|
+
geometry_collection: {},
|
87
|
+
line_string: {},
|
88
|
+
multi_line_string: {},
|
89
|
+
multi_point: {},
|
90
|
+
multi_polygon: {},
|
91
|
+
spatial: {},
|
92
|
+
st_point: {},
|
93
|
+
st_polygon: {},
|
94
|
+
}
|
95
|
+
|
96
|
+
# http://postgis.17.x6.nabble.com/Default-SRID-td5001115.html
|
97
|
+
DEFAULT_SRID = 0
|
98
|
+
|
48
99
|
include CockroachDB::SchemaStatements
|
49
100
|
include CockroachDB::ReferentialIntegrity
|
50
101
|
include CockroachDB::DatabaseStatements
|
51
102
|
include CockroachDB::Quoting
|
52
103
|
|
104
|
+
# override
|
105
|
+
# This method makes a sql query to gather information about columns
|
106
|
+
# in a table. It returns an array of arrays (one for each col) and
|
107
|
+
# passes each to the SchemaStatements#new_column_from_field method
|
108
|
+
# as the field parameter. This data is then used to format the column
|
109
|
+
# objects for the model and sent to the OID for data casting.
|
110
|
+
#
|
111
|
+
# The issue with the default method is that the sql_type field is
|
112
|
+
# retrieved with the `format_type` function, but this is implemented
|
113
|
+
# differently in CockroachDB than PostGIS, so geometry/geography
|
114
|
+
# types are missing information which makes parsing them impossible.
|
115
|
+
# Below is an example of what `format_type` returns for a geometry
|
116
|
+
# column.
|
117
|
+
#
|
118
|
+
# column_type: geometry(POINT, 4326)
|
119
|
+
# Expected: geometry(POINT, 4326)
|
120
|
+
# Actual: geometry
|
121
|
+
#
|
122
|
+
# The solution is to make the default query with super, then
|
123
|
+
# iterate through the columns and if it is a spatial type,
|
124
|
+
# access the proper column_type with the information_schema.columns
|
125
|
+
# table.
|
126
|
+
#
|
127
|
+
# @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
|
128
|
+
def column_definitions(table_name)
|
129
|
+
fields = super
|
130
|
+
# iterate through and identify all spatial fields based on format_type
|
131
|
+
# being geometry or geography, then query for the information_schema.column
|
132
|
+
# column_type because that contains the necessary information.
|
133
|
+
fields.map do |field|
|
134
|
+
dtype = field[1]
|
135
|
+
if dtype == 'geometry' || dtype == 'geography'
|
136
|
+
col_name = field[0]
|
137
|
+
data_type = \
|
138
|
+
query(<<~SQL, "SCHEMA")
|
139
|
+
SELECT c.data_type
|
140
|
+
FROM information_schema.columns c
|
141
|
+
WHERE c.table_name = #{quote(table_name)}
|
142
|
+
AND c.column_name = #{quote(col_name)}
|
143
|
+
SQL
|
144
|
+
field[1] = data_type[0][0]
|
145
|
+
end
|
146
|
+
field
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def arel_visitor
|
151
|
+
Arel::Visitors::CockroachDB.new(self)
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.spatial_column_options(key)
|
155
|
+
SPATIAL_COLUMN_OPTIONS[key]
|
156
|
+
end
|
157
|
+
|
158
|
+
def postgis_lib_version
|
159
|
+
@postgis_lib_version ||= select_value("SELECT PostGIS_Lib_Version()")
|
160
|
+
end
|
161
|
+
|
162
|
+
def default_srid
|
163
|
+
DEFAULT_SRID
|
164
|
+
end
|
165
|
+
|
166
|
+
def srs_database_columns
|
167
|
+
{
|
168
|
+
auth_name_column: "auth_name",
|
169
|
+
auth_srid_column: "auth_srid",
|
170
|
+
proj4text_column: "proj4text",
|
171
|
+
srtext_column: "srtext",
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
53
175
|
def debugging?
|
54
176
|
!!ENV["DEBUG_COCKROACHDB_ADAPTER"]
|
55
177
|
end
|
@@ -120,6 +242,10 @@ module ActiveRecord
|
|
120
242
|
@crdb_version >= 202
|
121
243
|
end
|
122
244
|
|
245
|
+
def supports_partitioned_indexes?
|
246
|
+
false
|
247
|
+
end
|
248
|
+
|
123
249
|
# This is hardcoded to 63 (as previously was in ActiveRecord 5.0) to aid in
|
124
250
|
# migration from PostgreSQL to CockroachDB. In practice, this limitation
|
125
251
|
# is arbitrary since CockroachDB supports index name lengths and table alias
|
@@ -137,6 +263,7 @@ module ActiveRecord
|
|
137
263
|
|
138
264
|
def initialize(connection, logger, conn_params, config)
|
139
265
|
super(connection, logger, conn_params, config)
|
266
|
+
|
140
267
|
crdb_version_string = query_value("SHOW crdb_version")
|
141
268
|
if crdb_version_string.include? "v1."
|
142
269
|
version_num = 1
|
@@ -154,10 +281,31 @@ module ActiveRecord
|
|
154
281
|
@crdb_version = version_num
|
155
282
|
end
|
156
283
|
|
284
|
+
def self.database_exists?(config)
|
285
|
+
!!ActiveRecord::Base.cockroachdb_connection(config)
|
286
|
+
rescue ActiveRecord::NoDatabaseError
|
287
|
+
false
|
288
|
+
end
|
289
|
+
|
157
290
|
private
|
158
291
|
|
159
292
|
def initialize_type_map(m = type_map)
|
160
|
-
|
293
|
+
%w(
|
294
|
+
geography
|
295
|
+
geometry
|
296
|
+
geometry_collection
|
297
|
+
line_string
|
298
|
+
multi_line_string
|
299
|
+
multi_point
|
300
|
+
multi_polygon
|
301
|
+
st_point
|
302
|
+
st_polygon
|
303
|
+
).each do |geo_type|
|
304
|
+
m.register_type(geo_type) do |oid, _, sql_type|
|
305
|
+
CockroachDB::OID::Spatial.new(oid, sql_type)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
161
309
|
# NOTE(joey): PostgreSQL intervals have a precision.
|
162
310
|
# CockroachDB intervals do not, so overide the type
|
163
311
|
# definition. Returning a ArgumentError may not be correct.
|
@@ -169,6 +317,8 @@ module ActiveRecord
|
|
169
317
|
end
|
170
318
|
OID::SpecializedString.new(:interval, precision: precision)
|
171
319
|
end
|
320
|
+
|
321
|
+
super(m)
|
172
322
|
end
|
173
323
|
|
174
324
|
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
|
@@ -278,6 +428,100 @@ module ActiveRecord
|
|
278
428
|
return "{}"
|
279
429
|
end
|
280
430
|
|
431
|
+
# override
|
432
|
+
# This method loads info about data types from the database to
|
433
|
+
# populate the TypeMap.
|
434
|
+
#
|
435
|
+
# Currently, querying from the pg_type catalog can be slow due to geo-partitioning
|
436
|
+
# so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
|
437
|
+
def load_additional_types(oids = nil)
|
438
|
+
if @config[:use_follower_reads_for_type_introspection]
|
439
|
+
initializer = OID::TypeMapInitializer.new(type_map)
|
440
|
+
|
441
|
+
query = <<~SQL
|
442
|
+
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
|
443
|
+
FROM pg_type as t
|
444
|
+
LEFT JOIN pg_range as r ON oid = rngtypid AS OF SYSTEM TIME '-10s'
|
445
|
+
SQL
|
446
|
+
|
447
|
+
if oids
|
448
|
+
query += "WHERE t.oid IN (%s)" % oids.join(", ")
|
449
|
+
else
|
450
|
+
query += initializer.query_conditions_for_initial_load
|
451
|
+
end
|
452
|
+
|
453
|
+
execute_and_clear(query, "SCHEMA", []) do |records|
|
454
|
+
initializer.run(records)
|
455
|
+
end
|
456
|
+
else
|
457
|
+
super
|
458
|
+
end
|
459
|
+
rescue ActiveRecord::StatementInvalid => e
|
460
|
+
raise e unless e.cause.is_a? PG::InvalidCatalogName
|
461
|
+
# use original if database is younger than 10s
|
462
|
+
super
|
463
|
+
end
|
464
|
+
|
465
|
+
# override
|
466
|
+
# This method maps data types to their proper decoder.
|
467
|
+
#
|
468
|
+
# Currently, querying from the pg_type catalog can be slow due to geo-partitioning
|
469
|
+
# so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
|
470
|
+
def add_pg_decoders
|
471
|
+
if @config[:use_follower_reads_for_type_introspection]
|
472
|
+
@default_timezone = nil
|
473
|
+
@timestamp_decoder = nil
|
474
|
+
|
475
|
+
coders_by_name = {
|
476
|
+
"int2" => PG::TextDecoder::Integer,
|
477
|
+
"int4" => PG::TextDecoder::Integer,
|
478
|
+
"int8" => PG::TextDecoder::Integer,
|
479
|
+
"oid" => PG::TextDecoder::Integer,
|
480
|
+
"float4" => PG::TextDecoder::Float,
|
481
|
+
"float8" => PG::TextDecoder::Float,
|
482
|
+
"numeric" => PG::TextDecoder::Numeric,
|
483
|
+
"bool" => PG::TextDecoder::Boolean,
|
484
|
+
"timestamp" => PG::TextDecoder::TimestampUtc,
|
485
|
+
"timestamptz" => PG::TextDecoder::TimestampWithTimeZone,
|
486
|
+
}
|
487
|
+
|
488
|
+
known_coder_types = coders_by_name.keys.map { |n| quote(n) }
|
489
|
+
query = <<~SQL % known_coder_types.join(", ")
|
490
|
+
SELECT t.oid, t.typname
|
491
|
+
FROM pg_type as t AS OF SYSTEM TIME '-10s'
|
492
|
+
WHERE t.typname IN (%s)
|
493
|
+
SQL
|
494
|
+
|
495
|
+
coders = execute_and_clear(query, "SCHEMA", []) do |result|
|
496
|
+
result
|
497
|
+
.map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
|
498
|
+
.compact
|
499
|
+
end
|
500
|
+
|
501
|
+
map = PG::TypeMapByOid.new
|
502
|
+
coders.each { |coder| map.add_coder(coder) }
|
503
|
+
@connection.type_map_for_results = map
|
504
|
+
|
505
|
+
@type_map_for_results = PG::TypeMapByOid.new
|
506
|
+
@type_map_for_results.default_type_map = map
|
507
|
+
@type_map_for_results.add_coder(PG::TextDecoder::Bytea.new(oid: 17, name: "bytea"))
|
508
|
+
|
509
|
+
# extract timestamp decoder for use in update_typemap_for_default_timezone
|
510
|
+
@timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
|
511
|
+
update_typemap_for_default_timezone
|
512
|
+
else
|
513
|
+
super
|
514
|
+
end
|
515
|
+
rescue ActiveRecord::StatementInvalid => e
|
516
|
+
raise e unless e.cause.is_a? PG::InvalidCatalogName
|
517
|
+
# use original if database is younger than 10s
|
518
|
+
super
|
519
|
+
end
|
520
|
+
|
521
|
+
def arel_visitor
|
522
|
+
Arel::Visitors::CockroachDB.new(self)
|
523
|
+
end
|
524
|
+
|
281
525
|
# end private
|
282
526
|
end
|
283
527
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-cockroachdb-adapter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 6.0.
|
4
|
+
version: 6.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cockroach Labs
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-04-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0.20'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rgeo-activerecord
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 7.0.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 7.0.0
|
41
55
|
description: Allows the use of CockroachDB as a backend for ActiveRecord and Rails
|
42
56
|
apps.
|
43
57
|
email:
|
@@ -48,6 +62,7 @@ extra_rdoc_files: []
|
|
48
62
|
files:
|
49
63
|
- ".gitignore"
|
50
64
|
- ".gitmodules"
|
65
|
+
- CHANGELOG.md
|
51
66
|
- CONTRIBUTING.md
|
52
67
|
- Gemfile
|
53
68
|
- LICENSE
|
@@ -61,13 +76,20 @@ files:
|
|
61
76
|
- build/local-test.sh
|
62
77
|
- build/teamcity-test.sh
|
63
78
|
- docker.sh
|
79
|
+
- lib/active_record/connection_adapters/cockroachdb/arel_tosql.rb
|
64
80
|
- lib/active_record/connection_adapters/cockroachdb/attribute_methods.rb
|
65
81
|
- lib/active_record/connection_adapters/cockroachdb/column.rb
|
82
|
+
- lib/active_record/connection_adapters/cockroachdb/column_methods.rb
|
66
83
|
- lib/active_record/connection_adapters/cockroachdb/database_statements.rb
|
67
84
|
- lib/active_record/connection_adapters/cockroachdb/database_tasks.rb
|
85
|
+
- lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb
|
86
|
+
- lib/active_record/connection_adapters/cockroachdb/oid/type_map_initializer.rb
|
68
87
|
- lib/active_record/connection_adapters/cockroachdb/quoting.rb
|
69
88
|
- lib/active_record/connection_adapters/cockroachdb/referential_integrity.rb
|
70
89
|
- lib/active_record/connection_adapters/cockroachdb/schema_statements.rb
|
90
|
+
- lib/active_record/connection_adapters/cockroachdb/setup.rb
|
91
|
+
- lib/active_record/connection_adapters/cockroachdb/spatial_column_info.rb
|
92
|
+
- lib/active_record/connection_adapters/cockroachdb/table_definition.rb
|
71
93
|
- lib/active_record/connection_adapters/cockroachdb/transaction_manager.rb
|
72
94
|
- lib/active_record/connection_adapters/cockroachdb/type.rb
|
73
95
|
- lib/active_record/connection_adapters/cockroachdb_adapter.rb
|
@@ -88,9 +110,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
110
|
version: '0'
|
89
111
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
112
|
requirements:
|
91
|
-
- - "
|
113
|
+
- - ">="
|
92
114
|
- !ruby/object:Gem::Version
|
93
|
-
version:
|
115
|
+
version: '0'
|
94
116
|
requirements: []
|
95
117
|
rubygems_version: 3.1.4
|
96
118
|
signing_key:
|