activerecord-cockroachdb-adapter 0.2.2 → 5.2.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,62 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euox pipefail
4
+
5
+ # Download CockroachDB
6
+ VERSION=v20.2.1
7
+ wget -qO- https://binaries.cockroachdb.com/cockroach-$VERSION.linux-amd64.tgz | tar xvz
8
+ readonly COCKROACH=./cockroach-$VERSION.linux-amd64/cockroach
9
+
10
+ # Make sure cockroach can be found on the path. This is required for the
11
+ # ActiveRecord Rakefile that rebuilds the test database.
12
+ export PATH=./cockroach-$VERSION.linux-amd64/:$PATH
13
+ readonly urlfile=cockroach-url
14
+
15
+ run_cockroach() {
16
+ # Start a CockroachDB server, wait for it to become ready, and arrange
17
+ # for it to be force-killed when the script exits.
18
+ rm -f "$urlfile"
19
+ # Clean out a past CockroachDB instance. This will clean out leftovers
20
+ # from the build agent, and also between CockroachDB runs.
21
+ cockroach quit --insecure || true
22
+ rm -rf cockroach-data
23
+ # Start CockroachDB.
24
+ cockroach start-single-node --max-sql-memory=25% --cache=25% --insecure --host=localhost --listening-url-file="$urlfile" >/dev/null 2>&1 &
25
+ # Ensure CockroachDB is stopped on script exit.
26
+ trap "echo 'Exit routine: Killing CockroachDB.' && kill -9 $! &> /dev/null" EXIT
27
+ # Wait until CockroachDB has started.
28
+ for i in {0..3}; do
29
+ [[ -f "$urlfile" ]] && break
30
+ backoff=$((2 ** i))
31
+ echo "server not yet available; sleeping for $backoff seconds"
32
+ sleep $backoff
33
+ done
34
+ cockroach sql --insecure -e 'CREATE DATABASE activerecord_unittest;'
35
+ cockroach sql --insecure -e 'CREATE DATABASE activerecord_unittest2;'
36
+ cockroach sql --insecure -e 'SET CLUSTER SETTING sql.stats.automatic_collection.enabled = false;'
37
+ cockroach sql --insecure -e 'SET CLUSTER SETTING sql.stats.histogram_collection.enabled = false;'
38
+ cockroach sql --insecure -e "SET CLUSTER SETTING jobs.retention_time = '180s';"
39
+ }
40
+
41
+ # Install ruby dependencies.
42
+ gem install bundler:2.1.4
43
+ bundle install
44
+
45
+ run_cockroach
46
+
47
+ if ! (RUBYOPT="-W0" TESTOPTS="-v" bundle exec rake test); then
48
+ echo "Tests failed"
49
+ HAS_FAILED=1
50
+ else
51
+ echo "Tests passed"
52
+ HAS_FAILED=0
53
+ fi
54
+
55
+ # Attempt a clean shutdown for good measure. We'll force-kill in the
56
+ # exit trap if this script fails.
57
+ cockroach quit --insecure
58
+ trap - EXIT
59
+
60
+ if [ $HAS_FAILED -eq 1 ]; then
61
+ exit 1
62
+ fi
@@ -0,0 +1,38 @@
1
+ #!/bin/bash
2
+ #
3
+ # This file is largely cargo-culted from cockroachdb/cockroach/build/builder.sh.
4
+
5
+ set -euox pipefail
6
+
7
+ DOCKER_IMAGE_TAG=activerecord_test_container
8
+
9
+ # Build the docker image to use.
10
+ docker build -t ${DOCKER_IMAGE_TAG} build/
11
+
12
+ # Absolute path to this repository.
13
+ repo_root=$(cd "$(dirname "${0}")" && pwd)
14
+
15
+ # Make a fake passwd file for the invoking user.
16
+ #
17
+ # This setup is so that files created from inside the container in a mounted
18
+ # volume end up being owned by the invoking user and not by root.
19
+ # We'll mount a fresh directory owned by the invoking user as /root inside the
20
+ # container because the container needs a $HOME (without one the default is /)
21
+ # and because various utilities (e.g. bash writing to .bash_history) need to be
22
+ # able to write to there.
23
+ username=$(id -un)
24
+ uid_gid=$(id -u):$(id -g)
25
+ container_root=${repo_root}/docker_root
26
+ mkdir -p "${container_root}"/{etc,home,home/"${username}"/activerecord-cockroachdb-adapter,home/.gems}
27
+ echo "${username}:x:${uid_gid}::/home/${username}:/bin/bash" > "${container_root}/etc/passwd"
28
+
29
+ docker run \
30
+ --volume="${container_root}/etc/passwd:/etc/passwd" \
31
+ --volume="${container_root}/home/${username}:/home/${username}" \
32
+ --volume="${repo_root}:/home/${username}/activerecord-cockroachdb-adapter" \
33
+ --workdir="/home/${username}/activerecord-cockroachdb-adapter" \
34
+ --env=PIP_USER=1 \
35
+ --env=GEM_HOME="/home/${username}/.gems" \
36
+ --user="${uid_gid}" \
37
+ "${DOCKER_IMAGE_TAG}" \
38
+ "$@"
@@ -0,0 +1,28 @@
1
+ module ActiveRecord
2
+ module CockroachDB
3
+ module AttributeMethodsMonkeyPatch
4
+
5
+ private
6
+
7
+ # Filter out rowid so it doesn't get inserted by ActiveRecord. rowid is a
8
+ # column added by CockroachDB for tables that don't define primary keys.
9
+ # CockroachDB will automatically insert rowid values. See
10
+ # https://www.cockroachlabs.com/docs/v19.2/create-table.html#create-a-table.
11
+ def attributes_for_create(attribute_names)
12
+ super.reject { |name| name == ConnectionAdapters::CockroachDBAdapter::DEFAULT_PRIMARY_KEY }
13
+ end
14
+
15
+ # Filter out rowid so it doesn't get updated by ActiveRecord. rowid is a
16
+ # column added by CockroachDB for tables that don't define primary keys.
17
+ # CockroachDB will automatically insert rowid values. See
18
+ # https://www.cockroachlabs.com/docs/v19.2/create-table.html#create-a-table.
19
+ def attributes_for_update(attribute_names)
20
+ super.reject { |name| name == ConnectionAdapters::CockroachDBAdapter::DEFAULT_PRIMARY_KEY }
21
+ end
22
+ end
23
+ end
24
+
25
+ class Base
26
+ prepend CockroachDB::AttributeMethodsMonkeyPatch
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module CockroachDB
4
+ module PostgreSQLColumnMonkeyPatch
5
+ def serial?
6
+ default_function == "unique_rowid()"
7
+ end
8
+ end
9
+ end
10
+
11
+ class PostgreSQLColumn
12
+ prepend CockroachDB::PostgreSQLColumnMonkeyPatch
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,102 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module CockroachDB
4
+ module DatabaseStatements
5
+ # Since CockroachDB will run all transactions with serializable isolation,
6
+ # READ UNCOMMITTED, READ COMMITTED, and REPEATABLE READ are all aliases
7
+ # for SERIALIZABLE. This lets the adapter support all isolation levels,
8
+ # but READ UNCOMMITTED has been removed from this list because the
9
+ # ActiveRecord transaction isolation test fails for READ UNCOMMITTED.
10
+ # See https://www.cockroachlabs.com/docs/v19.2/transactions.html#isolation-levels
11
+ def transaction_isolation_levels
12
+ {
13
+ read_committed: "READ COMMITTED",
14
+ repeatable_read: "REPEATABLE READ",
15
+ serializable: "SERIALIZABLE"
16
+ }
17
+ end
18
+
19
+ # Overridden to avoid using transactions for schema creation.
20
+ def insert_fixtures_set(fixture_set, tables_to_delete = [])
21
+ fixture_inserts = build_fixture_statements(fixture_set)
22
+ table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name(table)}" }
23
+ statements = table_deletes + fixture_inserts
24
+
25
+ with_multi_statements do
26
+ disable_referential_integrity do
27
+ execute_batch(statements, "Fixtures Load")
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+ def execute_batch(statements, name = nil)
34
+ statements.each do |statement|
35
+ execute(statement, name)
36
+ end
37
+ end
38
+
39
+ DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze
40
+ private_constant :DEFAULT_INSERT_VALUE
41
+
42
+ def default_insert_value(column)
43
+ DEFAULT_INSERT_VALUE
44
+ end
45
+
46
+ def build_fixture_sql(fixtures, table_name)
47
+ columns = schema_cache.columns_hash(table_name)
48
+
49
+ values_list = fixtures.map do |fixture|
50
+ fixture = fixture.stringify_keys
51
+
52
+ unknown_columns = fixture.keys - columns.keys
53
+ if unknown_columns.any?
54
+ raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.)
55
+ end
56
+
57
+ columns.map do |name, column|
58
+ if fixture.key?(name)
59
+ type = lookup_cast_type_from_column(column)
60
+ with_yaml_fallback(type.serialize(fixture[name]))
61
+ else
62
+ default_insert_value(column)
63
+ end
64
+ end
65
+ end
66
+
67
+ table = Arel::Table.new(table_name)
68
+ manager = Arel::InsertManager.new
69
+ manager.into(table)
70
+
71
+ if values_list.size == 1
72
+ values = values_list.shift
73
+ new_values = []
74
+ columns.each_key.with_index { |column, i|
75
+ unless values[i].equal?(DEFAULT_INSERT_VALUE)
76
+ new_values << values[i]
77
+ manager.columns << table[column]
78
+ end
79
+ }
80
+ values_list << new_values
81
+ else
82
+ columns.each_key { |column| manager.columns << table[column] }
83
+ end
84
+
85
+ manager.values = manager.create_values_list(values_list)
86
+ manager.to_sql
87
+ end
88
+
89
+ def build_fixture_statements(fixture_set)
90
+ fixture_set.map do |table_name, fixtures|
91
+ next if fixtures.empty?
92
+ build_fixture_sql(fixtures, table_name)
93
+ end.compact
94
+ end
95
+
96
+ def with_multi_statements
97
+ yield
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,28 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module CockroachDB
4
+ module Quoting
5
+ private
6
+
7
+ # CockroachDB does not allow inserting integer values into string
8
+ # columns, but ActiveRecord expects this to work. CockroachDB will
9
+ # however allow inserting string values into integer columns. It will
10
+ # try to parse string values and convert them to integers so they can be
11
+ # inserted in integer columns.
12
+ #
13
+ # We take advantage of this behavior here by forcing numeric values to
14
+ # always be strings. Then, we won't have to make any additional changes
15
+ # to ActiveRecord to support inserting integer values into string
16
+ # columns.
17
+ def _quote(value)
18
+ case value
19
+ when Numeric
20
+ "'#{quote_string(value.to_s)}'"
21
+ else
22
+ super
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The PostgresSQL Adapter's ReferentialIntegrity module can disable and
4
+ # re-enable foreign key constraints by disabling all table triggers. Since
5
+ # triggers are not available in CockroachDB, we have to remove foreign keys and
6
+ # re-add them via the ActiveRecord API.
7
+ #
8
+ # This module is commonly used to load test fixture data without having to worry
9
+ # about the order in which that data is loaded.
10
+ module ActiveRecord
11
+ module ConnectionAdapters
12
+ module CockroachDB
13
+ module ReferentialIntegrity
14
+ def disable_referential_integrity
15
+ foreign_keys = tables.map { |table| foreign_keys(table) }.flatten
16
+
17
+ foreign_keys.each do |foreign_key|
18
+ remove_foreign_key(foreign_key.from_table, name: foreign_key.options[:name])
19
+ end
20
+
21
+ yield
22
+
23
+ foreign_keys.each do |foreign_key|
24
+ begin
25
+ add_foreign_key(foreign_key.from_table, foreign_key.to_table, foreign_key.options)
26
+ rescue ActiveRecord::StatementInvalid => error
27
+ if error.cause.class == PG::DuplicateObject
28
+ # This error is safe to ignore because the yielded caller
29
+ # already re-added the foreign key constraint.
30
+ else
31
+ raise error
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,67 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module CockroachDB
4
+ module SchemaStatements
5
+ include ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements
6
+
7
+ def add_index(table_name, column_name, options = {})
8
+ super
9
+ rescue ActiveRecord::StatementInvalid => error
10
+ if debugging? && error.cause.class == PG::FeatureNotSupported
11
+ warn "#{error}\n\nThis error will be ignored and the index will not be created.\n\n"
12
+ else
13
+ raise error
14
+ end
15
+ end
16
+
17
+ # ActiveRecord allows for tables to exist without primary keys.
18
+ # Databases like PostgreSQL support this behavior, but CockroachDB does
19
+ # not. If a table is created without a primary key, CockroachDB will add
20
+ # a rowid column to serve as its primary key. This breaks a lot of
21
+ # ActiveRecord's assumptions so we'll treat tables with rowid primary
22
+ # keys as if they didn't have primary keys at all.
23
+ # https://www.cockroachlabs.com/docs/v19.2/create-table.html#create-a-table
24
+ # https://api.rubyonrails.org/v5.2.4/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-create_table
25
+ def primary_key(table_name)
26
+ pk = super
27
+
28
+ if pk == CockroachDBAdapter::DEFAULT_PRIMARY_KEY
29
+ nil
30
+ else
31
+ pk
32
+ end
33
+ end
34
+
35
+ # CockroachDB uses unique_rowid() for primary keys, not sequences. It's
36
+ # possible to force a table to use sequences, but since it's not the
37
+ # default behavior we'll always return nil for default_sequence_name.
38
+ def default_sequence_name(table_name, pk = "id")
39
+ nil
40
+ end
41
+
42
+ # CockroachDB will use INT8 if the SQL type is INTEGER, so we make it use
43
+ # INT4 explicitly when needed.
44
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc:
45
+ sql = \
46
+ case type.to_s
47
+ when "integer"
48
+ case limit
49
+ when nil; "int"
50
+ when 1, 2; "int2"
51
+ when 3, 4; "int4"
52
+ when 5..8; "int8"
53
+ else super
54
+ end
55
+ else
56
+ super
57
+ end
58
+ # The call to super might have appeneded [] already.
59
+ if array && type != :primary_key && !sql.end_with?("[]")
60
+ sql = "#{sql}[]"
61
+ end
62
+ sql
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module CockroachDB
6
+ module TransactionManagerMonkeyPatch
7
+ # Capture ActiveRecord::SerializationFailure errors caused by
8
+ # transactions that fail due to serialization errors. Failed
9
+ # transactions will be retried until they pass or the max retry limit is
10
+ # exceeded.
11
+ def within_new_transaction(options = {})
12
+ attempts = options.fetch(:attempts, 0)
13
+ super
14
+ rescue ActiveRecord::SerializationFailure => error
15
+ raise if attempts >= @connection.max_transaction_retries
16
+
17
+ attempts += 1
18
+ sleep_seconds = (2 ** attempts + rand) / 10
19
+ sleep(sleep_seconds)
20
+ within_new_transaction(options.merge(attempts: attempts)) { yield }
21
+ end
22
+ end
23
+ end
24
+
25
+ class TransactionManager
26
+ prepend CockroachDB::TransactionManagerMonkeyPatch
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveRecord
2
+ module Type
3
+ class << self
4
+ private
5
+
6
+ # Return :postgresql instead of :cockroachdb for current_adapter_name so
7
+ # we can continue using the ActiveRecord::Types defined in
8
+ # PostgreSQLAdapter.
9
+ def current_adapter_name
10
+ :postgresql
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,4 +1,12 @@
1
1
  require 'active_record/connection_adapters/postgresql_adapter'
