Skip to content

Commit 5dafe66

Browse files
OnurGumusncavedbrattli
authored
Add support for Guid.CreateVersion7 (#4472)
Co-authored-by: ncave <777696+ncave@users.noreply.github.com> Co-authored-by: Dag Brattli <dag@brattli.net>
1 parent a6ad738 commit 5dafe66

20 files changed

Lines changed: 283 additions & 13 deletions

File tree

src/Fable.Cli/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Added
11+
12+
* [All] Add support for `Guid.CreateVersion7()` and `Guid.CreateVersion7(DateTimeOffset)`
13+
1014
### Fixed
1115

16+
* [JS/TS] Fix `Guid` to use cryptographically strong random values (by @ncave)
17+
* [Python] Fix `DateTimeOffset` millisecond constructor and property (by @ncave)
1218
* [Python] Fix `String.IndexOf`/`LastIndexOf` with `StringComparison` argument emitting it as a start-index instead of a compile error (by @repo-assist)
1319
* [Beam] Fix `String.IndexOf`/`LastIndexOf` with `StringComparison` argument incorrectly treating the enum value as a start index
1420

src/Fable.Compiler/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Added
11+
12+
* [All] Add support for `Guid.CreateVersion7()` and `Guid.CreateVersion7(DateTimeOffset)`
13+
1014
### Fixed
1115

16+
* [JS/TS] Fix `Guid` to use cryptographically strong random values (by @ncave)
17+
* [Python] Fix `DateTimeOffset` millisecond constructor and property (by @ncave)
1218
* [Python] Fix `String.IndexOf`/`LastIndexOf` with `StringComparison` argument emitting it as a start-index instead of a compile error (by @repo-assist)
1319
* [Beam] Fix `String.IndexOf`/`LastIndexOf` with `StringComparison` argument incorrectly treating the enum value as a start index
1420

src/Fable.Transforms/Beam/Replacements.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3877,6 +3877,7 @@ let private guids
38773877
Helper.LibCall(com, "fable_guid", "from_bytes", t, [ arg ], ?loc = r) |> Some
38783878
| _ -> None
38793879
| "NewGuid" -> Helper.LibCall(com, "fable_guid", "new_guid", t, [], ?loc = r) |> Some
3880+
| "CreateVersion7" -> Helper.LibCall(com, "fable_guid", "create_version7", t, args, ?loc = r) |> Some
38803881
| "Parse" ->
38813882
match args with
38823883
| [ StringConst literalGuid ] ->

src/Fable.Transforms/Dart/Replacements.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3450,6 +3450,9 @@ let guids
34503450

34513451
match i.CompiledName with
34523452
| "NewGuid" -> Helper.LibCall(com, "Guid", "newGuid", t, []) |> Some
3453+
| "CreateVersion7" ->
3454+
Helper.LibCall(com, "Guid", "createVersion7", t, args, i.SignatureArgTypes)
3455+
|> Some
34533456
| "Parse" ->
34543457
match args with
34553458
| [ StringConst literalGuid ] -> parseGuid literalGuid

src/Fable.Transforms/Python/Replacements.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3494,6 +3494,9 @@ let guids
34943494
=
34953495
match i.CompiledName with
34963496
| "NewGuid" -> Helper.LibCall(com, "guid", "new_guid", t, []) |> Some
3497+
| "CreateVersion7" ->
3498+
Helper.LibCall(com, "guid", "create_version7", t, args, i.SignatureArgTypes)
3499+
|> Some
34973500
| "Parse" -> Helper.LibCall(com, "guid", "parse", t, args, i.SignatureArgTypes) |> Some
34983501
| "TryParse" -> Helper.LibCall(com, "guid", "try_parse", t, args, i.SignatureArgTypes) |> Some
34993502
| "ToByteArray" ->

src/Fable.Transforms/Replacements.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3822,6 +3822,9 @@ let guids
38223822

38233823
match i.CompiledName with
38243824
| "NewGuid" -> Helper.LibCall(com, "Guid", "newGuid", t, []) |> Some
3825+
| "CreateVersion7" ->
3826+
Helper.LibCall(com, "Guid", "createVersion7", t, args, i.SignatureArgTypes)
3827+
|> Some
38253828
| "Parse" ->
38263829
match args with
38273830
| [ StringConst literalGuid ] -> parseGuid literalGuid

src/Fable.Transforms/Rust/Replacements.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3121,6 +3121,10 @@ let guids
31213121
| _ -> None
31223122
// | "Empty", None, [] -> // it's a static field, see tryField
31233123
| "NewGuid", None, [] -> Helper.LibCall(com, "Guid", "new_guid", t, args, ?loc = r) |> Some
3124+
| "CreateVersion7", None, [] -> Helper.LibCall(com, "Guid", "create_version7", t, [], ?loc = r) |> Some
3125+
| "CreateVersion7", None, _ ->
3126+
Helper.LibCall(com, "Guid", "create_version7_with_timestamp", t, args, ?loc = r)
3127+
|> Some
31243128
| "Parse", None, [ ExprType String ] -> Helper.LibCall(com, "Guid", "parse", t, args, ?loc = r) |> Some
31253129
| "TryParse", None, [ ExprType String; _ ] -> Helper.LibCall(com, "Guid", "tryParse", t, args, ?loc = r) |> Some
31263130
| "ToByteArray", Some x, [] -> Helper.LibCall(com, "Guid", "toByteArray", t, [ x ], ?loc = r) |> Some

src/fable-library-beam/fable_guid.erl

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
-module(fable_guid).
22
-export([
3-
new_guid/0, parse/1, try_parse/1, try_parse/2, from_bytes/1, to_byte_array/1, to_string_format/2
3+
new_guid/0, create_version7/0, create_version7/1,
4+
parse/1, try_parse/1, try_parse/2, from_bytes/1, to_byte_array/1, to_string_format/2
45
]).
56

67
-spec new_guid() -> binary().
8+
-spec create_version7() -> binary().
9+
-spec create_version7(integer()) -> binary().
710
-spec parse(binary()) -> binary().
811
-spec try_parse(binary()) -> {boolean(), binary()}.
912
-spec try_parse(binary(), reference()) -> boolean().
@@ -26,6 +29,37 @@ new_guid() ->
2629
)
2730
).
2831

32+
%% Generate a new UUID v7 (RFC 9562) with current timestamp.
33+
create_version7() ->
34+
Ms = erlang:system_time(millisecond),
35+
create_version7(Ms).
36+
37+
%% Generate a new UUID v7 (RFC 9562) with given DateTimeOffset or ms since Unix epoch.
38+
create_version7({Ticks, _Kind, _Offset}) ->
39+
%% DateTimeOffset tuple: convert .NET ticks to Unix epoch milliseconds
40+
%% .NET ticks are 100ns intervals since 0001-01-01
41+
%% Unix epoch in .NET ticks = 621355968000000000
42+
Ms = (Ticks - 621355968000000000) div 10000,
43+
create_version7(Ms);
44+
create_version7(Timestamp) when is_integer(Timestamp) ->
45+
<<RandA0:16, RandB0:64>> = crypto:strong_rand_bytes(10),
46+
RandA = RandA0 band 16#0FFF,
47+
RandB = RandB0 band 16#3FFFFFFFFFFFFFFF,
48+
%% 48-bit timestamp split into A (high 32 bits) and B (low 16 bits)
49+
A = (Timestamp bsr 16) band 16#FFFFFFFF,
50+
B = Timestamp band 16#FFFF,
51+
%% Version 7: top 4 bits = 0111
52+
C = 16#7000 bor RandA,
53+
%% Variant: top 2 bits = 10
54+
D = 16#8000 bor ((RandB bsr 48) band 16#3FFF),
55+
E = RandB band 16#FFFFFFFFFFFF,
56+
list_to_binary(
57+
io_lib:format(
58+
"~8.16.0b-~4.16.0b-~4.16.0b-~4.16.0b-~12.16.0b",
59+
[A, B, C, D, E]
60+
)
61+
).
62+
2963
%% Construct a GUID from a 16-byte array (binary or list of integers).
3064
%% .NET uses mixed-endian: first 3 groups are little-endian, last 2 are big-endian.
3165
from_bytes({byte_array, _, _} = BA) ->

src/fable-library-py/fable_library/date_offset.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def create(
149149
ms = 0
150150

151151
if python_offset is None:
152-
dt = datetime(year, month, day, h, m, s, ms)
152+
dt = datetime(year, month, day, h, m, s, ms * 1000)
153153
if offset is not None:
154154
offset_ms = int(time_span.total_microseconds(offset) / 1000)
155155
python_offset = timedelta(microseconds=float(time_span.total_microseconds(offset)))
@@ -207,6 +207,14 @@ def second(d: DateTimeOffset) -> int:
207207
return d.second
208208

209209

210+
def millisecond(d: DateTimeOffset) -> int:
211+
return d.microsecond // 1000
212+
213+
214+
def microsecond(d: DateTimeOffset) -> int:
215+
return d.microsecond % 1000
216+
217+
210218
def op_subtraction(x: DateTimeOffset, y: DateTimeOffset | TimeSpan) -> DateTimeOffset | TimeSpan:
211219
if isinstance(y, TimeSpan):
212220
# Subtract TimeSpan from DateTimeOffset
@@ -231,6 +239,8 @@ def op_subtraction(x: DateTimeOffset, y: DateTimeOffset | TimeSpan) -> DateTimeO
231239
"op_subtraction",
232240
"parse",
233241
"second",
242+
"millisecond",
243+
"microsecond",
234244
"try_parse",
235245
"utc_now",
236246
"year",

src/fable-library-py/fable_library/guid.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import secrets
2+
import time
13
import uuid
24

35
from .array_ import Array
46
from .core import FSharpRef, byte
7+
from .date_offset import DateTimeOffset
58

69

710
def parse(string: str) -> uuid.UUID:
@@ -32,8 +35,38 @@ def array_to_guid(guid: Array[byte]) -> uuid.UUID:
3235
return uuid.UUID(bytes_le=bytes(guid))
3336

3437

38+
def create_version7(timestamp: DateTimeOffset | None = None) -> uuid.UUID:
39+
"""Create a UUID v7 per RFC 9562."""
40+
if timestamp is None:
41+
ms = int(time.time() * 1000)
42+
else:
43+
# DateTimeOffset has getTime() returning ms since epoch
44+
ms = int(timestamp.getTime())
45+
46+
# Build 16 bytes per RFC 9562
47+
rand_bytes = secrets.token_bytes(10)
48+
49+
# Bytes 0-5: 48-bit big-endian ms timestamp
50+
ts_bytes = ms.to_bytes(6, byteorder="big")
51+
52+
# Bytes 6-7: version (0111) + 12 bits rand_a
53+
rand_a = int.from_bytes(rand_bytes[0:2], "big")
54+
ver_rand_a = (0x7000 | (rand_a & 0x0FFF)).to_bytes(2, "big")
55+
56+
# Bytes 8-9: variant (10) + 14 bits rand_b
57+
rand_b_hi = int.from_bytes(rand_bytes[2:4], "big")
58+
var_rand_b = (0x8000 | (rand_b_hi & 0x3FFF)).to_bytes(2, "big")
59+
60+
# Bytes 10-15: 48 bits rand_b continued
61+
rand_rest = rand_bytes[4:10]
62+
63+
all_bytes = ts_bytes + ver_rand_a + var_rand_b + rand_rest
64+
return uuid.UUID(bytes=bytes(all_bytes))
65+
66+
3567
__all__ = [
3668
"array_to_guid",
69+
"create_version7",
3770
"guid_to_array",
3871
"new_guid",
3972
"parse",

0 commit comments

Comments
 (0)