activerecord-cockroachdb-adapter 6.0.0beta2 → 6.0.1

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.
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module CockroachDB
6
+ module ColumnMethods
7
+ def spatial(name, options = {})
8
+ raise "You must set a type. For example: 't.spatial type: :st_point'" unless options[:type]
9
+
10
+ column(name, options[:type], **options)
11
+ end
12
+
13
+ def geography(name, options = {})
14
+ column(name, :geography, **options)
15
+ end
16
+
17
+ def geometry(name, options = {})
18
+ column(name, :geometry, **options)
19
+ end
20
+
21
+ def geometry_collection(name, options = {})
22
+ column(name, :geometry_collection, **options)
23
+ end
24
+
25
+ def line_string(name, options = {})
26
+ column(name, :line_string, **options)
27
+ end
28
+
29
+ def multi_line_string(name, options = {})
30
+ column(name, :multi_line_string, **options)
31
+ end
32
+
33
+ def multi_point(name, options = {})
34
+ column(name, :multi_point, **options)
35
+ end
36
+
37
+ def multi_polygon(name, options = {})
38
+ column(name, :multi_polygon, **options)
39
+ end
40
+
41
+ def st_point(name, options = {})
42
+ column(name, :st_point, **options)
43
+ end
44
+
45
+ def st_polygon(name, options = {})
46
+ column(name, :st_polygon, **options)
47
+ end
48
+ end
49
+ end
50
+
51
+ PostgreSQL::Table.include CockroachDB::ColumnMethods
52
+ end
53
+ end
@@ -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,26 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module CockroachDB
4
+ module OID
5
+ module TypeMapInitializer
6
+ # override
7
+ # Replaces the query with a faster version that doesn't rely on the
8
+ # use of 'array_in(cstring,oid,integer)'::regprocedure.
9
+ def query_conditions_for_initial_load
10
+ known_type_names = @store.keys.map { |n| "'#{n}'" }
11
+ known_type_types = %w('r' 'e' 'd')
12
+ <<~SQL % [known_type_names.join(", "), known_type_types.join(", ")]
13
+ WHERE
14
+ t.typname IN (%s)
15
+ OR t.typtype IN (%s)
16
+ OR (t.typarray = 0 AND t.typcategory='A')
17
+ OR t.typelem != 0
18
+ SQL
19
+ end
20
+ end
21
+
22
+ PostgreSQL::OID::TypeMapInitializer.prepend(TypeMapInitializer)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -14,10 +14,18 @@ module ActiveRecord
14
14
  # always be strings. Then, we won't have to make any additional changes
15
15
  # to ActiveRecord to support inserting integer values into string
16
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.
17
22
  def _quote(value)
18
- case value
19
- when Numeric
23
+ if value.is_a?(Numeric)
20
24
  "'#{quote_string(value.to_s)}'"
25
+ elsif RGeo::Feature::Geometry.check_type(value)
26
+ "'#{RGeo::WKRep::WKBGenerator.new(hex_format: true, type_format: :ewkb, emit_ewkb_srid: true).generate(value)}'"
27
+ elsif value.is_a?(RGeo::Cartesian::BoundingBox)
28
+ "'#{value.min_x},#{value.min_y},#{value.max_x},#{value.max_y}'::box"
21
29
  else
22
30
  super
23
31
  end
@@ -39,8 +39,43 @@ module ActiveRecord
39
39
  nil
40
40
  end
41
41
 
42
+ # override
43
+ # https://github.com/rails/rails/blob/6-0-stable/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L624
44
+ def new_column_from_field(table_name, field)
45
+ column_name, type, default, notnull, oid, fmod, collation, comment = field
46
+ type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i)
47
+ default_value = extract_value_from_default(default)
48
+ default_function = extract_default_function(default_value, default)
49
+
50
+ serial =
51
+ if (match = default_function&.match(/\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z/))
52
+ sequence_name_from_parts(table_name, column_name, match[:suffix]) == match[:sequence_name]
53
+ end
54
+
55
+ # {:dimension=>2, :has_m=>false, :has_z=>false, :name=>"latlon", :srid=>0, :type=>"GEOMETRY"}
56
+ spatial = spatial_column_info(table_name).get(column_name, type_metadata.sql_type)
57
+
58
+ PostgreSQL::Column.new(
59
+ column_name,
60
+ default_value,
61
+ type_metadata,
62
+ !notnull,
63
+ default_function,
64
+ collation: collation,
65
+ comment: comment.presence,
66
+ serial: serial,
67
+ spatial: spatial
68
+ )
69
+ end
70
+
42
71
  # CockroachDB will use INT8 if the SQL type is INTEGER, so we make it use
