activerecord-cockroachdb-adapter 6.0.0beta2 → 6.0.1

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