Support NULLS NOT DISTINCT in Postgres 15+
This commit is contained in:
parent
f936b93d07
commit
10bad051a8
@ -1,4 +1,12 @@
|
||||
* Support decrypting data encrypted non-deterministically with a SHA1 hash digest.
|
||||
* Fully support `NULLS [NOT] DISTINCT` for PostgreSQL 15+ indexes
|
||||
|
||||
Previous work was done to allow the index to be created in a migration, but it was not
|
||||
supported in schema.rb. Additionally, the matching for `NULLS [NOT] DISTINCT` was not
|
||||
in the correct order, which could have resulted in inconsistent schema detection.
|
||||
|
||||
*Gregory Jones*
|
||||
|
||||
* Support decrypting data encrypted non-deterministically with a SHA1 hash digest.
|
||||
|
||||
This adds a new Active Record encryption option to support decrypting data encrypted
|
||||
non-deterministically with a SHA1 hash digest:
|
||||
|
@ -17,6 +17,7 @@ def accept(o)
|
||||
:options_include_default?, :supports_indexes_in_create?, :use_foreign_keys?,
|
||||
:quoted_columns_for_index, :supports_partial_index?, :supports_check_constraints?,
|
||||
:supports_index_include?, :supports_exclusion_constraints?, :supports_unique_keys?,
|
||||
:supports_nulls_not_distinct?,
|
||||
to: :@conn, private: true
|
||||
|
||||
private
|
||||
@ -110,6 +111,7 @@ def visit_CreateIndexDefinition(o)
|
||||
sql << "USING #{index.using}" if supports_index_using? && index.using
|
||||
sql << "(#{quoted_columns(index)})"
|
||||
sql << "INCLUDE (#{quoted_include_columns(index.include)})" if supports_index_include? && index.include
|
||||
sql << "NULLS NOT DISTINCT" if supports_nulls_not_distinct? && index.nulls_not_distinct
|
||||
sql << "WHERE #{index.where}" if supports_partial_index? && index.where
|
||||
|
||||
sql.join(" ")
|
||||
|
@ -7,7 +7,7 @@ module ConnectionAdapters # :nodoc:
|
||||
# this type are typically created and returned by methods in database
|
||||
# adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes
|
||||
class IndexDefinition # :nodoc:
|
||||
attr_reader :table, :name, :unique, :columns, :lengths, :orders, :opclasses, :where, :type, :using, :include, :comment, :valid
|
||||
attr_reader :table, :name, :unique, :columns, :lengths, :orders, :opclasses, :where, :type, :using, :include, :nulls_not_distinct, :comment, :valid
|
||||
|
||||
def initialize(
|
||||
table, name,
|
||||
@ -20,6 +20,7 @@ def initialize(
|
||||
type: nil,
|
||||
using: nil,
|
||||
include: nil,
|
||||
nulls_not_distinct: nil,
|
||||
comment: nil,
|
||||
valid: true
|
||||
)
|
||||
@ -34,6 +35,7 @@ def initialize(
|
||||
@type = type
|
||||
@using = using
|
||||
@include = include
|
||||
@nulls_not_distinct = nulls_not_distinct
|
||||
@comment = comment
|
||||
@valid = valid
|
||||
end
|
||||
@ -50,13 +52,14 @@ def column_options
|
||||
}
|
||||
end
|
||||
|
||||
def defined_for?(columns = nil, name: nil, unique: nil, valid: nil, include: nil, **options)
|
||||
def defined_for?(columns = nil, name: nil, unique: nil, valid: nil, include: nil, nulls_not_distinct: nil, **options)
|
||||
columns = options[:column] if columns.blank?
|
||||
(columns.nil? || Array(self.columns) == Array(columns).map(&:to_s)) &&
|
||||
(name.nil? || self.name == name.to_s) &&
|
||||
(unique.nil? || self.unique == unique) &&
|
||||
(valid.nil? || self.valid == valid) &&
|
||||
(include.nil? || Array(self.include) == Array(include).map(&:to_s))
|
||||
(include.nil? || Array(self.include) == Array(include).map(&:to_s) &&
|
||||
(nulls_not_distinct.nil? || self.nulls_not_distinct == nulls_not_distinct))
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -1402,7 +1402,7 @@ def update_table_definition(table_name, base) # :nodoc:
|
||||
end
|
||||
|
||||
def add_index_options(table_name, column_name, name: nil, if_not_exists: false, internal: false, **options) # :nodoc:
|
||||
options.assert_valid_keys(:unique, :length, :order, :opclass, :where, :type, :using, :comment, :algorithm, :include)
|
||||
options.assert_valid_keys(:unique, :length, :order, :opclass, :where, :type, :using, :comment, :algorithm, :include, :nulls_not_distinct)
|
||||
|
||||
column_names = index_column_names(column_name)
|
||||
|
||||
@ -1422,6 +1422,7 @@ def add_index_options(table_name, column_name, name: nil, if_not_exists: false,
|
||||
type: options[:type],
|
||||
using: options[:using],
|
||||
include: options[:include],
|
||||
nulls_not_distinct: options[:nulls_not_distinct],
|
||||
comment: options[:comment]
|
||||
)
|
||||
|
||||
|
@ -580,6 +580,10 @@ def supports_concurrent_connections?
|
||||
true
|
||||
end
|
||||
|
||||
def supports_nulls_not_distinct?
|
||||
false
|
||||
end
|
||||
|
||||
def return_value_after_insert?(column) # :nodoc:
|
||||
column.auto_incremented_by_db?
|
||||
end
|
||||
|
@ -108,7 +108,7 @@ def indexes(table_name) # :nodoc:
|
||||
oid = row[4]
|
||||
comment = row[5]
|
||||
valid = row[6]
|
||||
using, expressions, include, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: NULLS(?: NOT)? DISTINCT)?(?: INCLUDE \((.+?)\))?(?: WHERE (.+))?\z/m).flatten
|
||||
using, expressions, include, nulls_not_distinct, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: INCLUDE \((.+?)\))?( NULLS NOT DISTINCT)?(?: WHERE (.+))?\z/m).flatten
|
||||
|
||||
orders = {}
|
||||
opclasses = {}
|
||||
@ -149,6 +149,7 @@ def indexes(table_name) # :nodoc:
|
||||
where: where,
|
||||
using: using.to_sym,
|
||||
include: include_columns.presence,
|
||||
nulls_not_distinct: nulls_not_distinct.present?,
|
||||
comment: comment.presence,
|
||||
valid: valid
|
||||
)
|
||||
|
@ -277,6 +277,10 @@ def supports_virtual_columns?
|
||||
database_version >= 12_00_00 # >= 12.0
|
||||
end
|
||||
|
||||
def supports_nulls_not_distinct?
|
||||
database_version >= 15_00_00 # >= 15.0
|
||||
end
|
||||
|
||||
def index_algorithms
|
||||
{ concurrently: "CONCURRENTLY" }
|
||||
end
|
||||
|
@ -249,6 +249,7 @@ def index_parts(index)
|
||||
index_parts << "where: #{index.where.inspect}" if index.where
|
||||
index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index)
|
||||
index_parts << "include: #{index.include.inspect}" if index.include
|
||||
index_parts << "nulls_not_distinct: #{index.nulls_not_distinct.inspect}" if index.nulls_not_distinct
|
||||
index_parts << "type: #{index.type.inspect}" if index.type
|
||||
index_parts << "comment: #{index.comment.inspect}" if index.comment
|
||||
index_parts
|
||||
|
@ -73,6 +73,9 @@ def test_add_index
|
||||
expected = %(CREATE INDEX IF NOT EXISTS "index_people_on_last_name" ON "people" ("last_name"))
|
||||
assert_equal expected, add_index(:people, :last_name, if_not_exists: true)
|
||||
|
||||
expected = %(CREATE INDEX "index_people_on_last_name" ON "people" ("last_name") NULLS NOT DISTINCT)
|
||||
assert_equal expected, add_index(:people, :last_name, nulls_not_distinct: true)
|
||||
|
||||
assert_raise ArgumentError do
|
||||
add_index(:people, :last_name, algorithm: :copy)
|
||||
end
|
||||
|
@ -375,7 +375,7 @@ def test_invalid_index
|
||||
end
|
||||
|
||||
def test_index_with_not_distinct_nulls
|
||||
skip if ActiveRecord::Base.connection.database_version < 15_00_00
|
||||
skip unless supports_nulls_not_distinct?
|
||||
|
||||
with_example_table do
|
||||
@connection.execute(<<~SQL)
|
||||
|
@ -780,3 +780,46 @@ def test_schema_dumps_index_included_columns
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class SchemaIndexNullsNotDistinctTest < ActiveRecord::PostgreSQLTestCase
|
||||
include SchemaDumpingHelper
|
||||
|
||||
setup do
|
||||
@connection = ActiveRecord::Base.connection
|
||||
@connection.create_table "trains" do |t|
|
||||
t.string :name
|
||||
end
|
||||
end
|
||||
|
||||
teardown do
|
||||
@connection.drop_table "trains", if_exists: true
|
||||
end
|
||||
|
||||
def test_nulls_not_distinct_is_dumped
|
||||
if supports_nulls_not_distinct?
|
||||
@connection.execute "CREATE INDEX trains_name ON trains USING btree(name) NULLS NOT DISTINCT"
|
||||
|
||||
output = dump_table_schema "trains"
|
||||
|
||||
assert_match(/nulls_not_distinct: true/, output)
|
||||
end
|
||||
end
|
||||
|
||||
def test_nulls_distinct_is_dumped
|
||||
if supports_nulls_not_distinct?
|
||||
@connection.execute "CREATE INDEX trains_name ON trains USING btree(name) NULLS DISTINCT"
|
||||
|
||||
output = dump_table_schema "trains"
|
||||
|
||||
assert_no_match(/nulls_not_distinct/, output)
|
||||
end
|
||||
end
|
||||
|
||||
def test_nulls_not_set_is_dumped
|
||||
@connection.execute "CREATE INDEX trains_name ON trains USING btree(name)"
|
||||
|
||||
output = dump_table_schema "trains"
|
||||
|
||||
assert_no_match(/nulls_not_distinct/, output)
|
||||
end
|
||||
end
|
||||
|
@ -20,7 +20,7 @@ def invalid_add_column_option_exception_message(key)
|
||||
end
|
||||
|
||||
def invalid_add_index_option_exception_message(key)
|
||||
"Unknown key: :#{key}. Valid keys are: :unique, :length, :order, :opclass, :where, :type, :using, :comment, :algorithm, :include"
|
||||
"Unknown key: :#{key}. Valid keys are: :unique, :length, :order, :opclass, :where, :type, :using, :comment, :algorithm, :include, :nulls_not_distinct"
|
||||
end
|
||||
|
||||
def invalid_create_table_option_exception_message(key)
|
||||
|
@ -55,6 +55,7 @@ def supports_text_column_with_default?
|
||||
supports_insert_conflict_target?
|
||||
supports_optimizer_hints?
|
||||
supports_datetime_with_precision?
|
||||
supports_nulls_not_distinct?
|
||||
].each do |method_name|
|
||||
define_method method_name do
|
||||
ActiveRecord::Base.connection.public_send(method_name)
|
||||
|
Loading…
Reference in New Issue
Block a user