activerecord-cockroachdb-adapter 7.0.2 → 7.1.0
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 +4 -4
- data/.editorconfig +7 -0
- data/.github/workflows/ci.yml +98 -0
- data/CHANGELOG.md +16 -0
- data/CONTRIBUTING.md +6 -26
- data/Gemfile +44 -44
- data/README.md +1 -1
- data/Rakefile +5 -15
- data/activerecord-cockroachdb-adapter.gemspec +3 -6
- data/bin/console +27 -7
- data/bin/console_schemas/default.rb +9 -0
- data/bin/console_schemas/schemas.rb +23 -0
- data/bin/start-cockroachdb +48 -0
- data/build/Dockerfile +1 -1
- data/build/teamcity-test.sh +2 -7
- data/lib/active_record/connection_adapters/cockroachdb/arel_tosql.rb +8 -0
- data/lib/active_record/connection_adapters/cockroachdb/column.rb +3 -11
- data/lib/active_record/connection_adapters/cockroachdb/column_methods.rb +8 -0
- data/lib/active_record/connection_adapters/cockroachdb/database_statements.rb +0 -83
- data/lib/active_record/connection_adapters/cockroachdb/database_tasks.rb +86 -2
- data/lib/active_record/connection_adapters/cockroachdb/quoting.rb +6 -0
- data/lib/active_record/connection_adapters/cockroachdb/referential_integrity.rb +14 -10
- data/lib/active_record/connection_adapters/cockroachdb/schema_statements.rb +43 -15
- data/lib/active_record/connection_adapters/cockroachdb/type.rb +7 -3
- data/lib/active_record/connection_adapters/cockroachdb_adapter.rb +88 -148
- data/lib/active_record/relation/query_methods_ext.rb +127 -0
- data/lib/activerecord-cockroachdb-adapter.rb +1 -1
- data/lib/arel/nodes/join_source_ext.rb +28 -0
- data/lib/version.rb +1 -1
- metadata +12 -5
@@ -2,21 +2,6 @@ module ActiveRecord
|
|
2
2
|
module ConnectionAdapters
|
3
3
|
module CockroachDB
|
4
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
|
-
read_uncommitted: "READ UNCOMMITTED"
|
17
|
-
}
|
18
|
-
end
|
19
|
-
|
20
5
|
# Overridden to avoid using transactions for schema creation.
|
21
6
|
def insert_fixtures_set(fixture_set, tables_to_delete = [])
|
22
7
|
fixture_inserts = build_fixture_statements(fixture_set)
|
@@ -29,74 +14,6 @@ module ActiveRecord
|
|
29
14
|
end
|
30
15
|
end
|
31
16
|
end
|
32
|
-
|
33
|
-
private
|
34
|
-
def execute_batch(statements, name = nil)
|
35
|
-
statements.each do |statement|
|
36
|
-
execute(statement, name)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze
|
41
|
-
private_constant :DEFAULT_INSERT_VALUE
|
42
|
-
|
43
|
-
def default_insert_value(column)
|
44
|
-
DEFAULT_INSERT_VALUE
|
45
|
-
end
|
46
|
-
|
47
|
-
def build_fixture_sql(fixtures, table_name)
|
48
|
-
columns = schema_cache.columns_hash(table_name)
|
49
|
-
|
50
|
-
values_list = fixtures.map do |fixture|
|
51
|
-
fixture = fixture.stringify_keys
|
52
|
-
|
53
|
-
unknown_columns = fixture.keys - columns.keys
|
54
|
-
if unknown_columns.any?
|
55
|
-
raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.)
|
56
|
-
end
|
57
|
-
|
58
|
-
columns.map do |name, column|
|
59
|
-
if fixture.key?(name)
|
60
|
-
type = lookup_cast_type_from_column(column)
|
61
|
-
with_yaml_fallback(type.serialize(fixture[name]))
|
62
|
-
else
|
63
|
-
default_insert_value(column)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
table = Arel::Table.new(table_name)
|
69
|
-
manager = Arel::InsertManager.new
|
70
|
-
manager.into(table)
|
71
|
-
|
72
|
-
if values_list.size == 1
|
73
|
-
values = values_list.shift
|
74
|
-
new_values = []
|
75
|
-
columns.each_key.with_index { |column, i|
|
76
|
-
unless values[i].equal?(DEFAULT_INSERT_VALUE)
|
77
|
-
new_values << values[i]
|
78
|
-
manager.columns << table[column]
|
79
|
-
end
|
80
|
-
}
|
81
|
-
values_list << new_values
|
82
|
-
else
|
83
|
-
columns.each_key { |column| manager.columns << table[column] }
|
84
|
-
end
|
85
|
-
|
86
|
-
manager.values = manager.create_values_list(values_list)
|
87
|
-
manager.to_sql
|
88
|
-
end
|
89
|
-
|
90
|
-
def build_fixture_statements(fixture_set)
|
91
|
-
fixture_set.map do |table_name, fixtures|
|
92
|
-
next if fixtures.empty?
|
93
|
-
build_fixture_sql(fixtures, table_name)
|
94
|
-
end.compact
|
95
|
-
end
|
96
|
-
|
97
|
-
def with_multi_statements
|
98
|
-
yield
|
99
|
-
end
|
100
17
|
end
|
101
18
|
end
|
102
19
|
end
|
@@ -5,12 +5,96 @@ module ActiveRecord
|
|
5
5
|
module CockroachDB
|
6
6
|
class DatabaseTasks < ActiveRecord::Tasks::PostgreSQLDatabaseTasks
|
7
7
|
def structure_dump(filename, extra_flags=nil)
|
8
|
-
|
8
|
+
if extra_flags
|
9
|
+
raise "No flag supported yet, please raise an issue if needed. " \
|
10
|
+
"https://github.com/cockroachdb/activerecord-cockroachdb-adapter/issues/new"
|
11
|
+
end
|
12
|
+
|
13
|
+
# "See https://github.com/cockroachdb/cockroach/issues/26443."
|
14
|
+
search_path =
|
15
|
+
case ActiveRecord.dump_schemas
|
16
|
+
when :schema_search_path
|
17
|
+
configuration_hash[:schema_search_path]
|
18
|
+
when :all
|
19
|
+
nil
|
20
|
+
when String
|
21
|
+
ActiveRecord.dump_schemas
|
22
|
+
end
|
23
|
+
|
24
|
+
conn = ActiveRecord::Base.connection
|
25
|
+
begin
|
26
|
+
old_search_path = conn.schema_search_path
|
27
|
+
conn.schema_search_path = search_path
|
28
|
+
File.open(filename, "w") do |file|
|
29
|
+
%w(SCHEMAS TYPES).each do |object_kind|
|
30
|
+
ActiveRecord::Base.connection.execute("SHOW CREATE ALL #{object_kind}").each_row { file.puts _1 }
|
31
|
+
end
|
32
|
+
|
33
|
+
ignore_tables = ActiveRecord::SchemaDumper.ignore_tables.to_set
|
34
|
+
|
35
|
+
conn.execute("SHOW CREATE ALL TABLES").each_row do |(sql)|
|
36
|
+
if sql.start_with?("CREATE")
|
37
|
+
table_name = sql[/CREATE TABLE (?:.*?\.)?\"?(.*?)[\" ]/, 1]
|
38
|
+
next if ignore_tables.member?(table_name)
|
39
|
+
elsif sql.start_with?("ALTER")
|
40
|
+
table_name = sql[/ALTER TABLE (?:.*?\.)?\"?(.*?)[\" ]/, 1]
|
41
|
+
ref_table_name = sql[/REFERENCES (?:.*?\.)?\"?(.*?)[\" ]/, 1]
|
42
|
+
next if ignore_tables.member?(table_name) || ignore_tables.member?(ref_table_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
file.puts sql
|
46
|
+
end
|
47
|
+
file.puts "SET seach_path TO #{conn.schema_search_path};\n\n"
|
48
|
+
end
|
49
|
+
ensure
|
50
|
+
conn.schema_search_path = old_search_path
|
51
|
+
end
|
9
52
|
end
|
10
53
|
|
11
54
|
def structure_load(filename, extra_flags=nil)
|
12
|
-
|
55
|
+
if extra_flags
|
56
|
+
raise "No flag supported yet, please raise an issue if needed. " \
|
57
|
+
"https://github.com/cockroachdb/activerecord-cockroachdb-adapter/issues/new"
|
58
|
+
end
|
59
|
+
|
60
|
+
run_cmd("cockroach", ["sql", "--set", "errexit=false", "--file", filename], "loading")
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Adapted from https://github.com/rails/rails/blob/a5fc471b3/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb#L106.
|
66
|
+
# Using https://www.cockroachlabs.com/docs/stable/connection-parameters.html#additional-connection-parameters.
|
67
|
+
def cockroach_env
|
68
|
+
usr_pwd = ""
|
69
|
+
if configuration_hash[:username]
|
70
|
+
usr_pwd += configuration_hash[:username].to_s
|
71
|
+
if configuration_hash[:password]
|
72
|
+
usr_pwd += ":"
|
73
|
+
usr_pwd += configuration_hash[:password].to_s
|
74
|
+
end
|
75
|
+
usr_pwd += "@"
|
76
|
+
end
|
77
|
+
|
78
|
+
port = ""
|
79
|
+
port = ":#{configuration_hash[:port]}" if configuration_hash[:port]
|
80
|
+
|
81
|
+
params = %i(sslmode sslrootcert sslcert sslkey).filter_map do |key|
|
82
|
+
"#{key}=#{configuration_hash[key]}" if configuration_hash[key]
|
83
|
+
end.join("&")
|
84
|
+
params = "?#{params}" unless params.empty?
|
85
|
+
|
86
|
+
url = "postgres://#{usr_pwd}#{db_config.host}#{port}/#{db_config.database}#{params}"
|
87
|
+
|
88
|
+
{
|
89
|
+
# NOTE: sslmode in the url will take precedence over this setting, hence
|
90
|
+
# we don't need to conditionally set it.
|
91
|
+
"COCKROACH_INSECURE" => "true",
|
92
|
+
"COCKROACH_URL" => url
|
93
|
+
}
|
13
94
|
end
|
95
|
+
# The `#run_cmd` method use `psql_env` to set environments variables.
|
96
|
+
# We override it with cockroach env variables.
|
97
|
+
alias_method :psql_env, :cockroach_env
|
14
98
|
end
|
15
99
|
end
|
16
100
|
end
|
@@ -19,6 +19,12 @@ module ActiveRecord
|
|
19
19
|
# converting to WKB, so this does it automatically.
|
20
20
|
def quote(value)
|
21
21
|
if value.is_a?(Numeric)
|
22
|
+
# NOTE: The fact that integers are quoted is important and helps
|
23
|
+
# mitigate a potential vulnerability.
|
24
|
+
#
|
25
|
+
# See
|
26
|
+
# - https://nvd.nist.gov/vuln/detail/CVE-2022-44566
|
27
|
+
# - https://github.com/cockroachdb/activerecord-cockroachdb-adapter/pull/280#discussion_r1288692977
|
22
28
|
"'#{quote_string(value.to_s)}'"
|
23
29
|
elsif RGeo::Feature::Geometry.check_type(value)
|
24
30
|
"'#{RGeo::WKRep::WKBGenerator.new(hex_format: true, type_format: :ewkb, emit_ewkb_srid: true).generate(value)}'"
|
@@ -11,6 +11,14 @@ module ActiveRecord
|
|
11
11
|
module ConnectionAdapters
|
12
12
|
module CockroachDB
|
13
13
|
module ReferentialIntegrity
|
14
|
+
# CockroachDB will raise a `PG::ForeignKeyViolation` when re-enabling
|
15
|
+
# referential integrity (e.g: adding a foreign key with invalid data
|
16
|
+
# raises).
|
17
|
+
# So foreign keys should always be valid for that matter.
|
18
|
+
def check_all_foreign_keys_valid!
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
14
22
|
def disable_referential_integrity
|
15
23
|
foreign_keys = tables.map { |table| foreign_keys(table) }.flatten
|
16
24
|
|
@@ -31,16 +39,12 @@ module ActiveRecord
|
|
31
39
|
|
32
40
|
begin
|
33
41
|
foreign_keys.each do |foreign_key|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
else
|
41
|
-
raise error
|
42
|
-
end
|
43
|
-
end
|
42
|
+
# Avoid having PG:DuplicateObject error if a test is ran in transaction.
|
43
|
+
# TODO: verify that there is no cache issue related to running this (e.g: fk
|
44
|
+
# still in cache but not in db)
|
45
|
+
next if foreign_key_exists?(foreign_key.from_table, name: foreign_key.options[:name])
|
46
|
+
|
47
|
+
add_foreign_key(foreign_key.from_table, foreign_key.to_table, **foreign_key.options)
|
44
48
|
end
|
45
49
|
ensure
|
46
50
|
ActiveRecord::Base.table_name_prefix = old_prefix
|
@@ -36,16 +36,25 @@ module ActiveRecord
|
|
36
36
|
# Modified version of the postgresql foreign_keys method.
|
37
37
|
# Replaces t2.oid::regclass::text with t2.relname since this is
|
38
38
|
# more efficient in CockroachDB.
|
39
|
+
# Also, CockroachDB does not append the schema name in relname,
|
40
|
+
# so we append it manually.
|
39
41
|
def foreign_keys(table_name)
|
40
42
|
scope = quoted_scope(table_name)
|
41
43
|
fk_info = exec_query(<<~SQL, "SCHEMA")
|
42
|
-
SELECT
|
44
|
+
SELECT CASE
|
45
|
+
WHEN n2.nspname = current_schema()
|
46
|
+
THEN ''
|
47
|
+
ELSE n2.nspname || '.'
|
48
|
+
END || t2.relname AS to_table,
|
49
|
+
a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred,
|
50
|
+
c.conkey, c.confkey, c.conrelid, c.confrelid
|
43
51
|
FROM pg_constraint c
|
44
52
|
JOIN pg_class t1 ON c.conrelid = t1.oid
|
45
53
|
JOIN pg_class t2 ON c.confrelid = t2.oid
|
46
54
|
JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
|
47
55
|
JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
|
48
56
|
JOIN pg_namespace t3 ON c.connamespace = t3.oid
|
57
|
+
JOIN pg_namespace n2 ON t2.relnamespace = n2.oid
|
49
58
|
WHERE c.contype = 'f'
|
50
59
|
AND t1.relname = #{scope[:name]}
|
51
60
|
AND t3.nspname = #{scope[:schema]}
|
@@ -53,17 +62,31 @@ module ActiveRecord
|
|
53
62
|
SQL
|
54
63
|
|
55
64
|
fk_info.map do |row|
|
65
|
+
to_table = PostgreSQL::Utils.unquote_identifier(row["to_table"])
|
66
|
+
conkey = row["conkey"].scan(/\d+/).map(&:to_i)
|
67
|
+
confkey = row["confkey"].scan(/\d+/).map(&:to_i)
|
68
|
+
|
69
|
+
if conkey.size > 1
|
70
|
+
column = column_names_from_column_numbers(row["conrelid"], conkey)
|
71
|
+
primary_key = column_names_from_column_numbers(row["confrelid"], confkey)
|
72
|
+
else
|
73
|
+
column = PostgreSQL::Utils.unquote_identifier(row["column"])
|
74
|
+
primary_key = row["primary_key"]
|
75
|
+
end
|
76
|
+
|
56
77
|
options = {
|
57
|
-
column:
|
78
|
+
column: column,
|
58
79
|
name: row["name"],
|
59
|
-
primary_key:
|
80
|
+
primary_key: primary_key
|
60
81
|
}
|
61
|
-
|
62
82
|
options[:on_delete] = extract_foreign_key_action(row["on_delete"])
|
63
83
|
options[:on_update] = extract_foreign_key_action(row["on_update"])
|
84
|
+
options[:deferrable] = extract_constraint_deferrable(row["deferrable"], row["deferred"])
|
85
|
+
|
64
86
|
options[:validate] = row["valid"]
|
87
|
+
to_table = PostgreSQL::Utils.unquote_identifier(row["to_table"])
|
65
88
|
|
66
|
-
ForeignKeyDefinition.new(table_name,
|
89
|
+
ForeignKeyDefinition.new(table_name, to_table, options)
|
67
90
|
end
|
68
91
|
end
|
69
92
|
|
@@ -76,21 +99,25 @@ module ActiveRecord
|
|
76
99
|
|
77
100
|
# override
|
78
101
|
# https://github.com/rails/rails/blob/6-0-stable/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L624
|
79
|
-
def new_column_from_field(table_name, field)
|
80
|
-
column_name, type, default, notnull, oid, fmod, collation, comment,
|
102
|
+
def new_column_from_field(table_name, field, _definition)
|
103
|
+
column_name, type, default, notnull, oid, fmod, collation, comment, identity, attgenerated, hidden = field
|
81
104
|
type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i)
|
82
105
|
default_value = extract_value_from_default(default)
|
83
|
-
default_function = extract_default_function(default_value, default)
|
84
106
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
107
|
+
if attgenerated.present?
|
108
|
+
default_function = default
|
109
|
+
else
|
110
|
+
default_function = extract_default_function(default_value, default)
|
111
|
+
end
|
112
|
+
|
113
|
+
if match = default_function&.match(/\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z/)
|
114
|
+
serial = sequence_name_from_parts(table_name, column_name, match[:suffix]) == match[:sequence_name]
|
115
|
+
end
|
89
116
|
|
90
117
|
# {:dimension=>2, :has_m=>false, :has_z=>false, :name=>"latlon", :srid=>0, :type=>"GEOMETRY"}
|
91
118
|
spatial = spatial_column_info(table_name).get(column_name, type_metadata.sql_type)
|
92
119
|
|
93
|
-
|
120
|
+
CockroachDB::Column.new(
|
94
121
|
column_name,
|
95
122
|
default_value,
|
96
123
|
type_metadata,
|
@@ -99,8 +126,9 @@ module ActiveRecord
|
|
99
126
|
collation: collation,
|
100
127
|
comment: comment.presence,
|
101
128
|
serial: serial,
|
129
|
+
identity: identity.presence,
|
102
130
|
spatial: spatial,
|
103
|
-
generated:
|
131
|
+
generated: attgenerated,
|
104
132
|
hidden: hidden
|
105
133
|
)
|
106
134
|
end
|
@@ -112,7 +140,7 @@ module ActiveRecord
|
|
112
140
|
# since type alone is not enough to format the column.
|
113
141
|
# Ex. type_to_sql(:geography, limit: "Point,4326")
|
114
142
|
# => "geography(Point,4326)"
|
115
|
-
#
|
143
|
+
#
|
116
144
|
def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc:
|
117
145
|
sql = \
|
118
146
|
case type.to_s
|
@@ -1,12 +1,16 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
module Type
|
3
|
-
|
3
|
+
module CRDBExt
|
4
4
|
# Return :postgresql instead of :cockroachdb for current_adapter_name so
|
5
5
|
# we can continue using the ActiveRecord::Types defined in
|
6
6
|
# PostgreSQLAdapter.
|
7
|
-
def adapter_name_from(
|
8
|
-
|
7
|
+
def adapter_name_from(model)
|
8
|
+
name = super
|
9
|
+
return :postgresql if name == :cockroachdb
|
10
|
+
|
11
|
+
name
|
9
12
|
end
|
10
13
|
end
|
14
|
+
singleton_class.prepend CRDBExt
|
11
15
|
end
|
12
16
|
end
|