2
+ require "active_record/connection_adapters/cockroachdb/schema_statements"
3
+ require "active_record/connection_adapters/cockroachdb/referential_integrity"
4
+ require "active_record/connection_adapters/cockroachdb/transaction_manager"
5
+ require "active_record/connection_adapters/cockroachdb/column"
6
+ require "active_record/connection_adapters/cockroachdb/database_statements"
7
+ require "active_record/connection_adapters/cockroachdb/quoting"
8
+ require "active_record/connection_adapters/cockroachdb/type"
9
+ require "active_record/connection_adapters/cockroachdb/attribute_methods"
2
10
 
3
11
  module ActiveRecord
4
12
  module ConnectionHandling
@@ -13,7 +21,7 @@ module ActiveRecord
13
21
  conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
14
22
 
15
23
  # Forward only valid config params to PG::Connection.connect.
16
- valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
24
+ valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:sslmode, :application_name]
17
25
  conn_params.slice!(*valid_conn_param_keys)
18
26
 
19
27
  # The postgres drivers don't allow the creation of an unconnected
@@ -24,125 +32,251 @@ module ActiveRecord
24
32
  end
25
33
  end
26
34
 
27
- class ActiveRecord::ConnectionAdapters::CockroachDBAdapter < ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
28
- ADAPTER_NAME = "CockroachDB".freeze
35
+ module ActiveRecord
36
+ module ConnectionAdapters
37
+ class CockroachDBAdapter < PostgreSQLAdapter
38
+ ADAPTER_NAME = "CockroachDB".freeze
39
+ DEFAULT_PRIMARY_KEY = "rowid"
29
40
 
