hyperuuid-wasm 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +106 -78
- data/ext/hyperuuid_native/3.4/hyperuuid_native.a.gz +0 -0
- data/ext/hyperuuid_native/4.0/hyperuuid_native.a.gz +0 -0
- data/lib/hyperuuid/errors.rb +4 -5
- data/lib/hyperuuid/runtime.rb +21 -88
- data/lib/hyperuuid/uuid.rb +47 -68
- data/lib/hyperuuid.rb +13 -51
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23b7842046b78dff468073474bdb004a03c6d4710ff13a7ff3f048c879d141f9
|
|
4
|
+
data.tar.gz: 04204e9e6c775cb35a8370b53ffd8fc8c7538ad9220fffc5a05fc7e76c07ec7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1dd276963bf50735b2e9f7cd1be16dbeb7c7edebef8c027471cbaf6686237ef971b91f87f328a42650232b25e2bd5bd5d0cdb39309674fe2f5f743b58553a0d
|
|
7
|
+
data.tar.gz: 9e2ee0585a21dbdd3266ef635134e2013a09b3cb5538427a0b71df0e9126d4d9d3fbc7f8a0dc73f9b4ed5579c3f1c181e8d2de73b54e04c240b6e1eb8701637e
|
data/README.md
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
[](https://rubygems.org/gems/hyperuuid)
|
|
5
5
|
[](https://github.com/SkunkWerkx/HyperUuid/blob/master/LICENSE)
|
|
6
6
|
|
|
7
|
-
**Ruby's own stdlib stops at `SecureRandom.uuid` and `SecureRandom.uuid_v7` — random v4 and v7, one at a time. No v5, no
|
|
7
|
+
**Ruby's own stdlib stops at `SecureRandom.uuid` and `SecureRandom.uuid_v7` — random v4 and v7, one at a time. No v5, no batches, no timestamp read-back, no SQL Server order. This gem fills that in, with zero gem dependency beyond `Fiddle` (which ships with every Ruby install) — and it's faster than `SecureRandom.uuid` too.**
|
|
8
8
|
|
|
9
|
-
RFC 9562 UUID v4 (random), v5 (deterministic)
|
|
10
|
-
two backends sharing one public surface. Ruby 3.3 is the floor. The fast path is a native
|
|
9
|
+
RFC 9562 UUID v4 (random), v5 (deterministic) and v7 (time-sortable) generation, with a
|
|
10
|
+
version 8 SQL Server order for v7 and two backends sharing one public surface. Ruby 3.3 is the floor. The fast path is a native
|
|
11
11
|
extension built with [Magnus](https://github.com/matsadler/magnus) — the Rust core linked
|
|
12
12
|
directly into the Ruby VM, auto-selected when loadable — which redefines the low-level
|
|
13
13
|
`Runtime` methods in place on require; everything above them (`Uuid`, the module doors and
|
|
@@ -22,13 +22,12 @@ require "hyperuuid"
|
|
|
22
22
|
|
|
23
23
|
id = HyperUuid.new_v4
|
|
24
24
|
id2 = HyperUuid.new_v5(HyperUuid::Namespaces::DNS, "example.com")
|
|
25
|
-
id3 = HyperUuid.
|
|
26
|
-
id4 = HyperUuid.new_v7
|
|
25
|
+
id3 = HyperUuid.new_v7
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
sql =
|
|
27
|
+
id3.timestamp # recover the embedded UTC Time
|
|
28
|
+
id3.timestamp(raise_on_mismatch: false) # nil instead of raising if id3 isn't an RFC 9562 v7
|
|
29
|
+
id3.rfc?(7) # the RFC 9562 variant and version 7, the guard before trusting its fields
|
|
30
|
+
sql = id3.to_sql_order # a version 8 laid out so SQL Server's uniqueidentifier sorts by creation order
|
|
32
31
|
sql.timestamp(layout: :sql_server) # read straight from SQL order, no round trip
|
|
33
32
|
|
|
34
33
|
# One native call, one random-bytes fetch, one counter reservation for the whole batch:
|
|
@@ -41,9 +40,9 @@ batch = HyperUuid.new_v7_batch(1000)
|
|
|
41
40
|
|---|---|---|
|
|
42
41
|
| `new_v4` | `Uuid` | — |
|
|
43
42
|
| `new_v5(namespace, name)` | `Uuid`, the same one for the same pair | a `Uuid` namespace (`Namespaces::DNS`/`URL`/`OID`/`X500`, or your own) and a `String` name — text is hashed as UTF-8, a binary String as its bytes, and it may be empty |
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
43
|
+
| `new_v7(time = nil)` | `Uuid` | nothing (now), a `Time`, or an Integer of Unix-epoch milliseconds |
|
|
44
|
+
| `new_v7_batch(count, time = nil)` | `Array` of `Uuid` | a count (at most `MAX_V7_BATCH`), and the same time forms |
|
|
45
|
+
| `new_v7_batch_bytes(count, time = nil)` | one binary `String`, 16 bytes per UUID | as `new_v7_batch` — see [Bulk generation into bytes](#bulk-generation-into-bytes) |
|
|
47
46
|
| `native_version` | the loaded core's `"major.minor.patch"` | — |
|
|
48
47
|
| `available?` | `true`/`false`, never raising | — |
|
|
49
48
|
|
|
@@ -56,50 +55,70 @@ runtime dependency on the `uuid` gem:
|
|
|
56
55
|
| `Uuid.new(bytes)` | wraps 16 raw RFC 9562-ordered bytes |
|
|
57
56
|
| `Uuid::NIL` `Uuid::MAX` | the RFC 9562 §5.9/§5.10 special values |
|
|
58
57
|
| `#bytes` `#to_s` | the frozen binary String, the hyphenated text |
|
|
59
|
-
| `#version(layout: :rfc9562)` | the version in that layout: the nibble, 0-15, in RFC order;
|
|
58
|
+
| `#version(layout: :rfc9562)` | the version in that layout: the nibble, 0-15, in RFC order; in `:sql_server`, 7, the version a SQL-ordered value was converted from, and 0 when the bytes aren't one |
|
|
60
59
|
| `#variant` | the RFC 9562 §4.1 variant, as a Symbol: `:rfc9562` (every UUID this gem mints, and the only variant with versions), `:ncs` (Nil among them), `:microsoft`, `:future` (Max among them) |
|
|
61
60
|
| `#rfc?(version, layout: :rfc9562)` | whether it is an RFC 9562 UUID of that version: the variant and the version together. A version outside 0-15 is `false` |
|
|
62
|
-
| `#timestamp(raise_on_mismatch: true, layout: :rfc9562)` | the UTC `Time` embedded in an RFC 9562 version
|
|
63
|
-
| `#to_sql_order` `#from_sql_order` | to and from the
|
|
61
|
+
| `#timestamp(raise_on_mismatch: true, layout: :rfc9562)` | the UTC `Time` embedded in an RFC 9562 version 7 UUID held in that layout, in one native call that checks the variant as well as the version (a 7 nibble under another variant has no timestamp, and nor does a v1 or v6 minted elsewhere); `nil` instead of an `ArgumentError` for anything that isn't one when passed `false` |
|
|
62
|
+
| `#to_sql_order` `#from_sql_order` | to and from the layout SQL Server's `uniqueidentifier` sorts by, refusing what doesn't fit it |
|
|
64
63
|
| `==` `eql?` `hash` `<=>` | value equality and byte-order comparison (`Comparable`) |
|
|
65
64
|
|
|
66
|
-
`#to_sql_order`/`#from_sql_order` convert a version
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
`#to_sql_order`/`#from_sql_order` convert a version 7 UUID to and from the layout SQL Server's
|
|
66
|
+
`uniqueidentifier` needs on the wire to sort by creation order — computed once in the native
|
|
67
|
+
Rust core rather than reimplemented in Ruby, and verified there (and independently against the
|
|
68
|
+
real `System.Data.SqlTypes.SqlGuid` comparator in the C# binding's test suite). The converted
|
|
69
|
+
value is an RFC 9562 version 8: the 8 and the RFC variant sit where SQL Server's text form and
|
|
70
|
+
a .NET `Guid` show them, and the v7's 122 other bits (timestamp, counter, random) fill the rest
|
|
71
|
+
in the order SQL Server compares them. Every v7 converts, and the conversion back is exact.
|
|
72
|
+
Either direction refuses a value of the wrong kind with an `ArgumentError`: anything that isn't
|
|
73
|
+
an RFC 9562 v7 for `#to_sql_order`, anything without the layout's 8 and variant for
|
|
74
|
+
`#from_sql_order`.
|
|
76
75
|
|
|
77
76
|
### Layouts
|
|
78
77
|
|
|
79
78
|
`#version`, `#rfc?` and `#timestamp` take the byte order the value is held in as `layout:`, a
|
|
80
79
|
Symbol: `:rfc9562` (the default, what every other method takes and returns) or `:sql_server`
|
|
81
|
-
(what `#to_sql_order` returns,
|
|
80
|
+
(what `#to_sql_order` returns, a version 8 holding a converted v7). `Uuid::LAYOUTS` lists them.
|
|
82
81
|
Anything else is an `ArgumentError`, never a guess. With the default, `#version` and `#rfc?`
|
|
83
82
|
read the value as RFC 9562 order, which looks at the wrong bytes of a SQL-ordered value (one
|
|
84
83
|
from `#to_sql_order`, or a `uniqueidentifier` read back from SQL Server), so pass
|
|
85
84
|
`layout: :sql_server` at every call site that can see one. There is deliberately no
|
|
86
85
|
layout-agnostic check: whoever holds the value knows its order.
|
|
87
86
|
|
|
88
|
-
`#variant` reads RFC 9562 order only and has no layout form
|
|
89
|
-
variant
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
`#variant` reads RFC 9562 order only and has no layout form. The SQL Server layout keeps the
|
|
88
|
+
variant in the same place, so a SQL-ordered value reads `:rfc9562` there, but that says nothing
|
|
89
|
+
about whether it holds a converted v7; to validate one, or a `uniqueidentifier` read-back,
|
|
90
|
+
`#rfc?(7, layout: :sql_server)` checks the layout's 8 and variant.
|
|
92
91
|
|
|
93
92
|
```ruby
|
|
94
|
-
sql = HyperUuid.
|
|
95
|
-
sql.version(layout: :sql_server) # =>
|
|
96
|
-
sql.rfc?(
|
|
93
|
+
sql = HyperUuid.new_v7.to_sql_order
|
|
94
|
+
sql.version(layout: :sql_server) # => 7
|
|
95
|
+
sql.rfc?(7, layout: :sql_server) # => true
|
|
97
96
|
sql.timestamp(layout: :sql_server) # the same Time as sql.from_sql_order.timestamp
|
|
98
97
|
```
|
|
99
98
|
|
|
100
99
|
The bytes alone cannot say which order they are in, so the layout is yours to know: an
|
|
101
|
-
RFC-ordered value can happen to
|
|
102
|
-
|
|
100
|
+
RFC-ordered value can happen to carry the layout's 8 and variant (a random v4 does one time in
|
|
101
|
+
16), and `#from_sql_order` then converts it rather than refusing it. The same goes the other
|
|
102
|
+
way: a value already in SQL order carries a 7 where `#to_sql_order` looks for the version about
|
|
103
|
+
one time in 16, and is then converted a second time rather than refused.
|
|
104
|
+
|
|
105
|
+
### Binding to SQL Server
|
|
106
|
+
|
|
107
|
+
Send `#to_sql_order`'s result as its raw `#bytes`, not as UUID text. SQL Server stores a UUID
|
|
108
|
+
string's first three groups byte-reversed, which loses the creation order
|
|
109
|
+
([details](https://github.com/SkunkWerkx/HyperUuid#sql-server-ordering)); tiny_tds 3.4 and an ActiveRecord `:uuid` attribute both send text. A binary
|
|
110
|
+
literal lands byte for byte:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
sql = HyperUuid.new_v7.to_sql_order
|
|
114
|
+
client.execute("INSERT INTO orders (id) VALUES (0x#{sql.bytes.unpack1('H*')})").do
|
|
115
|
+
|
|
116
|
+
row = client.execute("SELECT CAST(id AS binary(16)) AS id FROM orders").first
|
|
117
|
+
sql = HyperUuid::Uuid.new(row["id"]) # read with layout: :sql_server
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
In ActiveRecord, map the column as `:binary`, which quotes as `0x…`. Reading needs the `CAST`
|
|
121
|
+
too: tiny_tds returns a `uniqueidentifier` as text with those groups reversed.
|
|
103
122
|
|
|
104
123
|
### The version 7 batch limit
|
|
105
124
|
|
|
@@ -109,7 +128,7 @@ mints. A larger count is an `ArgumentError` naming the limit, raised before anyt
|
|
|
109
128
|
allocated. Every batch is in strictly increasing order: the counter is one process-wide
|
|
110
129
|
sequence, so a batch can straddle the point where it wraps back to 0, and the UUIDs from there
|
|
111
130
|
on carry a timestamp one millisecond later than the one supplied rather than sorting before the
|
|
112
|
-
ones ahead of them.
|
|
131
|
+
ones ahead of them.
|
|
113
132
|
|
|
114
133
|
That roll-forward orders one batch, not the stream. The next batch or single `new_v7` in the
|
|
115
134
|
same real millisecond starts its counter just past the wrap and carries the supplied timestamp,
|
|
@@ -119,23 +138,20 @@ happens at most once per 2²⁶ UUIDs the process mints.
|
|
|
119
138
|
|
|
120
139
|
### Errors
|
|
121
140
|
|
|
122
|
-
Two exceptions
|
|
141
|
+
Two exceptions describe a failed call, and both carry the same message on every backend:
|
|
123
142
|
|
|
124
|
-
- `HyperUuid::TimestampOutOfRangeError` — the time cannot be embedded
|
|
125
|
-
48-bit millisecond field,
|
|
126
|
-
epoch included).
|
|
143
|
+
- `HyperUuid::TimestampOutOfRangeError` — the time cannot be embedded in a version 7: past
|
|
144
|
+
its 48-bit millisecond field, or negative (a `Time` before the Unix epoch included).
|
|
127
145
|
- `HyperUuid::RandomSourceError` — the operating system's random source failed.
|
|
128
146
|
|
|
129
|
-
Through 0.3.0 these were reachable only as `HyperUuid::Runtime::TimestampOutOfRangeError` and
|
|
130
|
-
`HyperUuid::Runtime::RandomSourceError`; those names remain, as aliases of the same classes.
|
|
131
|
-
|
|
132
147
|
Everything else is a caller bug and raises Ruby's own error for it, checked once in the shared
|
|
133
148
|
doors rather than left to whichever backend is live: a `TypeError` for a time that is not a
|
|
134
149
|
`Time`, an Integer or `nil`, for a count that is not an Integer, for a namespace that is not a
|
|
135
150
|
`Uuid` or a name that is not a `String`, for a version passed to `#rfc?` that is not an
|
|
136
|
-
Integer; an `ArgumentError` for a count outside 0
|
|
137
|
-
|
|
138
|
-
address (a 32-bit target such
|
|
151
|
+
Integer; an `ArgumentError` for a count outside 0..`MAX_V7_BATCH` and for a layout other than
|
|
152
|
+
`:rfc9562`/`:sql_server`. Two refusals come from the backend instead, each an `ArgumentError`
|
|
153
|
+
worded the same on both: a batch too large for the platform to address (a 32-bit target such
|
|
154
|
+
as ruby.wasm), and the SQL Server conversions' one refusal, a value of the wrong version.
|
|
139
155
|
|
|
140
156
|
### Is the native core there?
|
|
141
157
|
|
|
@@ -143,6 +159,8 @@ address (a 32-bit target such as ruby.wasm).
|
|
|
143
159
|
raising — for a consumer with a fallback of its own:
|
|
144
160
|
|
|
145
161
|
```ruby
|
|
162
|
+
require "securerandom"
|
|
163
|
+
|
|
146
164
|
id = HyperUuid.available? ? HyperUuid.new_v7.to_s : SecureRandom.uuid
|
|
147
165
|
```
|
|
148
166
|
|
|
@@ -155,9 +173,9 @@ looked for, or `HyperUuid::NativePlatform::UnsupportedPlatformError` naming the
|
|
|
155
173
|
|
|
156
174
|
## Why not `SecureRandom.uuid`?
|
|
157
175
|
|
|
158
|
-
`SecureRandom.uuid` gives you a random v4 UUID and `SecureRandom.uuid_v7` a v7 (Ruby 3.3 and later, so every Ruby this gem supports). Ruby's stdlib has no v5
|
|
176
|
+
`SecureRandom.uuid` gives you a random v4 UUID and `SecureRandom.uuid_v7` a v7 (Ruby 3.3 and later, so every Ruby this gem supports). Ruby's stdlib has no v5, nothing to read a timestamp back out, no batch form, and no SQL Server ordering. If you need more than that, the choice is really "which gem":
|
|
159
177
|
|
|
160
|
-
1. **
|
|
178
|
+
1. **The RFC 9562 versions new systems use, one gem, zero extra dependency.** v4/v5/v7 plus batch generation plus `Nil`/`Max`, and nothing added to your `Gemfile.lock` beyond `Fiddle` — Ruby's own bundled FFI layer, not a third-party C extension to compile — and on the precompiled platform gems not even that.
|
|
161
179
|
2. **No native-extension compile step.** Third-party UUID gems that go beyond v4 are typically pure Ruby or wrap a C extension compiled at install time; this gem ships its fast path as a prebuilt platform-gem extension and its fallback as a `dlopen`ed prebuilt library — either way, the gem itself compiles nothing at install time ([Install](#install) has the one caveat, which is Fiddle's own).
|
|
162
180
|
3. **Batch generation.** `new_v7_batch(1000)` shares one timestamp capture, one random-bytes fetch, and one counter reservation across the whole batch instead of paying per-item overhead a thousand times over.
|
|
163
181
|
4. **Cross-language consistency.** The same Rust core mints v5 namespace UUIDs for Python, Go, C#, and every other binding in this repo — verified in CI to match Python's own `uuid.uuid5` byte-for-byte. If your system isn't Ruby-only, no Ruby-only gem can offer that.
|
|
@@ -166,14 +184,14 @@ The honest trade-off: this gem is native code, not pure Ruby — a precompiled e
|
|
|
166
184
|
|
|
167
185
|
## Bulk generation into bytes
|
|
168
186
|
|
|
169
|
-
`
|
|
187
|
+
`new_v7_batch_bytes` returns the batch as one binary `String` of raw RFC 9562-ordered bytes — 16 per UUID — instead of an Array of `Uuid` objects:
|
|
170
188
|
|
|
171
189
|
```ruby
|
|
172
190
|
bytes = HyperUuid.new_v7_batch_bytes(1000)
|
|
173
191
|
first = bytes[0, 16] # ready for a BINARY(16) bind parameter
|
|
174
192
|
```
|
|
175
193
|
|
|
176
|
-
**About
|
|
194
|
+
**About 41x faster than `new_v7_batch`** for a 1000-UUID batch (4.9 µs versus 202 µs). The native call is identical — the difference is that `new_v7_batch` then allocates a `Uuid` object and its byte Strings for every item on top of it. This hands back the bytes the native core already produced, untouched.
|
|
177
195
|
|
|
178
196
|
The catch, and it inverts the advice: **if you need `Uuid` objects, keep using `new_v7_batch`.** Slicing these bytes into objects yourself just relocates the identical allocations into your own code, and measures no better — sometimes worse. Reach for the byte form only when bytes are the destination: a bind parameter, a wire format, a bulk load.
|
|
179
197
|
|
|
@@ -191,9 +209,7 @@ Real numbers, `benchmark-ips` on Ruby 4.0.7, linux-x64 on an Intel Core i9-11900
|
|
|
191
209
|
|---|---:|---:|
|
|
192
210
|
| `SecureRandom.uuid` | 943,917 | baseline |
|
|
193
211
|
| `HyperUuid.new_v4` | 5,236,000 | **5.5x faster** |
|
|
194
|
-
| `HyperUuid.new_v6` (explicit ms) | 3,811,000 | **4.0x faster** |
|
|
195
212
|
| `HyperUuid.new_v7` (explicit ms) | 3,785,000 | **4.0x faster** |
|
|
196
|
-
| `HyperUuid.new_v6` (current time) | 3,677,000 | **3.9x faster** |
|
|
197
213
|
| `HyperUuid.new_v7` (current time) | 3,574,000 | **3.8x faster** |
|
|
198
214
|
| `HyperUuid.new_v5` | 1,942,000 | 2.1x faster |
|
|
199
215
|
|
|
@@ -201,18 +217,16 @@ A `HyperUuid.new_v4` — real entropy, correct version/variant bits, minted by t
|
|
|
201
217
|
|
|
202
218
|
The "current time" rows deserve a footnote, because they will not look like this everywhere. Here they land beside the explicit-ms rows: the only difference between the two is one `Process.clock_gettime(CLOCK_REALTIME)` wall-clock read, and on this machine that read is too cheap to see. On a machine with a slow clock it is the whole story — where a virtualized clock defeats the vDSO fast path the same read costs ~1µs, which puts both current-time rows at parity with `SecureRandom.uuid` while the explicit-ms rows stay well ahead of it. `SecureRandom.uuid` never reads a clock — random v4 is the only thing it does. If your clock is slow and you are minting many, read it once and pass the timestamp, or use the batch doors.
|
|
203
219
|
|
|
204
|
-
The Fiddle fallback (`HYPERUUID_PURE=1`, and any platform without a prebuilt extension) keeps its own diet — a reused thread-local scratch buffer instead of two GC-finalizer-registering mallocs per call, zero-copy `String` passes for read-only inputs, an unsynchronized fast path past the load mutex — landing at 1.16x slower than `SecureRandom.uuid` for v4 (1.24 µs against 1.07 µs in its own run) and about 1.3x slower for
|
|
220
|
+
The Fiddle fallback (`HYPERUUID_PURE=1`, and any platform without a prebuilt extension) keeps its own diet — a reused thread-local scratch buffer instead of two GC-finalizer-registering mallocs per call, zero-copy `String` passes for read-only inputs, an unsynchronized fast path past the load mutex — landing at 1.16x slower than `SecureRandom.uuid` for v4 (1.24 µs against 1.07 µs in its own run) and about 1.3x slower for v7, with the same structural story as before: `Fiddle`'s interpreted marshalling is the floor, and the batch doors are how you amortize it.
|
|
205
221
|
|
|
206
222
|
Batch generation still amortizes per-call cost on both backends — one native call for the whole batch:
|
|
207
223
|
|
|
208
224
|
| Call | i/s (Magnus backend) |
|
|
209
225
|
|---|---:|
|
|
210
|
-
| `new_v6` × 1000 (individual) | 3,761 |
|
|
211
|
-
| `new_v6_batch(1000)` | 4,857 (**1.3x**) |
|
|
212
226
|
| `new_v7` × 1000 (individual) | 3,742 |
|
|
213
227
|
| `new_v7_batch(1000)` | 4,850 (**1.3x**) |
|
|
214
228
|
|
|
215
|
-
The multiplier is small on this backend for the best reason available: the individual calls are cheap, so there is little waste left to amortize, and what `new_v7_batch` spends its 206 µs on is building a thousand `Uuid` objects — the byte form above does the same native work in
|
|
229
|
+
The multiplier is small on this backend for the best reason available: the individual calls are cheap, so there is little waste left to amortize, and what `new_v7_batch` spends its 206 µs on (in this table's run; 202 µs in the bytes-versus-objects run above) is building a thousand `Uuid` objects — the byte form above does the same native work in 4.9 µs. On the Fiddle backend, where each call costs 1.4 µs, the same batch is 6.7x the loop. If you need v5/v7, need many at once, or need this Ruby service's IDs to agree byte-for-byte with a Go or Python service's, that's what this gem is for — and now it's the fast option too, not just the capable one.
|
|
216
230
|
|
|
217
231
|
## Backends
|
|
218
232
|
|
|
@@ -239,34 +253,42 @@ backend where Ruby threads run the core truly in parallel — each through its o
|
|
|
239
253
|
buffer, with the core's v7 counter shared and atomic. `spec/hyperuuid_spec.rb`'s "concurrent
|
|
240
254
|
callers" examples run under both.
|
|
241
255
|
|
|
242
|
-
**Ractors.** Main Ractor only, on every backend: called from another Ractor the doors raise
|
|
243
|
-
`Ractor::UnsafeError
|
|
256
|
+
**Ractors.** Main Ractor only, on every backend: called from another Ractor the doors raise a
|
|
257
|
+
`Ractor::Error` (`Ractor::IsolationError`, or `Ractor::UnsafeError`, by door and backend).
|
|
244
258
|
|
|
245
259
|
## Verifying provenance
|
|
246
260
|
|
|
247
|
-
Every gem RubyGems.org serves — the universal fallback, each of the seven precompiled
|
|
248
|
-
|
|
249
|
-
this
|
|
250
|
-
|
|
261
|
+
Every gem RubyGems.org serves — the universal fallback, each of the seven precompiled platform
|
|
262
|
+
gems and `hyperuuid-wasm` — verifies from the release tag alone: the release attestation,
|
|
263
|
+
signed by this repository's `release.yml` at the tag, names every one of them, with the
|
|
264
|
+
release inventory as its predicate.
|
|
265
|
+
|
|
266
|
+
```sh
|
|
267
|
+
gem fetch hyperuuid -v <version> --platform <platform> # or omit --platform for the universal gem
|
|
268
|
+
gh attestation verify hyperuuid-<version>-<platform>.gem --repo SkunkWerkx/HyperUuid \
|
|
269
|
+
--source-ref refs/tags/v<version> --predicate-type https://github.com/SkunkWerkx/.github/predicates/release/v1
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Each gem's build provenance is this repository's too: the `ruby-pack` job attests
|
|
273
|
+
`ruby/pkg/*.gem` as soon as they are built and `rubygems-publish` pushes those preserved
|
|
274
|
+
bytes, so plain `--repo` verifies it:
|
|
251
275
|
|
|
252
276
|
```sh
|
|
253
|
-
|
|
254
|
-
gh attestation verify hyperuuid-X.Y.Z-<platform>.gem --repo SkunkWerkx/HyperUuid
|
|
277
|
+
gh attestation verify hyperuuid-<version>-<platform>.gem --repo SkunkWerkx/HyperUuid
|
|
255
278
|
```
|
|
256
279
|
|
|
257
|
-
That
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
`hyper-build-wasm.yml
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
more on why `--signer-repo` is needed for some artifacts here and not others.
|
|
280
|
+
That is the release's second layer of checking, not the only one: before any gem is built, the
|
|
281
|
+
same job verifies every native binary it packs — the Fiddle libraries, the Magnus extensions
|
|
282
|
+
(one per Ruby ABI per platform), the `wasm32-wasip1` archives — against *their own* build
|
|
283
|
+
provenance, signed from `SkunkWerkx/.github` by `hyper-build-native.yml` and
|
|
284
|
+
`hyper-build-wasm.yml` (so that check needs `--signer-repo SkunkWerkx/.github` added), and
|
|
285
|
+
refuses to proceed on an unverified one. RubyGems.org has no unpublish and no
|
|
286
|
+
duplicate-version overwrite, so all of this happens while a bad artifact is still reversible,
|
|
287
|
+
and the push itself is a separate dispatch from the preserved bytes. The release run then
|
|
288
|
+
re-fetches every gem from rubygems.org and fails unless each matches the attested bytes
|
|
289
|
+
exactly, turning "rubygems.org stores an upload verbatim" into a per-release check rather than
|
|
290
|
+
an assumption. The root README's [Provenance](../README.md#provenance) section has the whole
|
|
291
|
+
picture.
|
|
270
292
|
|
|
271
293
|
## Install
|
|
272
294
|
|
|
@@ -375,7 +397,7 @@ bundle exec rbwasm build --ruby-version 4.0 -o ruby.wasm
|
|
|
375
397
|
Load `ruby.wasm` with [`@ruby/wasm-wasi`](https://www.npmjs.com/package/@ruby/wasm-wasi)
|
|
376
398
|
(`DefaultRubyVM` in a browser, `RubyVM.instantiateModule` under Node), then
|
|
377
399
|
`require "/bundle/setup"` and `require "hyperuuid"` as anywhere else. `HyperUuid::BACKEND` is
|
|
378
|
-
`:native`: every door, the batch and raw-bytes forms, and `
|
|
400
|
+
`:native`: every door, the batch and raw-bytes forms, and `new_v7` with no argument,
|
|
379
401
|
which read the clock through WASI.
|
|
380
402
|
|
|
381
403
|
- **Ruby 3.4 and 4.0**, one archive each, the same minors the platform gems cover, picked by
|
|
@@ -384,6 +406,12 @@ which read the clock through WASI.
|
|
|
384
406
|
- **No Rust toolchain.** The gem's `extconf.rb` only hands rbwasm the prebuilt archive, and
|
|
385
407
|
rbwasm downloads its own wasi-sdk. The first `rbwasm build` compiles Ruby itself and takes
|
|
386
408
|
15–20 minutes; later builds reuse it.
|
|
409
|
+
- **macOS with Homebrew: limit pkg-config to `openssl@3`.** Run the build as
|
|
410
|
+
`PKG_CONFIG_LIBDIR="$(brew --prefix openssl@3)/lib/pkgconfig" bundle exec rbwasm build …`.
|
|
411
|
+
Without it Homebrew's pkg-config files reach into the build twice: its `openssl` is OpenSSL
|
|
412
|
+
4, which the native Ruby rbwasm builds first does not compile against, and its `libyaml`
|
|
413
|
+
is a macOS library that psych then links into the wasm interpreter, which fails for Ruby
|
|
414
|
+
4.0.
|
|
387
415
|
- **Static linking only,** into ruby.wasm's default `wasm32-unknown-wasip1` interpreter.
|
|
388
416
|
rbwasm's dynamic-linking build (a pic target, for the component model) is not supported.
|
|
389
417
|
- **Size.** rbwasm packs every gem's files into the interpreter, the archives included, so the
|
|
Binary file
|
|
Binary file
|
data/lib/hyperuuid/errors.rb
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
module HyperUuid
|
|
2
2
|
# Raised when the operating system's random source fails while a UUID is being minted —
|
|
3
|
-
# the one way `new_v4`, `
|
|
3
|
+
# the one way `new_v4`, `new_v7` and its batch forms can fail once their
|
|
4
4
|
# arguments are valid. Carries the same message on every backend.
|
|
5
5
|
class RandomSourceError < StandardError; end
|
|
6
6
|
|
|
7
|
-
# Raised when a timestamp cannot be embedded in the UUID being minted:
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# every backend.
|
|
7
|
+
# Raised when a timestamp cannot be embedded in the version 7 UUID being minted: one past
|
|
8
|
+
# RFC 9562's 48-bit millisecond field, or a negative one (a `Time` before the Unix epoch
|
|
9
|
+
# included). Carries the same message on every backend.
|
|
11
10
|
class TimestampOutOfRangeError < StandardError; end
|
|
12
11
|
end
|
data/lib/hyperuuid/runtime.rb
CHANGED
|
@@ -15,17 +15,9 @@ module HyperUuid
|
|
|
15
15
|
# A Ruby gem's files are plain files on disk once installed, so native/{rid}/{lib} can be
|
|
16
16
|
# dlopen'd directly, with no extraction step.
|
|
17
17
|
module Runtime
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# release still catches.
|
|
22
|
-
RandomSourceError = HyperUuid::RandomSourceError
|
|
23
|
-
TimestampOutOfRangeError = HyperUuid::TimestampOutOfRangeError
|
|
24
|
-
|
|
25
|
-
# One copy of each out-of-range message for the Fiddle backend and for the
|
|
26
|
-
# doors' own range check in hyperuuid.rb; the Magnus extension (rust/src/ruby_ext.rs)
|
|
27
|
-
# carries the same text, and spec/native_backend_spec.rb pins that the two agree.
|
|
28
|
-
V6_TIMESTAMP_OUT_OF_RANGE = "unix_millis does not fit the 60-bit v6 timestamp field"
|
|
18
|
+
# One copy of the out-of-range message for the Fiddle backend and for the doors' own
|
|
19
|
+
# range check in hyperuuid.rb; the Magnus extension (rust/src/ruby_ext.rs) carries the
|
|
20
|
+
# same text, and spec/native_backend_spec.rb pins that the two agree.
|
|
29
21
|
V7_TIMESTAMP_OUT_OF_RANGE = "unix_millis must fit within the RFC 9562 48-bit field"
|
|
30
22
|
|
|
31
23
|
# The two batch-size refusals, shared the same way: a version 7 batch past the 26-bit
|
|
@@ -36,6 +28,11 @@ module HyperUuid
|
|
|
36
28
|
V7_BATCH_TOO_LARGE = "a single version 7 batch takes at most 67108864 UUIDs (the 26-bit counter space)"
|
|
37
29
|
BATCH_TOO_LARGE_TO_ADDRESS = "the batch is too large to address on this platform"
|
|
38
30
|
|
|
31
|
+
# The SQL Server order conversions' one refusal, in the core's own words (the Magnus
|
|
32
|
+
# extension raises its SqlOrderError's text): a value that isn't a UUID of the
|
|
33
|
+
# conversion's version (code 5), an ArgumentError. Every version 7 converts.
|
|
34
|
+
SQL_ORDER_WRONG_VERSION = "the value is not a UUID of the version this conversion takes"
|
|
35
|
+
|
|
39
36
|
# The core's v7::MAX_BATCH, which hyperuuid.rb publishes as HyperUuid::MAX_V7_BATCH.
|
|
40
37
|
MAX_V7_BATCH = 1 << 26
|
|
41
38
|
|
|
@@ -65,33 +62,6 @@ module HyperUuid
|
|
|
65
62
|
out[0, 16]
|
|
66
63
|
end
|
|
67
64
|
|
|
68
|
-
def new_v6(unix_millis)
|
|
69
|
-
out = scratch
|
|
70
|
-
rc = functions[:new_v6].call(unix_millis, out)
|
|
71
|
-
case rc
|
|
72
|
-
when 0 then out[0, 16]
|
|
73
|
-
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
74
|
-
else raise random_source_failure("uuid_new_v6")
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def v6_unix_millis(bytes)
|
|
79
|
-
functions[:v6_unix_millis].call(bytes)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def new_v6_batch(count, unix_millis)
|
|
83
|
-
return "" if count.zero?
|
|
84
|
-
|
|
85
|
-
out = buffer(count * 16)
|
|
86
|
-
rc = functions[:new_v6_batch].call(unix_millis, count, out)
|
|
87
|
-
case rc
|
|
88
|
-
when 0 then out[0, count * 16]
|
|
89
|
-
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
90
|
-
when 3 then raise ArgumentError, BATCH_TOO_LARGE_TO_ADDRESS
|
|
91
|
-
else raise random_source_failure("uuid_new_v6_batch")
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
65
|
def new_v7(unix_millis)
|
|
96
66
|
out = scratch
|
|
97
67
|
rc = functions[:new_v7].call(unix_millis, out)
|
|
@@ -140,19 +110,16 @@ module HyperUuid
|
|
|
140
110
|
functions[:is_rfc].call(bytes, version, layout) == 1
|
|
141
111
|
end
|
|
142
112
|
|
|
143
|
-
# The Unix ms of an RFC 9562 version
|
|
144
|
-
# anything else — another version, another variant. One
|
|
145
|
-
# answers with is not needed above Runtime, so it is dropped
|
|
113
|
+
# The Unix ms of an RFC 9562 version 7 UUID held in +layout+'s order, or nil for
|
|
114
|
+
# anything else — another version (a version 6 or 1 included), another variant. One
|
|
115
|
+
# core call; the version it answers with is not needed above Runtime, so it is dropped
|
|
116
|
+
# here.
|
|
146
117
|
def get_timestamp(bytes, layout)
|
|
147
118
|
out = millis_scratch
|
|
148
119
|
version = functions[:get_timestamp].call(bytes, layout, out)
|
|
149
120
|
version.zero? ? nil : out[0, 8].unpack1("Q")
|
|
150
121
|
end
|
|
151
122
|
|
|
152
|
-
def v6_unix_millis_in(bytes, layout)
|
|
153
|
-
functions[:v6_unix_millis_in].call(bytes, layout)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
123
|
def v7_unix_millis_in(bytes, layout)
|
|
157
124
|
functions[:v7_unix_millis_in].call(bytes, layout)
|
|
158
125
|
end
|
|
@@ -165,14 +132,6 @@ module HyperUuid
|
|
|
165
132
|
rewrite(:v7_to_rfc_order, bytes)
|
|
166
133
|
end
|
|
167
134
|
|
|
168
|
-
def v6_to_sql_order(bytes)
|
|
169
|
-
rewrite(:v6_to_sql_order, bytes)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def v6_to_rfc_order(bytes)
|
|
173
|
-
rewrite(:v6_to_rfc_order, bytes)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
135
|
# The loaded core's version word, major << 16 | minor << 8 | patch, straight from the
|
|
177
136
|
# library's zero-argument hyperuuid_version export. HyperUuid.native_version unpacks
|
|
178
137
|
# it; like every method here, each backend replaces this one in place.
|
|
@@ -253,12 +212,16 @@ module HyperUuid
|
|
|
253
212
|
end
|
|
254
213
|
|
|
255
214
|
# The in-place byte-order rewrites are the one shape that must copy in: the native
|
|
256
|
-
# call genuinely mutates the buffer, and the input String is frozen.
|
|
215
|
+
# call genuinely mutates the buffer, and the input String is frozen. A refusal leaves
|
|
216
|
+
# the buffer as it was and raises what the Magnus extension raises for it.
|
|
257
217
|
def rewrite(symbol, bytes)
|
|
258
218
|
buf = scratch
|
|
259
219
|
buf[0, 16] = bytes
|
|
260
|
-
functions[symbol].call(buf)
|
|
261
|
-
buf[0, 16]
|
|
220
|
+
case (rc = functions[symbol].call(buf))
|
|
221
|
+
when 0 then buf[0, 16]
|
|
222
|
+
when 5 then raise ArgumentError, SQL_ORDER_WRONG_VERSION
|
|
223
|
+
else raise "uuid_#{symbol}: unexpected return code #{rc}"
|
|
224
|
+
end
|
|
262
225
|
end
|
|
263
226
|
|
|
264
227
|
# Loaded lazily and exactly once, mirroring Java's class initializer / Swift's lazy
|
|
@@ -281,21 +244,6 @@ module HyperUuid
|
|
|
281
244
|
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
|
|
282
245
|
Fiddle::TYPE_INT
|
|
283
246
|
),
|
|
284
|
-
new_v6: Fiddle::Function.new(
|
|
285
|
-
handle["uuid_new_v6"],
|
|
286
|
-
[Fiddle::TYPE_UINT64_T, Fiddle::TYPE_VOIDP],
|
|
287
|
-
Fiddle::TYPE_INT
|
|
288
|
-
),
|
|
289
|
-
v6_unix_millis: Fiddle::Function.new(
|
|
290
|
-
handle["uuid_v6_unix_millis"],
|
|
291
|
-
[Fiddle::TYPE_VOIDP],
|
|
292
|
-
Fiddle::TYPE_UINT64_T
|
|
293
|
-
),
|
|
294
|
-
new_v6_batch: Fiddle::Function.new(
|
|
295
|
-
handle["uuid_new_v6_batch"],
|
|
296
|
-
[Fiddle::TYPE_UINT64_T, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
|
|
297
|
-
Fiddle::TYPE_INT
|
|
298
|
-
),
|
|
299
247
|
new_v7: Fiddle::Function.new(
|
|
300
248
|
handle["uuid_new_v7"],
|
|
301
249
|
[Fiddle::TYPE_UINT64_T, Fiddle::TYPE_VOIDP],
|
|
@@ -314,22 +262,12 @@ module HyperUuid
|
|
|
314
262
|
v7_to_sql_order: Fiddle::Function.new(
|
|
315
263
|
handle["uuid_v7_to_sql_order"],
|
|
316
264
|
[Fiddle::TYPE_VOIDP],
|
|
317
|
-
Fiddle::
|
|
265
|
+
Fiddle::TYPE_INT
|
|
318
266
|
),
|
|
319
267
|
v7_to_rfc_order: Fiddle::Function.new(
|
|
320
268
|
handle["uuid_v7_to_rfc_order"],
|
|
321
269
|
[Fiddle::TYPE_VOIDP],
|
|
322
|
-
Fiddle::
|
|
323
|
-
),
|
|
324
|
-
v6_to_sql_order: Fiddle::Function.new(
|
|
325
|
-
handle["uuid_v6_to_sql_order"],
|
|
326
|
-
[Fiddle::TYPE_VOIDP],
|
|
327
|
-
Fiddle::TYPE_VOID
|
|
328
|
-
),
|
|
329
|
-
v6_to_rfc_order: Fiddle::Function.new(
|
|
330
|
-
handle["uuid_v6_to_rfc_order"],
|
|
331
|
-
[Fiddle::TYPE_VOIDP],
|
|
332
|
-
Fiddle::TYPE_VOID
|
|
270
|
+
Fiddle::TYPE_INT
|
|
333
271
|
),
|
|
334
272
|
version: Fiddle::Function.new(
|
|
335
273
|
handle["uuid_version"],
|
|
@@ -347,11 +285,6 @@ module HyperUuid
|
|
|
347
285
|
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
|
|
348
286
|
Fiddle::TYPE_UINT32_T
|
|
349
287
|
),
|
|
350
|
-
v6_unix_millis_in: Fiddle::Function.new(
|
|
351
|
-
handle["uuid_v6_unix_millis_in"],
|
|
352
|
-
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T],
|
|
353
|
-
Fiddle::TYPE_UINT64_T
|
|
354
|
-
),
|
|
355
288
|
v7_unix_millis_in: Fiddle::Function.new(
|
|
356
289
|
handle["uuid_v7_unix_millis_in"],
|
|
357
290
|
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T],
|
data/lib/hyperuuid/uuid.rb
CHANGED
|
@@ -40,8 +40,8 @@ module HyperUuid
|
|
|
40
40
|
|
|
41
41
|
# The byte orders a Uuid can hold, as the +layout:+ keyword of #version, #rfc? and
|
|
42
42
|
# #timestamp takes them, mapped to the native core's layout codes. +:rfc9562+ is RFC 9562
|
|
43
|
-
# order, what every other method here takes and returns. +:sql_server+ is the
|
|
44
|
-
# #to_sql_order returns,
|
|
43
|
+
# order, what every other method here takes and returns. +:sql_server+ is the layout
|
|
44
|
+
# #to_sql_order returns, a version 8 that holds a converted version 7.
|
|
45
45
|
LAYOUTS = { rfc9562: 1, sql_server: 2 }.freeze
|
|
46
46
|
|
|
47
47
|
# The Symbols #variant returns, indexed by the native core's variant code (1-4; 0 is
|
|
@@ -66,10 +66,11 @@ module HyperUuid
|
|
|
66
66
|
#
|
|
67
67
|
# In +:rfc9562+ (the default) it is the version nibble (bits 48-51, the high nibble of
|
|
68
68
|
# octet 6), 0 through 15: Nil reads as 0 and Max as 15. It says nothing about the variant;
|
|
69
|
-
# #rfc? checks both. In +:sql_server+, the
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
# happen to
|
|
69
|
+
# #rfc? checks both. In +:sql_server+, the layout #to_sql_order returns, it is the version
|
|
70
|
+
# the value was converted from, 7, when the bytes carry the layout's version 8 and RFC
|
|
71
|
+
# variant, and 0 when they don't. It reads the bytes as given: an
|
|
72
|
+
# RFC-ordered value can happen to carry them (a random v4 does one time in 16), so the
|
|
73
|
+
# layout is the caller's to know.
|
|
73
74
|
#
|
|
74
75
|
# With the default layout, the value is read as RFC 9562 order. A SQL-ordered value — from
|
|
75
76
|
# #to_sql_order, or a +uniqueidentifier+ read back from SQL Server — needs
|
|
@@ -86,19 +87,19 @@ module HyperUuid
|
|
|
86
87
|
# versions), +:ncs+ (+0xxx+, Nil among them), +:microsoft+ (+110x+) or +:future+ (+111x+,
|
|
87
88
|
# Max among them).
|
|
88
89
|
#
|
|
89
|
-
# RFC 9562 order only
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
# <tt>rfc?(
|
|
93
|
-
#
|
|
90
|
+
# RFC 9562 order only, with no layout form. The SQL Server layout keeps the variant in the
|
|
91
|
+
# same place, so a SQL-ordered value reads +:rfc9562+ here, but that says nothing about
|
|
92
|
+
# which version it holds. To validate one, or a +uniqueidentifier+ read back from SQL
|
|
93
|
+
# Server, use <tt>rfc?(7, layout: :sql_server)</tt>, which checks the layout's version 8
|
|
94
|
+
# and variant.
|
|
94
95
|
def variant
|
|
95
96
|
VARIANTS.fetch(Runtime.variant(bytes))
|
|
96
97
|
end
|
|
97
98
|
|
|
98
99
|
# Whether this is an RFC 9562 UUID of version +version+, read in +layout+'s byte order —
|
|
99
100
|
# the RFC 9562 variant and that version together, the guard to run before trusting a
|
|
100
|
-
# value's version-specific fields. In +:sql_server+
|
|
101
|
-
# outside 0-15 is simply false.
|
|
101
|
+
# value's version-specific fields. In +:sql_server+ +version+ is the version the value was
|
|
102
|
+
# converted from, so only 7 can be true. A version outside 0-15 is simply false.
|
|
102
103
|
#
|
|
103
104
|
# With the default layout, the value is read as RFC 9562 order. A SQL-ordered value — from
|
|
104
105
|
# #to_sql_order, or a +uniqueidentifier+ read back from SQL Server — needs
|
|
@@ -121,17 +122,19 @@ module HyperUuid
|
|
|
121
122
|
end
|
|
122
123
|
alias_method :to_str, :to_s
|
|
123
124
|
|
|
124
|
-
# The UTC timestamp embedded in an RFC 9562 version
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
# timestamp (#rfc? is the same check on
|
|
125
|
+
# The UTC timestamp embedded in an RFC 9562 version 7 UUID, held in +layout+'s byte order:
|
|
126
|
+
# +:rfc9562+ (the default), or +:sql_server+ for a value #to_sql_order returned (the
|
|
127
|
+
# timestamp of the version 7 it was converted from). One native call that checks the
|
|
128
|
+
# variant as well as the version: a 7 in the version nibble under any variant but
|
|
129
|
+
# +:rfc9562+ is not a version 7 UUID, so it has no timestamp (#rfc? is the same check on
|
|
130
|
+
# its own). Every other version has none either, a version 6 or 1 from another generator
|
|
131
|
+
# included.
|
|
129
132
|
#
|
|
130
|
-
# Raises by default for anything that isn't an RFC 9562 version
|
|
133
|
+
# Raises by default for anything that isn't an RFC 9562 version 7 UUID; pass
|
|
131
134
|
# `raise_on_mismatch: false` to get `nil` back instead — for a caller that doesn't already
|
|
132
135
|
# know (or want to separately check) whether this UUID is time-based.
|
|
133
136
|
#
|
|
134
|
-
# @raise [ArgumentError] if this isn't an RFC 9562 version
|
|
137
|
+
# @raise [ArgumentError] if this isn't an RFC 9562 version 7 UUID in +layout+ (unless
|
|
135
138
|
# +raise_on_mismatch+ is false), or +layout+ isn't one of LAYOUTS' keys.
|
|
136
139
|
def timestamp(raise_on_mismatch: true, layout: :rfc9562)
|
|
137
140
|
millis = Runtime.get_timestamp(bytes, layout_code(layout))
|
|
@@ -141,63 +144,39 @@ module HyperUuid
|
|
|
141
144
|
raise ArgumentError, timestamp_mismatch(layout)
|
|
142
145
|
end
|
|
143
146
|
|
|
144
|
-
# Converts an RFC 9562
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
+
# Converts an RFC 9562 version 7 UUID to the layout SQL Server's `uniqueidentifier` needs
|
|
148
|
+
# on the wire to sort by creation order: an RFC 9562 version 8 UUID (§5.8, the version for
|
|
149
|
+
# custom layouts, where §6.11 sends a byte order other than the network one).
|
|
147
150
|
#
|
|
148
151
|
# `System.Data.SqlTypes.SqlGuid` comparison — and therefore T-SQL `ORDER BY` on a
|
|
149
152
|
# `uniqueidentifier` column — doesn't compare a GUID's 16 bytes left to right; it uses a
|
|
150
153
|
# fixed, non-sequential byte significance order (octets 10,11,12,13,14,15,8,9,6,7,4,5,
|
|
151
|
-
# 0,1,2,3, most significant first).
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
# the
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
# significant bytes instead, with `clock_seq`/`node` (independently random per call, not
|
|
162
|
-
# a counter, so no ordering value either way) relocated into the rest. v6's much simpler
|
|
163
|
-
# byte layout needs no bit-level repacking to do this — just whole-octet-group
|
|
164
|
-
# relocation — unlike v7's, and its version/variant land at different sql-order offsets
|
|
165
|
-
# as a result (octet 8's top nibble / octet 6's top two bits, not 7/8).
|
|
166
|
-
#
|
|
167
|
-
# **v6-specific caveat, unlike v7:** two version 6 UUIDs minted at the same millisecond
|
|
168
|
-
# have identical timestamp bits, so they aren't guaranteed to sort in creation order any
|
|
169
|
-
# more than plain RFC order already does — a pre-existing RFC 9562 v6 limitation, not one
|
|
170
|
-
# this transform introduces.
|
|
171
|
-
#
|
|
172
|
-
# Meaningful only for a genuine version 6 or 7 UUID.
|
|
154
|
+
# 0,1,2,3, most significant first). The layout writes the version nibble 8 at octet 7 and
|
|
155
|
+
# the RFC variant at octet 8, where SQL Server's text form and a .NET `Guid` show them, and
|
|
156
|
+
# fills the other 122 bits in that comparison's order: the 48-bit millisecond timestamp,
|
|
157
|
+
# the 26-bit counter and the 48 random bits as one intact block. A version 7 has exactly
|
|
158
|
+
# those 122 free bits, so every one converts, and #from_sql_order gives back the exact
|
|
159
|
+
# original. Computed once in the native Rust core and verified there (and independently,
|
|
160
|
+
# against the real SqlGuid comparator, in this project's C# test suite); this binding calls
|
|
161
|
+
# the same native functions rather than reimplementing the bit math.
|
|
162
|
+
#
|
|
163
|
+
# @raise [ArgumentError] if this isn't an RFC 9562 version 7 UUID.
|
|
173
164
|
def to_sql_order
|
|
174
|
-
|
|
175
|
-
when 7 then self.class.new(Runtime.v7_to_sql_order(bytes), true)
|
|
176
|
-
when 6 then self.class.new(Runtime.v6_to_sql_order(bytes), true)
|
|
177
|
-
else raise ArgumentError, "to_sql_order is only defined for version 6 or 7 UUIDs, got version #{version}"
|
|
178
|
-
end
|
|
165
|
+
self.class.new(Runtime.v7_to_sql_order(bytes), true)
|
|
179
166
|
end
|
|
180
167
|
|
|
181
|
-
# Inverse of #to_sql_order — converts a SQL-Server-ordered
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
# A SQL-ordered value's version sits at a different octet depending on which version
|
|
185
|
-
# produced it, so this asks the native core which one it is — `version(layout:
|
|
186
|
-
# :sql_server)`, which checks each version's nibble together with its variant bits and
|
|
187
|
-
# so never mistakes a v6 whose random `clock_seq` byte happens to read as a v7 nibble.
|
|
168
|
+
# Inverse of #to_sql_order — converts a SQL-Server-ordered value back to the RFC 9562
|
|
169
|
+
# version 7 UUID it was converted from, exactly. Refuses a value without the layout's
|
|
170
|
+
# version 8 and RFC variant, an RFC-ordered version 7 among them.
|
|
188
171
|
#
|
|
189
172
|
# The layout is the caller's to know: bytes alone cannot say which order they are in. A
|
|
190
|
-
# value that was never SQL-ordered can still
|
|
191
|
-
# than refused (a random RFC-ordered v4
|
|
173
|
+
# value that was never SQL-ordered can still carry the layout's 8 and variant, and is then
|
|
174
|
+
# converted rather than refused (a random RFC-ordered v4 does one time in 16, since its
|
|
192
175
|
# octet 8 already carries the RFC variant and its octet 7 is random).
|
|
193
176
|
#
|
|
194
|
-
# @raise [ArgumentError] if this isn't a SQL-ordered
|
|
177
|
+
# @raise [ArgumentError] if this isn't a SQL-ordered value converted from version 7.
|
|
195
178
|
def from_sql_order
|
|
196
|
-
|
|
197
|
-
when 6 then self.class.new(Runtime.v6_to_rfc_order(bytes), true)
|
|
198
|
-
when 7 then self.class.new(Runtime.v7_to_rfc_order(bytes), true)
|
|
199
|
-
else raise ArgumentError, "from_sql_order: not a recognized version 6 or 7 SQL-ordered UUID"
|
|
200
|
-
end
|
|
179
|
+
self.class.new(Runtime.v7_to_rfc_order(bytes), true)
|
|
201
180
|
end
|
|
202
181
|
|
|
203
182
|
# Whether +other+ wraps the same 16 raw bytes.
|
|
@@ -227,11 +206,11 @@ module HyperUuid
|
|
|
227
206
|
|
|
228
207
|
# Why #timestamp found none: the message is worked out only once it is going to be raised.
|
|
229
208
|
def timestamp_mismatch(layout)
|
|
230
|
-
return "timestamp: not a SQL-ordered version
|
|
209
|
+
return "timestamp: not a SQL-ordered version 7 UUID" if layout == :sql_server
|
|
231
210
|
|
|
232
211
|
found = version
|
|
233
|
-
message = "timestamp is only defined for version
|
|
234
|
-
|
|
212
|
+
message = "timestamp is only defined for version 7 UUIDs, got version #{found}"
|
|
213
|
+
found == 7 ? "#{message} with the #{variant.inspect} variant" : message
|
|
235
214
|
end
|
|
236
215
|
|
|
237
216
|
# The native core's code for +layout+, the one place a layout is checked: an unknown one
|
data/lib/hyperuuid.rb
CHANGED
|
@@ -4,21 +4,22 @@ require_relative "hyperuuid/namespaces"
|
|
|
4
4
|
require_relative "hyperuuid/native_platform"
|
|
5
5
|
require_relative "hyperuuid/runtime"
|
|
6
6
|
|
|
7
|
-
# RFC 9562 UUID v4 (random), v5 (deterministic)
|
|
8
|
-
# Rust core, reached through whichever of two
|
|
9
|
-
# linked straight into a Magnus native extension
|
|
10
|
-
# or the native libhyperuuid shared library
|
|
11
|
-
# one per supported platform, picked by
|
|
12
|
-
# either, and nothing compiled at install
|
|
13
|
-
# is at the bottom of
|
|
7
|
+
# RFC 9562 UUID v4 (random), v5 (deterministic) and v7 (time-sortable) generation, with a
|
|
8
|
+
# version 8 SQL Server order for v7, from one Rust core, reached through whichever of two
|
|
9
|
+
# backends this install can load: the core linked straight into a Magnus native extension
|
|
10
|
+
# (shipped precompiled in the platform gems), or the native libhyperuuid shared library
|
|
11
|
+
# called through Fiddle (the universal gem bundles one per supported platform, picked by
|
|
12
|
+
# HyperUuid::NativePlatform). No runtime bridge in either, and nothing compiled at install
|
|
13
|
+
# time. HyperUuid::BACKEND names the one that loaded; the selection logic is at the bottom of
|
|
14
|
+
# this file.
|
|
14
15
|
#
|
|
15
16
|
# The doors below — with Uuid, Namespaces and the two exception classes — are the public
|
|
16
17
|
# surface, and are shared by every backend, argument validation included: a caller bug raises
|
|
17
18
|
# the same error whichever backend is live.
|
|
18
19
|
module HyperUuid
|
|
19
|
-
# This gem's own version — distinct from the RFC 9562 UUID *versions* (v4/v5/
|
|
20
|
-
#
|
|
21
|
-
VERSION = "0.
|
|
20
|
+
# This gem's own version — distinct from the RFC 9562 UUID *versions* (v4/v5/v7) the rest
|
|
21
|
+
# of this module generates.
|
|
22
|
+
VERSION = "1.0.0"
|
|
22
23
|
|
|
23
24
|
# The widest batch count, and the longest v5 name in bytes, the native ABI carries (a u32).
|
|
24
25
|
# (The millisecond count is a u64; unix_millis_from refuses anything wider.)
|
|
@@ -27,8 +28,7 @@ module HyperUuid
|
|
|
27
28
|
|
|
28
29
|
# The most UUIDs one version 7 batch (#new_v7_batch, #new_v7_batch_bytes) mints:
|
|
29
30
|
# 67,108,864, the size of the 26-bit counter that orders UUIDs within a millisecond. A larger
|
|
30
|
-
# count is refused with ArgumentError before anything is allocated.
|
|
31
|
-
# and no such limit.
|
|
31
|
+
# count is refused with ArgumentError before anything is allocated.
|
|
32
32
|
#
|
|
33
33
|
# Every batch up to this size is in strictly increasing order: the counter is one
|
|
34
34
|
# process-wide sequence, so a batch can straddle the point where it wraps back to 0, and the
|
|
@@ -68,7 +68,7 @@ module HyperUuid
|
|
|
68
68
|
# Converts +value+ to a Unix-epoch millisecond integer: +nil+ becomes the current time, a
|
|
69
69
|
# +Time+ is converted exactly (via its own Rational seconds, avoiding float rounding), and
|
|
70
70
|
# an Integer millisecond count passes through unchanged. Shared by every
|
|
71
|
-
# `
|
|
71
|
+
# `new_v7`/batch door below so a caller can pass either a `Time` or a raw
|
|
72
72
|
# millisecond count interchangeably.
|
|
73
73
|
#
|
|
74
74
|
# This is also where a caller bug is caught once, for every backend: anything else is a
|
|
@@ -102,29 +102,6 @@ module HyperUuid
|
|
|
102
102
|
count
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
-
# Creates a time-sortable UUID version 6 (RFC 9562 §5.6), a field-compatible reordering of
|
|
106
|
-
# version 1 for better sort/index locality. Defaults to the current time; pass an explicit
|
|
107
|
-
# `Time` or Unix-epoch millisecond integer to embed a specific time instead. `clock_seq` and
|
|
108
|
-
# `node` are randomly generated on every call — unlike version 7, there is no monotonic
|
|
109
|
-
# counter, so calls within the same millisecond are not guaranteed to sort in creation order.
|
|
110
|
-
#
|
|
111
|
-
# @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
|
|
112
|
-
# @raise [TypeError] if +unix_millis+ isn't a Time, an Integer or nil.
|
|
113
|
-
def self.new_v6(unix_millis = nil)
|
|
114
|
-
Uuid.new(Runtime.new_v6(unix_millis_from(unix_millis, Runtime::V6_TIMESTAMP_OUT_OF_RANGE)), true)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Creates `count` time-sortable version 6 UUIDs sharing one timestamp capture — one FFI call
|
|
118
|
-
# and one random-bytes fetch instead of `count` of each. Defaults to the current time; pass
|
|
119
|
-
# an explicit `Time` or Unix-epoch millisecond integer to embed a specific time instead.
|
|
120
|
-
#
|
|
121
|
-
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
|
|
122
|
-
# @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
|
|
123
|
-
def self.new_v6_batch(count, unix_millis = nil)
|
|
124
|
-
bytes = new_v6_batch_bytes(count, unix_millis)
|
|
125
|
-
Array.new(count) { |i| Uuid.new(bytes[i * 16, 16], true) }
|
|
126
|
-
end
|
|
127
|
-
|
|
128
105
|
# Creates a time-sortable UUID version 7 (RFC 9562 §6.2). Defaults to the current time; pass
|
|
129
106
|
# an explicit `Time` or Unix-epoch millisecond integer (non-negative, fitting in 48 bits) to
|
|
130
107
|
# embed a specific time instead.
|
|
@@ -184,21 +161,6 @@ module HyperUuid
|
|
|
184
161
|
Runtime.new_v7_batch(count, unix_millis_from(unix_millis, Runtime::V7_TIMESTAMP_OUT_OF_RANGE))
|
|
185
162
|
end
|
|
186
163
|
|
|
187
|
-
# Returns `count` version 6 UUIDs as one binary String of raw RFC 9562-ordered bytes,
|
|
188
|
-
# 16 per UUID. The version 6 counterpart to #new_v7_batch_bytes, with the same rationale and
|
|
189
|
-
# the same guidance about when it is the right call.
|
|
190
|
-
#
|
|
191
|
-
# clock_seq and node are independently random per item; unlike version 7 there is no
|
|
192
|
-
# monotonic counter, so items minted in the same millisecond are not guaranteed to sort in
|
|
193
|
-
# creation order.
|
|
194
|
-
#
|
|
195
|
-
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
|
|
196
|
-
# @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
|
|
197
|
-
def self.new_v6_batch_bytes(count, unix_millis = nil)
|
|
198
|
-
count = batch_count(count)
|
|
199
|
-
Runtime.new_v6_batch(count, unix_millis_from(unix_millis, Runtime::V6_TIMESTAMP_OUT_OF_RANGE))
|
|
200
|
-
end
|
|
201
|
-
|
|
202
164
|
# The version of the native core actually loaded, as "major.minor.patch" — read from the
|
|
203
165
|
# library itself (its `hyperuuid_version` export), not from this gem, so a consumer can
|
|
204
166
|
# prove the core behind the doors is the one this binding was built against (and name the
|