hyperuuid-wasm 0.6.1
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +431 -0
- data/ext/hyperuuid_native/3.4/hyperuuid_native.a.gz +0 -0
- data/ext/hyperuuid_native/4.0/hyperuuid_native.a.gz +0 -0
- data/ext/hyperuuid_native/extconf.rb +107 -0
- data/lib/hyperuuid/errors.rb +12 -0
- data/lib/hyperuuid/namespaces.rb +13 -0
- data/lib/hyperuuid/native_platform.rb +28 -0
- data/lib/hyperuuid/runtime.rb +279 -0
- data/lib/hyperuuid/uuid.rb +179 -0
- data/lib/hyperuuid.rb +247 -0
- metadata +87 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Autoloaded, not required: the platform gems carry no library for Fiddle to open and do not
|
|
2
|
+
# declare fiddle, so loading it eagerly would make `require "hyperuuid"` fail under Bundler
|
|
3
|
+
# wherever fiddle is a bundled rather than a default gem (Ruby 4.0). It loads the first time
|
|
4
|
+
# the Fiddle backend runs, and every path there goes through `functions` first, so a platform
|
|
5
|
+
# gem forced onto Fiddle still gets missing_library_message rather than a bare LoadError.
|
|
6
|
+
autoload :Fiddle, "fiddle"
|
|
7
|
+
|
|
8
|
+
module HyperUuid
|
|
9
|
+
# Fiddle plumbing for the native libhyperuuid shared library — dlopen/dlsym plus a raw
|
|
10
|
+
# C-ABI call, no runtime bridge (the same "no shim" positioning as the PHP binding's
|
|
11
|
+
# FFI and C#'s P/Invoke). Fiddle ships with every Ruby install; it's a plain gem dependency of the
|
|
12
|
+
# universal gem (see hyperuuid.gemspec) rather than a third-party one — mirroring Python's
|
|
13
|
+
# zero-dependency PyO3 wheels.
|
|
14
|
+
#
|
|
15
|
+
# A Ruby gem's files are plain files on disk once installed, so native/{rid}/{lib} can be
|
|
16
|
+
# dlopen'd directly, with no extraction step.
|
|
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"
|
|
29
|
+
V7_TIMESTAMP_OUT_OF_RANGE = "unix_millis must fit within the RFC 9562 48-bit field"
|
|
30
|
+
|
|
31
|
+
NATIVE_DIR = File.join(__dir__, "native")
|
|
32
|
+
|
|
33
|
+
@mutex = Mutex.new
|
|
34
|
+
@functions = nil
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
def new_v4
|
|
38
|
+
out = scratch
|
|
39
|
+
rc = functions[:new_v4].call(out)
|
|
40
|
+
raise random_source_failure("uuid_new_v4") unless rc.zero?
|
|
41
|
+
out[0, 16]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def new_v5(namespace_bytes, name_bytes)
|
|
45
|
+
out = scratch
|
|
46
|
+
# Fiddle passes a String's own bytes for void* (read-only, no copy of them) — the
|
|
47
|
+
# same crossing every other input here uses. An empty name crosses as it is, a
|
|
48
|
+
# valid pointer with length 0, never as nil: NULL is not part of this export's
|
|
49
|
+
# contract.
|
|
50
|
+
rc = functions[:new_v5].call(namespace_bytes, name_bytes, name_bytes.bytesize, out)
|
|
51
|
+
raise random_source_failure("uuid_new_v5") unless rc.zero?
|
|
52
|
+
out[0, 16]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def new_v6(unix_millis)
|
|
56
|
+
out = scratch
|
|
57
|
+
rc = functions[:new_v6].call(unix_millis, out)
|
|
58
|
+
case rc
|
|
59
|
+
when 0 then out[0, 16]
|
|
60
|
+
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
61
|
+
else raise random_source_failure("uuid_new_v6")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def v6_unix_millis(bytes)
|
|
66
|
+
functions[:v6_unix_millis].call(bytes)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def new_v6_batch(count, unix_millis)
|
|
70
|
+
return "" if count.zero?
|
|
71
|
+
out = buffer(count * 16)
|
|
72
|
+
rc = functions[:new_v6_batch].call(unix_millis, count, out)
|
|
73
|
+
case rc
|
|
74
|
+
when 0 then out[0, count * 16]
|
|
75
|
+
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
76
|
+
else raise random_source_failure("uuid_new_v6_batch")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def new_v7(unix_millis)
|
|
81
|
+
out = scratch
|
|
82
|
+
rc = functions[:new_v7].call(unix_millis, out)
|
|
83
|
+
case rc
|
|
84
|
+
when 0 then out[0, 16]
|
|
85
|
+
when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
|
|
86
|
+
else raise random_source_failure("uuid_new_v7")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def v7_unix_millis(bytes)
|
|
91
|
+
functions[:v7_unix_millis].call(bytes)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def new_v7_batch(count, unix_millis)
|
|
95
|
+
return "" if count.zero?
|
|
96
|
+
out = buffer(count * 16)
|
|
97
|
+
rc = functions[:new_v7_batch].call(unix_millis, count, out)
|
|
98
|
+
case rc
|
|
99
|
+
when 0 then out[0, count * 16]
|
|
100
|
+
when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
|
|
101
|
+
else raise random_source_failure("uuid_new_v7_batch")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def v7_to_sql_order(bytes)
|
|
106
|
+
rewrite(:v7_to_sql_order, bytes)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def v7_to_rfc_order(bytes)
|
|
110
|
+
rewrite(:v7_to_rfc_order, bytes)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def v6_to_sql_order(bytes)
|
|
114
|
+
rewrite(:v6_to_sql_order, bytes)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def v6_to_rfc_order(bytes)
|
|
118
|
+
rewrite(:v6_to_rfc_order, bytes)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The loaded core's version word, major << 16 | minor << 8 | patch, straight from the
|
|
122
|
+
# library's zero-argument hyperuuid_version export. HyperUuid.native_version unpacks
|
|
123
|
+
# it; like every method here, each backend replaces this one in place.
|
|
124
|
+
def packed_version
|
|
125
|
+
functions[:version].call
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The one failure every generating export shares, with the one message every backend
|
|
129
|
+
# raises for it: the export's name, and that the system's random source failed.
|
|
130
|
+
def random_source_failure(export)
|
|
131
|
+
RandomSourceError.new("#{export}: the system random source failed")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
# The shared library to dlopen: this install's native/{rid}/{lib}, or — the
|
|
137
|
+
# development loop — the in-repo cargo build, exactly what the other bindings' local
|
|
138
|
+
# staging does (HyperCast's runtime has had this since its first release). Nil when
|
|
139
|
+
# neither exists.
|
|
140
|
+
def library_path
|
|
141
|
+
rid, lib_name = NativePlatform.rid_and_library_name
|
|
142
|
+
path = File.join(NATIVE_DIR, rid, lib_name)
|
|
143
|
+
return path if File.exist?(path)
|
|
144
|
+
|
|
145
|
+
repo_build = File.expand_path(File.join(__dir__, "../../../rust/target/release", lib_name))
|
|
146
|
+
File.exist?(repo_build) ? repo_build : nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Why Fiddle found nothing to load. A precompiled platform gem is the one install where
|
|
150
|
+
# that is by design rather than a gap: it carries only its Magnus extensions, and the
|
|
151
|
+
# Fiddle backend is reached there only by forcing it (HYPERUUID_PURE) or because none
|
|
152
|
+
# of its extensions loaded — a gem RubyGems matched to a Ruby it was not built for,
|
|
153
|
+
# such as the glibc Linux gem that `gem install` on RubyGems 3.x picks on Alpine. So
|
|
154
|
+
# that case names its fix, the universal gem, which carries every platform's library,
|
|
155
|
+
# instead of a missing path that reads like a packaging bug. Both arguments are
|
|
156
|
+
# parameters only so the specs can ask for every wording.
|
|
157
|
+
def missing_library_message(gem_platform = Gem.loaded_specs["hyperuuid"]&.platform,
|
|
158
|
+
forced = ENV.key?("HYPERUUID_PURE"))
|
|
159
|
+
rid, lib_name = NativePlatform.rid_and_library_name
|
|
160
|
+
missing = File.join(NATIVE_DIR, rid, lib_name)
|
|
161
|
+
if gem_platform && gem_platform.to_s != Gem::Platform::RUBY
|
|
162
|
+
reason =
|
|
163
|
+
if forced
|
|
164
|
+
"HYPERUUID_PURE forces the Fiddle backend (unset it to use the extension)"
|
|
165
|
+
else
|
|
166
|
+
"none of its extensions loads on this Ruby (#{RUBY_VERSION}, #{RUBY_PLATFORM})"
|
|
167
|
+
end
|
|
168
|
+
"hyperuuid: this #{gem_platform} platform gem carries only Magnus extensions, no " \
|
|
169
|
+
"Fiddle library, and #{reason}. The universal gem has the Fiddle backend for every " \
|
|
170
|
+
"platform: `gem install hyperuuid --platform ruby`, or Bundler's force_ruby_platform " \
|
|
171
|
+
"(#{missing} not found)"
|
|
172
|
+
else
|
|
173
|
+
"hyperuuid: #{missing} not found (unsupported platform, or this gem was built " \
|
|
174
|
+
"without a native library for it)"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# One 16-byte scratch allocation per thread, reused by every single-item call —
|
|
179
|
+
# Fiddle::Pointer.malloc(..., RUBY_FREE) registers a GC finalizer per call, measured
|
|
180
|
+
# (in HyperCast, same mechanism) as the dominant per-call cost by an order of
|
|
181
|
+
# magnitude. Batches keep a per-call buffer: one malloc amortized over `count` IDs.
|
|
182
|
+
def scratch
|
|
183
|
+
Thread.current[:hyperuuid_scratch] ||= buffer(16)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Every Fiddle allocation goes through here, and loads the library first: that is what
|
|
187
|
+
# keeps a missing library reported as missing_library_message instead of as whatever
|
|
188
|
+
# touching Fiddle raises first. The scratch buffer pays for it once per thread.
|
|
189
|
+
def buffer(size)
|
|
190
|
+
functions
|
|
191
|
+
Fiddle::Pointer.malloc(size, Fiddle::RUBY_FREE)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The in-place byte-order rewrites are the one shape that must copy in: the native
|
|
195
|
+
# call genuinely mutates the buffer, and the input String is frozen.
|
|
196
|
+
def rewrite(symbol, bytes)
|
|
197
|
+
buf = scratch
|
|
198
|
+
buf[0, 16] = bytes
|
|
199
|
+
functions[symbol].call(buf)
|
|
200
|
+
buf[0, 16]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Loaded lazily and exactly once, mirroring Java's class initializer / Swift's lazy
|
|
204
|
+
# static let — the native library and its function pointers live for the process's
|
|
205
|
+
# lifetime, same as every other binding (never dlclose'd). The unsynchronized read is
|
|
206
|
+
# the hot path; the mutex only guards the one-time load (a benign race — idempotent).
|
|
207
|
+
def functions
|
|
208
|
+
@functions || @mutex.synchronize { @functions ||= load_functions }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def load_functions
|
|
212
|
+
path = library_path
|
|
213
|
+
raise LoadError, missing_library_message if path.nil?
|
|
214
|
+
|
|
215
|
+
handle = Fiddle.dlopen(path)
|
|
216
|
+
{
|
|
217
|
+
new_v4: Fiddle::Function.new(handle["uuid_new_v4"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT),
|
|
218
|
+
new_v5: Fiddle::Function.new(
|
|
219
|
+
handle["uuid_new_v5"],
|
|
220
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
|
|
221
|
+
Fiddle::TYPE_INT
|
|
222
|
+
),
|
|
223
|
+
new_v6: Fiddle::Function.new(
|
|
224
|
+
handle["uuid_new_v6"],
|
|
225
|
+
[Fiddle::TYPE_UINT64_T, Fiddle::TYPE_VOIDP],
|
|
226
|
+
Fiddle::TYPE_INT
|
|
227
|
+
),
|
|
228
|
+
v6_unix_millis: Fiddle::Function.new(
|
|
229
|
+
handle["uuid_v6_unix_millis"],
|
|
230
|
+
[Fiddle::TYPE_VOIDP],
|
|
231
|
+
Fiddle::TYPE_UINT64_T
|
|
232
|
+
),
|
|
233
|
+
new_v6_batch: Fiddle::Function.new(
|
|
234
|
+
handle["uuid_new_v6_batch"],
|
|
235
|
+
[Fiddle::TYPE_UINT64_T, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
|
|
236
|
+
Fiddle::TYPE_INT
|
|
237
|
+
),
|
|
238
|
+
new_v7: Fiddle::Function.new(
|
|
239
|
+
handle["uuid_new_v7"],
|
|
240
|
+
[Fiddle::TYPE_UINT64_T, Fiddle::TYPE_VOIDP],
|
|
241
|
+
Fiddle::TYPE_INT
|
|
242
|
+
),
|
|
243
|
+
v7_unix_millis: Fiddle::Function.new(
|
|
244
|
+
handle["uuid_v7_unix_millis"],
|
|
245
|
+
[Fiddle::TYPE_VOIDP],
|
|
246
|
+
Fiddle::TYPE_UINT64_T
|
|
247
|
+
),
|
|
248
|
+
new_v7_batch: Fiddle::Function.new(
|
|
249
|
+
handle["uuid_new_v7_batch"],
|
|
250
|
+
[Fiddle::TYPE_UINT64_T, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
|
|
251
|
+
Fiddle::TYPE_INT
|
|
252
|
+
),
|
|
253
|
+
v7_to_sql_order: Fiddle::Function.new(
|
|
254
|
+
handle["uuid_v7_to_sql_order"],
|
|
255
|
+
[Fiddle::TYPE_VOIDP],
|
|
256
|
+
Fiddle::TYPE_VOID
|
|
257
|
+
),
|
|
258
|
+
v7_to_rfc_order: Fiddle::Function.new(
|
|
259
|
+
handle["uuid_v7_to_rfc_order"],
|
|
260
|
+
[Fiddle::TYPE_VOIDP],
|
|
261
|
+
Fiddle::TYPE_VOID
|
|
262
|
+
),
|
|
263
|
+
v6_to_sql_order: Fiddle::Function.new(
|
|
264
|
+
handle["uuid_v6_to_sql_order"],
|
|
265
|
+
[Fiddle::TYPE_VOIDP],
|
|
266
|
+
Fiddle::TYPE_VOID
|
|
267
|
+
),
|
|
268
|
+
v6_to_rfc_order: Fiddle::Function.new(
|
|
269
|
+
handle["uuid_v6_to_rfc_order"],
|
|
270
|
+
[Fiddle::TYPE_VOIDP],
|
|
271
|
+
Fiddle::TYPE_VOID
|
|
272
|
+
),
|
|
273
|
+
# The one export that mints nothing: the zero-argument version probe.
|
|
274
|
+
version: Fiddle::Function.new(handle["hyperuuid_version"], [], Fiddle::TYPE_UINT32_T),
|
|
275
|
+
}
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
module HyperUuid
|
|
2
|
+
# A parsed 16-byte RFC 9562 UUID value. Minimal by design — this gem has no runtime
|
|
3
|
+
# dependency on the `uuid` gem, the same "no extra dependency" positioning as the Go
|
|
4
|
+
# binding's lone google/uuid requirement and the Python binding's dependency-free PyO3
|
|
5
|
+
# wheels.
|
|
6
|
+
class Uuid
|
|
7
|
+
include Comparable
|
|
8
|
+
|
|
9
|
+
# The UUID's 16 raw bytes in RFC 9562 (big-endian) order.
|
|
10
|
+
attr_reader :bytes
|
|
11
|
+
|
|
12
|
+
# Wraps a raw 16-byte RFC 9562 (big-endian) UUID value. The bytes are copied, so the
|
|
13
|
+
# String passed in stays the caller's.
|
|
14
|
+
#
|
|
15
|
+
# +owned+ is for this gem's own use: it says +bytes+ is a String the native core has
|
|
16
|
+
# just produced — sixteen bytes, already ASCII-8BIT, held by nothing else — so there is
|
|
17
|
+
# nothing to check, copy or re-tag, and that copy is most of what constructing one costs.
|
|
18
|
+
#
|
|
19
|
+
# @raise [ArgumentError] if +bytes+ isn't exactly 16 bytes.
|
|
20
|
+
def initialize(bytes, owned = false)
|
|
21
|
+
if owned
|
|
22
|
+
@bytes = bytes.freeze
|
|
23
|
+
else
|
|
24
|
+
raise ArgumentError, "bytes must be exactly 16 bytes" unless bytes.bytesize == 16
|
|
25
|
+
|
|
26
|
+
@bytes = bytes.dup.force_encoding(Encoding::BINARY).freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The RFC 9562 §5.9 Nil UUID — all 128 bits zero.
|
|
31
|
+
NIL = new(("\x00" * 16).b).freeze
|
|
32
|
+
|
|
33
|
+
# The RFC 9562 §5.10 Max UUID — all 128 bits one.
|
|
34
|
+
MAX = new(("\xFF" * 16).b).freeze
|
|
35
|
+
|
|
36
|
+
# The one text shape .parse accepts: 8-4-4-4-12 hex digits, either case, the four hyphens
|
|
37
|
+
# exactly where #to_s puts them.
|
|
38
|
+
HYPHENATED = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
|
|
39
|
+
private_constant :HYPHENATED
|
|
40
|
+
|
|
41
|
+
# Parses an 8-4-4-4-12 hyphenated hex UUID string — exactly the shape #to_s produces, in
|
|
42
|
+
# either case. Nothing else parses: not the bare 32 hex digits, not hyphens anywhere but
|
|
43
|
+
# those four positions, not braces or a `urn:uuid:` prefix.
|
|
44
|
+
#
|
|
45
|
+
# @raise [ArgumentError] if +string+ isn't a String in that shape.
|
|
46
|
+
def self.parse(string)
|
|
47
|
+
unless string.is_a?(String) && string.match?(HYPHENATED)
|
|
48
|
+
raise ArgumentError, "invalid UUID string: #{string.inspect}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
new([string.delete("-")].pack("H*"))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The RFC 9562 version nibble (bits 48-51, the high nibble of octet 6).
|
|
55
|
+
def version
|
|
56
|
+
(bytes.getbyte(6) >> 4) & 0x0F
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The RFC 9562 variant bits (top two bits of octet 8). +0b10+ means RFC 9562/4122.
|
|
60
|
+
def variant
|
|
61
|
+
(bytes.getbyte(8) >> 6) & 0b11
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The 8-4-4-4-12 hyphenated hex string representation.
|
|
65
|
+
def to_s
|
|
66
|
+
hex = bytes.unpack1("H*")
|
|
67
|
+
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
68
|
+
end
|
|
69
|
+
alias_method :to_str, :to_s
|
|
70
|
+
|
|
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
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Converts an RFC 9562-ordered version 6 or 7 UUID to the byte order SQL Server's
|
|
92
|
+
# `uniqueidentifier` needs on the wire to sort by creation order. Dispatches on `version`
|
|
93
|
+
# the same way #timestamp does.
|
|
94
|
+
#
|
|
95
|
+
# `System.Data.SqlTypes.SqlGuid` comparison — and therefore T-SQL `ORDER BY` on a
|
|
96
|
+
# `uniqueidentifier` column — doesn't compare a GUID's 16 bytes left to right; it uses a
|
|
97
|
+
# fixed, non-sequential byte significance order (octets 10,11,12,13,14,15,8,9,6,7,4,5,
|
|
98
|
+
# 0,1,2,3, most significant first). Computed once in the native Rust core and verified
|
|
99
|
+
# there (and independently, against the real SqlGuid comparator, in this project's C#
|
|
100
|
+
# test suite); this binding calls the same native functions rather than reimplementing
|
|
101
|
+
# the byte math.
|
|
102
|
+
#
|
|
103
|
+
# For v7, this moves the timestamp and counter — the two fields that determine creation
|
|
104
|
+
# order — into that comparison's most-significant bytes, and moves the trailing entropy,
|
|
105
|
+
# which carries no ordering information, into the least-significant ones as one intact
|
|
106
|
+
# block. For v6, which has no monotonic counter the way v7 does, the only field that
|
|
107
|
+
# determines creation order is the 60-bit timestamp itself, so that moves into the most
|
|
108
|
+
# significant bytes instead, with `clock_seq`/`node` (independently random per call, not
|
|
109
|
+
# a counter, so no ordering value either way) relocated into the rest. v6's much simpler
|
|
110
|
+
# byte layout needs no bit-level repacking to do this — just whole-octet-group
|
|
111
|
+
# relocation — unlike v7's, and its version/variant land at different sql-order offsets
|
|
112
|
+
# as a result (octet 8's top nibble / octet 6's top two bits, not 7/8).
|
|
113
|
+
#
|
|
114
|
+
# **v6-specific caveat, unlike v7:** two version 6 UUIDs minted at the same millisecond
|
|
115
|
+
# have identical timestamp bits, so they aren't guaranteed to sort in creation order any
|
|
116
|
+
# more than plain RFC order already does — a pre-existing RFC 9562 v6 limitation, not one
|
|
117
|
+
# this transform introduces.
|
|
118
|
+
#
|
|
119
|
+
# Meaningful only for a genuine version 6 or 7 UUID.
|
|
120
|
+
def to_sql_order
|
|
121
|
+
case version
|
|
122
|
+
when 7 then self.class.new(Runtime.v7_to_sql_order(bytes), true)
|
|
123
|
+
when 6 then self.class.new(Runtime.v6_to_sql_order(bytes), true)
|
|
124
|
+
else raise ArgumentError, "to_sql_order is only defined for version 6 or 7 UUIDs, got version #{version}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Inverse of #to_sql_order — converts a SQL-Server-ordered version 6 or 7 UUID back to
|
|
129
|
+
# RFC 9562 order.
|
|
130
|
+
#
|
|
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.
|
|
145
|
+
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"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Whether +other+ wraps the same 16 raw bytes.
|
|
158
|
+
def ==(other)
|
|
159
|
+
other.is_a?(Uuid) && bytes == other.bytes
|
|
160
|
+
end
|
|
161
|
+
alias_method :eql?, :==
|
|
162
|
+
|
|
163
|
+
# Hash code consistent with #==, based on the raw bytes.
|
|
164
|
+
def hash
|
|
165
|
+
bytes.hash
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Byte-order comparison against +other+, or +nil+ if +other+ isn't a Uuid.
|
|
169
|
+
def <=>(other)
|
|
170
|
+
return nil unless other.is_a?(Uuid)
|
|
171
|
+
bytes <=> other.bytes
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Debug representation, e.g. <tt>#<HyperUuid::Uuid ...></tt>.
|
|
175
|
+
def inspect
|
|
176
|
+
"#<HyperUuid::Uuid #{self}>"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|