1414require "active_record/connection_adapters/cockroachdb/spatial_column_info"
1515require "active_record/connection_adapters/cockroachdb/setup"
1616require "active_record/connection_adapters/cockroachdb/oid/spatial"
17+ require "active_record/connection_adapters/cockroachdb/oid/interval"
1718require "active_record/connection_adapters/cockroachdb/arel_tosql"
1819
1920# Run to ignore spatial tables that will break schemna dumper.
@@ -24,26 +25,22 @@ module ActiveRecord
2425 module ConnectionHandling
2526 def cockroachdb_connection ( config )
2627 # This is copied from the PostgreSQL adapter.
27- conn_params = config . symbolize_keys
28-
29- conn_params . delete_if { |_ , v | v . nil? }
28+ conn_params = config . symbolize_keys . compact
3029
3130 # Map ActiveRecords param names to PGs.
3231 conn_params [ :user ] = conn_params . delete ( :username ) if conn_params [ :username ]
3332 conn_params [ :dbname ] = conn_params . delete ( :database ) if conn_params [ :database ]
3433
3534 # Forward only valid config params to PG::Connection.connect.
36- valid_conn_param_keys = PG ::Connection . conndefaults_hash . keys + [ :sslmode , :application_name ]
35+ valid_conn_param_keys = PG ::Connection . conndefaults_hash . keys + [ :requiressl ]
3736 conn_params . slice! ( *valid_conn_param_keys )
3837
39- conn = PG . connect ( conn_params )
40- ConnectionAdapters ::CockroachDBAdapter . new ( conn , logger , conn_params , config )
41- rescue ::PG ::Error , ActiveRecord ::ActiveRecordError => error
42- if error . message . include? ( "does not exist" )
43- raise ActiveRecord ::NoDatabaseError
44- else
45- raise
46- end
38+ ConnectionAdapters ::CockroachDBAdapter . new (
39+ ConnectionAdapters ::CockroachDBAdapter . new_client ( conn_params ) ,
40+ logger ,
41+ conn_params ,
42+ config
43+ )
4744 end
4845 end
4946end
@@ -76,56 +73,6 @@ class CockroachDBAdapter < PostgreSQLAdapter
7673 include CockroachDB ::DatabaseStatements
7774 include CockroachDB ::Quoting
7875
79- # override
80- # This method makes a sql query to gather information about columns
81- # in a table. It returns an array of arrays (one for each col) and
82- # passes each to the SchemaStatements#new_column_from_field method
83- # as the field parameter. This data is then used to format the column
84- # objects for the model and sent to the OID for data casting.
85- #
86- # The issue with the default method is that the sql_type field is
87- # retrieved with the `format_type` function, but this is implemented
88- # differently in CockroachDB than PostGIS, so geometry/geography
89- # types are missing information which makes parsing them impossible.
90- # Below is an example of what `format_type` returns for a geometry
91- # column.
92- #
93- # column_type: geometry(POINT, 4326)
94- # Expected: geometry(POINT, 4326)
95- # Actual: geometry
96- #
97- # The solution is to make the default query with super, then
98- # iterate through the columns and if it is a spatial type,
99- # access the proper column_type with the information_schema.columns
100- # table.
101- #
102- # @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
103- def column_definitions ( table_name )
104- fields = super
105- # iterate through and identify all spatial fields based on format_type
106- # being geometry or geography, then query for the information_schema.column
107- # column_type because that contains the necessary information.
108- fields . map do |field |
109- dtype = field [ 1 ]
110- if dtype == 'geometry' || dtype == 'geography'
111- col_name = field [ 0 ]
112- data_type = \
113- query ( <<~SQL , "SCHEMA" )
114- SELECT c.data_type
115- FROM information_schema.columns c
116- WHERE c.table_name = #{ quote ( table_name ) }
117- AND c.column_name = #{ quote ( col_name ) }
118- SQL
119- field [ 1 ] = data_type [ 0 ] [ 0 ]
120- end
121- field
122- end
123- end
124-
125- def arel_visitor
126- Arel ::Visitors ::CockroachDB . new ( self )
127- end
128-
12976 def self . spatial_column_options ( key )
13077 SPATIAL_COLUMN_OPTIONS [ key ]
13178 end
@@ -276,19 +223,28 @@ def initialize_type_map(m = type_map)
276223 end
277224 end
278225
279- # NOTE(joey): PostgreSQL intervals have a precision.
280- # CockroachDB intervals do not, so overide the type
281- # definition. Returning a ArgumentError may not be correct.
282- # This needs to be tested.
283- m . register_type "interval" do |_ , _ , sql_type |
226+ # Belongs after other types are defined because of issues described
227+ # in this https://github.com/rails/rails/pull/38571
228+ # Once that PR is merged, we can call super at the top.
229+ super ( m )
230+
231+ # Override numeric type. This is almost identical to the default,
232+ # except that the conditional based on the fmod is changed.
233+ m . register_type "numeric" do |_ , fmod , sql_type |
284234 precision = extract_precision ( sql_type )
285- if precision
286- raise ( ArgumentError , "CockroachDB does not support precision on intervals, but got precision: #{ precision } " )
235+ scale = extract_scale ( sql_type )
236+
237+ # If fmod is -1, that means that precision is defined, but not
238+ # scale or neither is defined.
239+ if fmod && fmod == -1
240+ # Below comment is from ActiveRecord
241+ # FIXME: Remove this class, and the second argument to
242+ # lookups on PG
243+ Type ::DecimalWithoutScale . new ( precision : precision )
244+ else
245+ OID ::Decimal . new ( precision : precision , scale : scale )
287246 end
288- OID ::SpecializedString . new ( :interval , precision : precision )
289247 end
290-
291- super ( m )
292248 end
293249
294250 # Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -398,6 +354,84 @@ def extract_empty_array_from_default(default)
398354 return "{}"
399355 end
400356
357+ # override
358+ # This method makes a query to gather information about columns
359+ # in a table. It returns an array of arrays (one for each col) and
360+ # passes each to the SchemaStatements#new_column_from_field method
361+ # as the field parameter. This data is then used to format the column
362+ # objects for the model and sent to the OID for data casting.
363+ #
364+ # Sometimes there are differences between how data is formatted
365+ # in Postgres and CockroachDB, so additional queries for certain types
366+ # may be necessary to properly form the column definition.
367+ #
368+ # @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
369+ def column_definitions ( table_name )
370+ fields = super
371+
372+ # Use regex comparison because if a type is an array it will
373+ # have [] appended to the end of it.
374+ target_types = [
375+ /geometry/ ,
376+ /geography/ ,
377+ /interval/ ,
378+ /numeric/
379+ ]
380+ re = Regexp . union ( target_types )
381+ fields . map do |field |
382+ dtype = field [ 1 ]
383+ if re . match ( dtype )
384+ crdb_column_definition ( field , table_name )
385+ else
386+ field
387+ end
388+ end
389+ end
390+
391+ # Use the crdb_sql_type instead of the sql_type returned by
392+ # column_definitions. This will include limit,
393+ # precision, and scale information in the type.
394+ # Ex. geometry -> geometry(point, 4326)
395+ def crdb_column_definition ( field , table_name )
396+ col_name = field [ 0 ]
397+ data_type = \
398+ query ( <<~SQL , "SCHEMA" )
399+ SELECT c.crdb_sql_type
400+ FROM information_schema.columns c
401+ WHERE c.table_name = #{ quote ( table_name ) }
402+ AND c.column_name = #{ quote ( col_name ) }
403+ SQL
404+ field [ 1 ] = data_type [ 0 ] [ 0 ] . downcase
405+ field
406+ end
407+
408+ # override
409+ # This method is used to determine if a
410+ # FEATURE_NOT_SUPPORTED error from the PG gem should
411+ # be an ActiveRecord::PreparedStatementCacheExpired
412+ # error.
413+ #
414+ # ActiveRecord handles this by checking that the sql state matches the
415+ # FEATURE_NOT_SUPPORTED code and that the source function
416+ # is "RevalidateCachedQuery" since that is the only function
417+ # in postgres that will create this error.
418+ #
419+ # That method will not work for CockroachDB because the error
420+ # originates from the "runExecBuilder" function, so we need
421+ # to modify the original to match the CockroachDB behavior.
422+ def is_cached_plan_failure? ( e )
423+ pgerror = e . cause
424+
425+ pgerror . result . result_error_field ( PG ::PG_DIAG_SQLSTATE ) == FEATURE_NOT_SUPPORTED &&
426+ pgerror . result . result_error_field ( PG ::PG_DIAG_SOURCE_FUNCTION ) == "runExecBuilder"
427+ rescue
428+ false
429+ end
430+
431+ def arel_visitor
432+ Arel ::Visitors ::CockroachDB . new ( self )
433+ end
434+
401435 # end private
402436 end
403437 end
0 commit comments