30
- # Note that in the migration from ActiveRecord 5.0 to 5.1, the
31
- # `extract_schema_qualified_name` method was aliased in the PostgreSQLAdapter.
32
- # To ensure backward compatibility with both <5.1 and 5.1, we rename it here
33
- # to use the same original `Utils` module.
34
- Utils = ActiveRecord::ConnectionAdapters::PostgreSQL::Utils
41
+ include CockroachDB::SchemaStatements
42
+ include CockroachDB::ReferentialIntegrity
43
+ include CockroachDB::DatabaseStatements
44
+ include CockroachDB::Quoting
35
45
 
36
- def supports_json?
37
- false
38
- end
46
+ def debugging?
47
+ !!ENV["DEBUG_COCKROACHDB_ADAPTER"]
48
+ end
39
49
 
40
- def supports_ddl_transactions?
41
- false
42
- end
50
+ def max_transaction_retries
51
+ @max_transaction_retries ||= @config.fetch(:max_transaction_retries, 3)
52
+ end
43
53
 
44
- def supports_extensions?
45
- false
46
- end
54
+ # CockroachDB 20.1 can run queries that work against PostgreSQL 10+.
55
+ def postgresql_version
56
+ 100000
57
+ end
47
58
 
48
- def supports_ranges?
49
- false
50
- end
59
+ def supports_bulk_alter?
60
+ false
61
+ end
51
62
 
