@@ -50,6 +50,7 @@ class Binary
50
50
ciphertext : 6 . chr ,
51
51
column : 7 . chr ,
52
52
sensitive : 8 . chr ,
53
+ vector : 9 . chr ,
53
54
user : 128 . chr ,
54
55
} . freeze
55
56
@@ -61,6 +62,16 @@ class Binary
61
62
# @since 2.0.0
62
63
TYPES = SUBTYPES . invert . freeze
63
64
65
+ # Types of vector data.
66
+ VECTOR_DATA_TYPES = {
67
+ int8 : '0x03' . hex ,
68
+ float32 : '0x27' . hex ,
69
+ packed_bit : '0x10' . hex
70
+ } . freeze
71
+
72
+ # @api private
73
+ VECTOR_DATA_TYPES_INVERSE = VECTOR_DATA_TYPES . invert . freeze
74
+
64
75
# @return [ String ] The raw binary data.
65
76
#
66
77
# The string is always stored in BINARY encoding.
@@ -89,6 +100,7 @@ def ==(other)
89
100
90
101
type == other . type && data == other . data
91
102
end
103
+
92
104
alias eql? ==
93
105
94
106
# Compare this binary object to another object. The two objects must have
@@ -113,7 +125,7 @@ def <=>(other)
113
125
#
114
126
# @since 2.3.1
115
127
def hash
116
- [ data , type ] . hash
128
+ [ data , type ] . hash
117
129
end
118
130
119
131
# Return a representation of the object for use in
@@ -146,6 +158,26 @@ def as_extended_json(**options)
146
158
end
147
159
end
148
160
161
+ # Decode the binary data as a vector data type.
162
+ #
163
+ # @return [ BSON::Vector ] The decoded vector data.
164
+ def as_vector
165
+ raise BSON ::Error , "Cannot decode subtype #{ type } as vector" unless type == :vector
166
+
167
+ dtype_value , padding , = data [ 0 ..1 ] . unpack ( 'CC' )
168
+ dtype = VECTOR_DATA_TYPES_INVERSE [ dtype_value ]
169
+ raise ArgumentError , "Unsupported vector type: #{ dtype_value } " unless dtype
170
+
171
+ format = case dtype
172
+ when :int8 then 'c*'
173
+ when :float32 then 'f*'
174
+ when :packed_bit then 'C*'
175
+ else
176
+ raise ArgumentError , "Unsupported type: #{ dtype } "
177
+ end
178
+ BSON ::Vector . new ( data [ 2 ..-1 ] . unpack ( format ) , dtype , padding )
179
+ end
180
+
149
181
# Instantiate the new binary object.
150
182
#
151
183
# This method accepts a string in any encoding; however, if a string is
@@ -368,8 +400,97 @@ def self.from_python_legacy_uuid(uuid_binary)
368
400
new ( uuid_binary , :uuid_old )
369
401
end
370
402
403
+ # Constructs a new binary object from a binary vector.
404
+
405
+ # @param [ BSON::Vector | Array ] vector The vector data.
406
+ # @param [ Symbol | nil ] dtype The vector data type, must be nil if vector is a BSON::Vector.
407
+ # @param [ Integer ] padding The number of bits in the final byte that are to
408
+ # be ignored when a vector element's size is less than a byte. Must be 0 if vector is a BSON::Vector.
409
+ # @param [ Boolean ] validate_vector_data Whether to validate the vector data.
410
+ #
411
+ # @return [ BSON::Binary ] The binary object.
412
+ def self . from_vector ( vector , dtype = nil , padding = 0 , validate_vector_data : false )
413
+ data , dtype , padding = extract_args_for_vector ( vector , dtype , padding )
414
+ validate_args_for_vector! ( data , dtype , padding )
415
+
416
+ format = case dtype
417
+ when :int8 then 'c*'
418
+ when :float32 then 'f*'
419
+ when :packed_bit then 'C*'
420
+ else raise ArgumentError , "Unsupported type: #{ dtype } "
421
+ end
422
+ if validate_vector_data
423
+ validate_vector_data! ( data , dtype )
424
+ end
425
+ metadata = [ VECTOR_DATA_TYPES [ dtype ] , padding ] . pack ( 'CC' )
426
+ data = data . pack ( format )
427
+ new ( metadata . concat ( data ) , :vector )
428
+ end
429
+
371
430
private
372
431
432
+ # Extracts the arguments for a binary vector.
433
+ #
434
+ # @param [ BSON::Vector | Array ] vector The vector data.
435
+ # @param [ ::Symbol | nil ] dtype The vector data type, must be nil if vector is a BSON::Vector.
436
+ # @param [ Integer ] padding The padding. Must be 0 if vector is a BSON::Vector.
437
+ #
438
+ # @return [ Array ] The extracted data, dtype, and padding.
439
+ def self . extract_args_for_vector ( vector , dtype , padding )
440
+ if vector . is_a? ( BSON ::Vector )
441
+ if dtype || padding != 0
442
+ raise ArgumentError , 'Do not specify dtype and padding if the first argument is BSON::Vector'
443
+ end
444
+
445
+ data = vector . data
446
+ dtype = vector . dtype
447
+ padding = vector . padding
448
+ else
449
+ data = vector
450
+ end
451
+ [ data , dtype , padding ]
452
+ end
453
+ private_class_method :extract_args_for_vector
454
+
455
+ # Validate the arguments for a binary vector.
456
+ # @param [ Array ] data The vector data.
457
+ # @param [ ::Symbol ] dtype The vector data type.
458
+ # @param [ Integer | nil ] padding The padding. Must be 0 if vector is a BSON::Vector.
459
+ # @raise [ ArgumentError ] If the arguments are invalid.
460
+ def self . validate_args_for_vector! ( data , dtype , padding )
461
+ raise ArgumentError , "Unknown dtype #{ dtype } " unless VECTOR_DATA_TYPES . key? ( dtype )
462
+
463
+ if %i[ int8 float32 ] . include? ( dtype )
464
+ raise ArgumentError , 'Padding applies only to packed_bit' if padding != 0
465
+ elsif padding . positive? && data . empty?
466
+ raise ArgumentError , 'Padding must be zero when the vector is empty for PACKED_BIT'
467
+ elsif padding . negative? || padding > 7
468
+ raise ArgumentError , "Padding must be between 0 and 7, got #{ padding } "
469
+ end
470
+ end
471
+ private_class_method :validate_args_for_vector!
472
+
473
+ # Validate that all the values in the vector data are valid for the given dtype.
474
+ #
475
+ # @param [ Array ] data The vector data.
476
+ # @param [ ::Symbol ] dtype The vector data type.
477
+ def self . validate_vector_data! ( data , dtype )
478
+ validator = case dtype
479
+ when :int8
480
+ -> ( v ) { v . is_a? ( Integer ) && v . between? ( -128 , 127 ) }
481
+ when :float32
482
+ -> ( v ) { v . is_a? ( Float ) }
483
+ when :packed_bit
484
+ -> ( v ) { v . is_a? ( Integer ) && v . between? ( 0 , 255 ) }
485
+ else
486
+ raise ArgumentError , "Unsupported type: #{ dtype } "
487
+ end
488
+ data . each do |v |
489
+ raise ArgumentError , "Invalid value #{ v } for type #{ dtype } " unless validator . call ( v )
490
+ end
491
+ end
492
+ private_class_method :validate_vector_data!
493
+
373
494
# initializes an instance of BSON::Binary.
374
495
#
375
496
# @param [ String ] data the data to initialize the object with
@@ -398,7 +519,7 @@ def from_uuid_to_uuid(representation)
398
519
if representation != :standard
399
520
raise ArgumentError ,
400
521
'Binary of type :uuid can only be stringified to :standard representation, ' \
401
- "requested: #{ representation . inspect } "
522
+ "requested: #{ representation . inspect } "
402
523
end
403
524
404
525
data
@@ -490,7 +611,8 @@ def validate_type!(type)
490
611
validate_integer_type! ( type . bytes . first )
491
612
end
492
613
when Symbol then validate_symbol_type! ( type )
493
- else raise BSON ::Error ::InvalidBinaryType , type
614
+ else
615
+ raise BSON ::Error ::InvalidBinaryType , type
494
616
end
495
617
end
496
618
0 commit comments