@@ -366,6 +366,7 @@ defmodule Ecto.Migration do
366366 concurrently: false ,
367367 using: nil ,
368368 include: [ ] ,
369+ nulls_distinct: nil ,
369370 where: nil ,
370371 comment: nil ,
371372 options: nil
@@ -379,6 +380,7 @@ defmodule Ecto.Migration do
379380 concurrently: boolean ,
380381 using: atom | String . t ,
381382 include: [ atom | String . t ] ,
383+ nulls_distinct: boolean | nil ,
382384 where: atom | String . t ,
383385 comment: String . t | nil ,
384386 options: String . t
@@ -705,6 +707,12 @@ defmodule Ecto.Migration do
705707 * `:include` - specify fields for a covering index. This is not supported
706708 by all databases. For more information on PostgreSQL support, please
707709 [read the official docs](https://www.postgresql.org/docs/current/indexes-index-only-scans.html).
710+ * `:nulls_distinct` - specify whether null values should be considered
711+ distinct for a unique index. Defaults to `nil`, which will not add the
712+ parameter to the generated SQL and thus use the database default.
713+ This option is currently only supported by PostgreSQL 15+.
714+ For MySQL, it is always false. For MSSQL, it is always true.
715+ See the dedicated section on this option for more information.
708716 * `:comment` - adds a comment to the index.
709717
710718 ## Adding/dropping indexes concurrently
@@ -763,6 +771,29 @@ defmodule Ecto.Migration do
763771 More information on partial indexes can be found in the [PostgreSQL
764772 docs](http://www.postgresql.org/docs/current/indexes-partial.html).
765773
774+ ## The `:nulls_distinct` option
775+
776+ A unique index does not prevent multiple null values by default in most databases.
777+
778+ For example, imagine we have a "products" table and need to guarantee that
779+ sku's are unique within their category, but the category is optional.
780+ Creating a regular unique index over the sku and category_id fields with:
781+
782+ create index("products", [:sku, :category_id], unique: true)
783+
784+ will allow products with the same sku to be inserted if their category_id is `nil`.
785+ The `:nulls_distinct` option can be used to change this behavior by considering
786+ null values as equal, i.e. not distinct:
787+
788+ create index("products", [:sku, :category_id], unique: true, nulls_distinct: false)
789+
790+ This option is currently only supported by PostgreSQL 15+.
791+ As a workaround for older PostgreSQL versions and other databases, an
792+ additional partial unique index for the sku can be created:
793+
794+ create index("products", [:sku, :category_id], unique: true)
795+ create index("products", [:sku], unique: true, where: "category_id IS NULL")
796+
766797 ## Examples
767798
768799 # With no name provided, the name of the below index defaults to
@@ -1343,6 +1374,10 @@ defmodule Ecto.Migration do
13431374 end
13441375
13451376 defp validate_index_opts! ( opts ) when is_list ( opts ) do
1377+ if opts [ :nulls_distinct ] != nil and opts [ :unique ] != true do
1378+ raise ArgumentError , "the `nulls_distinct` option can only be used with unique indexes"
1379+ end
1380+
13461381 case Keyword . get_values ( opts , :where ) do
13471382 [ _ , _ | _ ] ->
13481383 raise ArgumentError ,
0 commit comments