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.
Files changed (27) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.gitmodules +0 -3
  4. data/CONTRIBUTING.md +25 -53
  5. data/Gemfile +58 -6
  6. data/README.md +293 -2
  7. data/Rakefile +17 -5
  8. data/activerecord-cockroachdb-adapter.gemspec +3 -6
  9. data/build/Dockerfile +1 -1
  10. data/build/teamcity-test.sh +17 -37
  11. data/docker.sh +1 -1
  12. data/lib/active_record/connection_adapters/cockroachdb/arel_tosql.rb +27 -0
  13. data/lib/active_record/connection_adapters/cockroachdb/attribute_methods.rb +28 -0
  14. data/lib/active_record/connection_adapters/cockroachdb/column.rb +94 -0
  15. data/lib/active_record/connection_adapters/cockroachdb/column_methods.rb +53 -0
  16. data/lib/active_record/connection_adapters/cockroachdb/database_statements.rb +102 -0
  17. data/lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb +121 -0
  18. data/lib/active_record/connection_adapters/cockroachdb/quoting.rb +37 -0
  19. data/lib/active_record/connection_adapters/cockroachdb/referential_integrity.rb +23 -38
  20. data/lib/active_record/connection_adapters/cockroachdb/schema_statements.rb +123 -40
  21. data/lib/active_record/connection_adapters/cockroachdb/setup.rb +19 -0
  22. data/lib/active_record/connection_adapters/cockroachdb/spatial_column_info.rb +44 -0
  23. data/lib/active_record/connection_adapters/cockroachdb/table_definition.rb +56 -0
  24. data/lib/active_record/connection_adapters/cockroachdb/transaction_manager.rb +14 -16
  25. data/lib/active_record/connection_adapters/cockroachdb/type.rb +14 -0
  26. data/lib/active_record/connection_adapters/cockroachdb_adapter.rb +218 -123
  27. metadata +18 -42
@@ -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,25 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record/connection_adapters/abstract/transaction'
4
-
5
3
  module ActiveRecord
6
4
  module ConnectionAdapters
7
-
8
- # NOTE(joey): This is a very sad monkey patch. Unfortunately, it is
9
- # required in order to prevent doing more than 2 nested transactions
10
- # while still allowing a single nested transaction. This is because
11
- # CockroachDB only supports a single savepoint at the beginning of a
12
- # transaction. Allowing this works for the common case of testing.
13
5
  module CockroachDB
14
6
  module TransactionManagerMonkeyPatch
15
- def begin_transaction(options={})
16
- @connection.lock.synchronize do
17
- # If the transaction nesting is already 2 deep, raise an error.
18
- if @connection.adapter_name == "CockroachDB" && @stack.is_a?(ActiveRecord::ConnectionAdapters::SavepointTransaction)
19
- raise(ArgumentError, "cannot nest more than 1 transaction at a time. this is a CockroachDB limitation")
20
- end
21
- end
22
- super(options)
7
+ # Capture ActiveRecord::SerializationFailure errors caused by
8
+ # transactions that fail due to serialization errors. Failed
9
+ # transactions will be retried until they pass or the max retry limit is
10
+ # exceeded.
11
+ def within_new_transaction(options = {})
12
+ attempts = options.fetch(:attempts, 0)
13
+ super
14
+ rescue ActiveRecord::SerializationFailure => error
15
+ raise if attempts >= @connection.max_transaction_retries
16
+
17
+ attempts += 1
18
+ sleep_seconds = (2 ** attempts + rand) / 10
19
+ sleep(sleep_seconds)
20
+ within_new_transaction(options.merge(attempts: attempts)) { yield }
23
21
  end
24
22
  end
25
23
  end
@@ -0,0 +1,14 @@
1
+ module ActiveRecord
2
+ module Type
3
+ class << self
4
+ private
5
+
6
+ # Return :postgresql instead of :cockroachdb for current_adapter_name so
7
+ # we can continue using the ActiveRecord::Types defined in
8
+ # PostgreSQLAdapter.
9
+ def current_adapter_name
10
+ :postgresql
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,8 +1,24 @@
1
+ require "rgeo/active_record"
2
+
1
3
  require 'active_record/connection_adapters/postgresql_adapter'
2
- require "active_record/connection_adapters/postgresql/schema_statements"
4
+ require "active_record/connection_adapters/cockroachdb/column_methods"
3
5
  require "active_record/connection_adapters/cockroachdb/schema_statements"
4
6
  require "active_record/connection_adapters/cockroachdb/referential_integrity"
5
7
  require "active_record/connection_adapters/cockroachdb/transaction_manager"
8
+ require "active_record/connection_adapters/cockroachdb/database_statements"
9
+ require "active_record/connection_adapters/cockroachdb/table_definition"
10
+ require "active_record/connection_adapters/cockroachdb/quoting"
11
+ require "active_record/connection_adapters/cockroachdb/type"
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/spatial"
17
+ require "active_record/connection_adapters/cockroachdb/arel_tosql"
18
+
19
+ # Run to ignore spatial tables that will break schemna dumper.
20
+ # Defined in ./setup.rb
21
+ ActiveRecord::ConnectionAdapters::CockroachDB.initial_setup
6
22
 
7
23
  module ActiveRecord
8
24
  module ConnectionHandling
@@ -32,16 +48,115 @@ module ActiveRecord
32
48
  module ConnectionAdapters
33
49
  class CockroachDBAdapter < PostgreSQLAdapter
34
50
  ADAPTER_NAME = "CockroachDB".freeze
51
+ DEFAULT_PRIMARY_KEY = "rowid"
52
+
53
+ SPATIAL_COLUMN_OPTIONS =
54
+ {
55
+ geography: { geographic: true },
56
+ geometry: {},
57
+ geometry_collection: {},
58
+ line_string: {},
59
+ multi_line_string: {},
60
+ multi_point: {},
61
+ multi_polygon: {},
62
+ spatial: {},
63
+ st_point: {},
64
+ st_polygon: {},
65
+ }
66
+
67
+ # http://postgis.17.x6.nabble.com/Default-SRID-td5001115.html
68
+ DEFAULT_SRID = 0
35
69
 
36
70
  include CockroachDB::SchemaStatements
37
71
  include CockroachDB::ReferentialIntegrity
72
+ include CockroachDB::DatabaseStatements
73
+ include CockroachDB::Quoting
74
+
75
+ # override
76
+ # This method makes a sql query to gather information about columns
77
+ # in a table. It returns an array of arrays (one for each col) and
78
+ # is mapped to columns in the SchemaStatements#columns method.
79
+ #
80
+ # The issue with the default method is that the sql_type field is
81
+ # retrieved with the `format_type` function, but this is implemented
82
+ # differently in CockroachDB than PostGIS, so geometry/geography
83
+ # types are missing information which makes parsing them impossible.
84
+ # Below is an example of what `format_type` returns for a geometry
85
+ # column.
86
+ #
87
+ # column_type: geometry(POINT, 4326)
88
+ # Expected: geometry(POINT, 4326)
89
+ # Actual: geometry
90
+ #
91
+ # The solution is to make the default query with super, then
92
+ # iterate through the columns and if it is a spatial type,
93
+ # access the proper column_type with the information_schema.columns
94
+ # table.
95
+ #
96
+ # @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
97
+ def column_definitions(table_name)
98
+ fields = super
99
+ # iterate through and identify all spatial fields based on format_type
100
+ # being geometry or geography, then query for the information_schema.column
101
+ # column_type because that contains the necessary information.
102
+ fields.map do |field|
103
+ dtype = field[1]
104
+ if dtype == 'geometry' || dtype == 'geography'
105
+ col_name = field[0]
106
+ data_type = \
107
+ query(<<~SQL, "SCHEMA")
108
+ SELECT c.data_type
109
+ FROM information_schema.columns c
110
+ WHERE c.table_name = #{quote(table_name)}
111
+ AND c.column_name = #{quote(col_name)}
112
+ SQL
113
+ field[1] = data_type[0][0]
114
+ end
115
+ field
116
+ end
117
+ end
118
+
119
+ def arel_visitor
120
+ Arel::Visitors::CockroachDB.new(self)
121
+ end
38
122
 
123
+ def self.spatial_column_options(key)
124
+ SPATIAL_COLUMN_OPTIONS[key]
125
+ end
39
126
 
40
- # Note that in the migration from ActiveRecord 5.0 to 5.1, the
41
- # `extract_schema_qualified_name` method was aliased in the PostgreSQLAdapter.
42
- # To ensure backward compatibility with both <5.1 and 5.1, we rename it here
43
- # to use the same original `Utils` module.
44
- Utils = PostgreSQL::Utils
127
+ def postgis_lib_version
128
+ @postgis_lib_version ||= select_value("SELECT PostGIS_Lib_Version()")
129
+ end
130
+
131
+ def default_srid
132
+ DEFAULT_SRID
133
+ end
134
+
135
+ def srs_database_columns
136
+ {
137
+ auth_name_column: "auth_name",
138
+ auth_srid_column: "auth_srid",
139
+ proj4text_column: "proj4text",
140
+ srtext_column: "srtext",
141
+ }
142
+ end
143
+
144
+ def debugging?
145
+ !!ENV["DEBUG_COCKROACHDB_ADAPTER"]
146
+ end
147
+
148
+ def max_transaction_retries
149
+ @max_transaction_retries ||= @config.fetch(:max_transaction_retries, 3)
150
+ end
151
+
152
+ # CockroachDB 20.1 can run queries that work against PostgreSQL 10+.
153
+ def postgresql_version
154
+ 100000
155
+ end
156
+
157
+ def supports_bulk_alter?
158
+ false
159
+ end
45
160
 
46
161
  def supports_json?
47
162
  # FIXME(joey): Add a version check.
@@ -65,13 +180,8 @@ module ActiveRecord
65
180
  false
66
181
  end
67
182
 
68
- def supports_pg_crypto_uuid?
69
- false
70
- end
71
-
72
183
  def supports_partial_index?
73
- # See cockroachdb/cockroach#9683
74
- false
184
+ @crdb_version >= 202
75
185
  end
76
186
 
77
187
  def supports_expression_index?
@@ -94,8 +204,7 @@ module ActiveRecord
94
204
  end
95
205
 
96
206
  def supports_advisory_locks?
97
- # FIXME(joey): We may want to make this false.
98
- true
207
+ false
99
208
  end
100
209
 
101
210
  def supports_virtual_columns?
@@ -103,114 +212,8 @@ module ActiveRecord
103
212
  false
104
213
  end
105
214
 
106
- def supports_savepoints?
107
- # See cockroachdb/cockroach#10735.
108
- false
109
- end
110
-
111
- def transaction_isolation_levels
112
- {
113
- # Explicitly prevent READ UNCOMMITTED from being used. This
114
- # was due to the READ UNCOMMITTED test failing.
115
- # read_uncommitted: "READ UNCOMMITTED",
116
- read_committed: "READ COMMITTED",
117
- repeatable_read: "REPEATABLE READ",
118
- serializable: "SERIALIZABLE"
119
- }
120
- end
121
-
122
-
123
- # Sadly, we can only do savepoints at the beginning of
124
- # transactions. This means that we cannot use them for most cases
125
- # of transaction, so we just pretend they're usable.
126
- def create_savepoint(name = "COCKROACH_RESTART"); end
127
-
128
- def exec_rollback_to_savepoint(name = "COCKROACH_RESTART"); end
129
-
130
- def release_savepoint(name = "COCKROACH_RESTART"); end
131
-
132
- def indexes(table_name, name = nil) # :nodoc:
133
- # The PostgreSQL adapter uses a correlated subquery in the following query,
134
- # which CockroachDB does not yet support. That portion of the query fetches
135
- # any non-standard opclasses that each index uses. CockroachDB also doesn't
136
- # support opclasses at this time, so the query is modified to just remove
137
- # the section about opclasses entirely.
138
- if name
139
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
140
- Passing name to #indexes is deprecated without replacement.
141
- MSG
142
- end
143
-
144
- table = Utils.extract_schema_qualified_name(table_name.to_s)
145
-
146
- result = query(<<-SQL, "SCHEMA")
147
- SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
148
- pg_catalog.obj_description(i.oid, 'pg_class') AS comment
149
- FROM pg_class t
150
- INNER JOIN pg_index d ON t.oid = d.indrelid
151
- INNER JOIN pg_class i ON d.indexrelid = i.oid
152
- LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
153
- WHERE i.relkind = 'i'
154
- AND d.indisprimary = 'f'
155
- AND t.relname = '#{table.identifier}'
156
- AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'}
157
- ORDER BY i.relname
158
- SQL
159
-
160
- result.map do |row|
161
- index_name = row[0]
162
- unique = row[1]
163
- indkey = row[2].split(" ").map(&:to_i)
164
- inddef = row[3]
165
- oid = row[4]
166
- comment = row[5]
167
-
168
- expressions, where = inddef.scan(/\((.+?)\)(?: WHERE (.+))?\z/).flatten
169
-
170
- if indkey.include?(0)
171
- columns = expressions
172
- else
173
- columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact
174
- SELECT a.attnum, a.attname
175
- FROM pg_attribute a
176
- WHERE a.attrelid = #{oid}
177
- AND a.attnum IN (#{indkey.join(",")})
178
- SQL
179
-
180
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
181
- orders = Hash[
182
- expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] }
183
- ]
184
- end
185
-
186
- # FIXME(joey): This may be specific to ActiveRecord 5.2.
187
- IndexDefinition.new(
188
- table_name,
189
- index_name,
190
- unique,
191
- columns,
192
- orders: orders,
193
- where: where,
194
- comment: comment.presence
195
- )
196
- end.compact
197
- end
198
-
199
-
200
- def primary_keys(table_name)
201
- name = Utils.extract_schema_qualified_name(table_name.to_s)
202
- select_values(<<-SQL.strip_heredoc, "SCHEMA")
203
- SELECT column_name
204
- FROM information_schema.key_column_usage kcu
205
- JOIN information_schema.table_constraints tc
206
- ON kcu.table_name = tc.table_name
207
- AND kcu.table_schema = tc.table_schema
208
- AND kcu.constraint_name = tc.constraint_name
209
- WHERE constraint_type = 'PRIMARY KEY'
210
- AND kcu.table_name = #{quote(name.identifier)}
211
- AND kcu.table_schema = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"}
212
- ORDER BY kcu.ordinal_position
213
- SQL
215
+ def supports_string_to_array_coercion?
216
+ @crdb_version >= 202
214
217
  end
215
218
 
216
219
  # This is hardcoded to 63 (as previously was in ActiveRecord 5.0) to aid in
@@ -228,10 +231,44 @@ module ActiveRecord
228
231
  alias index_name_length max_identifier_length
229
232
  alias table_alias_length max_identifier_length
230
233
 
234
+ def initialize(connection, logger, conn_params, config)
235
+ super(connection, logger, conn_params, config)
236
+ crdb_version_string = query_value("SHOW crdb_version")
237
+ if crdb_version_string.include? "v1."
238
+ version_num = 1
239
+ elsif crdb_version_string.include? "v2."
240
+ version_num 2
241
+ elsif crdb_version_string.include? "v19.1."
242
+ version_num = 191
243
+ elsif crdb_version_string.include? "v19.2."
244
+ version_num = 192
245
+ elsif crdb_version_string.include? "v20.1."
246
+ version_num = 201
247
+ else
248
+ version_num = 202
249
+ end
250
+ @crdb_version = version_num
251
+ end
252
+
231
253
  private