52
- def supports_materialized_views?
53
- false
54
- end
63
+ def supports_json?
64
+ # FIXME(joey): Add a version check.
65
+ true
66
+ end
55
67
 
56
- def supports_pg_crypto_uuid?
57
- false
58
- end
68
+ def supports_ddl_transactions?
69
+ false
70
+ end
59
71
 
60
- def indexes(table_name, name = nil) # :nodoc:
61
- # The PostgreSQL adapter uses a correlated subquery in the following query,
62
- # which CockroachDB does not yet support. That portion of the query fetches
63
- # any non-standard opclasses that each index uses. CockroachDB also doesn't
64
- # support opclasses at this time, so the query is modified to just remove
65
- # the section about opclasses entirely.
66
- if name
67
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
68
- Passing name to #indexes is deprecated without replacement.
69
- MSG
70
- end
72
+ def supports_extensions?
73
+ false
74
+ end
71
75
 
72
- table = Utils.extract_schema_qualified_name(table_name.to_s)
73
-
74
- result = query(<<-SQL, "SCHEMA")
75
- SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
76
- pg_catalog.obj_description(i.oid, 'pg_class') AS comment
77
- FROM pg_class t
78
- INNER JOIN pg_index d ON t.oid = d.indrelid
79
- INNER JOIN pg_class i ON d.indexrelid = i.oid
80
- LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
81
- WHERE i.relkind = 'i'
82
- AND d.indisprimary = 'f'
83
- AND t.relname = '#{table.identifier}'
84
- AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'}
85
- ORDER BY i.relname
86
- SQL
87
-
88
- result.map do |row|
89
- index_name = row[0]
90
- unique = row[1]
91
- indkey = row[2].split(" ").map(&:to_i)
92
- inddef = row[3]
93
- oid = row[4]
94
- comment = row[5]
95
-
96
- expressions, where = inddef.scan(/\((.+?)\)(?: WHERE (.+))?\z/).flatten
97
-
98
- if indkey.include?(0)
99
- columns = expressions
100
- else
101
- columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact
102
- SELECT a.attnum, a.attname
103
- FROM pg_attribute a
104
- WHERE a.attrelid = #{oid}
105
- AND a.attnum IN (#{indkey.join(",")})
106
- SQL
107
-
108
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
109
- orders = Hash[
110
- expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] }
111
- ]
112
- end
113
-
114
- ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, columns, [], orders, where, nil, nil, comment.presence)
115
- end.compact
116
- end
76
+ def supports_ranges?
77
+ # See cockroachdb/cockroach#17022
78
+ false
79
+ end
117
80
 