43
72
  # INT4 explicitly when needed.
73
+ #
74
+ # For spatial columns, include the limit to properly format the column name
75
+ # since type alone is not enough to format the column.
76
+ # Ex. type_to_sql(:geography, limit: "Point,4326")
77
+ # => "geography(Point,4326)"
78
+ #
44
79
  def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc:
45
80
  sql = \
46
81
  case type.to_s
@@ -52,6 +87,8 @@ module ActiveRecord
52
87
  when 5..8; "int8"
53
88
  else super
54
89
  end
90
+ when "geometry", "geography"
91
+ "#{type}(#{limit})"
55
92
  else
56
93
  super
57
94
  end
@@ -86,6 +123,34 @@ module ActiveRecord
86
123
  query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false})", "SCHEMA")
87
124
  end
88
125
  end
126
+
127
+ # override
128
+ def native_database_types
129
+ # Add spatial types
130
+ super.merge(
131
+ geography: { name: "geography" },
132
+ geometry: { name: "geometry" },
133
+ geometry_collection: { name: "geometry_collection" },
134
+ line_string: { name: "line_string" },
135
+ multi_line_string: { name: "multi_line_string" },
136
+ multi_point: { name: "multi_point" },
137
+ multi_polygon: { name: "multi_polygon" },
138
+ spatial: { name: "geometry" },
139
+ st_point: { name: "st_point" },
140
+ st_polygon: { name: "st_polygon" }
141
+ )
142
+ end
143
+
144
+ # override
145
+ def create_table_definition(*args, **kwargs)
146
+ CockroachDB::TableDefinition.new(self, *args, **kwargs)
147
+ end
148
+
149
+ # memoize hash of column infos for tables
150
+ def spatial_column_info(table_name)
151
+ @spatial_column_info ||= {}
152
+ @spatial_column_info[table_name.to_sym] ||= SpatialColumnInfo.new(self, table_name.to_s)
153
+ end
89
154
  end
90
155
  end
91
156
  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
@@ -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,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
@@ -38,15 +51,132 @@ end
38
51
 
39
52
  module ActiveRecord
40
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
+
62
+ begin
63
+ with_connection do |conn|
64
+ if conn.active?
65
+ begin
66
+ query = "SELECT crdb_internal.increment_feature_counter('ActiveRecord %d.%d')"
67
+ conn.execute(query % [ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR])
68
+ rescue ActiveRecord::StatementInvalid
69
+ # The increment_feature_counter built-in is not supported on this
70
+ # CockroachDB version. Ignore.
71
+ rescue StandardError => e
72
+ conn.logger.warn "Unexpected error when incrementing feature counter: #{e}"
73
+ end
74
+ end
75
+ end
76
+ rescue ActiveRecord::NoDatabaseError
77
+ # Prevent failures on db creation and parallel testing.
78
+ end
79
+ end
80
+ end
81
+ ConnectionPool.prepend(CockroachDBConnectionPool)
82
+
41
83
  class CockroachDBAdapter < PostgreSQLAdapter
42
84
  ADAPTER_NAME = "CockroachDB".freeze
43
85
  DEFAULT_PRIMARY_KEY = "rowid"
44
86
 
87
+ SPATIAL_COLUMN_OPTIONS =
88
+ {
89
+ geography: { geographic: true },
90
+ geometry: {},
91
+ geometry_collection: {},
92
+ line_string: {},
93
+ multi_line_string: {},
94
+ multi_point: {},
95
+ multi_polygon: {},
96
+ spatial: {},
97
+ st_point: {},
98
+ st_polygon: {},
99
+ }
100
+
101
+ # http://postgis.17.x6.nabble.com/Default-SRID-td5001115.html
102
+ DEFAULT_SRID = 0
103
+
45
104
  include CockroachDB::SchemaStatements
46
105
  include CockroachDB::ReferentialIntegrity
47
106
  include CockroachDB::DatabaseStatements
48
107
  include CockroachDB::Quoting
