hyperuuid 0.6.1-aarch64-linux-gnu → 0.8.0-aarch64-linux-gnu
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 +84 -33
- data/lib/hyperuuid/3.4/hyperuuid_native.so +0 -0
- data/lib/hyperuuid/4.0/hyperuuid_native.so +0 -0
- data/lib/hyperuuid/runtime.rb +89 -2
- data/lib/hyperuuid/uuid.rb +111 -45
- data/lib/hyperuuid.rb +34 -4
- 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: 221d2a2e9a1564888302c7cb069bb0271046f38e93d087cd591b9962201c88d5
|
|
4
|
+
data.tar.gz: d06a8fdde5a46ee78c51c164b44fceed7ae9e73b92e14632e95f7e97cf125aab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 626567b029b27c9bf0d747c47ac64d25b29574b2288ae245bbc4349ac16bd8b3c3d4b0820b291bcc6b9b19672940680d69fae743bcbcb0d75cd42f2f7820000d
|
|
7
|
+
data.tar.gz: e45f5a863b94118fa6a4fabc2a50d01c8c0a2ea1750d3911f55694fa2b2d07fb44f1a213822141a3a3e6eb5c1c63107588985d06862b22db366fae827bce185b
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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` — random v4,
|
|
7
|
+
**Ruby's own stdlib stops at `SecureRandom.uuid` and `SecureRandom.uuid_v7` — random v4 and v7, one at a time. No v5, no v6, no batches, no SQL Server order. This gem is the whole RFC, with zero gem dependency beyond `Fiddle` (which ships with every Ruby install) — and it's faster than `SecureRandom.uuid` too.**
|
|
8
8
|
|
|
9
9
|
RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation, with
|
|
10
10
|
two backends sharing one public surface. Ruby 3.3 is the floor. The fast path is a native
|
|
@@ -26,8 +26,10 @@ id3 = HyperUuid.new_v6
|
|
|
26
26
|
id4 = HyperUuid.new_v7
|
|
27
27
|
|
|
28
28
|
id4.timestamp # recover the embedded UTC Time
|
|
29
|
-
id4.timestamp(raise_on_mismatch: false) # nil instead of raising if id4 isn't v6/v7
|
|
30
|
-
id4.
|
|
29
|
+
id4.timestamp(raise_on_mismatch: false) # nil instead of raising if id4 isn't an RFC 9562 v6/v7
|
|
30
|
+
id4.rfc?(7) # the RFC 9562 variant and version 7, the guard before trusting its fields
|
|
31
|
+
sql = id4.to_sql_order # byte order SQL Server's uniqueidentifier needs to sort by creation order
|
|
32
|
+
sql.timestamp(layout: :sql_server) # read straight from SQL order, no round trip
|
|
31
33
|
|
|
32
34
|
# One native call, one random-bytes fetch, one counter reservation for the whole batch:
|
|
33
35
|
batch = HyperUuid.new_v7_batch(1000)
|
|
@@ -40,7 +42,7 @@ batch = HyperUuid.new_v7_batch(1000)
|
|
|
40
42
|
| `new_v4` | `Uuid` | — |
|
|
41
43
|
| `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 |
|
|
42
44
|
| `new_v6(time = nil)` `new_v7(time = nil)` | `Uuid` | nothing (now), a `Time`, or an Integer of Unix-epoch milliseconds |
|
|
43
|
-
| `new_v6_batch(count, time = nil)` `new_v7_batch(count, time = nil)` | `Array` of `Uuid` | a count, and the same time forms |
|
|
45
|
+
| `new_v6_batch(count, time = nil)` `new_v7_batch(count, time = nil)` | `Array` of `Uuid` | a count (at most `MAX_V7_BATCH` for v7), and the same time forms |
|
|
44
46
|
| `new_v6_batch_bytes(count, time = nil)` `new_v7_batch_bytes(count, time = nil)` | one binary `String`, 16 bytes per UUID | as the batch doors — see [Bulk generation into bytes](#bulk-generation-into-bytes) |
|
|
45
47
|
| `native_version` | the loaded core's `"major.minor.patch"` | — |
|
|
46
48
|
| `available?` | `true`/`false`, never raising | — |
|
|
@@ -53,8 +55,11 @@ runtime dependency on the `uuid` gem:
|
|
|
53
55
|
| `Uuid.parse(text)` | the 8-4-4-4-12 hyphenated form `#to_s` produces, either case, and nothing else — an `ArgumentError` otherwise |
|
|
54
56
|
| `Uuid.new(bytes)` | wraps 16 raw RFC 9562-ordered bytes |
|
|
55
57
|
| `Uuid::NIL` `Uuid::MAX` | the RFC 9562 §5.9/§5.10 special values |
|
|
56
|
-
| `#bytes` `#to_s`
|
|
57
|
-
| `#
|
|
58
|
+
| `#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; 6 or 7 for a SQL-ordered v6/v7 in `:sql_server`, 0 when the bytes don't form one |
|
|
60
|
+
| `#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
|
+
| `#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 6 or 7 UUID held in that layout, in one native call that checks the variant as well as the version (a 6 or 7 nibble under another variant has no timestamp); `nil` instead of an `ArgumentError` for anything that isn't one when passed `false` |
|
|
58
63
|
| `#to_sql_order` `#from_sql_order` | to and from the byte order SQL Server's `uniqueidentifier` sorts by |
|
|
59
64
|
| `==` `eql?` `hash` `<=>` | value equality and byte-order comparison (`Comparable`) |
|
|
60
65
|
|
|
@@ -65,9 +70,52 @@ the native Rust core rather than reimplemented in Ruby, and verified there (and
|
|
|
65
70
|
against the real `System.Data.SqlTypes.SqlGuid` comparator in the C# binding's test suite).
|
|
66
71
|
Same-millisecond v6 UUIDs aren't guaranteed to sort correctly afterward — v6 has no counter,
|
|
67
72
|
so `clock_seq`/`node` (not the timestamp) decide ties, the same pre-existing RFC 9562 v6
|
|
68
|
-
limitation plain order already has. `#from_sql_order`
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
limitation plain order already has. `#from_sql_order` asks the core which version it holds
|
|
74
|
+
(`#version(layout: :sql_server)`, which checks each version's nibble together with its variant
|
|
75
|
+
bits, so a v6 whose random `clock_seq` byte reads as a v7 nibble is still a v6).
|
|
76
|
+
|
|
77
|
+
### Layouts
|
|
78
|
+
|
|
79
|
+
`#version`, `#rfc?` and `#timestamp` take the byte order the value is held in as `layout:`, a
|
|
80
|
+
Symbol: `:rfc9562` (the default, what every other method takes and returns) or `:sql_server`
|
|
81
|
+
(what `#to_sql_order` returns, defined for versions 6 and 7 only). `Uuid::LAYOUTS` lists them.
|
|
82
|
+
Anything else is an `ArgumentError`, never a guess. With the default, `#version` and `#rfc?`
|
|
83
|
+
read the value as RFC 9562 order, which looks at the wrong bytes of a SQL-ordered value (one
|
|
84
|
+
from `#to_sql_order`, or a `uniqueidentifier` read back from SQL Server), so pass
|
|
85
|
+
`layout: :sql_server` at every call site that can see one. There is deliberately no
|
|
86
|
+
layout-agnostic check: whoever holds the value knows its order.
|
|
87
|
+
|
|
88
|
+
`#variant` reads RFC 9562 order only and has no layout form, because in SQL Server order the
|
|
89
|
+
variant sits at a different byte for each version. Don't hand it a SQL-ordered value or a
|
|
90
|
+
`uniqueidentifier` read-back; to validate one, `#rfc?(version, layout: :sql_server)` checks the
|
|
91
|
+
variant where that version puts it.
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
sql = HyperUuid.new_v6.to_sql_order
|
|
95
|
+
sql.version(layout: :sql_server) # => 6
|
|
96
|
+
sql.rfc?(6, layout: :sql_server) # => true
|
|
97
|
+
sql.timestamp(layout: :sql_server) # the same Time as sql.from_sql_order.timestamp
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
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 form a SQL-ordered one (a random v4 reads as a SQL-ordered v7
|
|
102
|
+
one time in 16), and `#from_sql_order` then converts it rather than refusing it.
|
|
103
|
+
|
|
104
|
+
### The version 7 batch limit
|
|
105
|
+
|
|
106
|
+
`HyperUuid::MAX_V7_BATCH` is 67,108,864 (2²⁶), the size of the counter that orders version 7
|
|
107
|
+
UUIDs within a millisecond, and the most one `new_v7_batch` or `new_v7_batch_bytes` call
|
|
108
|
+
mints. A larger count is an `ArgumentError` naming the limit, raised before anything is
|
|
109
|
+
allocated. Every batch is in strictly increasing order: the counter is one process-wide
|
|
110
|
+
sequence, so a batch can straddle the point where it wraps back to 0, and the UUIDs from there
|
|
111
|
+
on carry a timestamp one millisecond later than the one supplied rather than sorting before the
|
|
112
|
+
ones ahead of them. Version 6 has no counter and no such limit.
|
|
113
|
+
|
|
114
|
+
That roll-forward orders one batch, not the stream. The next batch or single `new_v7` in the
|
|
115
|
+
same real millisecond starts its counter just past the wrap and carries the supplied timestamp,
|
|
116
|
+
so it sorts before the previous batch's tail, which was stamped a millisecond later. Two single
|
|
117
|
+
`new_v7` calls either side of the wrap in one millisecond sort in reverse the same way. It
|
|
118
|
+
happens at most once per 2²⁶ UUIDs the process mints.
|
|
71
119
|
|
|
72
120
|
### Errors
|
|
73
121
|
|
|
@@ -84,8 +132,10 @@ Through 0.3.0 these were reachable only as `HyperUuid::Runtime::TimestampOutOfRa
|
|
|
84
132
|
Everything else is a caller bug and raises Ruby's own error for it, checked once in the shared
|
|
85
133
|
doors rather than left to whichever backend is live: a `TypeError` for a time that is not a
|
|
86
134
|
`Time`, an Integer or `nil`, for a count that is not an Integer, for a namespace that is not a
|
|
87
|
-
`Uuid` or a name that is not a `String
|
|
88
|
-
0..2³² − 1
|
|
135
|
+
`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..2³² − 1 (0..`MAX_V7_BATCH` for version 7),
|
|
137
|
+
for a layout other than `:rfc9562`/`:sql_server`, and for a batch too large for the platform to
|
|
138
|
+
address (a 32-bit target such as ruby.wasm).
|
|
89
139
|
|
|
90
140
|
### Is the native core there?
|
|
91
141
|
|
|
@@ -105,14 +155,14 @@ looked for, or `HyperUuid::NativePlatform::UnsupportedPlatformError` naming the
|
|
|
105
155
|
|
|
106
156
|
## Why not `SecureRandom.uuid`?
|
|
107
157
|
|
|
108
|
-
`SecureRandom.uuid`
|
|
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 or v6, 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":
|
|
109
159
|
|
|
110
160
|
1. **Full RFC 9562 coverage, one gem, zero extra dependency.** v4/v5/v6/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.
|
|
111
161
|
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).
|
|
112
162
|
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.
|
|
113
163
|
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.
|
|
114
164
|
|
|
115
|
-
The honest trade-off: this gem is native code, not pure Ruby — a precompiled extension in each platform gem, and on the universal gem a platform-specific `libhyperuuid.so`/`.dylib`/`.dll` loaded through Fiddle — so it runs only where one of those was built. If
|
|
165
|
+
The honest trade-off: this gem is native code, not pure Ruby — a precompiled extension in each platform gem, and on the universal gem a platform-specific `libhyperuuid.so`/`.dylib`/`.dll` loaded through Fiddle — so it runs only where one of those was built. If one v4 or v7 at a time is all you need, `SecureRandom.uuid` and `SecureRandom.uuid_v7` are simpler and already in stdlib — that's a completely reasonable choice.
|
|
116
166
|
|
|
117
167
|
## Bulk generation into bytes
|
|
118
168
|
|
|
@@ -123,7 +173,7 @@ bytes = HyperUuid.new_v7_batch_bytes(1000)
|
|
|
123
173
|
first = bytes[0, 16] # ready for a BINARY(16) bind parameter
|
|
124
174
|
```
|
|
125
175
|
|
|
126
|
-
**About
|
|
176
|
+
**About 40x 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.
|
|
127
177
|
|
|
128
178
|
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.
|
|
129
179
|
|
|
@@ -139,30 +189,30 @@ Real numbers, `benchmark-ips` on Ruby 4.0.7, linux-x64 on an Intel Core i9-11900
|
|
|
139
189
|
|
|
140
190
|
| Call | i/s | vs `SecureRandom.uuid` |
|
|
141
191
|
|---|---:|---:|
|
|
142
|
-
| `SecureRandom.uuid` |
|
|
143
|
-
| `HyperUuid.new_v4` |
|
|
144
|
-
| `HyperUuid.
|
|
145
|
-
| `HyperUuid.
|
|
146
|
-
| `HyperUuid.new_v6` (current time) | 3,
|
|
147
|
-
| `HyperUuid.new_v7` (current time) | 3,
|
|
148
|
-
| `HyperUuid.new_v5` | 1,
|
|
192
|
+
| `SecureRandom.uuid` | 943,917 | baseline |
|
|
193
|
+
| `HyperUuid.new_v4` | 5,236,000 | **5.5x faster** |
|
|
194
|
+
| `HyperUuid.new_v6` (explicit ms) | 3,811,000 | **4.0x faster** |
|
|
195
|
+
| `HyperUuid.new_v7` (explicit ms) | 3,785,000 | **4.0x faster** |
|
|
196
|
+
| `HyperUuid.new_v6` (current time) | 3,677,000 | **3.9x faster** |
|
|
197
|
+
| `HyperUuid.new_v7` (current time) | 3,574,000 | **3.8x faster** |
|
|
198
|
+
| `HyperUuid.new_v5` | 1,942,000 | 2.1x faster |
|
|
149
199
|
|
|
150
|
-
A `HyperUuid.new_v4` — real entropy, correct version/variant bits, minted by the shared Rust core — costs a fifth of what `SecureRandom.uuid` does, because the Magnus extension is an ordinary native method call with nothing marshalled around it.
|
|
200
|
+
A `HyperUuid.new_v4` — real entropy, correct version/variant bits, minted by the shared Rust core — costs less than a fifth of what `SecureRandom.uuid` does, because the Magnus extension is an ordinary native method call with nothing marshalled around it.
|
|
151
201
|
|
|
152
202
|
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.
|
|
153
203
|
|
|
154
|
-
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.
|
|
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 v6/v7, with the same structural story as before: `Fiddle`'s interpreted marshalling is the floor, and the batch doors are how you amortize it.
|
|
155
205
|
|
|
156
206
|
Batch generation still amortizes per-call cost on both backends — one native call for the whole batch:
|
|
157
207
|
|
|
158
208
|
| Call | i/s (Magnus backend) |
|
|
159
209
|
|---|---:|
|
|
160
|
-
| `new_v6` × 1000 (individual) | 3,
|
|
161
|
-
| `new_v6_batch(1000)` | 4,
|
|
162
|
-
| `new_v7` × 1000 (individual) | 3,
|
|
163
|
-
| `new_v7_batch(1000)` | 4,
|
|
210
|
+
| `new_v6` × 1000 (individual) | 3,761 |
|
|
211
|
+
| `new_v6_batch(1000)` | 4,857 (**1.3x**) |
|
|
212
|
+
| `new_v7` × 1000 (individual) | 3,742 |
|
|
213
|
+
| `new_v7_batch(1000)` | 4,850 (**1.3x**) |
|
|
164
214
|
|
|
165
|
-
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
|
|
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 5 µs. On the Fiddle backend, where each call costs 1.4 µs, the same batch is 6.7x the loop. If you need v5/v6/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.
|
|
166
216
|
|
|
167
217
|
## Backends
|
|
168
218
|
|
|
@@ -248,8 +298,7 @@ one at `require` time:
|
|
|
248
298
|
| 3.4 (until its EOL 2028-03-31) | Magnus, `BACKEND == :native` | platform |
|
|
249
299
|
| 3.3 (the floor, until its EOL 2027-03-31) | Fiddle | universal |
|
|
250
300
|
|
|
251
|
-
What stands behind each cell: CI runs the whole suite through each Magnus extension on every
|
|
252
|
-
push — Ruby 3.4 and 4.0 on all seven platform-gem platforms, the two musl ones inside each
|
|
301
|
+
What stands behind each cell: CI runs the whole suite through each Magnus extension — Ruby 4.0 on every pull request and Ruby 3.4 in the weekly and release builds, on all seven platform-gem platforms, the two musl ones inside each
|
|
253
302
|
Ruby's own Alpine image — and the Fiddle suite on Ruby 4.0 on every one of them, plus on Ruby
|
|
254
303
|
3.3 inside Alpine. Intel macOS has no CI leg: its library is cross-built and tested at the
|
|
255
304
|
core, and Ruby there runs the universal gem's Fiddle backend over it. Anywhere else — a platform with no native build at
|
|
@@ -367,6 +416,7 @@ bundle install
|
|
|
367
416
|
bundle exec rake native:dev # build the Magnus extension for this Ruby and stage it
|
|
368
417
|
bundle exec rspec # BACKEND == :native
|
|
369
418
|
HYPERUUID_PURE=1 bundle exec rspec # BACKEND == :fiddle
|
|
419
|
+
HYPERUUID_BIG_BATCH=1 bundle exec rspec spec/inspection_spec.rb # adds the 1 GiB MAX_V7_BATCH batch
|
|
370
420
|
bundle exec rake docs:check # every public object carries a doc comment
|
|
371
421
|
ruby benchmark/uuid_benchmark.rb # prints the backend it measured
|
|
372
422
|
```
|
|
@@ -393,11 +443,12 @@ flags in the forge's `build-magnus.sh`, which is also what CI uses on every plat
|
|
|
393
443
|
|
|
394
444
|
The musl build has no host to run on outside a container. With the core built for musl at
|
|
395
445
|
`rust/target/musl/linux-musl-x64/libhyperuuid.so`, this runs the Fiddle suite against it on
|
|
396
|
-
Alpine, with the checkout mounted read-only:
|
|
446
|
+
Alpine, with the checkout mounted read-only (`corpus/` comes along: `spec/corpus_spec.rb` finds
|
|
447
|
+
it by walking up from the spec):
|
|
397
448
|
|
|
398
449
|
```sh
|
|
399
450
|
docker run --rm -v "$PWD/..":/src:ro ruby:4.0-alpine sh -euc '
|
|
400
|
-
mkdir /work && cp -r /src/ruby /work/
|
|
451
|
+
mkdir /work && cp -r /src/ruby /src/corpus /work/
|
|
401
452
|
mkdir -p /work/ruby/lib/hyperuuid/native/linux-musl-x64
|
|
402
453
|
cp /src/rust/target/musl/linux-musl-x64/libhyperuuid.so /work/ruby/lib/hyperuuid/native/linux-musl-x64/
|
|
403
454
|
cd /work/ruby && rm -f Gemfile.lock
|
|
@@ -411,7 +462,7 @@ Fiddle that Ruby 3.3 ships:
|
|
|
411
462
|
|
|
412
463
|
```sh
|
|
413
464
|
docker run --rm -v "$PWD/..":/src:ro ruby:3.3-alpine sh -euc '
|
|
414
|
-
mkdir /work && cp -r /src/ruby /work/
|
|
465
|
+
mkdir /work && cp -r /src/ruby /src/corpus /work/
|
|
415
466
|
mkdir -p /work/ruby/lib/hyperuuid/native/linux-musl-x64
|
|
416
467
|
cp /src/rust/target/musl/linux-musl-x64/libhyperuuid.so /work/ruby/lib/hyperuuid/native/linux-musl-x64/
|
|
417
468
|
cd /work/ruby && rm -f Gemfile Gemfile.lock
|
|
Binary file
|
|
Binary file
|
data/lib/hyperuuid/runtime.rb
CHANGED
|
@@ -28,6 +28,17 @@ module HyperUuid
|
|
|
28
28
|
V6_TIMESTAMP_OUT_OF_RANGE = "unix_millis does not fit the 60-bit v6 timestamp field"
|
|
29
29
|
V7_TIMESTAMP_OUT_OF_RANGE = "unix_millis must fit within the RFC 9562 48-bit field"
|
|
30
30
|
|
|
31
|
+
# The two batch-size refusals, shared the same way: a version 7 batch past the 26-bit
|
|
32
|
+
# counter space (the core's return code 4, which the doors in hyperuuid.rb refuse before
|
|
33
|
+
# either backend sees it), and a batch whose byte length the platform cannot address
|
|
34
|
+
# (code 3, reachable only on a 32-bit target such as ruby.wasm). Both are argument
|
|
35
|
+
# errors, never a random source failure.
|
|
36
|
+
V7_BATCH_TOO_LARGE = "a single version 7 batch takes at most 67108864 UUIDs (the 26-bit counter space)"
|
|
37
|
+
BATCH_TOO_LARGE_TO_ADDRESS = "the batch is too large to address on this platform"
|
|
38
|
+
|
|
39
|
+
# The core's v7::MAX_BATCH, which hyperuuid.rb publishes as HyperUuid::MAX_V7_BATCH.
|
|
40
|
+
MAX_V7_BATCH = 1 << 26
|
|
41
|
+
|
|
31
42
|
NATIVE_DIR = File.join(__dir__, "native")
|
|
32
43
|
|
|
33
44
|
@mutex = Mutex.new
|
|
@@ -38,6 +49,7 @@ module HyperUuid
|
|
|
38
49
|
out = scratch
|
|
39
50
|
rc = functions[:new_v4].call(out)
|
|
40
51
|
raise random_source_failure("uuid_new_v4") unless rc.zero?
|
|
52
|
+
|
|
41
53
|
out[0, 16]
|
|
42
54
|
end
|
|
43
55
|
|
|
@@ -49,6 +61,7 @@ module HyperUuid
|
|
|
49
61
|
# contract.
|
|
50
62
|
rc = functions[:new_v5].call(namespace_bytes, name_bytes, name_bytes.bytesize, out)
|
|
51
63
|
raise random_source_failure("uuid_new_v5") unless rc.zero?
|
|
64
|
+
|
|
52
65
|
out[0, 16]
|
|
53
66
|
end
|
|
54
67
|
|
|
@@ -68,11 +81,13 @@ module HyperUuid
|
|
|
68
81
|
|
|
69
82
|
def new_v6_batch(count, unix_millis)
|
|
70
83
|
return "" if count.zero?
|
|
84
|
+
|
|
71
85
|
out = buffer(count * 16)
|
|
72
86
|
rc = functions[:new_v6_batch].call(unix_millis, count, out)
|
|
73
87
|
case rc
|
|
74
88
|
when 0 then out[0, count * 16]
|
|
75
89
|
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
90
|
+
when 3 then raise ArgumentError, BATCH_TOO_LARGE_TO_ADDRESS
|
|
76
91
|
else raise random_source_failure("uuid_new_v6_batch")
|
|
77
92
|
end
|
|
78
93
|
end
|
|
@@ -93,15 +108,55 @@ module HyperUuid
|
|
|
93
108
|
|
|
94
109
|
def new_v7_batch(count, unix_millis)
|
|
95
110
|
return "" if count.zero?
|
|
111
|
+
# Before the buffer: a count past the counter space would otherwise allocate up to
|
|
112
|
+
# 64 GiB only for the core to refuse it with code 4.
|
|
113
|
+
raise ArgumentError, V7_BATCH_TOO_LARGE if count > MAX_V7_BATCH
|
|
114
|
+
|
|
96
115
|
out = buffer(count * 16)
|
|
97
116
|
rc = functions[:new_v7_batch].call(unix_millis, count, out)
|
|
98
117
|
case rc
|
|
99
118
|
when 0 then out[0, count * 16]
|
|
100
119
|
when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
|
|
120
|
+
when 3 then raise ArgumentError, BATCH_TOO_LARGE_TO_ADDRESS
|
|
121
|
+
when 4 then raise ArgumentError, V7_BATCH_TOO_LARGE
|
|
101
122
|
else raise random_source_failure("uuid_new_v7_batch")
|
|
102
123
|
end
|
|
103
124
|
end
|
|
104
125
|
|
|
126
|
+
# The inspection exports. +layout+ is the core's layout code (1 RFC 9562, 2 SQL Server),
|
|
127
|
+
# which Uuid has already validated; +bytes+ are 16, held in that layout's order.
|
|
128
|
+
def version(bytes, layout)
|
|
129
|
+
functions[:version].call(bytes, layout)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The core's variant code: 1 NCS, 2 RFC 9562, 3 Microsoft, 4 future (Uuid#variant maps
|
|
133
|
+
# it to a Symbol, for both backends).
|
|
134
|
+
def variant(bytes)
|
|
135
|
+
functions[:variant].call(bytes)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# +version+ is 0-15 here: Uuid#rfc? answers false for anything else without asking.
|
|
139
|
+
def is_rfc(bytes, version, layout)
|
|
140
|
+
functions[:is_rfc].call(bytes, version, layout) == 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The Unix ms of an RFC 9562 version 6 or 7 UUID held in +layout+'s order, or nil for
|
|
144
|
+
# anything else — another version, another variant. One core call; the version it
|
|
145
|
+
# answers with is not needed above Runtime, so it is dropped here.
|
|
146
|
+
def get_timestamp(bytes, layout)
|
|
147
|
+
out = millis_scratch
|
|
148
|
+
version = functions[:get_timestamp].call(bytes, layout, out)
|
|
149
|
+
version.zero? ? nil : out[0, 8].unpack1("Q")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def v6_unix_millis_in(bytes, layout)
|
|
153
|
+
functions[:v6_unix_millis_in].call(bytes, layout)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def v7_unix_millis_in(bytes, layout)
|
|
157
|
+
functions[:v7_unix_millis_in].call(bytes, layout)
|
|
158
|
+
end
|
|
159
|
+
|
|
105
160
|
def v7_to_sql_order(bytes)
|
|
106
161
|
rewrite(:v7_to_sql_order, bytes)
|
|
107
162
|
end
|
|
@@ -122,7 +177,7 @@ module HyperUuid
|
|
|
122
177
|
# library's zero-argument hyperuuid_version export. HyperUuid.native_version unpacks
|
|
123
178
|
# it; like every method here, each backend replaces this one in place.
|
|
124
179
|
def packed_version
|
|
125
|
-
functions[:
|
|
180
|
+
functions[:packed_version].call
|
|
126
181
|
end
|
|
127
182
|
|
|
128
183
|
# The one failure every generating export shares, with the one message every backend
|
|
@@ -183,6 +238,12 @@ module HyperUuid
|
|
|
183
238
|
Thread.current[:hyperuuid_scratch] ||= buffer(16)
|
|
184
239
|
end
|
|
185
240
|
|
|
241
|
+
# The u64 get_timestamp writes, per thread for the same reason as #scratch, and apart from
|
|
242
|
+
# it so the UUID bytes passed in are never the buffer being written.
|
|
243
|
+
def millis_scratch
|
|
244
|
+
Thread.current[:hyperuuid_millis_scratch] ||= buffer(8)
|
|
245
|
+
end
|
|
246
|
+
|
|
186
247
|
# Every Fiddle allocation goes through here, and loads the library first: that is what
|
|
187
248
|
# keeps a missing library reported as missing_library_message instead of as whatever
|
|
188
249
|
# touching Fiddle raises first. The scratch buffer pays for it once per thread.
|
|
@@ -270,8 +331,34 @@ module HyperUuid
|
|
|
270
331
|
[Fiddle::TYPE_VOIDP],
|
|
271
332
|
Fiddle::TYPE_VOID
|
|
272
333
|
),
|
|
334
|
+
version: Fiddle::Function.new(
|
|
335
|
+
handle["uuid_version"],
|
|
336
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T],
|
|
337
|
+
Fiddle::TYPE_UINT32_T
|
|
338
|
+
),
|
|
339
|
+
variant: Fiddle::Function.new(handle["uuid_variant"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_UINT32_T),
|
|
340
|
+
is_rfc: Fiddle::Function.new(
|
|
341
|
+
handle["uuid_is_rfc"],
|
|
342
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_UINT32_T],
|
|
343
|
+
Fiddle::TYPE_UINT32_T
|
|
344
|
+
),
|
|
345
|
+
get_timestamp: Fiddle::Function.new(
|
|
346
|
+
handle["uuid_get_timestamp"],
|
|
347
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
|
|
348
|
+
Fiddle::TYPE_UINT32_T
|
|
349
|
+
),
|
|
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
|
+
v7_unix_millis_in: Fiddle::Function.new(
|
|
356
|
+
handle["uuid_v7_unix_millis_in"],
|
|
357
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T],
|
|
358
|
+
Fiddle::TYPE_UINT64_T
|
|
359
|
+
),
|
|
273
360
|
# The one export that mints nothing: the zero-argument version probe.
|
|
274
|
-
|
|
361
|
+
packed_version: Fiddle::Function.new(handle["hyperuuid_version"], [], Fiddle::TYPE_UINT32_T),
|
|
275
362
|
}
|
|
276
363
|
end
|
|
277
364
|
end
|
data/lib/hyperuuid/uuid.rb
CHANGED
|
@@ -38,6 +38,17 @@ module HyperUuid
|
|
|
38
38
|
HYPHENATED = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
|
|
39
39
|
private_constant :HYPHENATED
|
|
40
40
|
|
|
41
|
+
# The byte orders a Uuid can hold, as the +layout:+ keyword of #version, #rfc? and
|
|
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 order
|
|
44
|
+
# #to_sql_order returns, defined for versions 6 and 7 only.
|
|
45
|
+
LAYOUTS = { rfc9562: 1, sql_server: 2 }.freeze
|
|
46
|
+
|
|
47
|
+
# The Symbols #variant returns, indexed by the native core's variant code (1-4; 0 is
|
|
48
|
+
# reserved and never returned). The one place that code becomes a Symbol, for both backends.
|
|
49
|
+
VARIANTS = [nil, :ncs, :rfc9562, :microsoft, :future].freeze
|
|
50
|
+
private_constant :VARIANTS
|
|
51
|
+
|
|
41
52
|
# Parses an 8-4-4-4-12 hyphenated hex UUID string — exactly the shape #to_s produces, in
|
|
42
53
|
# either case. Nothing else parses: not the bare 32 hex digits, not hyphens anywhere but
|
|
43
54
|
# those four positions, not braces or a `urn:uuid:` prefix.
|
|
@@ -51,14 +62,56 @@ module HyperUuid
|
|
|
51
62
|
new([string.delete("-")].pack("H*"))
|
|
52
63
|
end
|
|
53
64
|
|
|
54
|
-
# The
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
# The version of this UUID, read by the native core in +layout+'s byte order.
|
|
66
|
+
#
|
|
67
|
+
# In +:rfc9562+ (the default) it is the version nibble (bits 48-51, the high nibble of
|
|
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 order #to_sql_order returns, it is 6 or 7 for a
|
|
70
|
+
# SQL-ordered version 6 or 7 UUID and 0 when the bytes don't form one, since no other
|
|
71
|
+
# version has a SQL Server order. It reads the bytes as given: an RFC-ordered value can
|
|
72
|
+
# happen to form a SQL-ordered v6 or v7, so the layout is the caller's to know.
|
|
73
|
+
#
|
|
74
|
+
# With the default layout, the value is read as RFC 9562 order. A SQL-ordered value — from
|
|
75
|
+
# #to_sql_order, or a +uniqueidentifier+ read back from SQL Server — needs
|
|
76
|
+
# +layout: :sql_server+, because the RFC-order read looks at the wrong bytes there. There
|
|
77
|
+
# is deliberately no layout-agnostic check: whoever holds the value knows its order.
|
|
78
|
+
#
|
|
79
|
+
# @raise [ArgumentError] if +layout+ isn't one of LAYOUTS' keys.
|
|
80
|
+
def version(layout: :rfc9562)
|
|
81
|
+
Runtime.version(bytes, layout_code(layout))
|
|
57
82
|
end
|
|
58
83
|
|
|
59
|
-
# The
|
|
84
|
+
# The variant of this UUID (RFC 9562 §4.1), read by the native core in RFC 9562 order, as a
|
|
85
|
+
# Symbol: +:rfc9562+ (+10xx+, every UUID this gem mints, and the only variant that has
|
|
86
|
+
# versions), +:ncs+ (+0xxx+, Nil among them), +:microsoft+ (+110x+) or +:future+ (+111x+,
|
|
87
|
+
# Max among them).
|
|
88
|
+
#
|
|
89
|
+
# RFC 9562 order only. There is no layout form, because in SQL Server order the variant
|
|
90
|
+
# sits at a different byte for each version, so don't hand it a SQL-ordered value or a
|
|
91
|
+
# +uniqueidentifier+ read back from SQL Server. To validate one of those, use
|
|
92
|
+
# <tt>rfc?(version, layout: :sql_server)</tt>, which checks the variant where that version
|
|
93
|
+
# puts it.
|
|
60
94
|
def variant
|
|
61
|
-
(
|
|
95
|
+
VARIANTS.fetch(Runtime.variant(bytes))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Whether this is an RFC 9562 UUID of version +version+, read in +layout+'s byte order —
|
|
99
|
+
# the RFC 9562 variant and that version together, the guard to run before trusting a
|
|
100
|
+
# value's version-specific fields. In +:sql_server+ only 6 and 7 can be true. A version
|
|
101
|
+
# outside 0-15 is simply false.
|
|
102
|
+
#
|
|
103
|
+
# With the default layout, the value is read as RFC 9562 order. A SQL-ordered value — from
|
|
104
|
+
# #to_sql_order, or a +uniqueidentifier+ read back from SQL Server — needs
|
|
105
|
+
# +layout: :sql_server+, because the RFC-order read looks at the wrong bytes there. There
|
|
106
|
+
# is deliberately no layout-agnostic check: whoever holds the value knows its order.
|
|
107
|
+
#
|
|
108
|
+
# @raise [TypeError] if +version+ isn't an Integer.
|
|
109
|
+
# @raise [ArgumentError] if +layout+ isn't one of LAYOUTS' keys.
|
|
110
|
+
def rfc?(version, layout: :rfc9562)
|
|
111
|
+
raise TypeError, "version must be an Integer; got #{version.class}" unless version.is_a?(Integer)
|
|
112
|
+
|
|
113
|
+
code = layout_code(layout)
|
|
114
|
+
version.between?(0, 15) && Runtime.is_rfc(bytes, version, code)
|
|
62
115
|
end
|
|
63
116
|
|
|
64
117
|
# The 8-4-4-4-12 hyphenated hex string representation.
|
|
@@ -68,24 +121,24 @@ module HyperUuid
|
|
|
68
121
|
end
|
|
69
122
|
alias_method :to_str, :to_s
|
|
70
123
|
|
|
71
|
-
# The UTC timestamp embedded in
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
124
|
+
# The UTC timestamp embedded in an RFC 9562 version 6 or 7 UUID, held in +layout+'s byte
|
|
125
|
+
# order: +:rfc9562+ (the default), or +:sql_server+ for a value #to_sql_order returned. One
|
|
126
|
+
# native call that checks the variant as well as the version: a 6 or 7 in the version
|
|
127
|
+
# nibble under any variant but +:rfc9562+ is not a version 6 or 7 UUID, so it has no
|
|
128
|
+
# timestamp (#rfc? is the same check on its own).
|
|
129
|
+
#
|
|
130
|
+
# Raises by default for anything that isn't an RFC 9562 version 6 or 7 UUID; pass
|
|
131
|
+
# `raise_on_mismatch: false` to get `nil` back instead — for a caller that doesn't already
|
|
132
|
+
# know (or want to separately check) whether this UUID is time-based.
|
|
133
|
+
#
|
|
134
|
+
# @raise [ArgumentError] if this isn't an RFC 9562 version 6 or 7 UUID in +layout+ (unless
|
|
135
|
+
# +raise_on_mismatch+ is false), or +layout+ isn't one of LAYOUTS' keys.
|
|
136
|
+
def timestamp(raise_on_mismatch: true, layout: :rfc9562)
|
|
137
|
+
millis = Runtime.get_timestamp(bytes, layout_code(layout))
|
|
138
|
+
return Time.at(millis / 1000, millis % 1000, :millisecond).utc if millis
|
|
139
|
+
return nil unless raise_on_mismatch
|
|
140
|
+
|
|
141
|
+
raise ArgumentError, timestamp_mismatch(layout)
|
|
89
142
|
end
|
|
90
143
|
|
|
91
144
|
# Converts an RFC 9562-ordered version 6 or 7 UUID to the byte order SQL Server's
|
|
@@ -128,29 +181,22 @@ module HyperUuid
|
|
|
128
181
|
# Inverse of #to_sql_order — converts a SQL-Server-ordered version 6 or 7 UUID back to
|
|
129
182
|
# RFC 9562 order.
|
|
130
183
|
#
|
|
131
|
-
# A SQL-ordered value's version
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
# byte, which has a real (~1-in-16) chance of a top nibble that also happens to read 0x7 —
|
|
143
|
-
# confirmed by an actual test failure during development, not a hypothetical. Checking
|
|
144
|
-
# octet 8 first rules v6 in or out unambiguously before octet 7's reading can matter.
|
|
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.
|
|
188
|
+
#
|
|
189
|
+
# 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 read as one, and is then converted rather
|
|
191
|
+
# than refused (a random RFC-ordered v4 reads as a SQL-ordered v7 one time in 16, since its
|
|
192
|
+
# octet 8 already carries the RFC variant and its octet 7 is random).
|
|
193
|
+
#
|
|
194
|
+
# @raise [ArgumentError] if this isn't a SQL-ordered version 6 or 7 UUID.
|
|
145
195
|
def from_sql_order
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
elsif octet7_version == 7
|
|
151
|
-
self.class.new(Runtime.v7_to_rfc_order(bytes), true)
|
|
152
|
-
else
|
|
153
|
-
raise ArgumentError, "from_sql_order: not a recognized version 6 or 7 SQL-ordered UUID"
|
|
196
|
+
case Runtime.version(bytes, LAYOUTS[:sql_server])
|
|
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"
|
|
154
200
|
end
|
|
155
201
|
end
|
|
156
202
|
|
|
@@ -168,6 +214,7 @@ module HyperUuid
|
|
|
168
214
|
# Byte-order comparison against +other+, or +nil+ if +other+ isn't a Uuid.
|
|
169
215
|
def <=>(other)
|
|
170
216
|
return nil unless other.is_a?(Uuid)
|
|
217
|
+
|
|
171
218
|
bytes <=> other.bytes
|
|
172
219
|
end
|
|
173
220
|
|
|
@@ -175,5 +222,24 @@ module HyperUuid
|
|
|
175
222
|
def inspect
|
|
176
223
|
"#<HyperUuid::Uuid #{self}>"
|
|
177
224
|
end
|
|
225
|
+
|
|
226
|
+
private
|
|
227
|
+
|
|
228
|
+
# Why #timestamp found none: the message is worked out only once it is going to be raised.
|
|
229
|
+
def timestamp_mismatch(layout)
|
|
230
|
+
return "timestamp: not a SQL-ordered version 6 or 7 UUID" if layout == :sql_server
|
|
231
|
+
|
|
232
|
+
found = version
|
|
233
|
+
message = "timestamp is only defined for version 6 or 7 UUIDs, got version #{found}"
|
|
234
|
+
[6, 7].include?(found) ? "#{message} with the #{variant.inspect} variant" : message
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# The native core's code for +layout+, the one place a layout is checked: an unknown one
|
|
238
|
+
# is refused here, never passed through to the core.
|
|
239
|
+
def layout_code(layout)
|
|
240
|
+
LAYOUTS.fetch(layout) do
|
|
241
|
+
raise ArgumentError, "layout must be one of #{LAYOUTS.keys.map(&:inspect).join(', ')}; got #{layout.inspect}"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
178
244
|
end
|
|
179
245
|
end
|
data/lib/hyperuuid.rb
CHANGED
|
@@ -18,13 +18,28 @@ require_relative "hyperuuid/runtime"
|
|
|
18
18
|
module HyperUuid
|
|
19
19
|
# This gem's own version — distinct from the RFC 9562 UUID *versions* (v4/v5/v6/v7) the
|
|
20
20
|
# rest of this module generates.
|
|
21
|
-
VERSION = "0.
|
|
21
|
+
VERSION = "0.8.0"
|
|
22
22
|
|
|
23
23
|
# The widest batch count, and the longest v5 name in bytes, the native ABI carries (a u32).
|
|
24
24
|
# (The millisecond count is a u64; unix_millis_from refuses anything wider.)
|
|
25
25
|
U32_MAX = 0xFFFF_FFFF
|
|
26
26
|
private_constant :U32_MAX
|
|
27
27
|
|
|
28
|
+
# The most UUIDs one version 7 batch (#new_v7_batch, #new_v7_batch_bytes) mints:
|
|
29
|
+
# 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. Version 6 has no counter
|
|
31
|
+
# and no such limit.
|
|
32
|
+
#
|
|
33
|
+
# Every batch up to this size is in strictly increasing order: the counter is one
|
|
34
|
+
# process-wide sequence, so a batch can straddle the point where it wraps back to 0, and the
|
|
35
|
+
# UUIDs from there on carry a timestamp one millisecond later than the one supplied.
|
|
36
|
+
# The roll-forward orders one batch, not the stream: the next batch or single #new_v7 in
|
|
37
|
+
# the same real millisecond starts its counter just past the wrap and carries the supplied
|
|
38
|
+
# timestamp, so it sorts before this batch's tail, which was stamped a millisecond later.
|
|
39
|
+
# Two single #new_v7 calls either side of the wrap in one millisecond sort in reverse the
|
|
40
|
+
# same way. It happens at most once per 2**26 UUIDs the process mints.
|
|
41
|
+
MAX_V7_BATCH = Runtime::MAX_V7_BATCH
|
|
42
|
+
|
|
28
43
|
# Creates a random UUID version 4 (RFC 9562 §5.4).
|
|
29
44
|
def self.new_v4
|
|
30
45
|
Uuid.new(Runtime.new_v4, true)
|
|
@@ -125,7 +140,18 @@ module HyperUuid
|
|
|
125
140
|
# instead of `count` of each. Defaults to the current time; pass an explicit `Time` or
|
|
126
141
|
# Unix-epoch millisecond integer to embed a specific time instead.
|
|
127
142
|
#
|
|
128
|
-
#
|
|
143
|
+
# Every batch is in strictly increasing order. The counter is one process-wide sequence, so
|
|
144
|
+
# a batch can straddle the point where it wraps back to 0; the UUIDs from there on carry a
|
|
145
|
+
# timestamp one millisecond later than the one supplied rather than sorting before the ones
|
|
146
|
+
# ahead of them. That is also why one batch takes at most MAX_V7_BATCH.
|
|
147
|
+
#
|
|
148
|
+
# The roll-forward orders one batch, not the stream: the next batch or single #new_v7 in
|
|
149
|
+
# the same real millisecond starts its counter just past the wrap and carries the supplied
|
|
150
|
+
# timestamp, so it sorts before this batch's tail, which was stamped a millisecond later.
|
|
151
|
+
# Two single #new_v7 calls either side of the wrap in one millisecond sort in reverse the
|
|
152
|
+
# same way. It happens at most once per 2**26 UUIDs the process mints.
|
|
153
|
+
#
|
|
154
|
+
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and MAX_V7_BATCH.
|
|
129
155
|
# @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
|
|
130
156
|
def self.new_v7_batch(count, unix_millis = nil)
|
|
131
157
|
bytes = new_v7_batch_bytes(count, unix_millis)
|
|
@@ -145,11 +171,15 @@ module HyperUuid
|
|
|
145
171
|
# format, a bulk COPY. If you need Uuid objects, keep using #new_v7_batch — slicing this
|
|
146
172
|
# String into them yourself just moves the same allocations into your own code.
|
|
147
173
|
#
|
|
148
|
-
# Slice it with `bytes[i * 16, 16]`, which is what #new_v7_batch does internally.
|
|
174
|
+
# Slice it with `bytes[i * 16, 16]`, which is what #new_v7_batch does internally. The
|
|
175
|
+
# order, the possible extra millisecond and the MAX_V7_BATCH limit are #new_v7_batch's.
|
|
149
176
|
#
|
|
150
|
-
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and
|
|
177
|
+
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and MAX_V7_BATCH.
|
|
151
178
|
# @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
|
|
152
179
|
def self.new_v7_batch_bytes(count, unix_millis = nil)
|
|
180
|
+
# First, so any count past the limit (2**32 and beyond included) names the limit itself.
|
|
181
|
+
raise ArgumentError, Runtime::V7_BATCH_TOO_LARGE if count.is_a?(Integer) && count > MAX_V7_BATCH
|
|
182
|
+
|
|
153
183
|
count = batch_count(count)
|
|
154
184
|
Runtime.new_v7_batch(count, unix_millis_from(unix_millis, Runtime::V7_TIMESTAMP_OUT_OF_RANGE))
|
|
155
185
|
end
|