Skip to content

Commit dfc4e74

Browse files
authored
improvement: Update MigrationGenerator to use uuidv7 when available (#674)
1 parent 10e595d commit dfc4e74

File tree

5 files changed

+171
-2
lines changed

5 files changed

+171
-2
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<!--
2+
SPDX-FileCopyrightText: 2020 Zach Daniel
3+
4+
SPDX-License-Identifier: MIT
5+
-->
6+
7+
# Using Postgres 18
8+
9+
Postgres 18 adds built-in functions for working with version 7 (time-ordered) UUIDs.
10+
11+
`AshPostgres` adds a `uuid_generate_v7` function to the database as part of the initial migration.
12+
This Ash function is normally used as the default for `Ash.Type.UUIDv7` types.
13+
14+
## Using Postgres 18's Built-In `uuidv7`
15+
To take advantage of Postgres 18's built-in `uuidv7` function, you need to update your `Repo.min_pg_version/0` to a `Version` with `major` at 18 or above.
16+
17+
```elixir
18+
def min_pg_version do
19+
%Version{major: 18, minor: 0, patch: 0}
20+
end
21+
```
22+
23+
Then when you run `ash.codegen`, migrations will be added to update the defaults for your `UUIDv7` type attributes.
24+
25+
```sh
26+
$ mix ash.codegen update_uuid_v7_default
27+
```
28+
29+
Giving you migrations like so
30+
```elixir
31+
def up do
32+
alter table(:items) do
33+
modify(:id, :uuid, default: fragment("uuidv7()"))
34+
end
35+
end
36+
37+
def down do
38+
alter table(:items) do
39+
modify(:id, :uuid, default: fragment("uuid_generate_v7()"))
40+
end
41+
end
42+
```
43+
44+
The Ash function `uuid_generate_v7` won't be automatically removed for you, but you can add a migration to remove this on your own.
45+
```elixir
46+
47+
defmodule SomeProject.Repo.Migrations.RemoveAshUuidv7 do
48+
use Ecto.Migration
49+
50+
def up do
51+
execute("DROP FUNCTION IF EXISTS uuid_generate_v7")
52+
end
53+
54+
def down do
55+
# copied from initialize_extensions_1 migration
56+
execute("""
57+
CREATE OR REPLACE FUNCTION uuid_generate_v7()
58+
RETURNS UUID
59+
AS $$
60+
DECLARE
61+
timestamp TIMESTAMPTZ;
62+
microseconds INT;
63+
BEGIN
64+
timestamp = clock_timestamp();
65+
microseconds = (cast(extract(microseconds FROM timestamp)::INT - (floor(extract(milliseconds FROM timestamp))::INT * 1000) AS DOUBLE PRECISION) * 4.096)::INT;
66+
67+
RETURN encode(
68+
set_byte(
69+
set_byte(
70+
overlay(uuid_send(gen_random_uuid()) placing substring(int8send(floor(extract(epoch FROM timestamp) * 1000)::BIGINT) FROM 3) FROM 1 FOR 6
71+
),
72+
6, (b'0111' || (microseconds >> 8)::bit(4))::bit(8)::int
73+
),
74+
7, microseconds::bit(8)::int
75+
),
76+
'hex')::UUID;
77+
END
78+
$$
79+
LANGUAGE PLPGSQL
80+
SET search_path = ''
81+
VOLATILE;
82+
""")
83+
end
84+
end
85+
```
86+
87+
## Opt Out of Changing Defaults
88+
89+
If you want to upgrade your `min_pg_version` *without* changing the default for `UUIDv7`s, you can override the `use_builtin_uuidv7_function?` callback in your `Repo` module.
90+
91+
```elixir
92+
defmodule SomeProject.Repo do
93+
use AshPostgres.Repo, otp_app: :some_project
94+
95+
def min_pg_version do
96+
%Version{major: 18, minor: 0, patch: 0}
97+
end
98+
99+
def use_builtin_uuidv7_function?, do: false
100+
101+
...
102+
end
103+
```

lib/migration_generator/migration_generator.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3500,13 +3500,16 @@ defmodule AshPostgres.MigrationGenerator do
35003500

35013501
@uuid_functions [&Ash.UUID.generate/0, &Ecto.UUID.generate/0]
35023502

3503-
defp default(%{name: name, default: default, type: type}, resource, _repo)
3503+
defp default(%{name: name, default: default, type: type}, resource, repo)
35043504
when is_function(default) do
35053505
configured_default(resource, name) ||
35063506
cond do
35073507
default in @uuid_functions ->
35083508
~S[fragment("gen_random_uuid()")]
35093509

3510+
default == (&Ash.UUIDv7.generate/0) and repo.use_builtin_uuidv7_function?() ->
3511+
~S[fragment("uuidv7()")]
3512+
35103513
default == (&Ash.UUIDv7.generate/0) ->
35113514
~S[fragment("uuid_generate_v7()")]
35123515

lib/repo.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ defmodule AshPostgres.Repo do
106106

107107
@doc "Allows overriding a given migration type for *all* fields, for example if you wanted to always use :timestamptz for :utc_datetime fields"
108108
@callback override_migration_type(atom) :: atom
109+
@doc "Whether or not to use the built-in `uuidv7` function (as opposed to the Ash `uuid_generate_v7` function) for `UUIDv7` fields."
110+
@callback use_builtin_uuidv7_function? :: boolean
109111
@doc "Should the repo should be created by `mix ash_postgres.create`?"
110112
@callback create?() :: boolean
111113
@doc "Should the repo should be dropped by `mix ash_postgres.drop`?"
@@ -152,6 +154,12 @@ defmodule AshPostgres.Repo do
152154
def create_schemas_in_migrations?, do: true
153155
def default_prefix, do: "public"
154156
def override_migration_type(type), do: type
157+
158+
def use_builtin_uuidv7_function? do
159+
%Version{major: major} = min_pg_version()
160+
major >= 18
161+
end
162+
155163
def create?, do: true
156164
def drop?, do: true
157165
def disable_atomic_actions?, do: false
@@ -328,7 +336,8 @@ defmodule AshPostgres.Repo do
328336
drop?: 0,
329337
disable_atomic_actions?: 0,
330338
disable_expr_error?: 0,
331-
immutable_expr_error?: 0
339+
immutable_expr_error?: 0,
340+
use_builtin_uuidv7_function?: 0
332341

333342
# We do this switch because `!@warn_on_missing_ash_functions` in the function body triggers
334343
# a dialyzer error

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ defmodule AshPostgres.MixProject do
112112
"documentation/topics/development/migrations-and-tasks.md",
113113
"documentation/topics/development/testing.md",
114114
"documentation/topics/development/upgrading-to-2.0.md",
115+
"documentation/topics/development/upgrading-to-postgres-18.md",
115116
"documentation/topics/advanced/expressions.md",
116117
"documentation/topics/advanced/manual-relationships.md",
117118
"documentation/topics/advanced/partitioned-tables.md",

test/migration_generator_test.exs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,59 @@ defmodule AshPostgres.MigrationGeneratorTest do
286286
end
287287
end
288288

289+
describe "creating initial snapshots with native uuidv7 on PG 18" do
290+
setup do
291+
prev_pg_version_env = System.fetch_env("PG_VERSION")
292+
System.put_env("PG_VERSION", "18")
293+
294+
on_exit(fn ->
295+
File.rm_rf!("test_snapshots_path")
296+
File.rm_rf!("test_migration_path")
297+
298+
case prev_pg_version_env do
299+
# there was a previous env var set, restore it
300+
{:ok, value} -> System.put_env("PG_VERSION", value)
301+
# there was nothing set, delete what we set
302+
:error -> System.delete_env("PG_VERSION")
303+
end
304+
end)
305+
306+
defposts do
307+
attributes do
308+
uuid_v7_primary_key(:id)
309+
end
310+
end
311+
312+
defdomain([Post])
313+
314+
AshPostgres.MigrationGenerator.generate(Domain,
315+
snapshot_path: "test_snapshots_path",
316+
migration_path: "test_migration_path",
317+
quiet: true,
318+
format: false,
319+
auto_name: true
320+
)
321+
322+
:ok
323+
end
324+
325+
test "the migration uses the native uuidv7 function" do
326+
# the snapshot exists and contains valid json
327+
assert File.read!(Path.wildcard("test_snapshots_path/test_repo/posts/*.json"))
328+
|> Jason.decode!(keys: :atoms!)
329+
330+
assert [file] =
331+
Path.wildcard("test_migration_path/**/*_migrate_resources*.exs")
332+
|> Enum.reject(&String.contains?(&1, "extensions"))
333+
334+
file_contents = File.read!(file)
335+
336+
# the migration adds the id using the native uuidv7 function
337+
assert file_contents =~
338+
~S[add :id, :uuid, null: false, default: fragment("uuidv7()"), primary_key: true]
339+
end
340+
end
341+
289342
describe "creating initial snapshots for resources with a schema" do
290343
setup do
291344
on_exit(fn ->

0 commit comments

Comments
 (0)