49
108
 
109
+ # override
110
+ # This method makes a sql query to gather information about columns
111
+ # in a table. It returns an array of arrays (one for each col) and
112
+ # passes each to the SchemaStatements#new_column_from_field method
113
+ # as the field parameter. This data is then used to format the column
114
+ # objects for the model and sent to the OID for data casting.
115
+ #
116
+ # The issue with the default method is that the sql_type field is
117
+ # retrieved with the `format_type` function, but this is implemented
118
+ # differently in CockroachDB than PostGIS, so geometry/geography
119
+ # types are missing information which makes parsing them impossible.
120
+ # Below is an example of what `format_type` returns for a geometry
121
+ # column.
122
+ #
123
+ # column_type: geometry(POINT, 4326)
124
+ # Expected: geometry(POINT, 4326)
125
+ # Actual: geometry
126
+ #
127
+ # The solution is to make the default query with super, then
128
+ # iterate through the columns and if it is a spatial type,
129
+ # access the proper column_type with the information_schema.columns
130
+ # table.
131
+ #
132
+ # @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
133
+ def column_definitions(table_name)
134
+ fields = super
135
+ # iterate through and identify all spatial fields based on format_type
136
+ # being geometry or geography, then query for the information_schema.column
137
+ # column_type because that contains the necessary information.
138
+ fields.map do |field|
139
+ dtype = field[1]
140
+ if dtype == 'geometry' || dtype == 'geography'
141
+ col_name = field[0]
142
+ data_type = \
143
+ query(<<~SQL, "SCHEMA")
144
+ SELECT c.data_type
145
+ FROM information_schema.columns c
146
+ WHERE c.table_name = #{quote(table_name)}
147
+ AND c.column_name = #{quote(col_name)}
148
+ SQL
149
+ field[1] = data_type[0][0]
150
+ end
151
+ field
152
+ end
153
+ end
154
+
155
+ def arel_visitor
156
+ Arel::Visitors::CockroachDB.new(self)
157
+ end
158
+
159
+ def self.spatial_column_options(key)
160
+ SPATIAL_COLUMN_OPTIONS[key]
161
+ end
162
+
163
+ def postgis_lib_version
164
+ @postgis_lib_version ||= select_value("SELECT PostGIS_Lib_Version()")
165
+ end
166
+
167
+ def default_srid
168
+ DEFAULT_SRID
169
+ end
170
+
171
+ def srs_database_columns
172
+ {
173
+ auth_name_column: "auth_name",
174
+ auth_srid_column: "auth_srid",
175
+ proj4text_column: "proj4text",
176
+ srtext_column: "srtext",
177
+ }
178
+ end
179
+
50
180
  def debugging?
51
181
  !!ENV["DEBUG_COCKROACHDB_ADAPTER"]
52
182
  end
@@ -117,6 +247,10 @@ module ActiveRecord
117
247
  @crdb_version >= 202
118
248
  end
119
249
 
250
+ def supports_partitioned_indexes?
251
+ false
252
+ end
253
+
120
254
  # This is hardcoded to 63 (as previously was in ActiveRecord 5.0) to aid in
121
255
  # migration from PostgreSQL to CockroachDB. In practice, this limitation
122
256
  # is arbitrary since CockroachDB supports index name lengths and table alias
@@ -134,6 +268,7 @@ module ActiveRecord
134
268
 
135
269
  def initialize(connection, logger, conn_params, config)
136
270
  super(connection, logger, conn_params, config)
271
+
137
272
  crdb_version_string = query_value("SHOW crdb_version")
138
273
  if crdb_version_string.include? "v1."
139
274
  version_num = 1
@@ -160,7 +295,22 @@ module ActiveRecord
160
295
  private
161
296
 
162
297
  def initialize_type_map(m = type_map)
163
- super(m)
298
+ %w(
299
+ geography
300
+ geometry
301
+ geometry_collection
302
+ line_string
303
+ multi_line_string
304
+ multi_point
305
+ multi_polygon
306
+ st_point
307
+ st_polygon
308
+ ).each do |geo_type|
309
+ m.register_type(geo_type) do |oid, _, sql_type|
310
+ CockroachDB::OID::Spatial.new(oid, sql_type)
311
+ end
312
+ end
313
+
164
314
  # NOTE(joey): PostgreSQL intervals have a precision.