81
+ def supports_materialized_views?
82
+ false
83
+ end
118
84
 
119
- def primary_keys(table_name)
120
- name = Utils.extract_schema_qualified_name(table_name.to_s)
121
- select_values(<<-SQL.strip_heredoc, "SCHEMA")
122
- SELECT column_name
123
- FROM information_schema.key_column_usage kcu
124
- JOIN information_schema.table_constraints tc
125
- ON kcu.table_name = tc.table_name
126
- AND kcu.table_schema = tc.table_schema
127
- AND kcu.constraint_name = tc.constraint_name
128
- WHERE constraint_type = 'PRIMARY KEY'
129
- AND kcu.table_name = #{quote(name.identifier)}
130
- AND kcu.table_schema = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"}
131
- ORDER BY kcu.ordinal_position
132
- SQL
133
- end
85
+ def supports_partial_index?
86
+ @crdb_version >= 202
87
+ end
88
+
89
+ def supports_expression_index?
90
+ # See cockroachdb/cockroach#9682
91
+ false
92
+ end
93
+
94
+ def supports_datetime_with_precision?
95
+ false
96
+ end
97
+
98
+ def supports_comments?
99
+ # See cockroachdb/cockroach#19472.
100
+ false
101
+ end
102
+
103
+ def supports_comments_in_create?
104
+ # See cockroachdb/cockroach#19472.
105
+ false
106
+ end
107
+
108
+ def supports_advisory_locks?
109
+ false
110
+ end
111
+
112
+ def supports_virtual_columns?
113
+ # See cockroachdb/cockroach#20882.
114
+ false
115
+ end
116
+
117
+ def supports_string_to_array_coercion?
118
+ @crdb_version >= 202
119
+ end
120
+
121
+ # This is hardcoded to 63 (as previously was in ActiveRecord 5.0) to aid in
122
+ # migration from PostgreSQL to CockroachDB. In practice, this limitation
123
+ # is arbitrary since CockroachDB supports index name lengths and table alias
124
+ # lengths far greater than this value. For the time being though, we match
125
+ # the original behavior for PostgreSQL to simplify migrations.
126
+ #
127
+ # Note that in the migration to ActiveRecord 5.1, this was changed in
128
+ # PostgreSQLAdapter to use `SHOW max_identifier_length` (which does not
129
+ # exist in CockroachDB). Therefore, we have to redefine this here.
130
+ def max_identifier_length
131
+ 63
132
+ end
133
+ alias index_name_length max_identifier_length
134
+ alias table_alias_length max_identifier_length
135
+
136
+ def initialize(connection, logger, conn_params, config)
137
+ super(connection, logger, conn_params, config)
138
+ crdb_version_string = query_value("SHOW crdb_version")
139
+ if crdb_version_string.include? "v1."
140
+ version_num = 1
141
+ elsif crdb_version_string.include? "v2."
142
+ version_num 2
143
+ elsif crdb_version_string.include? "v19.1."
144
+ version_num = 191
145
+ elsif crdb_version_string.include? "v19.2."
146
+ version_num = 192
147
+ elsif crdb_version_string.include? "v20.1."
148
+ version_num = 201
149
+ else
150
+ version_num = 202
151
+ end
152
+ @crdb_version = version_num
153
+ end
154
+
155
+ private
156
+
157
+ def initialize_type_map(m = type_map)
158
+ super(m)
159
+ # NOTE(joey): PostgreSQL intervals have a precision.
160
+ # CockroachDB intervals do not, so overide the type
161
+ # definition. Returning a ArgumentError may not be correct.
162
+ # This needs to be tested.
163
+ m.register_type "interval" do |_, _, sql_type|
164
+ precision = extract_precision(sql_type)
165
+ if precision
166
+ raise(ArgumentError, "CockroachDB does not support precision on intervals, but got precision: #{precision}")
167
+ end
168
+ OID::SpecializedString.new(:interval, precision: precision)
169
+ end
170
+ end
171
+
172
+ # Configures the encoding, verbosity, schema search path, and time zone of the connection.
173
+ # This is called by #connect and should not be called manually.
174
+ #
175
+ # NOTE(joey): This was cradled from postgresql_adapter.rb. This
176
+ # was due to needing to override configuration statements.
177
+ def configure_connection
178
+ if @config[:encoding]
179
+ @connection.set_client_encoding(@config[:encoding])
180
+ end
181
+ self.client_min_messages = @config[:min_messages] || "warning"
182
+ self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
183
+
184
+ # Use standard-conforming strings so we don't have to do the E'...' dance.
185
+ set_standard_conforming_strings
134
186
 
