Support NULLS NOT DISTINCT in Postgres 15+

This commit is contained in:
Gregory Jones 2023-06-28 21:50:12 -04:00
parent f936b93d07
commit 10bad051a8
13 changed files with 79 additions and 8 deletions

@ -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)