activerecord-cockroachdb-adapter 0.2.2 → 5.2.1

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