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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba3ad5f11b5973d062a7dfff6d85f657a809873d39b166c7837ee30d99092a2d
4
- data.tar.gz: efde3c81dba7991a8238aaa2929f0abcf026c8c6b6b6454af334a2c6473860d1
3
+ metadata.gz: 23b7842046b78dff468073474bdb004a03c6d4710ff13a7ff3f048c879d141f9
4
+ data.tar.gz: 04204e9e6c775cb35a8370b53ffd8fc8c7538ad9220fffc5a05fc7e76c07ec7a
5
5
  SHA512:
6
- metadata.gz: 5e10e781dad666e3734d3bb01c05cf1eeb537e309e5a2e733561153a02bca3817cfe1b6358b4a2129eb86446be2f79cd0d72c210efbea37b0600c2776ed716e0
7
- data.tar.gz: cf012e5d94e0bbe5c9edd0d9c437fef470238deedad2871ab2aa74d3fc09323b07a47de12e98d9bbb7fb9e5a0cb7d26e98d3b76320f36930b84d904310405901
6
+ metadata.gz: b1dd276963bf50735b2e9f7cd1be16dbeb7c7edebef8c027471cbaf6686237ef971b91f87f328a42650232b25e2bd5bd5d0cdb39309674fe2f5f743b58553a0d
7
+ data.tar.gz: 9e2ee0585a21dbdd3266ef635134e2013a09b3cb5538427a0b71df0e9126d4d9d3fbc7f8a0dc73f9b4ed5579c3f1c181e8d2de73b54e04c240b6e1eb8701637e
data/README.md CHANGED
@@ -4,10 +4,10 @@
4
4
  [![RubyGems](https://img.shields.io/gem/v/hyperuuid.svg)](https://rubygems.org/gems/hyperuuid)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 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.**
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), v6 and v7 (time-sortable) generation, with
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.new_v6
26
- id4 = HyperUuid.new_v7
25
+ id3 = HyperUuid.new_v7
27
26
 
28
- id4.timestamp # recover the embedded UTC Time
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
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
- | `new_v6(time = nil)` `new_v7(time = nil)` | `Uuid` | nothing (now), a `Time`, or an Integer of Unix-epoch milliseconds |
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 |
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) |
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; 6 or 7 for a SQL-ordered v6/v7 in `:sql_server`, 0 when the bytes don't form one |
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 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` |
63
- | `#to_sql_order` `#from_sql_order` | to and from the byte order SQL Server's `uniqueidentifier` sorts by |
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 6 or 7 UUID to and from the byte order SQL
67
- Server's `uniqueidentifier` needs on the wire to sort by creation order (`#to_sql_order`
68
- dispatches on the UUID's own version, matching `#timestamp`'s convention) — computed once in
69
- the native Rust core rather than reimplemented in Ruby, and verified there (and independently
70
- against the real `System.Data.SqlTypes.SqlGuid` comparator in the C# binding's test suite).
71
- Same-millisecond v6 UUIDs aren't guaranteed to sort correctly afterward — v6 has no counter,
72
- so `clock_seq`/`node` (not the timestamp) decide ties, the same pre-existing RFC 9562 v6
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).
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, defined for versions 6 and 7 only). `Uuid::LAYOUTS` lists them.
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, 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.
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.new_v6.to_sql_order
95
- sql.version(layout: :sql_server) # => 6
96
- sql.rfc?(6, layout: :sql_server) # => true
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 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.
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. Version 6 has no counter and no such limit.
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 are this gem's own, and both carry the same message on every backend:
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: past version 7's
125
- 48-bit millisecond field, past version 6's 60-bit one, or negative (a `Time` before the Unix
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..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).
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 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":
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. **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.
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
- `new_v6_batch_bytes` and `new_v7_batch_bytes` return the batch as one binary `String` of raw RFC 9562-ordered bytes — 16 per UUID — instead of an Array of `Uuid` objects:
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 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.
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 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.
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 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.
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
- platform gems and `hyperuuid-wasm` — carries its own GitHub build-provenance attestation, signed directly by
249
- this repo's own `release.yml` (the `rubygems-publish` job attests `ruby/pkg/*.gem` right
250
- before the push), so plain `--repo` verifies any of them:
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
- gem fetch hyperuuid -v X.Y.Z --platform <platform> # or omit --platform for the universal gem
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's the release's second layer of checking, not the only one: before any gem gets built,
258
- the same job verifies every native binary it packs — the Fiddle libraries, the Magnus
259
- extensions (one per Ruby ABI per platform), the `wasm32-wasip1` archives — against *their own*
260
- attestations — those are signed from `SkunkWerkx/.github` by `hyper-build-native.yml` and
261
- `hyper-build-wasm.yml`, so
262
- that check needs `--signer-repo SkunkWerkx/.github` added — and refuses to proceed on an
263
- unverified one. RubyGems.org has no unpublish and no
264
- duplicate-version overwrite, so this all happens while a bad artifact is still reversible.
265
- The release run's job summary then re-fetches every gem from the CDN and records
266
- attested-vs-served digests, turning "rubygems.org stores an upload verbatim" into a
267
- per-release measurement rather than an assumption — see
268
- [csharp/README.md's provenance section](../csharp/README.md#native-binary-provenance) for
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 `new_v6`/`new_v7` with no argument,
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
@@ -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`, `new_v6`, `new_v7` and their batch forms can fail once their
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: a version 7 value
8
- # past RFC 9562's 48-bit millisecond field, a version 6 value past its 60-bit field, or
9
- # any negative one (a `Time` before the Unix epoch included). Carries the same message on
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
@@ -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
- # The package's two exceptions live on HyperUuid itself (errors.rb). These are the names
19
- # they had through 0.3.0, kept as aliases of the very same classes so a
20
- # `rescue HyperUuid::Runtime::TimestampOutOfRangeError` written against an earlier
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 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.
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::TYPE_VOID
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::TYPE_VOID
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],
@@ -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 order
44
- # #to_sql_order returns, defined for versions 6 and 7 only.
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 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.
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. 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.
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+ only 6 and 7 can be true. A version
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 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).
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 6 or 7 UUID; pass
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 6 or 7 UUID in +layout+ (unless
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-ordered version 6 or 7 UUID to the byte order SQL Server's
145
- # `uniqueidentifier` needs on the wire to sort by creation order. Dispatches on `version`
146
- # the same way #timestamp does.
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). Computed once in the native Rust core and verified
152
- # there (and independently, against the real SqlGuid comparator, in this project's C#
153
- # test suite); this binding calls the same native functions rather than reimplementing
154
- # the byte math.
155
- #
156
- # For v7, this moves the timestamp and counter — the two fields that determine creation
157
- # order — into that comparison's most-significant bytes, and moves the trailing entropy,
158
- # which carries no ordering information, into the least-significant ones as one intact
159
- # block. For v6, which has no monotonic counter the way v7 does, the only field that
160
- # determines creation order is the 60-bit timestamp itself, so that moves into the most
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
- case version
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 version 6 or 7 UUID back to
182
- # RFC 9562 order.
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 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
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 version 6 or 7 UUID.
177
+ # @raise [ArgumentError] if this isn't a SQL-ordered value converted from version 7.
195
178
  def from_sql_order
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"
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 6 or 7 UUID" if layout == :sql_server
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 6 or 7 UUIDs, got version #{found}"
234
- [6, 7].include?(found) ? "#{message} with the #{variant.inspect} variant" : message
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), v6 and v7 (time-sortable) generation from one
8
- # Rust core, reached through whichever of two backends this install can load: the core
9
- # linked straight into a Magnus native extension (shipped precompiled in the platform gems),
10
- # or the native libhyperuuid shared library called through Fiddle (the universal gem bundles
11
- # one per supported platform, picked by HyperUuid::NativePlatform). No runtime bridge in
12
- # either, and nothing compiled at install time. HyperUuid::BACKEND names the one that loaded; the selection logic
13
- # is at the bottom of this file.
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/v6/v7) the
20
- # rest of this module generates.
21
- VERSION = "0.8.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. Version 6 has no counter
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
- # `new_v6`/`new_v7`/batch door below so a caller can pass either a `Time` or a raw
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperuuid-wasm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Buvinghausen