class ActiveRecord::ConnectionAdapters::CockroachDBAdapter
Constants
- ADAPTER_NAME
- DEFAULT_PRIMARY_KEY
- DEFAULT_SRID
- SPATIAL_COLUMN_OPTIONS
Public Class Methods
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 250 def self.database_exists?(config) !!ActiveRecord::Base.cockroachdb_connection(config) rescue ActiveRecord::NoDatabaseError false end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 228 def initialize(connection, logger, conn_params, config) super(connection, logger, conn_params, config) crdb_version_string = query_value("SHOW crdb_version") if crdb_version_string.include? "v1." version_num = 1 elsif crdb_version_string.include? "v2." version_num 2 elsif crdb_version_string.include? "v19.1." version_num = 191 elsif crdb_version_string.include? "v19.2." version_num = 192 elsif crdb_version_string.include? "v20.1." version_num = 201 elsif crdb_version_string.include? "v20.2." version_num = 202 else version_num = 210 end @crdb_version = version_num end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 120 def self.spatial_column_options(key) SPATIAL_COLUMN_OPTIONS[key] end
Public Instance Methods
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 141 def debugging? !!ENV["DEBUG_COCKROACHDB_ADAPTER"] end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 128 def default_srid DEFAULT_SRID end
This is hardcoded to 63 (as previously was in ActiveRecord
5.0) to aid in migration from PostgreSQL to CockroachDB
. In practice, this limitation is arbitrary since CockroachDB
supports index name lengths and table alias lengths far greater than this value. For the time being though, we match the original behavior for PostgreSQL to simplify migrations.
Note that in the migration to ActiveRecord
5.1, this was changed in PostgreSQLAdapter to use `SHOW max_identifier_length
` (which does not exist in CockroachDB
). Therefore, we have to redefine this here.
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 222 def max_identifier_length 63 end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 145 def max_transaction_retries @max_transaction_retries ||= @config.fetch(:max_transaction_retries, 3) end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 124 def postgis_lib_version @postgis_lib_version ||= select_value("SELECT PostGIS_Lib_Version()") end
CockroachDB
20.1 can run queries that work against PostgreSQL 10+.
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 150 def postgresql_version 100000 end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 132 def srs_database_columns { auth_name_column: "auth_name", auth_srid_column: "auth_srid", proj4text_column: "proj4text", srtext_column: "srtext", } end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 196 def supports_advisory_locks? false end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 154 def supports_bulk_alter? false end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 188 def supports_comments? @crdb_version >= 201 end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 192 def supports_comments_in_create? false end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 184 def supports_datetime_with_precision? false end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 163 def supports_ddl_transactions? false end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 179 def supports_expression_index? # See cockroachdb/cockroach#9682 false end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 167 def supports_extensions? false end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 158 def supports_json? # FIXME(joey): Add a version check. true end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 171 def supports_materialized_views? false end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 175 def supports_partial_index? @crdb_version >= 202 end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 209 def supports_partitioned_indexes? false end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 205 def supports_string_to_array_coercion? @crdb_version >= 202 end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 200 def supports_virtual_columns? # See cockroachdb/cockroach#20882. false end
Private Instance Methods
override This method maps data types to their proper decoder.
Currently, querying from the pg_type catalog can be slow due to geo-partitioning so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 519 def add_pg_decoders if @config[:use_follower_reads_for_type_introspection] @default_timezone = nil @timestamp_decoder = nil coders_by_name = { "int2" => PG::TextDecoder::Integer, "int4" => PG::TextDecoder::Integer, "int8" => PG::TextDecoder::Integer, "oid" => PG::TextDecoder::Integer, "float4" => PG::TextDecoder::Float, "float8" => PG::TextDecoder::Float, "numeric" => PG::TextDecoder::Numeric, "bool" => PG::TextDecoder::Boolean, "timestamp" => PG::TextDecoder::TimestampUtc, "timestamptz" => PG::TextDecoder::TimestampWithTimeZone, } known_coder_types = coders_by_name.keys.map { |n| quote(n) } query = <<~SQL % known_coder_types.join(", ") SELECT t.oid, t.typname FROM pg_type as t AS OF SYSTEM TIME '-10s' WHERE t.typname IN (%s) SQL coders = execute_and_clear(query, "SCHEMA", []) do |result| result .map { |row| construct_coder(row, coders_by_name[row["typname"]]) } .compact end map = PG::TypeMapByOid.new coders.each { |coder| map.add_coder(coder) } @connection.type_map_for_results = map @type_map_for_results = PG::TypeMapByOid.new @type_map_for_results.default_type_map = map @type_map_for_results.add_coder(PG::TextDecoder::Bytea.new(oid: 17, name: "bytea")) @type_map_for_results.add_coder(MoneyDecoder.new(oid: 790, name: "money")) # extract timestamp decoder for use in update_typemap_for_default_timezone @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" } update_typemap_for_default_timezone else super end rescue ActiveRecord::StatementInvalid => e raise e unless e.cause.is_a? PG::InvalidCatalogName # use original if database is younger than 10s super end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 571 def arel_visitor Arel::Visitors::CockroachDB.new(self) end
override This method makes a query to gather information about columns in a table. It returns an array of arrays (one for each col) and passes each to the SchemaStatements#new_column_from_field method as the field parameter. This data is then used to format the column objects for the model and sent to the OID for data casting.
Sometimes there are differences between how data is formatted in Postgres and CockroachDB
, so additional queries for certain types may be necessary to properly form the column definition.
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 418 def column_definitions(table_name) fields = super # Use regex comparison because if a type is an array it will # have [] appended to the end of it. target_types = [ /geometry/, /geography/, /interval/, /numeric/ ] re = Regexp.union(target_types) fields.map do |field| dtype = field[1] if re.match(dtype) crdb_column_definition(field, table_name) else field end end end
Configures the encoding, verbosity, schema search path, and time zone of the connection. This is called by connect and should not be called manually.
NOTE(joey): This was cradled from postgresql_adapter.rb. This was due to needing to override configuration statements.
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 304 def configure_connection if @config[:encoding] @connection.set_client_encoding(@config[:encoding]) end self.client_min_messages = @config[:min_messages] || "warning" self.schema_search_path = @config[:schema_search_path] || @config[:schema_order] # Use standard-conforming strings so we don't have to do the E'...' dance. set_standard_conforming_strings variables = @config.fetch(:variables, {}).stringify_keys # If using Active Record's time zone support configure the connection to return # TIMESTAMP WITH ZONE types in UTC. unless variables["timezone"] if ActiveRecord::Base.default_timezone == :utc variables["timezone"] = "UTC" elsif @local_tz variables["timezone"] = @local_tz end end # NOTE(joey): This is a workaround as CockroachDB 1.1.x # supports SET TIME ZONE <...> and SET "time zone" = <...> but # not SET timezone = <...>. if variables.key?("timezone") tz = variables.delete("timezone") execute("SET TIME ZONE #{quote(tz)}", "SCHEMA") end # SET statements from :variables config hash # https://www.postgresql.org/docs/current/static/sql-set.html variables.map do |k, v| if v == ":default" || v == :default # Sets the value to the global or compile default # NOTE(joey): I am not sure if simply commenting this out # is technically correct. # execute("SET #{k} = DEFAULT", "SCHEMA") elsif !v.nil? execute("SET SESSION #{k} = #{quote(v)}", "SCHEMA") end end end
Use the crdb_sql_type instead of the sql_type returned by column_definitions. This will include limit, precision, and scale information in the type. Ex. geometry -> geometry(point, 4326)
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 444 def crdb_column_definition(field, table_name) col_name = field[0] data_type = \ query(<<~SQL, "SCHEMA") SELECT c.crdb_sql_type FROM information_schema.columns c WHERE c.table_name = #{quote(table_name)} AND c.column_name = #{quote(col_name)} SQL field[1] = data_type[0][0].downcase field end
CockroachDB
stores default values for arrays in the `ARRAY` format. In general, it is hard to parse that, but it is easy to handle the common case of an empty array.
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 400 def extract_empty_array_from_default(default) return unless supports_string_to_array_coercion? return unless default =~ /\AARRAY\[\]\z/ return "{}" end
Both PostgreSQL and CockroachDB
use C-style string escapes under the covers. PostgreSQL obscures this for us and unescapes the strings, but CockroachDB
does not. Here we'll use Ruby to unescape the string. See github.com/cockroachdb/cockroach/issues/47497 and www.postgresql.org/docs/9.2/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE.
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 363 def extract_escaped_string_from_default(default) # Escaped strings start with an e followed by the string in quotes (e'…') return unless default =~ /\A[\(B]?e'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m # String#undump doesn't account for escaped single quote characters "\"#{$1}\"".undump.gsub("\\'".freeze, "'".freeze) end
This method exists to extract the correct time and date defaults for a couple of reasons. 1) There's a bug in CockroachDB
where the date type is missing from the column info query. github.com/cockroachdb/cockroach/issues/47285 2) PostgreSQL's timestamp without time zone type maps to CockroachDB's TIMESTAMP type. TIMESTAMP includes a UTC time zone while timestamp without time zone doesn't. www.cockroachlabs.com/docs/v19.2/timestamp.html#variants
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 380 def extract_time_from_default(default) return unless default =~ /\A'(.*)'\z/ # If default has a UTC time zone, we'll drop the time zone information # so it acts like PostgreSQL's timestamp without time zone. Then, try # to parse the resulting string to verify if it's a time. time = if default =~ /\A'(.*)(\+00:00)'\z/ $1 else default end Time.parse(time).to_s rescue nil end
Override extract_value_from_default
because the upstream definition doesn't handle the variations in CockroachDB's behavior.
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 351 def extract_value_from_default(default) super || extract_escaped_string_from_default(default) || extract_time_from_default(default) || extract_empty_array_from_default(default) end
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 258 def initialize_type_map(m = type_map) %w( geography geometry geometry_collection line_string multi_line_string multi_point multi_polygon st_point st_polygon ).each do |geo_type| m.register_type(geo_type) do |oid, _, sql_type| CockroachDB::OID::Spatial.new(oid, sql_type) end end # Belongs after other types are defined because of issues described # in this https://github.com/rails/rails/pull/38571 # Once that PR is merged, we can call super at the top. super(m) # Override numeric type. This is almost identical to the default, # except that the conditional based on the fmod is changed. m.register_type "numeric" do |_, fmod, sql_type| precision = extract_precision(sql_type) scale = extract_scale(sql_type) # If fmod is -1, that means that precision is defined but not # scale, or neither is defined. if fmod && fmod == -1 && !precision.nil? # Below comment is from ActiveRecord # FIXME: Remove this class, and the second argument to # lookups on PG Type::DecimalWithoutScale.new(precision: precision) else OID::Decimal.new(precision: precision, scale: scale) end end end
override This method is used to determine if a FEATURE_NOT_SUPPORTED error from the PG gem should be an ActiveRecord::PreparedStatementCacheExpired error.
ActiveRecord
handles this by checking that the sql state matches the FEATURE_NOT_SUPPORTED code and that the source function is “RevalidateCachedQuery” since that is the only function in postgres that will create this error.
That method will not work for CockroachDB
because the error originates from the “runExecBuilder” function, so we need to modify the original to match the CockroachDB
behavior.
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 471 def is_cached_plan_failure?(e) pgerror = e.cause pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE) == FEATURE_NOT_SUPPORTED && pgerror.result.result_error_field(PG::PG_DIAG_SOURCE_FUNCTION) == "runExecBuilder" rescue false end
override This method loads info about data types from the database to populate the TypeMap.
Currently, querying from the pg_type catalog can be slow due to geo-partitioning so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 486 def load_additional_types(oids = nil) if @config[:use_follower_reads_for_type_introspection] initializer = OID::TypeMapInitializer.new(type_map) query = <<~SQL SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype FROM pg_type as t LEFT JOIN pg_range as r ON oid = rngtypid AS OF SYSTEM TIME '-10s' SQL if oids query += "WHERE t.oid IN (%s)" % oids.join(", ") else query += initializer.query_conditions_for_initial_load end execute_and_clear(query, "SCHEMA", []) do |records| initializer.run(records) end else super end rescue ActiveRecord::StatementInvalid => e raise e unless e.cause.is_a? PG::InvalidCatalogName # use original if database is younger than 10s super end