135
- # This is hardcoded to 63 (as previously was in ActiveRecord 5.0) to aid in
136
- # migration from PostgreSQL to Cockroachdb. In practice, this limitation
137
- # is arbitrary since CockroachDB supports index name lengths and table alias
138
- # lengths far greater than this value. For the time being though, we match
139
- # the original behavior for PostgreSQL to simplify migrations.
140
- #
141
- # Note that in the migration to ActiveRecord 5.1, this was changed in
142
- # PostgreSQLAdapter to use `SHOW max_identifier_length` (which does not
143
- # exist in CockroachDB). Therefore, we have to redefine this here.
144
- def table_alias_length
145
- 63
187
+ variables = @config.fetch(:variables, {}).stringify_keys
188
+
189
+ # If using Active Record's time zone support configure the connection to return
190
+ # TIMESTAMP WITH ZONE types in UTC.
191
+ unless variables["timezone"]
192
+ if ActiveRecord::Base.default_timezone == :utc
193
+ variables["timezone"] = "UTC"
194
+ elsif @local_tz
195
+ variables["timezone"] = @local_tz
196
+ end
197
+ end
198
+
199
+ # NOTE(joey): This is a workaround as CockroachDB 1.1.x
200
+ # supports SET TIME ZONE <...> and SET "time zone" = <...> but
201
+ # not SET timezone = <...>.
202
+ if variables.key?("timezone")
203
+ tz = variables.delete("timezone")
204
+ execute("SET TIME ZONE #{quote(tz)}", "SCHEMA")
205
+ end
206
+
207
+ # SET statements from :variables config hash
208
+ # https://www.postgresql.org/docs/current/static/sql-set.html
209
+ variables.map do |k, v|
210
+ if v == ":default" || v == :default
211
+ # Sets the value to the global or compile default
212
+
213
+ # NOTE(joey): I am not sure if simply commenting this out
214
+ # is technically correct.
215
+ # execute("SET #{k} = DEFAULT", "SCHEMA")
216
+ elsif !v.nil?
217
+ execute("SET SESSION #{k} = #{quote(v)}", "SCHEMA")
218
+ end
219
+ end
220
+ end
221
+
222
+ # Override extract_value_from_default because the upstream definition
223
+ # doesn't handle the variations in CockroachDB's behavior.
224
+ def extract_value_from_default(default)
225
+ super ||
226
+ extract_escaped_string_from_default(default) ||
227
+ extract_time_from_default(default) ||
228
+ extract_empty_array_from_default(default)
229
+ end
230
+
231
+ # Both PostgreSQL and CockroachDB use C-style string escapes under the
232
+ # covers. PostgreSQL obscures this for us and unescapes the strings, but
233
+ # CockroachDB does not. Here we'll use Ruby to unescape the string.
234
+ # See https://github.com/cockroachdb/cockroach/issues/47497 and
235
+ # https://www.postgresql.org/docs/9.2/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE.
236
+ def extract_escaped_string_from_default(default)
237
+ # Escaped strings start with an e followed by the string in quotes (e'…')
238
+ return unless default =~ /\A[\(B]?e'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
239
+
240
+ # String#undump doesn't account for escaped single quote characters
241
+ "\"#{$1}\"".undump.gsub("\\'".freeze, "'".freeze)
242
+ end
243
+
244
+ # This method exists to extract the correct time and date defaults for a
245
+ # couple of reasons.
246
+ # 1) There's a bug in CockroachDB where the date type is missing from
247
+ # the column info query.
248
+ # https://github.com/cockroachdb/cockroach/issues/47285
249
+ # 2) PostgreSQL's timestamp without time zone type maps to CockroachDB's
250
+ # TIMESTAMP type. TIMESTAMP includes a UTC time zone while timestamp
251
+ # without time zone doesn't.
252
+ # https://www.cockroachlabs.com/docs/v19.2/timestamp.html#variants
253
+ def extract_time_from_default(default)
254
+ return unless default =~ /\A'(.*)'\z/
255
+
256
+ # If default has a UTC time zone, we'll drop the time zone information
257
+ # so it acts like PostgreSQL's timestamp without time zone. Then, try
258
+ # to parse the resulting string to verify if it's a time.
259
+ time = if default =~ /\A'(.*)(\+00:00)'\z/
260
+ $1
261
+ else
262
+ default
263
+ end
264
+
265
+ Time.parse(time).to_s
266
+ rescue
267
+ nil
268
+ end
269
+
270
+ # CockroachDB stores default values for arrays in the `ARRAY[...]` format.
271
+ # In general, it is hard to parse that, but it is easy to handle the common
272
+ # case of an empty array.
273
+ def extract_empty_array_from_default(default)
274
+ return unless supports_string_to_array_coercion?
275
+ return unless default =~ /\AARRAY\[\]\z/
276
+ return "{}"
277
+ end
278
+
279
+ # end private
280
+ end
146
281
  end
147
- alias index_name_length table_alias_length
148
282
  end