improvement: add support for :uuid_v7 type (#333)

This commit is contained in:
Alessio Montagnani 2024-07-10 01:57:38 +02:00 committed by GitHub
parent edb24ef923
commit 72b029be21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 141 additions and 8 deletions

View file

@ -1,5 +1,5 @@
defmodule AshPostgres.MigrationGenerator.AshFunctions do
@latest_version 3
@latest_version 4
def latest_version, do: @latest_version
@ -66,6 +66,8 @@ defmodule AshPostgres.MigrationGenerator.AshFunctions do
\"\"\")
#{ash_raise_error()}
#{uuid_generate_v7()}
"""
end
@ -89,6 +91,8 @@ defmodule AshPostgres.MigrationGenerator.AshFunctions do
#{ash_raise_error()}
#{uuid_generate_v7()}
execute(\"\"\"
CREATE OR REPLACE FUNCTION ash_trim_whitespace(arr text[])
RETURNS text[] AS $$
@ -117,27 +121,47 @@ defmodule AshPostgres.MigrationGenerator.AshFunctions do
end
def install(1) do
ash_raise_error()
"""
#{ash_raise_error()}
#{uuid_generate_v7()}
"""
end
def install(2) do
ash_raise_error()
"""
#{ash_raise_error()}
#{uuid_generate_v7()}
"""
end
def install(3) do
uuid_generate_v7()
end
def drop(3) do
"execute(\"DROP FUNCTION IF EXISTS uuid_generate_v7(), timestamp_from_uuid_v7(uuid)\")"
end
def drop(2) do
ash_raise_error(false)
"""
#{ash_raise_error()}
"execute(\"DROP FUNCTION IF EXISTS uuid_generate_v7(), timestamp_from_uuid_v7(uuid)\")"
"""
end
def drop(1) do
"execute(\"DROP FUNCTION IF EXISTS ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE)\")"
"execute(\"DROP FUNCTION IF EXISTS uuid_generate_v7(), timestamp_from_uuid_v7(uuid), ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE)\")"
end
def drop(0) do
"execute(\"DROP FUNCTION IF EXISTS ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE), ash_trim_whitespace(text[])\")"
"execute(\"DROP FUNCTION IF EXISTS uuid_generate_v7(), timestamp_from_uuid_v7(uuid), ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE), ash_trim_whitespace(text[])\")"
end
def drop(nil) do
"execute(\"DROP FUNCTION IF EXISTS ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE), ash_elixir_and(BOOLEAN, ANYCOMPATIBLE), ash_elixir_and(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(BOOLEAN, ANYCOMPATIBLE), ash_trim_whitespace(text[])\")"
"execute(\"DROP FUNCTION IF EXISTS uuid_generate_v7(), timestamp_from_uuid_v7(uuid), ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE), ash_elixir_and(BOOLEAN, ANYCOMPATIBLE), ash_elixir_and(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(BOOLEAN, ANYCOMPATIBLE), ash_trim_whitespace(text[])\")"
end
defp ash_raise_error(prefix? \\ true) do
@ -174,4 +198,45 @@ defmodule AshPostgres.MigrationGenerator.AshFunctions do
\"\"\")
"""
end
defp uuid_generate_v7 do
"""
execute(\"\"\"
CREATE OR REPLACE FUNCTION uuid_generate_v7()
RETURNS UUID
AS $$
DECLARE
timestamp TIMESTAMPTZ;
microseconds INT;
BEGIN
timestamp = clock_timestamp();
microseconds = (cast(extract(microseconds FROM timestamp)::INT - (floor(extract(milliseconds FROM timestamp))::INT * 1000) AS DOUBLE PRECISION) * 4.096)::INT;
RETURN encode(
set_byte(
set_byte(
overlay(uuid_send(gen_random_uuid()) placing substring(int8send(floor(extract(epoch FROM timestamp) * 1000)::BIGINT) FROM 3) FROM 1 FOR 6
),
6, (b'0111' || (microseconds >> 8)::bit(4))::bit(8)::int
),
7, microseconds::bit(8)::int
),
'hex')::UUID;
END
$$
LANGUAGE PLPGSQL
VOLATILE;
\"\"\")
execute(\"\"\"
CREATE OR REPLACE FUNCTION timestamp_from_uuid_v7(_uuid uuid)
RETURNS TIMESTAMP WITHOUT TIME ZONE
AS $$
SELECT to_timestamp(('x0000' || substr(_uuid::TEXT, 1, 8) || substr(_uuid::TEXT, 10, 4))::BIT(64)::BIGINT::NUMERIC / 1000);
$$
LANGUAGE SQL
IMMUTABLE PARALLEL SAFE STRICT LEAKPROOF;
\"\"\")
"""
end
end

View file

@ -2848,6 +2848,7 @@ defmodule AshPostgres.MigrationGenerator do
defp migration_type(Ash.Type.CiString, _), do: :citext
defp migration_type(Ash.Type.UUID, _), do: :uuid
defp migration_type(Ash.Type.UUIDv7, _), do: :uuid
defp migration_type(Ash.Type.Integer, _), do: :bigint
defp migration_type(other, constraints) do
@ -2938,6 +2939,9 @@ defmodule AshPostgres.MigrationGenerator do
default in @uuid_functions ->
~S[fragment("gen_random_uuid()")]
default == (&Ash.UUIDv7.generate/0) ->
~S[fragment("uuid_generate_v7()")]
default == (&DateTime.utc_now/0) ->
~S[fragment("(now() AT TIME ZONE 'utc')")]

View file

@ -6,5 +6,5 @@
"citext",
"demo-functions_v1"
],
"ash_functions_version": 3
"ash_functions_version": 4
}