165
315
  # CockroachDB intervals do not, so overide the type
166
316
  # definition. Returning a ArgumentError may not be correct.
@@ -172,6 +322,8 @@ module ActiveRecord
172
322
  end
173
323
  OID::SpecializedString.new(:interval, precision: precision)
174
324
  end
325
+
326
+ super(m)
175
327
  end
176
328
 
177
329
  # Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -281,6 +433,100 @@ module ActiveRecord
281
433
  return "{}"
282
434
  end
283
435
 
436
+ # override
437
+ # This method loads info about data types from the database to
438
+ # populate the TypeMap.
439
+ #
440
+ # Currently, querying from the pg_type catalog can be slow due to geo-partitioning
441
+ # so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
442
+ def load_additional_types(oids = nil)
443
+ if @config[:use_follower_reads_for_type_introspection]
444
+ initializer = OID::TypeMapInitializer.new(type_map)
445
+
446
+ query = <<~SQL
447
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
448
+ FROM pg_type as t
449
+ LEFT JOIN pg_range as r ON oid = rngtypid AS OF SYSTEM TIME '-10s'
450
+ SQL
451
+
452
+ if oids
453
+ query += "WHERE t.oid IN (%s)" % oids.join(", ")
454
+ else
455
+ query += initializer.query_conditions_for_initial_load
456
+ end
457
+
458
+ execute_and_clear(query, "SCHEMA", []) do |records|
459
+ initializer.run(records)
460
+ end
461
+ else
462
+ super
463
+ end
464
+ rescue ActiveRecord::StatementInvalid => e
465
+ raise e unless e.cause.is_a? PG::InvalidCatalogName
466
+ # use original if database is younger than 10s
467
+ super
468
+ end
469
+
470
+ # override
471
+ # This method maps data types to their proper decoder.
472
+ #
473
+ # Currently, querying from the pg_type catalog can be slow due to geo-partitioning
474
+ # so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
475
+ def add_pg_decoders
476
+ if @config[:use_follower_reads_for_type_introspection]
477
+ @default_timezone = nil
478
+ @timestamp_decoder = nil
479
+
480
+ coders_by_name = {
481
+ "int2" => PG::TextDecoder::Integer,
482
+ "int4" => PG::TextDecoder::Integer,
483
+ "int8" => PG::TextDecoder::Integer,
484
+ "oid" => PG::TextDecoder::Integer,
485
+ "float4" => PG::TextDecoder::Float,
486
+ "float8" => PG::TextDecoder::Float,
487
+ "numeric" => PG::TextDecoder::Numeric,
488
+ "bool" => PG::TextDecoder::Boolean,
489
+ "timestamp" => PG::TextDecoder::TimestampUtc,
490
+ "timestamptz" => PG::TextDecoder::TimestampWithTimeZone,
491
+ }
492
+
493
+ known_coder_types = coders_by_name.keys.map { |n| quote(n) }
494
+ query = <<~SQL % known_coder_types.join(", ")
495
+ SELECT t.oid, t.typname
496
+ FROM pg_type as t AS OF SYSTEM TIME '-10s'
497
+ WHERE t.typname IN (%s)
498
+ SQL
499
+
500
+ coders = execute_and_clear(query, "SCHEMA", []) do |result|
501
+ result
502
+ .map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
503
+ .compact
504
+ end
505
+
506
+ map = PG::TypeMapByOid.new
507
+ coders.each { |coder| map.add_coder(coder) }
508
+ @connection.type_map_for_results = map
509
+
510
+ @type_map_for_results = PG::TypeMapByOid.new
511
+ @type_map_for_results.default_type_map = map
512
+ @type_map_for_results.add_coder(PG::TextDecoder::Bytea.new(oid: 17, name: "bytea"))
513
+
514
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
515
+ @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
516
+ update_typemap_for_default_timezone
517
+ else
518
+ super
519
+ end
520
+ rescue ActiveRecord::StatementInvalid => e
521
+ raise e unless e.cause.is_a? PG::InvalidCatalogName
522
+ # use original if database is younger than 10s
523
+ super
524
+ end
525
+
526
+ def arel_visitor
527
+ Arel::Visitors::CockroachDB.new(self)
528
+ end
529
+
284
530
  # end private
285
531
  end
286
532
  end