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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.gitmodules +0 -0
- data/CONTRIBUTING.md +220 -0
- data/Gemfile +61 -2
- data/README.md +12 -2
- data/Rakefile +17 -5
- data/activerecord-cockroachdb-adapter.gemspec +3 -7
- data/build/Dockerfile +17 -0
- data/build/config.teamcity.yml +28 -0
- data/build/local-test.sh +38 -0
- data/build/teamcity-test.sh +62 -0
- data/docker.sh +38 -0
- data/lib/active_record/connection_adapters/cockroachdb/attribute_methods.rb +28 -0
- data/lib/active_record/connection_adapters/cockroachdb/column.rb +15 -0
- data/lib/active_record/connection_adapters/cockroachdb/database_statements.rb +102 -0
- data/lib/active_record/connection_adapters/cockroachdb/quoting.rb +28 -0
- data/lib/active_record/connection_adapters/cockroachdb/referential_integrity.rb +39 -0
- data/lib/active_record/connection_adapters/cockroachdb/schema_statements.rb +67 -0
- data/lib/active_record/connection_adapters/cockroachdb/transaction_manager.rb +29 -0
- data/lib/active_record/connection_adapters/cockroachdb/type.rb +14 -0
- data/lib/active_record/connection_adapters/cockroachdb_adapter.rb +243 -109
- metadata +23 -64
- data/.travis.yml +0 -5
@@ -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
|
data/docker.sh
ADDED
@@ -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 + [:
|
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
|
-
|
28
|
-
|
35
|
+
module ActiveRecord
|
36
|
+
module ConnectionAdapters
|
37
|
+
class CockroachDBAdapter < PostgreSQLAdapter
|
38
|
+
ADAPTER_NAME = "CockroachDB".freeze
|
39
|
+
DEFAULT_PRIMARY_KEY = "rowid"
|
29
40
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
46
|
+
def debugging?
|
47
|
+
!!ENV["DEBUG_COCKROACHDB_ADAPTER"]
|
48
|
+
end
|
39
49
|
|
40
|
-
|
41
|
-
|
42
|
-
|
50
|
+
def max_transaction_retries
|
51
|
+
@max_transaction_retries ||= @config.fetch(:max_transaction_retries, 3)
|
52
|
+
end
|
43
53
|
|
44
|
-
|
45
|
-
|
46
|
-
|
54
|
+
# CockroachDB 20.1 can run queries that work against PostgreSQL 10+.
|
55
|
+
def postgresql_version
|
56
|
+
100000
|
57
|
+
end
|
47
58
|
|
48
|
-
|
49
|
-
|
50
|
-
|
59
|
+
def supports_bulk_alter?
|
60
|
+
false
|
61
|
+
end
|
51
62
|
|
52
|
-
|
53
|
-
|
54
|
-
|
63
|
+
def supports_json?
|
64
|
+
# FIXME(joey): Add a version check.
|
65
|
+
true
|
66
|
+
end
|
55
67
|
|
56
|
-
|
57
|
-
|
58
|
-
|
68
|
+
def supports_ddl_transactions?
|
69
|
+
false
|
70
|
+
end
|
59
71
|
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|