activerecord-cockroachdb-adapter 0.2.3 → 5.2.2

Sign up to get free protection for your applications and to get access to all the features.
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