Skip to content

Commit 5f14dd4

Browse files
authored
Allow Follower Reads on pg_type Queries (#192)
* Add an AS OF SYSTEM TIME clause to queries on the pg_type catalog. * Add use_follower_reads database config option * Reset connection after follower_reads test * Change use_follower_reads setting to user_follower_reads_for_type_introspection and add Configuration section to README.
1 parent d730a85 commit 5f14dd4

File tree

3 files changed

+118
-0
lines changed

3 files changed

+118
-0
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ development:
2222
user: <username>
2323
```
2424

25+
## Configuration
26+
27+
In addition to the standard adapter settings, CockroachDB also supports the following:
28+
29+
- `use_follower_reads_for_type_introspection`: Use follower reads on queries to the `pg_type` catalog when set to `true`. This helps to speed up initialization by reading historical data, but may not find recently created user-defined types.
30+
2531
## Working with Spatial Data
2632

2733
The adapter uses [RGeo](https://github.com/rgeo/rgeo) and [RGeo-ActiveRecord](https://github.com/rgeo/rgeo-activerecord) to represent geometric and geographic data as Ruby objects and easily interface them with the adapter. The following is a brief introduction to RGeo and tips to help setup your spatial application. More documentation about RGeo can be found in the [YARD Docs](https://rubydoc.info/github/rgeo/rgeo) and [wiki](https://github.com/rgeo/rgeo/wiki).

lib/active_record/connection_adapters/cockroachdb_adapter.rb

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,97 @@ def is_cached_plan_failure?(e)
444444
false
445445
end
446446

447+
# override
448+
# This method loads info about data types from the database to
449+
# populate the TypeMap.
450+
#
451+
# Currently, querying from the pg_type catalog can be slow due to geo-partitioning
452+
# so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
453+
def load_additional_types(oids = nil)
454+
if @config[:use_follower_reads_for_type_introspection]
455+
initializer = OID::TypeMapInitializer.new(type_map)
456+
457+
query = <<~SQL
458+
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
459+
FROM pg_type as t
460+
LEFT JOIN pg_range as r ON oid = rngtypid AS OF SYSTEM TIME '-10s'
461+
SQL
462+
463+
if oids
464+
query += "WHERE t.oid IN (%s)" % oids.join(", ")
465+
else
466+
query += initializer.query_conditions_for_initial_load
467+
end
468+
469+
execute_and_clear(query, "SCHEMA", []) do |records|
470+
initializer.run(records)
471+
end
472+
else
473+
super
474+
end
475+
rescue ActiveRecord::StatementInvalid => e
476+
raise e unless e.cause.is_a? PG::InvalidCatalogName
477+
# use original if database is younger than 10s
478+
super
479+
end
480+
481+
# override
482+
# This method maps data types to their proper decoder.
483+
#
484+
# Currently, querying from the pg_type catalog can be slow due to geo-partitioning
485+
# so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
486+
def add_pg_decoders
487+
if @config[:use_follower_reads_for_type_introspection]
488+
@default_timezone = nil
489+
@timestamp_decoder = nil
490+
491+
coders_by_name = {
492+
"int2" => PG::TextDecoder::Integer,
493+
"int4" => PG::TextDecoder::Integer,
494+
"int8" => PG::TextDecoder::Integer,
495+
"oid" => PG::TextDecoder::Integer,
496+
"float4" => PG::TextDecoder::Float,
497+
"float8" => PG::TextDecoder::Float,
498+
"numeric" => PG::TextDecoder::Numeric,
499+
"bool" => PG::TextDecoder::Boolean,
500+
"timestamp" => PG::TextDecoder::TimestampUtc,
501+
"timestamptz" => PG::TextDecoder::TimestampWithTimeZone,
502+
}
503+
504+
known_coder_types = coders_by_name.keys.map { |n| quote(n) }
505+
query = <<~SQL % known_coder_types.join(", ")
506+
SELECT t.oid, t.typname
507+
FROM pg_type as t AS OF SYSTEM TIME '-10s'
508+
WHERE t.typname IN (%s)
509+
SQL
510+
511+
coders = execute_and_clear(query, "SCHEMA", []) do |result|
512+
result
513+
.map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
514+
.compact
515+
end
516+
517+
map = PG::TypeMapByOid.new
518+
coders.each { |coder| map.add_coder(coder) }
519+
@connection.type_map_for_results = map
520+
521+
@type_map_for_results = PG::TypeMapByOid.new
522+
@type_map_for_results.default_type_map = map
523+
@type_map_for_results.add_coder(PG::TextDecoder::Bytea.new(oid: 17, name: "bytea"))
524+
@type_map_for_results.add_coder(MoneyDecoder.new(oid: 790, name: "money"))
525+
526+
# extract timestamp decoder for use in update_typemap_for_default_timezone
527+
@timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
528+
update_typemap_for_default_timezone
529+
else
530+
super
531+
end
532+
rescue ActiveRecord::StatementInvalid => e
533+
raise e unless e.cause.is_a? PG::InvalidCatalogName
534+
# use original if database is younger than 10s
535+
super
536+
end
537+
447538
def arel_visitor
448539
Arel::Visitors::CockroachDB.new(self)
449540
end

test/cases/adapters/postgresql/postgresql_adapter_test.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ def setup
1515
@connection_handler = ActiveRecord::Base.connection_handler
1616
end
1717

18+
def teardown
19+
# use connection without follower_reads
20+
database_config = { "adapter" => "cockroachdb", "database" => "activerecord_unittest" }
21+
ar_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
22+
database_config.update(ar_config.configuration_hash)
23+
24+
ActiveRecord::Base.establish_connection(database_config)
25+
end
26+
1827
def test_database_exists_returns_false_when_the_database_does_not_exist
1928
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
2029
config = db_config.configuration_hash.dup
@@ -28,6 +37,18 @@ def test_database_exists_returns_true_when_the_database_exists
2837
assert ActiveRecord::ConnectionAdapters::CockroachDBAdapter.database_exists?(db_config.configuration_hash),
2938
"expected database #{db_config.database} to exist"
3039
end
40+
41+
def test_using_follower_reads_connects_properly
42+
database_config = { "use_follower_reads_for_type_introspection": true, "adapter" => "cockroachdb", "database" => "activerecord_unittest" }
43+
ar_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
44+
database_config.update(ar_config.configuration_hash)
45+
46+
ActiveRecord::Base.establish_connection(database_config)
47+
conn = ActiveRecord::Base.connection
48+
conn_config = conn.instance_variable_get("@config")
49+
50+
assert conn_config[:use_follower_reads_for_type_introspection]
51+
end
3152
end
3253
end
3354
end

0 commit comments

Comments
 (0)