View file

@ -0,0 +1,54 @@
defmodule AshPostgres.TestRepo.Migrations.InstallAshFunctionsExtension420240622192713 do
@moduledoc """
Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
execute("""
CREATE OR REPLACE FUNCTION uuid_generate_v7()
RETURNS UUID
AS $$
DECLARE
timestamp TIMESTAMPTZ;
microseconds INT;
BEGIN
timestamp = clock_timestamp();
microseconds = (cast(extract(microseconds FROM timestamp)::INT - (floor(extract(milliseconds FROM timestamp))::INT * 1000) AS DOUBLE PRECISION) * 4.096)::INT;
RETURN encode(
set_byte(
set_byte(
overlay(uuid_send(gen_random_uuid()) placing substring(int8send(floor(extract(epoch FROM timestamp) * 1000)::BIGINT) FROM 3) FROM 1 FOR 6
),
6, (b'0111' || (microseconds >> 8)::bit(4))::bit(8)::int
),
7, microseconds::bit(8)::int
),
'hex')::UUID;
END
$$
LANGUAGE PLPGSQL
VOLATILE;
""")
execute("""
CREATE OR REPLACE FUNCTION timestamp_from_uuid_v7(_uuid uuid)
RETURNS TIMESTAMP WITHOUT TIME ZONE
AS $$
SELECT to_timestamp(('x0000' || substr(_uuid::TEXT, 1, 8) || substr(_uuid::TEXT, 10, 4))::BIT(64)::BIGINT::NUMERIC / 1000);
$$
LANGUAGE SQL
IMMUTABLE PARALLEL SAFE STRICT LEAKPROOF;
""")
end
def down do
# Uncomment this if you actually want to uninstall the extensions
# when this migration is rolled back:
execute("DROP FUNCTION IF EXISTS uuid_generate_v7(), timestamp_from_uuid_v7(uuid)")
end
end

View file

@ -97,6 +97,7 @@ defmodule AshPostgres.MigrationGeneratorTest do
attributes do
uuid_primary_key(:id)
uuid_v7_primary_key(:other_id)
attribute(:title, :string, public?: true)
attribute(:second_title, :string, public?: true)
attribute(:title_with_source, :string, source: :t_w_s, public?: true)
@ -141,6 +142,10 @@ defmodule AshPostgres.MigrationGeneratorTest do
assert file_contents =~
~S[add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true]
# the migration adds the other_id, with its default
assert file_contents =~
~S[add :other_id, :uuid, null: false, default: fragment("uuid_generate_v7()"), primary_key: true]
# the migration adds the id, with its default
assert file_contents =~
~S[add :title_with_default, :text, default: "fred"]
@ -195,6 +200,7 @@ defmodule AshPostgres.MigrationGeneratorTest do
attributes do
uuid_primary_key(:id)
uuid_v7_primary_key(:other_id)
attribute(:title, :string, public?: true)
attribute(:second_title, :string, public?: true)
end
@ -247,6 +253,10 @@ defmodule AshPostgres.MigrationGeneratorTest do
assert file_contents =~
~S[add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true]
# the migration adds the other_id, with its default
assert file_contents =~
~S[add :other_id, :uuid, null: false, default: fragment("uuid_generate_v7()"), primary_key: true]
# the migration adds other attributes
assert file_contents =~ ~S[add :title, :text]