mirror of
https://github.com/ash-project/ash.git
synced 2024-09-19 13:03:02 +12:00
improvement: bring uuid version 7 into the core (#1253)
This commit is contained in:
parent
e17fa8a1c5
commit
7f2e7d3ec1
12 changed files with 537 additions and 1 deletions
|
@ -211,6 +211,8 @@ spark_locals_without_parens = [
|
||||||
upsert_identity: 1,
|
upsert_identity: 1,
|
||||||
uuid_primary_key: 1,
|
uuid_primary_key: 1,
|
||||||
uuid_primary_key: 2,
|
uuid_primary_key: 2,
|
||||||
|
uuid_v7_primary_key: 1,
|
||||||
|
uuid_v7_primary_key: 2,
|
||||||
validate: 1,
|
validate: 1,
|
||||||
validate: 2,
|
validate: 2,
|
||||||
validate_destination_attribute?: 1,
|
validate_destination_attribute?: 1,
|
||||||
|
|
12
benchmarks/uuids.exs
Normal file
12
benchmarks/uuids.exs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
Benchee.run(
|
||||||
|
%{
|
||||||
|
"uuid_v7 raw" => fn ->
|
||||||
|
Ash.UUIDv7.bingenerate()
|
||||||
|
end,
|
||||||
|
"uuid_v7 string" => fn ->
|
||||||
|
Ash.UUIDv7.generate()
|
||||||
|
end,
|
||||||
|
"uuid_v4 string" => fn ->
|
||||||
|
Ash.UUID.generate()
|
||||||
|
end
|
||||||
|
})
|
|
@ -15,6 +15,7 @@ A section for declaring attributes on the resource.
|
||||||
* [update_timestamp](#attributes-update_timestamp)
|
* [update_timestamp](#attributes-update_timestamp)
|
||||||
* [integer_primary_key](#attributes-integer_primary_key)
|
* [integer_primary_key](#attributes-integer_primary_key)
|
||||||
* [uuid_primary_key](#attributes-uuid_primary_key)
|
* [uuid_primary_key](#attributes-uuid_primary_key)
|
||||||
|
* [uuid_v7_primary_key](#attributes-uuid_v7_primary_key)
|
||||||
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
@ -349,6 +350,66 @@ uuid_primary_key :id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Introspection
|
||||||
|
|
||||||
|
Target: `Ash.Resource.Attribute`
|
||||||
|
|
||||||
|
## attributes.uuid_v7_primary_key
|
||||||
|
```elixir
|
||||||
|
uuid_v7_primary_key name
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Declares a non writable, non-nil, primary key column of type `uuid_v7`, which defaults to `Ash.UUIDv7.generate/0`.
|
||||||
|
|
||||||
|
Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets
|
||||||
|
the following different defaults:
|
||||||
|
|
||||||
|
writable? false
|
||||||
|
public? true
|
||||||
|
default &Ash.UUIDv7.generate/0
|
||||||
|
primary_key? true
|
||||||
|
type :uuid_v7
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```
|
||||||
|
uuid_v7_primary_key :id
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
| Name | Type | Default | Docs |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| [`name`](#attributes-uuid_v7_primary_key-name){: #attributes-uuid_v7_primary_key-name .spark-required} | `atom` | | The name of the attribute. |
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Name | Type | Default | Docs |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| [`type`](#attributes-uuid_v7_primary_key-type){: #attributes-uuid_v7_primary_key-type } | `module` | `:uuid_v7` | The type of the attribute. See `Ash.Type` for more. |
|
||||||
|
| [`constraints`](#attributes-uuid_v7_primary_key-constraints){: #attributes-uuid_v7_primary_key-constraints } | `keyword` | | Constraints to provide to the type when casting the value. For more, see `Ash.Type`. |
|
||||||
|
| [`description`](#attributes-uuid_v7_primary_key-description){: #attributes-uuid_v7_primary_key-description } | `String.t` | | An optional description for the attribute. |
|
||||||
|
| [`sensitive?`](#attributes-uuid_v7_primary_key-sensitive?){: #attributes-uuid_v7_primary_key-sensitive? } | `boolean` | `false` | Whether or not the attribute value contains sensitive information, like PII. See the [Sensitive Data guide](/documentation/topics/security/sensitive-data.md) for more. |
|
||||||
|
| [`source`](#attributes-uuid_v7_primary_key-source){: #attributes-uuid_v7_primary_key-source } | `atom` | | If the field should be mapped to a different name in the data layer. Support varies by data layer. |
|
||||||
|
| [`always_select?`](#attributes-uuid_v7_primary_key-always_select?){: #attributes-uuid_v7_primary_key-always_select? } | `boolean` | `false` | Whether or not to ensure this attribute is always selected when reading from the database, regardless of applied select statements. |
|
||||||
|
| [`primary_key?`](#attributes-uuid_v7_primary_key-primary_key?){: #attributes-uuid_v7_primary_key-primary_key? } | `boolean` | `true` | Whether the attribute is the primary key. Composite primary key is also possible by using `primary_key? true` in more than one attribute. If primary_key? is true, allow_nil? must be false. |
|
||||||
|
| [`generated?`](#attributes-uuid_v7_primary_key-generated?){: #attributes-uuid_v7_primary_key-generated? } | `boolean` | `false` | Whether or not the value may be generated by the data layer. |
|
||||||
|
| [`writable?`](#attributes-uuid_v7_primary_key-writable?){: #attributes-uuid_v7_primary_key-writable? } | `boolean` | `false` | Whether or not the value can be written to. Non-writable attributes can still be written with `Ash.Changeset.force_change_attribute/3`. |
|
||||||
|
| [`public?`](#attributes-uuid_v7_primary_key-public?){: #attributes-uuid_v7_primary_key-public? } | `boolean` | `true` | Whether or not the attribute should be shown over public interfaces. See the [sensitive data guide](/documentation/topics/security/sensitive-data.md) for more. |
|
||||||
|
| [`default`](#attributes-uuid_v7_primary_key-default){: #attributes-uuid_v7_primary_key-default } | `(-> any) \| mfa \| any` | `&Ash.UUIDv7.generate/0` | A value to be set on all creates, unless a value is being provided already. Note: The default value is casted according to the type's Ash.Type.* module, before it is saved. For `:string`, for example, if `constraints: [allow_empty?: _]` is false, the value `""` will be cast to `nil`. See the `:constraints` option, the `:allow_nil?` option, and the relevant `Ash.Type.*` documentation. |
|
||||||
|
| [`update_default`](#attributes-uuid_v7_primary_key-update_default){: #attributes-uuid_v7_primary_key-update_default } | `(-> any) \| mfa \| any` | | A value to be set on all updates, unless a value is being provided already. |
|
||||||
|
| [`filterable?`](#attributes-uuid_v7_primary_key-filterable?){: #attributes-uuid_v7_primary_key-filterable? } | `boolean \| :simple_equality` | `true` | Whether or not the attribute can be referenced in filters. |
|
||||||
|
| [`sortable?`](#attributes-uuid_v7_primary_key-sortable?){: #attributes-uuid_v7_primary_key-sortable? } | `boolean` | `true` | Whether or not the attribute can be referenced in sorts. |
|
||||||
|
| [`match_other_defaults?`](#attributes-uuid_v7_primary_key-match_other_defaults?){: #attributes-uuid_v7_primary_key-match_other_defaults? } | `boolean` | `false` | Ensures that other attributes that use the same "lazy" default (a function or an mfa), use the same default value. Has no effect unless `default` is a zero argument function. |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Introspection
|
### Introspection
|
||||||
|
|
||||||
Target: `Ash.Resource.Attribute`
|
Target: `Ash.Resource.Attribute`
|
||||||
|
|
|
@ -183,6 +183,18 @@ defmodule Ash.Resource.Attribute do
|
||||||
|> Keyword.delete(:allow_nil?)
|
|> Keyword.delete(:allow_nil?)
|
||||||
|> Ash.OptionsHelpers.hide_all_except([:name])
|
|> Ash.OptionsHelpers.hide_all_except([:name])
|
||||||
|
|
||||||
|
@uuid_v7_primary_key_schema @schema
|
||||||
|
|> Spark.Options.Helpers.set_default!(:public?, true)
|
||||||
|
|> Spark.Options.Helpers.set_default!(:writable?, false)
|
||||||
|
|> Spark.Options.Helpers.set_default!(
|
||||||
|
:default,
|
||||||
|
&Ash.UUIDv7.generate/0
|
||||||
|
)
|
||||||
|
|> Spark.Options.Helpers.set_default!(:primary_key?, true)
|
||||||
|
|> Spark.Options.Helpers.set_default!(:type, :uuid_v7)
|
||||||
|
|> Keyword.delete(:allow_nil?)
|
||||||
|
|> Ash.OptionsHelpers.hide_all_except([:name])
|
||||||
|
|
||||||
@integer_primary_key_schema @schema
|
@integer_primary_key_schema @schema
|
||||||
|> Spark.Options.Helpers.set_default!(:public?, true)
|
|> Spark.Options.Helpers.set_default!(:public?, true)
|
||||||
|> Spark.Options.Helpers.set_default!(:writable?, false)
|
|> Spark.Options.Helpers.set_default!(:writable?, false)
|
||||||
|
@ -201,5 +213,6 @@ defmodule Ash.Resource.Attribute do
|
||||||
def create_timestamp_schema, do: @create_timestamp_schema
|
def create_timestamp_schema, do: @create_timestamp_schema
|
||||||
def update_timestamp_schema, do: @update_timestamp_schema
|
def update_timestamp_schema, do: @update_timestamp_schema
|
||||||
def uuid_primary_key_schema, do: @uuid_primary_key_schema
|
def uuid_primary_key_schema, do: @uuid_primary_key_schema
|
||||||
|
def uuid_v7_primary_key_schema, do: @uuid_v7_primary_key_schema
|
||||||
def integer_primary_key_schema, do: @integer_primary_key_schema
|
def integer_primary_key_schema, do: @integer_primary_key_schema
|
||||||
end
|
end
|
||||||
|
|
|
@ -144,6 +144,30 @@ defmodule Ash.Resource.Dsl do
|
||||||
transform: {Ash.Resource.Attribute, :transform, []}
|
transform: {Ash.Resource.Attribute, :transform, []}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@uuid_v7_primary_key %Spark.Dsl.Entity{
|
||||||
|
name: :uuid_v7_primary_key,
|
||||||
|
describe: """
|
||||||
|
Declares a non writable, non-nil, primary key column of type `uuid_v7`, which defaults to `Ash.UUIDv7.generate/0`.
|
||||||
|
|
||||||
|
Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets
|
||||||
|
the following different defaults:
|
||||||
|
|
||||||
|
writable? false
|
||||||
|
public? true
|
||||||
|
default &Ash.UUIDv7.generate/0
|
||||||
|
primary_key? true
|
||||||
|
type :uuid_v7
|
||||||
|
""",
|
||||||
|
examples: [
|
||||||
|
"uuid_v7_primary_key :id"
|
||||||
|
],
|
||||||
|
args: [:name],
|
||||||
|
target: Ash.Resource.Attribute,
|
||||||
|
schema: Ash.Resource.Attribute.uuid_v7_primary_key_schema(),
|
||||||
|
auto_set_fields: [allow_nil?: false],
|
||||||
|
transform: {Ash.Resource.Attribute, :transform, []}
|
||||||
|
}
|
||||||
|
|
||||||
@attributes %Spark.Dsl.Section{
|
@attributes %Spark.Dsl.Section{
|
||||||
name: :attributes,
|
name: :attributes,
|
||||||
describe: """
|
describe: """
|
||||||
|
@ -187,7 +211,8 @@ defmodule Ash.Resource.Dsl do
|
||||||
@create_timestamp,
|
@create_timestamp,
|
||||||
@update_timestamp,
|
@update_timestamp,
|
||||||
@integer_primary_key,
|
@integer_primary_key,
|
||||||
@uuid_primary_key
|
@uuid_primary_key,
|
||||||
|
@uuid_v7_primary_key
|
||||||
],
|
],
|
||||||
patchable?: true
|
patchable?: true
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ defmodule Ash.Type do
|
||||||
boolean: Ash.Type.Boolean,
|
boolean: Ash.Type.Boolean,
|
||||||
struct: Ash.Type.Struct,
|
struct: Ash.Type.Struct,
|
||||||
uuid: Ash.Type.UUID,
|
uuid: Ash.Type.UUID,
|
||||||
|
uuid_v7: Ash.Type.UUIDv7,
|
||||||
binary: Ash.Type.Binary,
|
binary: Ash.Type.Binary,
|
||||||
date: Ash.Type.Date,
|
date: Ash.Type.Date,
|
||||||
time: Ash.Type.Time,
|
time: Ash.Type.Time,
|
||||||
|
|
96
lib/ash/type/uuid_v7.ex
Normal file
96
lib/ash/type/uuid_v7.ex
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
defmodule Ash.Type.UUIDv7 do
|
||||||
|
@moduledoc """
|
||||||
|
Represents a UUID.
|
||||||
|
|
||||||
|
A builtin type that can be referenced via `:uuid_v7`
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ash.Type
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def storage_type(_), do: :uuid
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def generator(_constraints) do
|
||||||
|
StreamData.repeatedly(&Ash.UUIDv7.generate/0)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def matches_type?(value, constraints) do
|
||||||
|
case cast_input(value, constraints) do
|
||||||
|
{:ok, _} -> true
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def cast_input(nil, _), do: {:ok, nil}
|
||||||
|
|
||||||
|
def cast_input(
|
||||||
|
<<a1, a2, a3, a4, a5, a6, a7, a8, ?-, b1, b2, b3, b4, ?-, c1, c2, c3, c4, ?-, d1, d2, d3,
|
||||||
|
d4, ?-, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
<<c(a1), c(a2), c(a3), c(a4), c(a5), c(a6), c(a7), c(a8), ?-, c(b1), c(b2), c(b3), c(b4), ?-,
|
||||||
|
c(c1), c(c2), c(c3), c(c4), ?-, c(d1), c(d2), c(d3), c(d4), ?-, c(e1), c(e2), c(e3), c(e4),
|
||||||
|
c(e5), c(e6), c(e7), c(e8), c(e9), c(e10), c(e11), c(e12)>>
|
||||||
|
catch
|
||||||
|
:error -> :error
|
||||||
|
else
|
||||||
|
hex_uuid -> {:ok, hex_uuid}
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_input(<<_::128>> = value, _), do: {:ok, Ash.UUIDv7.encode(value)}
|
||||||
|
def cast_input(_, _), do: :error
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def cast_atomic(new_value, _constraints), do: {:atomic, new_value}
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def cast_stored(nil, _), do: {:ok, nil}
|
||||||
|
def cast_stored(value, constraints), do: cast_input(value, constraints)
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def dump_to_native(nil, _), do: {:ok, nil}
|
||||||
|
|
||||||
|
def dump_to_native(value, _) do
|
||||||
|
case Ash.UUIDv7.decode(value) do
|
||||||
|
:error -> :error
|
||||||
|
value -> {:ok, value}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def dump_to_embedded(value, constraints) do
|
||||||
|
cast_input(value, constraints)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def equal?(term1, term2), do: Ash.UUIDv7.decode(term1) == Ash.UUIDv7.decode(term2)
|
||||||
|
|
||||||
|
@compile {:inline, c: 1}
|
||||||
|
|
||||||
|
defp c(?0), do: ?0
|
||||||
|
defp c(?1), do: ?1
|
||||||
|
defp c(?2), do: ?2
|
||||||
|
defp c(?3), do: ?3
|
||||||
|
defp c(?4), do: ?4
|
||||||
|
defp c(?5), do: ?5
|
||||||
|
defp c(?6), do: ?6
|
||||||
|
defp c(?7), do: ?7
|
||||||
|
defp c(?8), do: ?8
|
||||||
|
defp c(?9), do: ?9
|
||||||
|
defp c(?A), do: ?a
|
||||||
|
defp c(?B), do: ?b
|
||||||
|
defp c(?C), do: ?c
|
||||||
|
defp c(?D), do: ?d
|
||||||
|
defp c(?E), do: ?e
|
||||||
|
defp c(?F), do: ?f
|
||||||
|
defp c(?a), do: ?a
|
||||||
|
defp c(?b), do: ?b
|
||||||
|
defp c(?c), do: ?c
|
||||||
|
defp c(?d), do: ?d
|
||||||
|
defp c(?e), do: ?e
|
||||||
|
defp c(?f), do: ?f
|
||||||
|
defp c(_), do: throw(:error)
|
||||||
|
end
|
194
lib/ash/uuid_v7.ex
Normal file
194
lib/ash/uuid_v7.ex
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
defmodule Ash.UUIDv7 do
|
||||||
|
@moduledoc """
|
||||||
|
Helpers for working with UUIDs version 7.
|
||||||
|
|
||||||
|
[RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-7)
|
||||||
|
|
||||||
|
Used for generating UUIDs version 7 with increased clock precision for better monotonicity,
|
||||||
|
as described by method 3 of the [Section 6.2](https://www.rfc-editor.org/rfc/rfc9562#name-monotonicity-and-counters
|
||||||
|
|
||||||
|
Inspired by the work of [Ryan Winchester](https://github.com/ryanwinchester/) on [uuidv7](https://github.com/ryanwinchester/uuidv7)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> UUIDv7.generate()
|
||||||
|
"018e90d8-06e8-7f9f-bfd7-6730ba98a51b"
|
||||||
|
|
||||||
|
iex> UUIDv7.bingenerate()
|
||||||
|
<<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@typedoc """
|
||||||
|
A hex-encoded UUID string.
|
||||||
|
"""
|
||||||
|
@type t :: <<_::288>>
|
||||||
|
|
||||||
|
@typedoc """
|
||||||
|
A raw binary representation of a UUID.
|
||||||
|
"""
|
||||||
|
@type raw :: <<_::128>>
|
||||||
|
|
||||||
|
@version 7
|
||||||
|
@variant 2
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generates a version 7 UUID using submilliseconds for increased clock precision.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> UUIDv7.generate()
|
||||||
|
"018e90d8-06e8-7f9f-bfd7-6730ba98a51b"
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec generate() :: t
|
||||||
|
def generate, do: bingenerate() |> encode()
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generates a version 7 UUID in the binary format.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> UUIDv7.bingenerate()
|
||||||
|
<<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>>
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec bingenerate() :: raw
|
||||||
|
def bingenerate do
|
||||||
|
timestamp_nanoseconds = System.system_time(:nanosecond)
|
||||||
|
timestamp_milliseconds = trunc(timestamp_nanoseconds / 1_000_000)
|
||||||
|
|
||||||
|
nanoseconds = timestamp_nanoseconds - timestamp_milliseconds * 1_000_000
|
||||||
|
rand_a = trunc(4096 * (nanoseconds / 1_000_000))
|
||||||
|
|
||||||
|
<<rand_b::62, _::2>> = :crypto.strong_rand_bytes(8)
|
||||||
|
|
||||||
|
<<
|
||||||
|
timestamp_milliseconds::big-unsigned-48,
|
||||||
|
@version::4,
|
||||||
|
rand_a::12,
|
||||||
|
@variant::2,
|
||||||
|
rand_b::62
|
||||||
|
>>
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Extract the millisecond timestamp from the UUID.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> UUIDv7.extract_timestamp("018ecb40-c457-73e6-a400-000398daddd9")
|
||||||
|
1712807003223
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec extract_timestamp(t | raw) :: integer
|
||||||
|
def extract_timestamp(<<timestamp_ms::big-unsigned-48, @version::4, _::76>>), do: timestamp_ms
|
||||||
|
|
||||||
|
def extract_timestamp(<<_::288>> = uuid) do
|
||||||
|
uuid |> decode() |> extract_timestamp()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Encode a raw UUID to the string representation.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> UUIDv7.encode(<<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>>)
|
||||||
|
"018e90d8-06e8-7f9f-bfd7-6730ba98a51b"
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec encode(t | raw) :: t | :error
|
||||||
|
def encode(
|
||||||
|
<<_, _, _, _, _, _, _, _, ?-, _, _, _, _, ?-, _, _, _, _, ?-, _, _, _, _, ?-, _, _, _, _,
|
||||||
|
_, _, _, _, _, _, _, _>> = hex_uuid
|
||||||
|
),
|
||||||
|
do: hex_uuid
|
||||||
|
|
||||||
|
def encode(
|
||||||
|
<<a1::4, a2::4, a3::4, a4::4, a5::4, a6::4, a7::4, a8::4, b1::4, b2::4, b3::4, b4::4,
|
||||||
|
c1::4, c2::4, c3::4, c4::4, d1::4, d2::4, d3::4, d4::4, e1::4, e2::4, e3::4, e4::4,
|
||||||
|
e5::4, e6::4, e7::4, e8::4, e9::4, e10::4, e11::4, e12::4>>
|
||||||
|
) do
|
||||||
|
<<e(a1), e(a2), e(a3), e(a4), e(a5), e(a6), e(a7), e(a8), ?-, e(b1), e(b2), e(b3), e(b4), ?-,
|
||||||
|
e(c1), e(c2), e(c3), e(c4), ?-, e(d1), e(d2), e(d3), e(d4), ?-, e(e1), e(e2), e(e3), e(e4),
|
||||||
|
e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12)>>
|
||||||
|
end
|
||||||
|
|
||||||
|
def encode(_), do: :error
|
||||||
|
|
||||||
|
@compile {:inline, e: 1}
|
||||||
|
|
||||||
|
defp e(0), do: ?0
|
||||||
|
defp e(1), do: ?1
|
||||||
|
defp e(2), do: ?2
|
||||||
|
defp e(3), do: ?3
|
||||||
|
defp e(4), do: ?4
|
||||||
|
defp e(5), do: ?5
|
||||||
|
defp e(6), do: ?6
|
||||||
|
defp e(7), do: ?7
|
||||||
|
defp e(8), do: ?8
|
||||||
|
defp e(9), do: ?9
|
||||||
|
defp e(10), do: ?a
|
||||||
|
defp e(11), do: ?b
|
||||||
|
defp e(12), do: ?c
|
||||||
|
defp e(13), do: ?d
|
||||||
|
defp e(14), do: ?e
|
||||||
|
defp e(15), do: ?f
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Decode a string representation of a UUID to the raw binary version.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> UUIDv7.decode("018e90d8-06e8-7f9f-bfd7-6730ba98a51b")
|
||||||
|
<<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>>
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec decode(t | raw) :: raw | :error
|
||||||
|
def decode(
|
||||||
|
<<_::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4,
|
||||||
|
_::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4,
|
||||||
|
_::4, _::4, _::4, _::4>> = raw_uuid
|
||||||
|
),
|
||||||
|
do: raw_uuid
|
||||||
|
|
||||||
|
def decode(
|
||||||
|
<<a1, a2, a3, a4, a5, a6, a7, a8, ?-, b1, b2, b3, b4, ?-, c1, c2, c3, c4, ?-, d1, d2, d3,
|
||||||
|
d4, ?-, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12>>
|
||||||
|
) do
|
||||||
|
<<d(a1)::4, d(a2)::4, d(a3)::4, d(a4)::4, d(a5)::4, d(a6)::4, d(a7)::4, d(a8)::4, d(b1)::4,
|
||||||
|
d(b2)::4, d(b3)::4, d(b4)::4, d(c1)::4, d(c2)::4, d(c3)::4, d(c4)::4, d(d1)::4, d(d2)::4,
|
||||||
|
d(d3)::4, d(d4)::4, d(e1)::4, d(e2)::4, d(e3)::4, d(e4)::4, d(e5)::4, d(e6)::4, d(e7)::4,
|
||||||
|
d(e8)::4, d(e9)::4, d(e10)::4, d(e11)::4, d(e12)::4>>
|
||||||
|
catch
|
||||||
|
:error -> :error
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode(_), do: :error
|
||||||
|
|
||||||
|
@compile {:inline, d: 1}
|
||||||
|
|
||||||
|
defp d(?0), do: 0
|
||||||
|
defp d(?1), do: 1
|
||||||
|
defp d(?2), do: 2
|
||||||
|
defp d(?3), do: 3
|
||||||
|
defp d(?4), do: 4
|
||||||
|
defp d(?5), do: 5
|
||||||
|
defp d(?6), do: 6
|
||||||
|
defp d(?7), do: 7
|
||||||
|
defp d(?8), do: 8
|
||||||
|
defp d(?9), do: 9
|
||||||
|
defp d(?A), do: 10
|
||||||
|
defp d(?B), do: 11
|
||||||
|
defp d(?C), do: 12
|
||||||
|
defp d(?D), do: 13
|
||||||
|
defp d(?E), do: 14
|
||||||
|
defp d(?F), do: 15
|
||||||
|
defp d(?a), do: 10
|
||||||
|
defp d(?b), do: 11
|
||||||
|
defp d(?c), do: 12
|
||||||
|
defp d(?d), do: 13
|
||||||
|
defp d(?e), do: 14
|
||||||
|
defp d(?f), do: 15
|
||||||
|
defp d(_), do: throw(:error)
|
||||||
|
end
|
1
mix.exs
1
mix.exs
|
@ -230,6 +230,7 @@ defmodule Ash.MixProject do
|
||||||
Ash.Vector,
|
Ash.Vector,
|
||||||
Ash.Union,
|
Ash.Union,
|
||||||
Ash.UUID,
|
Ash.UUID,
|
||||||
|
Ash.UUIDv7,
|
||||||
Ash.NotLoaded,
|
Ash.NotLoaded,
|
||||||
Ash.ForbiddenField,
|
Ash.ForbiddenField,
|
||||||
Ash.Changeset.ManagedRelationshipHelpers,
|
Ash.Changeset.ManagedRelationshipHelpers,
|
||||||
|
|
|
@ -51,6 +51,39 @@ defmodule Ash.Test.Resource.AttributesTest do
|
||||||
|
|
||||||
assert nil == Ash.Resource.Info.public_attribute(Post, :bar)
|
assert nil == Ash.Resource.Info.public_attribute(Post, :bar)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "uuid_v7 attributes are persisted on the resource properly" do
|
||||||
|
defposts do
|
||||||
|
attributes do
|
||||||
|
uuid_v7_primary_key :other_id
|
||||||
|
attribute :another_id, :uuid_v7
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert [
|
||||||
|
_,
|
||||||
|
%Attribute{
|
||||||
|
name: :other_id,
|
||||||
|
primary_key?: true,
|
||||||
|
public?: true,
|
||||||
|
sortable?: true,
|
||||||
|
type: Ash.Type.UUIDv7,
|
||||||
|
writable?: false
|
||||||
|
},
|
||||||
|
%Attribute{
|
||||||
|
name: :another_id,
|
||||||
|
primary_key?: false,
|
||||||
|
public?: false,
|
||||||
|
sortable?: true,
|
||||||
|
type: Ash.Type.UUIDv7,
|
||||||
|
writable?: true
|
||||||
|
}
|
||||||
|
] = Ash.Resource.Info.attributes(Post)
|
||||||
|
|
||||||
|
assert [_, %Attribute{name: :other_id}] = Ash.Resource.Info.public_attributes(Post)
|
||||||
|
|
||||||
|
assert %Attribute{name: :other_id} = Ash.Resource.Info.attribute(Post, :other_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "validation" do
|
describe "validation" do
|
||||||
|
|
38
test/type/uuid_v7_test.exs
Normal file
38
test/type/uuid_v7_test.exs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
defmodule Ash.Test.Type.UUIDv7Test do
|
||||||
|
@moduledoc false
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
test "it define the type correctly" do
|
||||||
|
assert :uuid = Ash.Type.storage_type(Ash.Type.UUIDv7)
|
||||||
|
assert true == Ash.Type.ash_type?(Ash.Type.UUIDv7)
|
||||||
|
assert true == Ash.Type.builtin?(Ash.Type.UUIDv7)
|
||||||
|
assert Ash.Type.UUIDv7.EctoType = Ash.Type.ecto_type(Ash.Type.UUIDv7)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it works" do
|
||||||
|
hex_uuid = "0188aadc-f449-7818-8862-5eff12733f64"
|
||||||
|
raw_uuid = Ash.UUIDv7.decode(hex_uuid)
|
||||||
|
|
||||||
|
assert {:ok, ^hex_uuid} = Ash.Type.cast_input(Ash.Type.UUIDv7, hex_uuid)
|
||||||
|
assert {:ok, ^hex_uuid} = Ash.Type.cast_input(Ash.Type.UUIDv7, raw_uuid)
|
||||||
|
|
||||||
|
assert {:ok, ^hex_uuid} = Ash.Type.cast_stored(Ash.Type.UUIDv7, hex_uuid)
|
||||||
|
assert {:ok, ^hex_uuid} = Ash.Type.cast_stored(Ash.Type.UUIDv7, raw_uuid)
|
||||||
|
|
||||||
|
assert {:ok, ^raw_uuid} = Ash.Type.dump_to_native(Ash.Type.UUIDv7, hex_uuid)
|
||||||
|
assert {:ok, ^raw_uuid} = Ash.Type.dump_to_native(Ash.Type.UUIDv7, raw_uuid)
|
||||||
|
|
||||||
|
assert true == Ash.Type.equal?(Ash.Type.UUIDv7, raw_uuid, hex_uuid)
|
||||||
|
|
||||||
|
assert {:ok, ^raw_uuid} = Ash.Type.apply_constraints(Ash.Type.UUIDv7, raw_uuid, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it casts binary UUIDs version 7 to string" do
|
||||||
|
uuid_v7 = "01903fa1-2523-7580-a9d6-84620dcbf2ba"
|
||||||
|
|
||||||
|
assert %StreamData{} = Ash.Type.UUIDv7.generator([])
|
||||||
|
|
||||||
|
assert {:ok, binary_uuid_v7} = Ash.Type.dump_to_native(Ash.Type.UUIDv7, uuid_v7)
|
||||||
|
assert {:ok, ^uuid_v7} = Ash.Type.cast_input(Ash.Type.UUIDv7, binary_uuid_v7)
|
||||||
|
end
|
||||||
|
end
|
60
test/uuid_v7_test.exs
Normal file
60
test/uuid_v7_test.exs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
defmodule Ash.Test.UUIDv7Test do
|
||||||
|
@moduledoc false
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
test "generate/1 is working" do
|
||||||
|
uuid = Ash.UUIDv7.generate()
|
||||||
|
|
||||||
|
assert <<_::64, ?-, _::32, ?-, "7", _::24, ?-, _::32, ?-, _::96>> = uuid
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generate/1 is ordered" do
|
||||||
|
uuids =
|
||||||
|
for _ <- 1..10_000 do
|
||||||
|
Ash.UUIDv7.generate()
|
||||||
|
end
|
||||||
|
|
||||||
|
assert uuids == Enum.sort(uuids)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "bingenerate/1 is ordered" do
|
||||||
|
uuids =
|
||||||
|
for _ <- 1..10_000 do
|
||||||
|
Ash.UUIDv7.bingenerate()
|
||||||
|
end
|
||||||
|
|
||||||
|
assert uuids == Enum.sort(uuids)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "encode/1 is working" do
|
||||||
|
hex_uuid = Ash.UUIDv7.generate()
|
||||||
|
raw_uuid = Ash.UUIDv7.bingenerate()
|
||||||
|
|
||||||
|
encoded_hex_uuid = Ash.UUIDv7.encode(hex_uuid)
|
||||||
|
encoded_raw_uuid = Ash.UUIDv7.encode(raw_uuid)
|
||||||
|
|
||||||
|
assert is_binary(encoded_hex_uuid)
|
||||||
|
assert Ash.UUIDv7.decode(hex_uuid) == Ash.UUIDv7.decode(encoded_hex_uuid)
|
||||||
|
|
||||||
|
assert is_binary(encoded_raw_uuid)
|
||||||
|
assert raw_uuid == Ash.UUIDv7.decode(encoded_raw_uuid)
|
||||||
|
|
||||||
|
assert :error = Ash.UUIDv7.encode("error")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "decode/1 is working" do
|
||||||
|
hex_uuid = Ash.UUIDv7.generate()
|
||||||
|
raw_uuid = Ash.UUIDv7.bingenerate()
|
||||||
|
|
||||||
|
decoded_hex_uuid = Ash.UUIDv7.decode(hex_uuid)
|
||||||
|
decoded_raw_uuid = Ash.UUIDv7.decode(raw_uuid)
|
||||||
|
|
||||||
|
assert is_binary(decoded_hex_uuid)
|
||||||
|
assert hex_uuid == Ash.UUIDv7.encode(decoded_hex_uuid)
|
||||||
|
|
||||||
|
assert is_binary(decoded_raw_uuid)
|
||||||
|
assert Ash.UUIDv7.encode(raw_uuid) == Ash.UUIDv7.encode(decoded_raw_uuid)
|
||||||
|
|
||||||
|
assert :error == Ash.UUIDv7.decode("error")
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue