zvec-ruby 0.1.0 → 0.2.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.
@@ -0,0 +1,258 @@
1
+ require_relative "test_helper"
2
+
3
+ class TestTypeDetection < Minitest::Test
4
+ # --- DataTypes.detect_type ---
5
+
6
+ def test_detect_type_nil
7
+ assert_nil Zvec::DataTypes.detect_type(nil)
8
+ end
9
+
10
+ def test_detect_type_string
11
+ assert_equal Zvec::Ext::DataType::STRING, Zvec::DataTypes.detect_type("hello")
12
+ end
13
+
14
+ def test_detect_type_integer
15
+ assert_equal Zvec::Ext::DataType::INT64, Zvec::DataTypes.detect_type(42)
16
+ end
17
+
18
+ def test_detect_type_float
19
+ assert_equal Zvec::Ext::DataType::DOUBLE, Zvec::DataTypes.detect_type(3.14)
20
+ end
21
+
22
+ def test_detect_type_true
23
+ assert_equal Zvec::Ext::DataType::BOOL, Zvec::DataTypes.detect_type(true)
24
+ end
25
+
26
+ def test_detect_type_false
27
+ assert_equal Zvec::Ext::DataType::BOOL, Zvec::DataTypes.detect_type(false)
28
+ end
29
+
30
+ # Integer vs Float distinction: 1 should be INT64, 1.0 should be DOUBLE
31
+ def test_detect_type_integer_one
32
+ assert_equal Zvec::Ext::DataType::INT64, Zvec::DataTypes.detect_type(1)
33
+ end
34
+
35
+ def test_detect_type_float_one
36
+ assert_equal Zvec::Ext::DataType::DOUBLE, Zvec::DataTypes.detect_type(1.0)
37
+ end
38
+
39
+ # --- Array type detection ---
40
+
41
+ def test_detect_type_empty_array
42
+ # Empty arrays default to VECTOR_FP32
43
+ assert_equal Zvec::Ext::DataType::VECTOR_FP32, Zvec::DataTypes.detect_type([])
44
+ end
45
+
46
+ def test_detect_type_float_array
47
+ assert_equal Zvec::Ext::DataType::VECTOR_FP32, Zvec::DataTypes.detect_type([1.0, 2.0])
48
+ end
49
+
50
+ def test_detect_type_integer_array
51
+ # Integers in arrays are treated as vectors (float)
52
+ assert_equal Zvec::Ext::DataType::VECTOR_FP32, Zvec::DataTypes.detect_type([1, 2, 3])
53
+ end
54
+
55
+ def test_detect_type_string_array
56
+ assert_equal Zvec::Ext::DataType::ARRAY_STRING, Zvec::DataTypes.detect_type(["a", "b"])
57
+ end
58
+
59
+ def test_detect_type_bool_array
60
+ assert_equal Zvec::Ext::DataType::ARRAY_BOOL, Zvec::DataTypes.detect_type([true, false])
61
+ end
62
+
63
+ def test_detect_type_nil_filled_array
64
+ # Array of all nils defaults to VECTOR_FP32
65
+ assert_equal Zvec::Ext::DataType::VECTOR_FP32, Zvec::DataTypes.detect_type([nil, nil, nil])
66
+ end
67
+
68
+ def test_detect_type_array_with_leading_nil
69
+ # Skips nils to find first real element
70
+ assert_equal Zvec::Ext::DataType::ARRAY_STRING, Zvec::DataTypes.detect_type([nil, "hello", "world"])
71
+ end
72
+
73
+ # --- DataTypes.coerce_value ---
74
+
75
+ def test_coerce_nil_returns_nil
76
+ assert_nil Zvec::DataTypes.coerce_value(nil, Zvec::Ext::DataType::STRING)
77
+ end
78
+
79
+ def test_coerce_to_string
80
+ assert_equal "42", Zvec::DataTypes.coerce_value(42, Zvec::Ext::DataType::STRING)
81
+ end
82
+
83
+ def test_coerce_string_true_to_bool
84
+ assert_equal true, Zvec::DataTypes.coerce_value("true", Zvec::Ext::DataType::BOOL)
85
+ end
86
+
87
+ def test_coerce_string_false_to_bool
88
+ assert_equal false, Zvec::DataTypes.coerce_value("false", Zvec::Ext::DataType::BOOL)
89
+ end
90
+
91
+ def test_coerce_actual_bool_true
92
+ assert_equal true, Zvec::DataTypes.coerce_value(true, Zvec::Ext::DataType::BOOL)
93
+ end
94
+
95
+ def test_coerce_actual_bool_false
96
+ assert_equal false, Zvec::DataTypes.coerce_value(false, Zvec::Ext::DataType::BOOL)
97
+ end
98
+
99
+ def test_coerce_integer_to_bool_nonzero
100
+ assert_equal true, Zvec::DataTypes.coerce_value(1, Zvec::Ext::DataType::BOOL)
101
+ end
102
+
103
+ def test_coerce_integer_to_bool_zero
104
+ assert_equal false, Zvec::DataTypes.coerce_value(0, Zvec::Ext::DataType::BOOL)
105
+ end
106
+
107
+ def test_coerce_invalid_string_to_bool_raises
108
+ assert_raises(ArgumentError) do
109
+ Zvec::DataTypes.coerce_value("maybe", Zvec::Ext::DataType::BOOL)
110
+ end
111
+ end
112
+
113
+ def test_coerce_float_to_integer
114
+ assert_equal 3, Zvec::DataTypes.coerce_value(3.7, Zvec::Ext::DataType::INT64)
115
+ end
116
+
117
+ def test_coerce_string_to_integer
118
+ assert_equal 42, Zvec::DataTypes.coerce_value("42", Zvec::Ext::DataType::INT64)
119
+ end
120
+
121
+ def test_coerce_bad_string_to_integer_raises
122
+ assert_raises(ArgumentError) do
123
+ Zvec::DataTypes.coerce_value("abc", Zvec::Ext::DataType::INT64)
124
+ end
125
+ end
126
+
127
+ def test_coerce_integer_to_float
128
+ assert_in_delta 42.0, Zvec::DataTypes.coerce_value(42, Zvec::Ext::DataType::DOUBLE), 0.001
129
+ end
130
+
131
+ def test_coerce_string_to_float
132
+ assert_in_delta 3.14, Zvec::DataTypes.coerce_value("3.14", Zvec::Ext::DataType::DOUBLE), 0.001
133
+ end
134
+
135
+ def test_coerce_bad_string_to_float_raises
136
+ assert_raises(ArgumentError) do
137
+ Zvec::DataTypes.coerce_value("xyz", Zvec::Ext::DataType::DOUBLE)
138
+ end
139
+ end
140
+
141
+ # --- Vector coercion ---
142
+
143
+ def test_coerce_integer_array_to_vector
144
+ result = Zvec::DataTypes.coerce_value([1, 2, 3], Zvec::Ext::DataType::VECTOR_FP32)
145
+ assert_equal [1.0, 2.0, 3.0], result
146
+ end
147
+
148
+ def test_coerce_nil_in_vector_becomes_zero
149
+ result = Zvec::DataTypes.coerce_value([1.0, nil, 3.0], Zvec::Ext::DataType::VECTOR_FP32)
150
+ assert_equal [1.0, 0.0, 3.0], result
151
+ end
152
+
153
+ def test_coerce_non_numeric_in_vector_raises
154
+ assert_raises(ArgumentError) do
155
+ Zvec::DataTypes.coerce_value([1.0, "bad", 3.0], Zvec::Ext::DataType::VECTOR_FP32)
156
+ end
157
+ end
158
+
159
+ def test_coerce_non_array_to_vector_raises
160
+ assert_raises(ArgumentError) do
161
+ Zvec::DataTypes.coerce_value("not array", Zvec::Ext::DataType::VECTOR_FP32)
162
+ end
163
+ end
164
+
165
+ # --- String array coercion ---
166
+
167
+ def test_coerce_string_array
168
+ result = Zvec::DataTypes.coerce_value(["a", "b"], Zvec::Ext::DataType::ARRAY_STRING)
169
+ assert_equal ["a", "b"], result
170
+ end
171
+
172
+ def test_coerce_nil_in_string_array_becomes_empty
173
+ result = Zvec::DataTypes.coerce_value(["a", nil, "c"], Zvec::Ext::DataType::ARRAY_STRING)
174
+ assert_equal ["a", "", "c"], result
175
+ end
176
+
177
+ def test_coerce_non_array_to_string_array_raises
178
+ assert_raises(ArgumentError) do
179
+ Zvec::DataTypes.coerce_value("not array", Zvec::Ext::DataType::ARRAY_STRING)
180
+ end
181
+ end
182
+
183
+ # --- Coerce error messages include field name ---
184
+
185
+ def test_coerce_error_message_includes_field_name
186
+ err = assert_raises(ArgumentError) do
187
+ Zvec::DataTypes.coerce_value("abc", Zvec::Ext::DataType::INT64, field_name: "count")
188
+ end
189
+ assert_includes err.message, "count"
190
+ end
191
+
192
+ # --- Doc auto-detection edge cases ---
193
+
194
+ def test_doc_auto_detect_integer_vs_float
195
+ doc = Zvec::Doc.new
196
+ doc["int_val"] = 42
197
+ doc["float_val"] = 42.0
198
+ # int_val stored via set_int64, float_val via set_double
199
+ assert_equal 42, doc["int_val"]
200
+ assert_in_delta 42.0, doc["float_val"], 0.001
201
+ end
202
+
203
+ def test_doc_auto_detect_true_boolean
204
+ doc = Zvec::Doc.new
205
+ doc["flag"] = true
206
+ assert_equal true, doc["flag"]
207
+ end
208
+
209
+ def test_doc_auto_detect_false_boolean
210
+ doc = Zvec::Doc.new
211
+ doc["flag"] = false
212
+ assert_equal false, doc["flag"]
213
+ end
214
+
215
+ def test_doc_string_true_stays_string
216
+ # Without schema, "true" is a String, not a Boolean
217
+ doc = Zvec::Doc.new
218
+ doc["val"] = "true"
219
+ assert_equal "true", doc["val"]
220
+ end
221
+
222
+ def test_doc_string_false_stays_string
223
+ doc = Zvec::Doc.new
224
+ doc["val"] = "false"
225
+ assert_equal "false", doc["val"]
226
+ end
227
+
228
+ def test_doc_nil_value_in_array_auto_detect
229
+ doc = Zvec::Doc.new
230
+ doc["vec"] = [1.0, nil, 3.0]
231
+ result = doc["vec"]
232
+ assert_kind_of Array, result
233
+ assert_equal 3, result.size
234
+ assert_in_delta 0.0, result[1], 0.001
235
+ end
236
+
237
+ def test_doc_empty_array_auto_detect
238
+ doc = Zvec::Doc.new
239
+ doc["vec"] = []
240
+ # Should not crash
241
+ result = doc["vec"]
242
+ assert(result.nil? || result == [])
243
+ end
244
+
245
+ def test_doc_schema_coerces_string_bool
246
+ schema = Zvec::Schema.new("test") { bool "active" }
247
+ doc = Zvec::Doc.new(schema: schema)
248
+ doc["active"] = "true"
249
+ assert_equal true, doc["active"]
250
+ end
251
+
252
+ def test_doc_schema_coerces_int_to_float
253
+ schema = Zvec::Schema.new("test") { double "score" }
254
+ doc = Zvec::Doc.new(schema: schema)
255
+ doc["score"] = 42
256
+ assert_in_delta 42.0, doc["score"], 0.001
257
+ end
258
+ end
@@ -0,0 +1,305 @@
1
+ require_relative "test_helper"
2
+
3
+ class TestValidation < Minitest::Test
4
+ include TempDirHelper
5
+
6
+ def setup
7
+ @schema = Zvec::Schema.new("test_validation") do
8
+ string "title"
9
+ int32 "count"
10
+ float "rating"
11
+ bool "active"
12
+ vector "embedding", dimension: 4
13
+ end
14
+ end
15
+
16
+ # --- Schema validation ---
17
+
18
+ def test_schema_rejects_nil_name
19
+ assert_raises(Zvec::SchemaError) { Zvec::Schema.new(nil) }
20
+ end
21
+
22
+ def test_schema_rejects_empty_name
23
+ assert_raises(Zvec::SchemaError) { Zvec::Schema.new("") }
24
+ end
25
+
26
+ def test_schema_rejects_blank_name
27
+ assert_raises(Zvec::SchemaError) { Zvec::Schema.new(" ") }
28
+ end
29
+
30
+ def test_schema_field_rejects_empty_name
31
+ schema = Zvec::Schema.new("test")
32
+ assert_raises(Zvec::SchemaError) { schema.field("", Zvec::DataTypes::STRING) }
33
+ end
34
+
35
+ def test_schema_vector_rejects_zero_dimension
36
+ assert_raises(ArgumentError) do
37
+ Zvec::Schema.new("test") { vector "v", dimension: 0 }
38
+ end
39
+ end
40
+
41
+ def test_schema_vector_rejects_negative_dimension
42
+ assert_raises(ArgumentError) do
43
+ Zvec::Schema.new("test") { vector "v", dimension: -1 }
44
+ end
45
+ end
46
+
47
+ def test_schema_vector_rejects_non_integer_dimension
48
+ assert_raises(ArgumentError) do
49
+ Zvec::Schema.new("test") { vector "v", dimension: 3.5 }
50
+ end
51
+ end
52
+
53
+ def test_schema_field_dimension_tracking
54
+ schema = Zvec::Schema.new("test") do
55
+ vector "embedding", dimension: 128
56
+ end
57
+ assert_equal 128, schema.field_dimension("embedding")
58
+ end
59
+
60
+ def test_schema_field_dimension_nil_for_non_vector
61
+ schema = Zvec::Schema.new("test") do
62
+ string "title"
63
+ end
64
+ assert_nil schema.field_dimension("title")
65
+ end
66
+
67
+ def test_schema_vector_fields_with_dimensions
68
+ schema = Zvec::Schema.new("test") do
69
+ string "title"
70
+ vector "embedding", dimension: 128
71
+ vector "small_embedding", dimension: 32
72
+ end
73
+ dims = schema.vector_fields_with_dimensions
74
+ assert_equal 128, dims["embedding"]
75
+ assert_equal 32, dims["small_embedding"]
76
+ refute dims.key?("title")
77
+ end
78
+
79
+ # --- Doc field name validation ---
80
+
81
+ def test_doc_set_rejects_empty_field_name
82
+ doc = Zvec::Doc.new
83
+ assert_raises(ArgumentError) { doc.set("", "value") }
84
+ end
85
+
86
+ def test_doc_set_rejects_blank_field_name
87
+ doc = Zvec::Doc.new
88
+ assert_raises(ArgumentError) { doc.set(" ", "value") }
89
+ end
90
+
91
+ # --- Dimension validation in Doc ---
92
+
93
+ def test_doc_dimension_mismatch_raises_error
94
+ doc = Zvec::Doc.new(schema: @schema)
95
+ assert_raises(Zvec::DimensionError) do
96
+ doc["embedding"] = [1.0, 2.0, 3.0] # expects 4
97
+ end
98
+ end
99
+
100
+ def test_doc_dimension_mismatch_error_message
101
+ doc = Zvec::Doc.new(schema: @schema)
102
+ err = assert_raises(Zvec::DimensionError) do
103
+ doc["embedding"] = [1.0, 2.0]
104
+ end
105
+ assert_includes err.message, "embedding"
106
+ assert_includes err.message, "4"
107
+ assert_includes err.message, "2"
108
+ end
109
+
110
+ def test_doc_correct_dimension_accepted
111
+ doc = Zvec::Doc.new(schema: @schema)
112
+ doc["embedding"] = [1.0, 2.0, 3.0, 4.0]
113
+ result = doc["embedding"]
114
+ assert_equal 4, result.size
115
+ end
116
+
117
+ def test_doc_empty_vector_accepted
118
+ doc = Zvec::Doc.new(schema: @schema)
119
+ doc["embedding"] = []
120
+ # Empty vectors should be accepted (no dimension to check)
121
+ end
122
+
123
+ # --- VectorQuery validation ---
124
+
125
+ def test_query_rejects_nil_field_name
126
+ assert_raises(Zvec::QueryError) do
127
+ Zvec::VectorQuery.new(field_name: nil, vector: [1.0])
128
+ end
129
+ end
130
+
131
+ def test_query_rejects_empty_field_name
132
+ assert_raises(Zvec::QueryError) do
133
+ Zvec::VectorQuery.new(field_name: "", vector: [1.0])
134
+ end
135
+ end
136
+
137
+ def test_query_rejects_empty_vector
138
+ assert_raises(Zvec::QueryError) do
139
+ Zvec::VectorQuery.new(field_name: "vec", vector: [])
140
+ end
141
+ end
142
+
143
+ def test_query_rejects_non_array_vector
144
+ assert_raises(Zvec::QueryError) do
145
+ Zvec::VectorQuery.new(field_name: "vec", vector: "not an array")
146
+ end
147
+ end
148
+
149
+ def test_query_rejects_non_numeric_vector_elements
150
+ assert_raises(Zvec::QueryError) do
151
+ Zvec::VectorQuery.new(field_name: "vec", vector: [1.0, "bad", 3.0])
152
+ end
153
+ end
154
+
155
+ def test_query_rejects_zero_topk
156
+ assert_raises(Zvec::QueryError) do
157
+ Zvec::VectorQuery.new(field_name: "vec", vector: [1.0], topk: 0)
158
+ end
159
+ end
160
+
161
+ def test_query_rejects_negative_topk
162
+ assert_raises(Zvec::QueryError) do
163
+ Zvec::VectorQuery.new(field_name: "vec", vector: [1.0], topk: -1)
164
+ end
165
+ end
166
+
167
+ # --- Collection validation (using stubs) ---
168
+
169
+ def test_collection_create_rejects_nil_path
170
+ assert_raises(ArgumentError) do
171
+ Zvec::Collection.create_and_open(nil, @schema)
172
+ end
173
+ end
174
+
175
+ def test_collection_create_rejects_empty_path
176
+ assert_raises(ArgumentError) do
177
+ Zvec::Collection.create_and_open("", @schema)
178
+ end
179
+ end
180
+
181
+ def test_collection_create_rejects_non_schema
182
+ assert_raises(ArgumentError) do
183
+ Zvec::Collection.create_and_open("/tmp/test", "not a schema")
184
+ end
185
+ end
186
+
187
+ def test_collection_open_rejects_nil_path
188
+ assert_raises(ArgumentError) do
189
+ Zvec::Collection.open(nil)
190
+ end
191
+ end
192
+
193
+ def test_collection_add_rejects_nil_pk
194
+ with_temp_dir("zvec_val") do |dir|
195
+ col = Zvec::Collection.create_and_open("#{dir}/col", @schema)
196
+ assert_raises(ArgumentError) do
197
+ col.add(pk: nil, title: "test")
198
+ end
199
+ end
200
+ end
201
+
202
+ def test_collection_search_rejects_empty_vector
203
+ with_temp_dir("zvec_val") do |dir|
204
+ col = Zvec::Collection.create_and_open("#{dir}/col", @schema)
205
+ assert_raises(ArgumentError) do
206
+ col.search([])
207
+ end
208
+ end
209
+ end
210
+
211
+ def test_collection_search_rejects_non_array_vector
212
+ with_temp_dir("zvec_val") do |dir|
213
+ col = Zvec::Collection.create_and_open("#{dir}/col", @schema)
214
+ assert_raises(ArgumentError) do
215
+ col.search("not an array")
216
+ end
217
+ end
218
+ end
219
+
220
+ def test_collection_delete_rejects_empty_pks
221
+ with_temp_dir("zvec_val") do |dir|
222
+ col = Zvec::Collection.create_and_open("#{dir}/col", @schema)
223
+ assert_raises(ArgumentError) do
224
+ col.delete
225
+ end
226
+ end
227
+ end
228
+
229
+ def test_collection_fetch_rejects_empty_pks
230
+ with_temp_dir("zvec_val") do |dir|
231
+ col = Zvec::Collection.create_and_open("#{dir}/col", @schema)
232
+ assert_raises(ArgumentError) do
233
+ col.fetch
234
+ end
235
+ end
236
+ end
237
+
238
+ def test_collection_query_dimension_mismatch
239
+ with_temp_dir("zvec_val") do |dir|
240
+ col = Zvec::Collection.create_and_open("#{dir}/col", @schema)
241
+ assert_raises(Zvec::DimensionError) do
242
+ col.query(field_name: "embedding", vector: [1.0, 2.0], topk: 5)
243
+ end
244
+ end
245
+ end
246
+
247
+ def test_collection_query_dimension_mismatch_message
248
+ with_temp_dir("zvec_val") do |dir|
249
+ col = Zvec::Collection.create_and_open("#{dir}/col", @schema)
250
+ err = assert_raises(Zvec::DimensionError) do
251
+ col.query(field_name: "embedding", vector: [1.0, 2.0], topk: 5)
252
+ end
253
+ assert_includes err.message, "embedding"
254
+ assert_includes err.message, "4"
255
+ assert_includes err.message, "2"
256
+ assert_includes err.message, "test_validation"
257
+ end
258
+ end
259
+
260
+ def test_collection_query_correct_dimension_accepted
261
+ with_temp_dir("zvec_val") do |dir|
262
+ col = Zvec::Collection.create_and_open("#{dir}/col", @schema)
263
+ col.add(pk: "d1", title: "Test", count: 1, rating: 1.0, active: true,
264
+ embedding: [1.0, 2.0, 3.0, 4.0])
265
+ results = col.query(field_name: "embedding", vector: [1.0, 2.0, 3.0, 4.0], topk: 5)
266
+ assert_kind_of Array, results
267
+ end
268
+ end
269
+
270
+ # --- Error context includes collection name ---
271
+
272
+ def test_error_prefix_includes_collection_name
273
+ with_temp_dir("zvec_val") do |dir|
274
+ col = Zvec::Collection.create_and_open("#{dir}/col", @schema)
275
+ err = assert_raises(ArgumentError) do
276
+ col.delete # no pks
277
+ end
278
+ assert_includes err.message, "test_validation"
279
+ end
280
+ end
281
+
282
+ # --- DimensionError is subclass of Error ---
283
+
284
+ def test_dimension_error_is_zvec_error
285
+ assert Zvec::DimensionError < Zvec::Error
286
+ end
287
+
288
+ # --- Collection name is accessible ---
289
+
290
+ def test_collection_name_from_schema
291
+ with_temp_dir("zvec_val") do |dir|
292
+ col = Zvec::Collection.create_and_open("#{dir}/col", @schema)
293
+ assert_equal "test_validation", col.collection_name
294
+ end
295
+ end
296
+
297
+ # --- Thread safety: collection has a monitor ---
298
+
299
+ def test_collection_has_monitor
300
+ with_temp_dir("zvec_val") do |dir|
301
+ col = Zvec::Collection.create_and_open("#{dir}/col", @schema)
302
+ assert_respond_to col.instance_variable_get(:@monitor), :synchronize
303
+ end
304
+ end
305
+ end
data/test/test_version.rb CHANGED
@@ -14,6 +14,6 @@ class TestVersion < Minitest::Test
14
14
  end
15
15
 
16
16
  def test_version_value
17
- assert_equal "0.1.0", Zvec::VERSION
17
+ assert_equal "0.2.0", Zvec::VERSION
18
18
  end
19
19
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zvec-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johannes Dwi Cahyo
@@ -94,11 +94,14 @@ files:
94
94
  - test/test_collection.rb
95
95
  - test/test_data_types.rb
96
96
  - test/test_doc.rb
97
+ - test/test_edge_cases.rb
97
98
  - test/test_ext_bindings.rb
98
99
  - test/test_helper.rb
99
100
  - test/test_query.rb
100
101
  - test/test_ruby_llm_store.rb
101
102
  - test/test_schema.rb
103
+ - test/test_type_detection.rb
104
+ - test/test_validation.rb
102
105
  - test/test_version.rb
103
106
  - zvec.gemspec
104
107
  homepage: https://github.com/johannesdwicahyo/zvec-ruby