232
254
 
233
255
  def initialize_type_map(m = type_map)
234
- super(m)
256
+ %w(
257
+ geography
258
+ geometry
259
+ geometry_collection
260
+ line_string
261
+ multi_line_string
262
+ multi_point
263
+ multi_polygon
264
+ st_point
265
+ st_polygon
266
+ ).each do |geo_type|
267
+ m.register_type(geo_type) do |oid, _, sql_type|
268
+ CockroachDB::OID::Spatial.new(oid, sql_type)
269
+ end
270
+ end
271
+
235
272
  # NOTE(joey): PostgreSQL intervals have a precision.
236
273
  # CockroachDB intervals do not, so overide the type
237
274
  # definition. Returning a ArgumentError may not be correct.
@@ -243,6 +280,8 @@ module ActiveRecord
243
280
  end
244
281
  OID::SpecializedString.new(:interval, precision: precision)
245
282
  end
283
+
284
+ super(m)
246
285
  end
247
286
 
248
287
  # Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -295,6 +334,62 @@ module ActiveRecord
295
334
  end
296
335
  end
297
336
 
337
+ # Override extract_value_from_default because the upstream definition
338
+ # doesn't handle the variations in CockroachDB's behavior.
339
+ def extract_value_from_default(default)
340
+ super ||
341
+ extract_escaped_string_from_default(default) ||
342
+ extract_time_from_default(default) ||
343
+ extract_empty_array_from_default(default)
344
+ end
345
+
346
+ # Both PostgreSQL and CockroachDB use C-style string escapes under the
347
+ # covers. PostgreSQL obscures this for us and unescapes the strings, but
348
+ # CockroachDB does not. Here we'll use Ruby to unescape the string.
349
+ # See https://github.com/cockroachdb/cockroach/issues/47497 and
350
+ # https://www.postgresql.org/docs/9.2/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE.
351
+ def extract_escaped_string_from_default(default)
352
+ # Escaped strings start with an e followed by the string in quotes (e'…')
353
+ return unless default =~ /\A[\(B]?e'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
354
+
355
+ # String#undump doesn't account for escaped single quote characters
356
+ "\"#{$1}\"".undump.gsub("\\'".freeze, "'".freeze)
357
+ end
358
+
359
+ # This method exists to extract the correct time and date defaults for a
360
+ # couple of reasons.
361
+ # 1) There's a bug in CockroachDB where the date type is missing from
362
+ # the column info query.
363
+ # https://github.com/cockroachdb/cockroach/issues/47285
364
+ # 2) PostgreSQL's timestamp without time zone type maps to CockroachDB's
365
+ # TIMESTAMP type. TIMESTAMP includes a UTC time zone while timestamp
366
+ # without time zone doesn't.
367
+ # https://www.cockroachlabs.com/docs/v19.2/timestamp.html#variants
368
+ def extract_time_from_default(default)
369
+ return unless default =~ /\A'(.*)'\z/
370
+
371
+ # If default has a UTC time zone, we'll drop the time zone information
372
+ # so it acts like PostgreSQL's timestamp without time zone. Then, try
373
+ # to parse the resulting string to verify if it's a time.
374
+ time = if default =~ /\A'(.*)(\+00:00)'\z/
375
+ $1
376
+ else
377
+ default
378
+ end
379
+
380
+ Time.parse(time).to_s
381
+ rescue
382
+ nil
383
+ end
384
+
385
+ # CockroachDB stores default values for arrays in the `ARRAY[...]` format.
386
+ # In general, it is hard to parse that, but it is easy to handle the common
387
+ # case of an empty array.
388
+ def extract_empty_array_from_default(default)
389
+ return unless supports_string_to_array_coercion?
390
+ return unless default =~ /\AARRAY\[\]\z/
391
+ return "{}"
392
+ end
298
393
 
299
394
  # end private
300
395
  end