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.
@@ -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
- raise "db:structure:dump is unimplemented. See https://github.com/cockroachdb/activerecord-cockroachdb-adapter/issues/2"
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
- raise "db:structure:load is unimplemented. See https://github.com/cockroachdb/activerecord-cockroachdb-adapter/issues/2"
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
- begin
35
- add_foreign_key(foreign_key.from_table, foreign_key.to_table, **foreign_key.options)
36
- rescue ActiveRecord::StatementInvalid => error
37
- if error.cause.class == PG::DuplicateObject
38
- # This error is safe to ignore because the yielded caller
39
- # already re-added the foreign key constraint.
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 t2.relname AS to_table, 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
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: row["column"],
78
+ column: column,
58
79
  name: row["name"],
59
- primary_key: row["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, row["to_table"], options)
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, generated, hidden = field
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
- serial =
86
- if (match = default_function&.match(/\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z/))
87
- sequence_name_from_parts(table_name, column_name, match[:suffix]) == match[:sequence_name]
88
- end
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
- PostgreSQL::Column.new(
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: 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
- class << self
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(_model)
8
- :postgresql
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