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.
- 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
|