Skip to content

Commit 8b53700

Browse files
authored
improvement: Added sort direction support to custom_indexes fields. (#775)
1 parent 0497dbc commit 8b53700

7 files changed

Lines changed: 149 additions & 28 deletions

File tree

documentation/dsls/DSL-AshPostgres.DataLayer.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ index ["column", "column2"], unique: true, where: "thing = TRUE"
103103

104104
| Name | Type | Default | Docs |
105105
|------|------|---------|------|
106-
| [`fields`](#postgres-custom_indexes-index-fields){: #postgres-custom_indexes-index-fields } | `atom \| String.t \| list(atom \| String.t)` | | The fields to include in the index. |
106+
| [`fields`](#postgres-custom_indexes-index-fields){: #postgres-custom_indexes-index-fields } | `atom \| String.t \| {:asc \| :desc, atom \| String.t} \| list(atom \| String.t \| {:asc \| :desc, atom \| String.t})` | | The fields to include in the index. Each entry can be an atom, string, or a tuple with an order using :desc or :asc and a field. |
107107
### Options
108108

109109
| Name | Type | Default | Docs |

lib/custom_index.ex

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
# SPDX-License-Identifier: MIT
44

55
defmodule AshPostgres.CustomIndex do
6-
@moduledoc "Represents a custom index on the table backing a resource"
6+
@moduledoc """
7+
Represents a custom index on the table backing a resource.
8+
9+
Each entry in `fields` can be an atom, string, or a tuple with an order and a field.
10+
"""
711
@fields [
812
:table,
913
:fields,
@@ -26,8 +30,19 @@ defmodule AshPostgres.CustomIndex do
2630

2731
@schema [
2832
fields: [
29-
type: {:wrap_list, {:or, [:atom, :string]}},
30-
doc: "The fields to include in the index."
33+
type:
34+
{:wrap_list,
35+
{:or,
36+
[
37+
:atom,
38+
:string,
39+
{:tuple, [{:one_of, [:asc, :desc]}, {:or, [:atom, :string]}]}
40+
]}},
41+
doc: """
42+
The fields to include in the index.
43+
44+
Each entry can be an atom, string, or a tuple with an order using :desc or :asc and a field.
45+
"""
3146
],
3247
error_fields: [
3348
type: {:list, :atom},
@@ -83,6 +98,41 @@ defmodule AshPostgres.CustomIndex do
8398

8499
def schema, do: @schema
85100

101+
def column_name(field) when is_atom(field) or is_binary(field), do: field
102+
103+
def column_name({order, field})
104+
when order in [:asc, :desc] and (is_atom(field) or is_binary(field)),
105+
do: field
106+
107+
def field_to_snapshot(field) when is_atom(field), do: %{type: "atom", value: field}
108+
def field_to_snapshot(field) when is_binary(field), do: %{type: "string", value: field}
109+
110+
def field_to_snapshot({order, field})
111+
when order in [:asc, :desc] and (is_atom(field) or is_binary(field)) do
112+
%{
113+
type: "directed",
114+
order: to_string(order),
115+
value: if(is_atom(field), do: to_string(field), else: field)
116+
}
117+
end
118+
119+
def field_comparison_key(field) when is_atom(field), do: to_string(field)
120+
def field_comparison_key(field) when is_binary(field), do: field
121+
122+
def field_comparison_key({order, field})
123+
when order in [:asc, :desc] and (is_atom(field) or is_binary(field)) do
124+
"#{order}:#{field}"
125+
end
126+
127+
@doc false
128+
def field_for_migration(field) when is_atom(field), do: inspect(field)
129+
def field_for_migration(field) when is_binary(field), do: inspect(field)
130+
131+
def field_for_migration({order, field})
132+
when order in [:asc, :desc] and (is_atom(field) or is_binary(field)) do
133+
"#{order}: #{inspect(field)}"
134+
end
135+
86136
def transform(index) do
87137
with {:ok, index} <- set_name(index) do
88138
set_error_fields(index)
@@ -99,11 +149,13 @@ defmodule AshPostgres.CustomIndex do
99149
index
100150
| error_fields:
101151
Enum.flat_map(index.fields, fn field ->
102-
if Regex.match?(~r/^[0-9a-zA-Z_]+$/, to_string(field)) do
103-
if is_binary(field) do
104-
[String.to_atom(field)]
152+
column = column_name(field)
153+
154+
if Regex.match?(~r/^[0-9a-zA-Z_]+$/, to_string(column)) do
155+
if is_binary(column) do
156+
[String.to_atom(column)]
105157
else
106-
[field]
158+
[column]
107159
end
108160
else
109161
[]
@@ -125,7 +177,8 @@ defmodule AshPostgres.CustomIndex do
125177

126178
mismatched_field =
127179
Enum.find(index.fields, fn field ->
128-
!Regex.match?(~r/^[0-9a-zA-Z_]+$/, to_string(field))
180+
column = column_name(field)
181+
!Regex.match?(~r/^[0-9a-zA-Z_]+$/, to_string(column))
129182
end) ->
130183
{:error,
131184
"""
@@ -149,7 +202,7 @@ defmodule AshPostgres.CustomIndex do
149202

150203
# sobelow_skip ["DOS.StringToAtom"]
151204
def name(table, %{fields: fields}) do
152-
[table, fields, "index"]
205+
[table, Enum.map(fields, &column_name/1), "index"]
153206
|> List.flatten()
154207
|> Enum.map(&to_string(&1))
155208
|> Enum.map(&String.replace(&1, ~r"[^\w_]", "_"))

lib/data_layer.ex

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3729,10 +3729,9 @@ defmodule AshPostgres.DataLayer do
37293729
fields -> fields
37303730
end
37313731
else
3732-
case Enum.filter(index.fields, &is_atom/1) do
3733-
[] -> pkey
3734-
fields -> fields
3735-
end
3732+
index.fields
3733+
|> Enum.map(&AshPostgres.CustomIndex.column_name/1)
3734+
|> Enum.uniq()
37363735
end
37373736

37383737
Ecto.Changeset.unique_constraint(changeset, fields, opts)

lib/migration_generator/migration_generator.ex

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2790,7 +2790,7 @@ defmodule AshPostgres.MigrationGenerator do
27902790
defp custom_index_comparison_key(index, snapshot) do
27912791
index
27922792
|> Map.update!(:fields, fn fields ->
2793-
Enum.map(fields, &to_string/1)
2793+
Enum.map(fields, &AshPostgres.CustomIndex.field_comparison_key/1)
27942794
end)
27952795
|> add_custom_index_name(snapshot.table)
27962796
|> Map.put(:where, {snapshot.base_filter, index.where})
@@ -4597,10 +4597,7 @@ defmodule AshPostgres.MigrationGenerator do
45974597
|> Map.update!(:custom_indexes, fn indexes ->
45984598
Enum.map(indexes, fn index ->
45994599
fields =
4600-
Enum.map(index.fields, fn
4601-
field when is_atom(field) -> %{type: "atom", value: field}
4602-
field when is_binary(field) -> %{type: "string", value: field}
4603-
end)
4600+
Enum.map(index.fields, &AshPostgres.CustomIndex.field_to_snapshot/1)
46044601

46054602
%{index | fields: fields}
46064603
|> Map.delete(:__spark_metadata__)
@@ -4708,9 +4705,17 @@ defmodule AshPostgres.MigrationGenerator do
47084705
custom_index
47094706
|> Map.update(:fields, [], fn fields ->
47104707
Enum.map(fields, fn
4711-
%{type: "atom", value: field} -> maybe_to_atom(field)
4712-
%{type: "string", value: field} -> field
4713-
field -> field
4708+
%{type: "atom", value: field} ->
4709+
maybe_to_atom(field)
4710+
4711+
%{type: "string", value: field} ->
4712+
field
4713+
4714+
%{type: "directed", order: order, value: value} ->
4715+
{maybe_to_atom(order), maybe_to_atom(value)}
4716+
4717+
field ->
4718+
field
47144719
end)
47154720
end)
47164721
|> Map.put_new(:include, [])

lib/migration_generator/operation.ex

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,7 +1098,10 @@ defmodule AshPostgres.MigrationGenerator.Operation do
10981098
base_filter: base_filter,
10991099
multitenancy: multitenancy
11001100
}) do
1101-
keys = index_keys(index.fields, index.all_tenants?, multitenancy)
1101+
keys =
1102+
index.fields
1103+
|> index_keys(index.all_tenants?, multitenancy)
1104+
|> Enum.map(&AshPostgres.CustomIndex.field_for_migration/1)
11021105

11031106
index =
11041107
case {index.where, base_filter} do
@@ -1120,26 +1123,33 @@ defmodule AshPostgres.MigrationGenerator.Operation do
11201123
option(:prefix, schema)
11211124
])
11221125

1126+
columns = Enum.join(keys, ", ")
1127+
11231128
if opts == "" do
1124-
"create index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}])"
1129+
"create index(:#{as_atom(table)}, [#{columns}])"
11251130
else
1126-
"create index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{opts})"
1131+
"create index(:#{as_atom(table)}, [#{columns}], #{opts})"
11271132
end
11281133
end
11291134

11301135
def down(%{schema: schema, index: index, table: table, multitenancy: multitenancy}) do
1131-
keys = index_keys(index.fields, index.all_tenants?, multitenancy)
1136+
keys =
1137+
index.fields
1138+
|> index_keys(index.all_tenants?, multitenancy)
1139+
|> Enum.map(&AshPostgres.CustomIndex.field_for_migration/1)
11321140

11331141
opts =
11341142
join([
11351143
option(:name, index.name),
11361144
option(:prefix, schema)
11371145
])
11381146

1147+
columns = Enum.join(keys, ", ")
1148+
11391149
if opts == "" do
1140-
"drop_if_exists index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}])"
1150+
"drop_if_exists index(:#{as_atom(table)}, [#{columns}])"
11411151
else
1142-
"drop_if_exists index(:#{as_atom(table)}, [#{Enum.map_join(keys, ", ", &inspect/1)}], #{opts})"
1152+
"drop_if_exists index(:#{as_atom(table)}, [#{columns}], #{opts})"
11431153
end
11441154
end
11451155
end

test/custom_index_test.exs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,15 @@ defmodule AshPostgres.Test.CustomIndexTest do
2929
|> Ash.create!()
3030
end
3131
end
32+
33+
test "directed custom index fields populate error_fields" do
34+
{:ok, index} =
35+
AshPostgres.CustomIndex.transform(%AshPostgres.CustomIndex{
36+
fields: [:tenant_id, {:desc, :occurred_at}],
37+
unique: true,
38+
name: "events_tenant_id_occurred_at_index"
39+
})
40+
41+
assert index.error_fields == [:tenant_id, :occurred_at]
42+
end
3243
end

test/migration_generator_test.exs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,49 @@ defmodule AshPostgres.MigrationGeneratorTest do
819819
end
820820
end
821821

822+
describe "custom_indexes with sort direction" do
823+
setup %{snapshot_path: snapshot_path, migration_path: migration_path} do
824+
:ok
825+
826+
defposts do
827+
postgres do
828+
custom_indexes do
829+
index([desc: :title], name: "posts_title_desc_index")
830+
index([:id, desc: :title], name: "posts_id_title_desc_index")
831+
end
832+
end
833+
834+
attributes do
835+
uuid_primary_key(:id)
836+
attribute(:title, :string, public?: true)
837+
end
838+
end
839+
840+
defdomain([Post])
841+
842+
AshPostgres.MigrationGenerator.generate(Domain,
843+
snapshot_path: snapshot_path,
844+
migration_path: migration_path,
845+
quiet: true,
846+
format: false,
847+
auto_name: true
848+
)
849+
end
850+
851+
test "it generates create index with sort direction", %{migration_path: migration_path} do
852+
assert [custom_index_migration] =
853+
Enum.sort(Path.wildcard("#{migration_path}/**/*_migrate_resources*.exs"))
854+
|> Enum.reject(&String.contains?(&1, "extensions"))
855+
856+
file = File.read!(custom_index_migration)
857+
858+
assert file =~ ~S/create index(:posts, [desc: :title], name: "posts_title_desc_index")/
859+
860+
assert file =~
861+
~S/create index(:posts, [:id, desc: :title], name: "posts_id_title_desc_index")/
862+
end
863+
end
864+
822865
describe "custom_indexes with follow up migrations" do
823866
setup %{snapshot_path: snapshot_path, migration_path: migration_path} do
824867
:ok

0 commit comments

Comments
 (0)