class ActiveRecord::ConnectionAdapters::CockroachDBAdapter

Constants

ADAPTER_NAME
DEFAULT_PRIMARY_KEY
DEFAULT_SRID

postgis.17.x6.nabble.com/Default-SRID-td5001115.html

SPATIAL_COLUMN_OPTIONS

Public Class Methods

database_exists?(config) click to toggle source
# 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
new(connection, logger, conn_params, config) click to toggle source
Calls superclass method
# 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
spatial_column_options(key) click to toggle source
# 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

debugging?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 141
def debugging?
  !!ENV["DEBUG_COCKROACHDB_ADAPTER"]
end
default_srid() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 128
def default_srid
  DEFAULT_SRID
end
index_name_length()
max_identifier_length() click to toggle source

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
max_transaction_retries() click to toggle source
# 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
postgis_lib_version() click to toggle source
# 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
postgresql_version() click to toggle source

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
srs_database_columns() click to toggle source
# 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
supports_advisory_locks?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 196
def supports_advisory_locks?
  false
end
supports_bulk_alter?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 154
def supports_bulk_alter?
  false
end
supports_comments?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 188
def supports_comments?
  @crdb_version >= 201
end
supports_comments_in_create?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 192
def supports_comments_in_create?
  false
end
supports_datetime_with_precision?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 184
def supports_datetime_with_precision?
  false
end
supports_ddl_transactions?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 163
def supports_ddl_transactions?
  false
end
supports_expression_index?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 179
def supports_expression_index?
  # See cockroachdb/cockroach#9682
  false
end
supports_extensions?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 167
def supports_extensions?
  false
end
supports_json?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 158
def supports_json?
  # FIXME(joey): Add a version check.
  true
end
supports_materialized_views?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 171
def supports_materialized_views?
  false
end
supports_partial_index?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 175
def supports_partial_index?
  @crdb_version >= 202
end
supports_partitioned_indexes?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 209
def supports_partitioned_indexes?
  false
end
supports_string_to_array_coercion?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 205
def supports_string_to_array_coercion?
  @crdb_version >= 202
end
supports_virtual_columns?() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 200
def supports_virtual_columns?
  # See cockroachdb/cockroach#20882.
  false
end
table_alias_length()

Private Instance Methods

add_pg_decoders() click to toggle source

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.

Calls superclass method
# 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
arel_visitor() click to toggle source
# File lib/active_record/connection_adapters/cockroachdb_adapter.rb, line 571
def arel_visitor
  Arel::Visitors::CockroachDB.new(self)
end
column_definitions(table_name) click to toggle source

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.

@see: github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829

Calls superclass method
# 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
configure_connection() click to toggle source

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
crdb_column_definition(field, table_name) click to toggle source

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
extract_empty_array_from_default(default) click to toggle source

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
extract_escaped_string_from_default(default) click to toggle source

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
extract_time_from_default(default) click to toggle source

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
extract_value_from_default(default) click to toggle source

Override extract_value_from_default because the upstream definition doesn't handle the variations in CockroachDB's behavior.

Calls superclass method
# 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
initialize_type_map(m = type_map) click to toggle source
Calls superclass method
# 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
is_cached_plan_failure?(e) click to toggle source

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
load_additional_types(oids = nil) click to toggle source

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.

Calls superclass method
# 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