hyperuuid 0.6.1-x86_64-linux-gnu → 0.8.0-x86_64-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d627ef885171a5aa3d5f12c8f53ac9cb6ba13da8d1c497e009dec0703d4f22dd
4
- data.tar.gz: 34fca766efc82f71027cc2a5c4140b9858030bd7b67a1657fc604bd515eb3d03
3
+ metadata.gz: 50210cbd12da585faab8ec401c564d290567a02569e3fa4fec76a0e7224262e1
4
+ data.tar.gz: 308948d1efd694544584b612de86ff9edd9541d18531f7d826a4acea34b74b33
5
5
  SHA512:
6
- metadata.gz: d3bc230eb26b9804d3294b75818b5519ac110c5f3dc7b4a9c6a071863e0ecb0c452ece80f151f9ad2758e8248d2bcc9343431d04141453dd2bba05123562abe7
7
- data.tar.gz: dd4e4f93ce8d87b77ad4cab6b6958b569cc5aee052ff5e666b92e9c7bf3002dcfb84c03fde16e2d903935ed585bdb85da867c02360e3906d9ab80b8366b9b602
6
+ metadata.gz: 7b9a3a0f5b880809af66e26c75d962b0b28437adfa47448cfefb31f6724ff16146a1eae883c48885af2fe0d279b64b963c89f65e309f0e9873615f34b65cda85
7
+ data.tar.gz: 29ac37f97a5a6d9821c5580702e8a51155b9da9e72add1423ebbc1003616c3826fd0e57dcc0ee0b5a5e09ef3fbda9f7175d3c780a75eb13eb41b1cf6e3b90bbe
data/README.md CHANGED
@@ -4,7 +4,7 @@
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` — random v4, full stop. No v5, no v6, no v7. 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 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.to_sql_order # byte order SQL Server's uniqueidentifier needs to sort by creation order
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` `#version` `#variant` | the frozen binary String, the hyphenated text, the version nibble, the variant bits |
57
- | `#timestamp(raise_on_mismatch: true)` | the UTC `Time` embedded in a version 6 or 7 UUID; `nil` instead of an `ArgumentError` for any other version when passed `false` |
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` figures out which version to invert by
69
- checking a byte position that's provably collision-free between the two (see the method's own
70
- doc comment).
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`; an `ArgumentError` for a count outside
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` only ever gives you a random v4 UUID — Ruby's stdlib has no built-in v5, v6, or v7 at all. If you need more than that, the choice is really "which gem":
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 plain v4 randomness is all you need, `SecureRandom.uuid` is simpler and already in stdlib — that's a completely reasonable choice.
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 20x faster than `new_v7_batch`** for a 1000-UUID batch (10.4 µs versus 210 µ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.
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` | 872,365 | baseline |
143
- | `HyperUuid.new_v4` | 4,278,000 | **4.9x faster** |
144
- | `HyperUuid.new_v7` (explicit ms) | 3,517,000 | **4.0x faster** |
145
- | `HyperUuid.new_v6` (explicit ms) | 3,487,000 | **4.0x faster** |
146
- | `HyperUuid.new_v6` (current time) | 3,324,000 | **3.8x faster** |
147
- | `HyperUuid.new_v7` (current time) | 3,320,000 | **3.8x faster** |
148
- | `HyperUuid.new_v5` | 1,838,000 | 2.1x faster |
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.25x slower than `SecureRandom.uuid` for v4 (1.26 µs against 1.01 µs in its own run) and 1.45x 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.
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,498 |
161
- | `new_v6_batch(1000)` | 4,633 (**1.3x**) |
162
- | `new_v7` × 1000 (individual) | 3,539 |
163
- | `new_v7_batch(1000)` | 4,677 (**1.3x**) |
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 214 µs on is building a thousand `Uuid` objects — the byte form above does the same native work in 10 µ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.
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/ruby
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/ruby
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
@@ -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[:version].call
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
- version: Fiddle::Function.new(handle["hyperuuid_version"], [], Fiddle::TYPE_UINT32_T),
361
+ packed_version: Fiddle::Function.new(handle["hyperuuid_version"], [], Fiddle::TYPE_UINT32_T),
275
362
  }
276
363
  end
277
364
  end
@@ -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 RFC 9562 version nibble (bits 48-51, the high nibble of octet 6).
55
- def version
56
- (bytes.getbyte(6) >> 4) & 0x0F
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 RFC 9562 variant bits (top two bits of octet 8). +0b10+ means RFC 9562/4122.
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
- (bytes.getbyte(8) >> 6) & 0b11
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 a version 6 or 7 UUID's timestamp field. Only meaningful
72
- # when `version` is 6 or 7 — the RFC 9562 bit layout doesn't distinguish "not a time-based
73
- # UUID" from "time-based UUID with a very early timestamp", so the caller is responsible
74
- # for checking `version` first if that matters.
75
- #
76
- # Raises by default for any other version; pass `raise_on_mismatch: false` to get `nil`
77
- # back instead — for a caller that doesn't already know (or want to separately check)
78
- # whether this UUID is time-based.
79
- def timestamp(raise_on_mismatch: true)
80
- millis =
81
- case version
82
- when 6 then Runtime.v6_unix_millis(bytes)
83
- when 7 then Runtime.v7_unix_millis(bytes)
84
- else
85
- raise ArgumentError, "timestamp is only defined for version 6 or 7 UUIDs, got version #{version}" if raise_on_mismatch
86
- return nil
87
- end
88
- Time.at(millis / 1000, millis % 1000, :millisecond).utc
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 nibble sits at a different octet depending on which
132
- # version produced it (octet 7's top nibble = 7 for v7-sql-order, octet 8's top nibble =
133
- # 6 for v6-sql-order — #version itself assumes RFC order's octet 6 and can't tell these
134
- # apart), so this checks both fixed positions directly rather than calling #version.
135
- #
136
- # Order matters here and isn't arbitrary: octet 8 must be checked *first*. For v6-sql-order
137
- # it's deterministic (top nibble always 0x6, by construction), and for v7-sql-order it's
138
- # also deterministic but structurally excluded from ever reading 0x6 (its top two bits are
139
- # the fixed variant `10`, so the nibble only ever lands in 0x8-0xB) — no collision either
140
- # way. Octet 7, by contrast, is *not* safe to check first: for v7-sql-order it's
141
- # deterministically 0x7, but for v6-sql-order it holds `clock_seq`'s fully random low
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
- octet8_version = (bytes.getbyte(8) >> 4) & 0x0F
147
- octet7_version = (bytes.getbyte(7) >> 4) & 0x0F
148
- if octet8_version == 6
149
- self.class.new(Runtime.v6_to_rfc_order(bytes), true)
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.6.1"
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
- # @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
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 2**32 - 1.
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperuuid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.8.0
5
5
  platform: x86_64-linux-gnu
6
6
  authors:
7
7
  - Brian Buvinghausen