mudis-ql 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +596 -0
- data/lib/mudis-ql/metrics_scope.rb +175 -0
- data/lib/mudis-ql/scope.rb +184 -0
- data/lib/mudis-ql/store.rb +79 -0
- data/lib/mudis-ql/version.rb +5 -0
- data/lib/mudis-ql.rb +49 -0
- data/spec/mudis-ql/error_handling_spec.rb +330 -0
- data/spec/mudis-ql/integration_spec.rb +337 -0
- data/spec/mudis-ql/metrics_scope_spec.rb +332 -0
- data/spec/mudis-ql/performance_spec.rb +295 -0
- data/spec/mudis-ql/scope_spec.rb +169 -0
- data/spec/mudis-ql/store_spec.rb +77 -0
- data/spec/mudis-ql_spec.rb +52 -0
- metadata +118 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe "MudisQL Error Handling and Edge Cases" do
|
|
4
|
+
before do
|
|
5
|
+
Mudis.serializer = JSON
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe "invalid namespace scenarios" do
|
|
9
|
+
it "handles empty namespace gracefully" do
|
|
10
|
+
store = MudisQL::Store.new("")
|
|
11
|
+
expect { store.all }.not_to raise_error
|
|
12
|
+
expect(store.all).to eq([])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "handles non-existent namespace" do
|
|
16
|
+
results = MudisQL.from("nonexistent_namespace_12345").all
|
|
17
|
+
expect(results).to eq([])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "handles querying for non-existent key in existing namespace" do
|
|
21
|
+
namespace = "existing_namespace_test"
|
|
22
|
+
Mudis.write("key1", { name: "Alice" }, namespace: namespace)
|
|
23
|
+
Mudis.write("key2", { name: "Bob" }, namespace: namespace)
|
|
24
|
+
|
|
25
|
+
# Query for a key that doesn't exist using _key field
|
|
26
|
+
results = MudisQL.from(namespace).where(_key: "nonexistent_key").all
|
|
27
|
+
expect(results).to eq([])
|
|
28
|
+
|
|
29
|
+
# Verify the namespace itself has data
|
|
30
|
+
all_results = MudisQL.from(namespace).all
|
|
31
|
+
expect(all_results.size).to eq(2)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe "malformed data scenarios" do
|
|
36
|
+
let(:namespace) { "malformed_test" }
|
|
37
|
+
|
|
38
|
+
it "handles non-hash values in cache" do
|
|
39
|
+
Mudis.write("string", "just a string", namespace: namespace)
|
|
40
|
+
Mudis.write("number", 42, namespace: namespace)
|
|
41
|
+
Mudis.write("array", [1, 2, 3], namespace: namespace)
|
|
42
|
+
|
|
43
|
+
results = MudisQL.from(namespace).all
|
|
44
|
+
|
|
45
|
+
expect(results.size).to eq(3)
|
|
46
|
+
expect(results.find { |r| r["_key"] == "string" }["value"]).to eq("just a string")
|
|
47
|
+
expect(results.find { |r| r["_key"] == "number" }["value"]).to eq(42)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "handles mixed hash and non-hash values" do
|
|
51
|
+
Mudis.write("obj1", { name: "Object" }, namespace: namespace)
|
|
52
|
+
Mudis.write("str1", "String value", namespace: namespace)
|
|
53
|
+
|
|
54
|
+
results = MudisQL.from(namespace).all
|
|
55
|
+
|
|
56
|
+
expect(results.size).to eq(2)
|
|
57
|
+
obj = results.find { |r| r["_key"] == "obj1" }
|
|
58
|
+
str = results.find { |r| r["_key"] == "str1" }
|
|
59
|
+
|
|
60
|
+
expect(obj).to have_key("name")
|
|
61
|
+
expect(str).to have_key("value")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe "query condition edge cases" do
|
|
66
|
+
let(:namespace) { "edge_query" }
|
|
67
|
+
|
|
68
|
+
before do
|
|
69
|
+
Mudis.write("1", { value: 10, name: "Ten" }, namespace: namespace)
|
|
70
|
+
Mudis.write("2", { value: 20, name: "Twenty" }, namespace: namespace)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "handles condition on non-existent field" do
|
|
74
|
+
results = MudisQL.from(namespace)
|
|
75
|
+
.where(nonexistent_field: "value")
|
|
76
|
+
.all
|
|
77
|
+
|
|
78
|
+
expect(results).to be_empty
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "handles proc that raises exception gracefully" do
|
|
82
|
+
# Proc that would error on nil
|
|
83
|
+
expect {
|
|
84
|
+
MudisQL.from(namespace)
|
|
85
|
+
.where(value: ->(v) { v.to_s.upcase })
|
|
86
|
+
.all
|
|
87
|
+
}.not_to raise_error
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "handles regex on non-string fields" do
|
|
91
|
+
results = MudisQL.from(namespace)
|
|
92
|
+
.where(value: /10/)
|
|
93
|
+
.all
|
|
94
|
+
|
|
95
|
+
# Should convert to string and match
|
|
96
|
+
expect(results.size).to eq(1)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "handles range comparisons with incompatible types" do
|
|
100
|
+
Mudis.write("3", { value: "string" }, namespace: namespace)
|
|
101
|
+
|
|
102
|
+
# Should not crash when range comparison fails
|
|
103
|
+
expect {
|
|
104
|
+
MudisQL.from(namespace)
|
|
105
|
+
.where(value: 15..25)
|
|
106
|
+
.all
|
|
107
|
+
}.not_to raise_error
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
describe "ordering edge cases" do
|
|
112
|
+
let(:namespace) { "order_edge" }
|
|
113
|
+
|
|
114
|
+
before do
|
|
115
|
+
Mudis.write("1", { name: "Alpha", score: 10 }, namespace: namespace)
|
|
116
|
+
Mudis.write("2", { name: "Beta", score: nil }, namespace: namespace)
|
|
117
|
+
Mudis.write("3", { name: "Gamma", score: 5 }, namespace: namespace)
|
|
118
|
+
Mudis.write("4", { name: nil, score: 15 }, namespace: namespace)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "orders with nil values ascending" do
|
|
122
|
+
results = MudisQL.from(namespace)
|
|
123
|
+
.order(:score, :asc)
|
|
124
|
+
.pluck(:name, :score)
|
|
125
|
+
|
|
126
|
+
# Nil should be at the end
|
|
127
|
+
expect(results.last[1]).to be_nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "orders with nil values descending" do
|
|
131
|
+
results = MudisQL.from(namespace)
|
|
132
|
+
.order(:score, :desc)
|
|
133
|
+
.pluck(:name, :score)
|
|
134
|
+
|
|
135
|
+
# Nil should be at the end (in desc, that's still last)
|
|
136
|
+
expect(results.last[1]).to be_nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "orders by non-existent field" do
|
|
140
|
+
results = MudisQL.from(namespace)
|
|
141
|
+
.order(:nonexistent)
|
|
142
|
+
.all
|
|
143
|
+
|
|
144
|
+
# Should not crash, order may be arbitrary
|
|
145
|
+
expect(results.size).to eq(4)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "orders strings and numbers in same field" do
|
|
149
|
+
Mudis.write("5", { mixed: "zebra" }, namespace: namespace)
|
|
150
|
+
Mudis.write("6", { mixed: 100 }, namespace: namespace)
|
|
151
|
+
|
|
152
|
+
expect {
|
|
153
|
+
MudisQL.from(namespace)
|
|
154
|
+
.order(:mixed)
|
|
155
|
+
.all
|
|
156
|
+
}.not_to raise_error
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
describe "pagination edge cases" do
|
|
161
|
+
let(:namespace) { "pagination_edge" }
|
|
162
|
+
|
|
163
|
+
before do
|
|
164
|
+
5.times { |i| Mudis.write("item#{i}", { value: i }, namespace: namespace) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "handles offset larger than result set" do
|
|
168
|
+
results = MudisQL.from(namespace)
|
|
169
|
+
.offset(100)
|
|
170
|
+
.all
|
|
171
|
+
|
|
172
|
+
expect(results).to be_empty
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "handles limit of 0" do
|
|
176
|
+
results = MudisQL.from(namespace)
|
|
177
|
+
.limit(0)
|
|
178
|
+
.all
|
|
179
|
+
|
|
180
|
+
expect(results).to be_empty
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "handles negative offset (should treat as 0)" do
|
|
184
|
+
results = MudisQL.from(namespace)
|
|
185
|
+
.offset(-5)
|
|
186
|
+
.all
|
|
187
|
+
|
|
188
|
+
# Should return all items
|
|
189
|
+
expect(results.size).to eq(5)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "handles very large limit" do
|
|
193
|
+
results = MudisQL.from(namespace)
|
|
194
|
+
.limit(1_000_000)
|
|
195
|
+
.all
|
|
196
|
+
|
|
197
|
+
# Should return all available items
|
|
198
|
+
expect(results.size).to eq(5)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
describe "pluck edge cases" do
|
|
203
|
+
let(:namespace) { "pluck_edge" }
|
|
204
|
+
|
|
205
|
+
before do
|
|
206
|
+
Mudis.write("1", { a: 1, b: 2, c: 3 }, namespace: namespace)
|
|
207
|
+
Mudis.write("2", { a: 4, b: nil, c: 6 }, namespace: namespace)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "plucks non-existent fields" do
|
|
211
|
+
results = MudisQL.from(namespace).pluck(:nonexistent)
|
|
212
|
+
|
|
213
|
+
expect(results).to eq([nil, nil])
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it "plucks with mixed existent and non-existent fields" do
|
|
217
|
+
results = MudisQL.from(namespace).pluck(:a, :nonexistent)
|
|
218
|
+
|
|
219
|
+
expect(results).to contain_exactly([1, nil], [4, nil])
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
it "plucks no fields" do
|
|
223
|
+
results = MudisQL.from(namespace).pluck
|
|
224
|
+
|
|
225
|
+
expect(results.size).to eq(2)
|
|
226
|
+
expect(results.first).to eq([])
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "plucks with nil values" do
|
|
230
|
+
results = MudisQL.from(namespace).pluck(:b)
|
|
231
|
+
|
|
232
|
+
expect(results).to contain_exactly(2, nil)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
describe "chaining and scope state" do
|
|
237
|
+
let(:namespace) { "chain_test" }
|
|
238
|
+
|
|
239
|
+
before do
|
|
240
|
+
3.times { |i| Mudis.write("i#{i}", { value: i * 10 }, namespace: namespace) }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it "maintains independent scope chains" do
|
|
244
|
+
# Create fresh scopes for each test
|
|
245
|
+
scope1 = MudisQL.from(namespace).where(value: 0)
|
|
246
|
+
count1 = scope1.count
|
|
247
|
+
|
|
248
|
+
scope2 = MudisQL.from(namespace).where(value: 10)
|
|
249
|
+
count2 = scope2.count
|
|
250
|
+
|
|
251
|
+
expect(count1).to eq(1)
|
|
252
|
+
expect(count2).to eq(1)
|
|
253
|
+
# Verify scope1 is still independent
|
|
254
|
+
expect(scope1.count).to eq(1)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it "allows multiple calls to same method" do
|
|
258
|
+
results = MudisQL.from(namespace)
|
|
259
|
+
.where(value: ->(v) { v >= 0 })
|
|
260
|
+
.where(value: ->(v) { v <= 20 })
|
|
261
|
+
.all
|
|
262
|
+
|
|
263
|
+
expect(results.size).to eq(3)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "order can be called multiple times (last wins)" do
|
|
267
|
+
results = MudisQL.from(namespace)
|
|
268
|
+
.order(:value, :asc)
|
|
269
|
+
.order(:value, :desc)
|
|
270
|
+
.pluck(:value)
|
|
271
|
+
|
|
272
|
+
expect(results.first).to be > results.last
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
describe "concurrent access scenarios" do
|
|
277
|
+
let(:namespace) { "concurrent" }
|
|
278
|
+
|
|
279
|
+
it "handles modifications during query execution" do
|
|
280
|
+
10.times { |i| Mudis.write("i#{i}", { value: i }, namespace: namespace) }
|
|
281
|
+
|
|
282
|
+
# Start a query
|
|
283
|
+
query = MudisQL.from(namespace).where(value: ->(v) { v < 5 })
|
|
284
|
+
|
|
285
|
+
# Modify data before executing
|
|
286
|
+
Mudis.write("i11", { value: 2 }, namespace: namespace)
|
|
287
|
+
|
|
288
|
+
results = query.all
|
|
289
|
+
|
|
290
|
+
# Should still execute without error
|
|
291
|
+
expect(results).to be_an(Array)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
describe "type coercion and comparison" do
|
|
296
|
+
let(:namespace) { "type_test" }
|
|
297
|
+
|
|
298
|
+
before do
|
|
299
|
+
Mudis.write("1", { value: "100", type: "string" }, namespace: namespace)
|
|
300
|
+
Mudis.write("2", { value: 100, type: "integer" }, namespace: namespace)
|
|
301
|
+
Mudis.write("3", { value: 100.0, type: "float" }, namespace: namespace)
|
|
302
|
+
Mudis.write("4", { value: true, type: "boolean" }, namespace: namespace)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it "handles exact matches with different types" do
|
|
306
|
+
string_results = MudisQL.from(namespace).where(value: "100").count
|
|
307
|
+
# Note: 100.0 and 100 are equal in Ruby and JSON doesn't distinguish
|
|
308
|
+
int_results = MudisQL.from(namespace).where(value: 100).count
|
|
309
|
+
|
|
310
|
+
expect(string_results).to eq(1)
|
|
311
|
+
expect(int_results).to eq(2) # Matches both integer and float
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it "handles boolean values" do
|
|
315
|
+
results = MudisQL.from(namespace).where(value: true).all
|
|
316
|
+
|
|
317
|
+
expect(results.size).to eq(1)
|
|
318
|
+
expect(results.first["type"]).to eq("boolean")
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
it "handles proc comparisons with type flexibility" do
|
|
322
|
+
results = MudisQL.from(namespace)
|
|
323
|
+
.where(value: ->(v) { v.to_s == "100" })
|
|
324
|
+
.all
|
|
325
|
+
|
|
326
|
+
expect(results.size).to eq(2) # String "100" and integer 100 (float becomes int in JSON)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe "MudisQL Integration Tests" do
|
|
4
|
+
let(:namespace) { "test_integration" }
|
|
5
|
+
|
|
6
|
+
before do
|
|
7
|
+
Mudis.serializer = JSON
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe "E-commerce product catalog scenario" do
|
|
11
|
+
before do
|
|
12
|
+
# Seed a realistic product catalog
|
|
13
|
+
products = [
|
|
14
|
+
{ id: "p001", name: "MacBook Pro 16", price: 2499, category: "laptops", brand: "Apple", rating: 4.8, stock: 15, tags: ["premium", "professional"] },
|
|
15
|
+
{ id: "p002", name: "Dell XPS 15", price: 1899, category: "laptops", brand: "Dell", rating: 4.6, stock: 25, tags: ["business", "professional"] },
|
|
16
|
+
{ id: "p003", name: "ThinkPad X1", price: 1599, category: "laptops", brand: "Lenovo", rating: 4.7, stock: 30, tags: ["business", "durable"] },
|
|
17
|
+
{ id: "p004", name: "Magic Mouse", price: 79, category: "accessories", brand: "Apple", rating: 4.2, stock: 100, tags: ["wireless"] },
|
|
18
|
+
{ id: "p005", name: "Logitech MX Master", price: 99, category: "accessories", brand: "Logitech", rating: 4.9, stock: 75, tags: ["wireless", "ergonomic"] },
|
|
19
|
+
{ id: "p006", name: "iPad Pro 12.9", price: 1099, category: "tablets", brand: "Apple", rating: 4.8, stock: 40, tags: ["premium", "creative"] },
|
|
20
|
+
{ id: "p007", name: "Surface Pro 9", price: 999, category: "tablets", brand: "Microsoft", rating: 4.5, stock: 35, tags: ["business", "versatile"] },
|
|
21
|
+
{ id: "p008", name: "AirPods Pro", price: 249, category: "accessories", brand: "Apple", rating: 4.7, stock: 200, tags: ["wireless", "premium"] },
|
|
22
|
+
{ id: "p009", name: "Samsung Galaxy Tab", price: 449, category: "tablets", brand: "Samsung", rating: 4.4, stock: 50, tags: ["android", "affordable"] },
|
|
23
|
+
{ id: "p010", name: "Mechanical Keyboard", price: 159, category: "accessories", brand: "Keychron", rating: 4.6, stock: 45, tags: ["mechanical", "rgb"] }
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
products.each do |product|
|
|
27
|
+
Mudis.write(product[:id], product, namespace: namespace, expires_in: 3600)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "finds premium Apple products over $1000" do
|
|
32
|
+
results = MudisQL.from(namespace)
|
|
33
|
+
.where(brand: "Apple")
|
|
34
|
+
.where(price: ->(p) { p > 1000 })
|
|
35
|
+
.order(:price, :desc)
|
|
36
|
+
.all
|
|
37
|
+
|
|
38
|
+
expect(results.size).to eq(2)
|
|
39
|
+
expect(results[0]["name"]).to eq("MacBook Pro 16")
|
|
40
|
+
expect(results[1]["name"]).to eq("iPad Pro 12.9")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "finds highly rated products (>4.5) with good stock" do
|
|
44
|
+
results = MudisQL.from(namespace)
|
|
45
|
+
.where(rating: ->(r) { r > 4.5 })
|
|
46
|
+
.where(stock: ->(s) { s >= 30 })
|
|
47
|
+
.order(:rating, :desc)
|
|
48
|
+
.pluck(:name, :rating, :stock)
|
|
49
|
+
|
|
50
|
+
expect(results.size).to eq(5)
|
|
51
|
+
expect(results.first).to eq(["Logitech MX Master", 4.9, 75])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "paginates through laptop results" do
|
|
55
|
+
page1 = MudisQL.from(namespace)
|
|
56
|
+
.where(category: "laptops")
|
|
57
|
+
.order(:price)
|
|
58
|
+
.limit(2)
|
|
59
|
+
.offset(0)
|
|
60
|
+
.all
|
|
61
|
+
|
|
62
|
+
page2 = MudisQL.from(namespace)
|
|
63
|
+
.where(category: "laptops")
|
|
64
|
+
.order(:price)
|
|
65
|
+
.limit(2)
|
|
66
|
+
.offset(2)
|
|
67
|
+
.all
|
|
68
|
+
|
|
69
|
+
expect(page1.map { |p| p["name"] }).to eq(["ThinkPad X1", "Dell XPS 15"])
|
|
70
|
+
expect(page2.map { |p| p["name"] }).to eq(["MacBook Pro 16"])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "finds products in price range with specific brand" do
|
|
74
|
+
results = MudisQL.from(namespace)
|
|
75
|
+
.where(price: 100..1000)
|
|
76
|
+
.where(brand: /^(Apple|Microsoft)$/i)
|
|
77
|
+
.order(:price)
|
|
78
|
+
.all
|
|
79
|
+
|
|
80
|
+
expect(results.map { |p| p["name"] }).to eq(["AirPods Pro", "Surface Pro 9"])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "gets product count by category" do
|
|
84
|
+
laptop_count = MudisQL.from(namespace).where(category: "laptops").count
|
|
85
|
+
accessory_count = MudisQL.from(namespace).where(category: "accessories").count
|
|
86
|
+
tablet_count = MudisQL.from(namespace).where(category: "tablets").count
|
|
87
|
+
|
|
88
|
+
expect(laptop_count).to eq(3)
|
|
89
|
+
expect(accessory_count).to eq(4)
|
|
90
|
+
expect(tablet_count).to eq(3)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe "User analytics scenario" do
|
|
95
|
+
before do
|
|
96
|
+
# Seed user activity data
|
|
97
|
+
users = [
|
|
98
|
+
{ id: "u001", name: "Alice", email: "alice@example.com", status: "active", last_login_days: 2, orders: 15, lifetime_value: 2500 },
|
|
99
|
+
{ id: "u002", name: "Bob", email: "bob@example.com", status: "active", last_login_days: 1, orders: 8, lifetime_value: 800 },
|
|
100
|
+
{ id: "u003", name: "Charlie", email: "charlie@example.com", status: "inactive", last_login_days: 45, orders: 3, lifetime_value: 150 },
|
|
101
|
+
{ id: "u004", name: "Diana", email: "diana@example.com", status: "active", last_login_days: 5, orders: 25, lifetime_value: 4200 },
|
|
102
|
+
{ id: "u005", name: "Eve", email: "eve@example.com", status: "suspended", last_login_days: 120, orders: 1, lifetime_value: 50 },
|
|
103
|
+
{ id: "u006", name: "Frank", email: "frank@example.com", status: "active", last_login_days: 3, orders: 12, lifetime_value: 1800 },
|
|
104
|
+
{ id: "u007", name: "Grace", email: "grace@example.com", status: "inactive", last_login_days: 60, orders: 0, lifetime_value: 0 }
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
users.each do |user|
|
|
108
|
+
Mudis.write(user[:id], user, namespace: "users", expires_in: 1800)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "identifies VIP customers (high value, active)" do
|
|
113
|
+
vips = MudisQL.from("users")
|
|
114
|
+
.where(status: "active")
|
|
115
|
+
.where(lifetime_value: ->(v) { v >= 2000 })
|
|
116
|
+
.order(:lifetime_value, :desc)
|
|
117
|
+
.all
|
|
118
|
+
|
|
119
|
+
expect(vips.size).to eq(2)
|
|
120
|
+
expect(vips.map { |u| u["name"] }).to eq(["Diana", "Alice"])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "finds at-risk users (active but haven't logged in recently)" do
|
|
124
|
+
at_risk = MudisQL.from("users")
|
|
125
|
+
.where(status: "active")
|
|
126
|
+
.where(last_login_days: ->(d) { d > 4 })
|
|
127
|
+
.pluck(:name, :last_login_days, :orders)
|
|
128
|
+
|
|
129
|
+
expect(at_risk.size).to eq(1)
|
|
130
|
+
expect(at_risk.first[0]).to eq("Diana")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "segments users by engagement level" do
|
|
134
|
+
high_engagement = MudisQL.from("users")
|
|
135
|
+
.where(orders: ->(o) { o >= 10 })
|
|
136
|
+
.where(status: "active")
|
|
137
|
+
.count
|
|
138
|
+
|
|
139
|
+
low_engagement = MudisQL.from("users")
|
|
140
|
+
.where(orders: ->(o) { o < 5 })
|
|
141
|
+
.where(status: "active")
|
|
142
|
+
.count
|
|
143
|
+
|
|
144
|
+
expect(high_engagement).to eq(3)
|
|
145
|
+
expect(low_engagement).to eq(0) # Bob has 8 orders, so no active users with < 5 orders
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "finds reactivation candidates" do
|
|
149
|
+
candidates = MudisQL.from("users")
|
|
150
|
+
.where(status: "inactive")
|
|
151
|
+
.where(orders: ->(o) { o > 0 })
|
|
152
|
+
.where(lifetime_value: ->(v) { v > 100 })
|
|
153
|
+
.order(:last_login_days)
|
|
154
|
+
.all
|
|
155
|
+
|
|
156
|
+
expect(candidates.size).to eq(1)
|
|
157
|
+
expect(candidates.first["name"]).to eq("Charlie")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
describe "Complex query combinations" do
|
|
162
|
+
before do
|
|
163
|
+
# Mixed data types
|
|
164
|
+
data = [
|
|
165
|
+
{ id: "1", name: "Alpha", score: 95, tags: ["premium"], created_at: "2025-01-01" },
|
|
166
|
+
{ id: "2", name: "Beta", score: 82, tags: ["standard"], created_at: "2025-01-15" },
|
|
167
|
+
{ id: "3", name: "Gamma", score: 78, tags: ["premium", "featured"], created_at: "2025-02-01" },
|
|
168
|
+
{ id: "4", name: "Delta", score: 91, tags: ["standard"], created_at: "2025-01-20" },
|
|
169
|
+
{ id: "5", name: "Epsilon", score: 88, tags: ["featured"], created_at: "2025-02-10" }
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
data.each { |d| Mudis.write(d[:id], d, namespace: "items") }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "handles multiple regex patterns" do
|
|
176
|
+
results = MudisQL.from("items")
|
|
177
|
+
.where(name: /^[AE]/)
|
|
178
|
+
.where(score: ->(s) { s > 85 })
|
|
179
|
+
.all
|
|
180
|
+
|
|
181
|
+
expect(results.map { |r| r["name"] }).to contain_exactly("Alpha", "Epsilon")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "combines ranges with other conditions" do
|
|
185
|
+
results = MudisQL.from("items")
|
|
186
|
+
.where(score: 80..90)
|
|
187
|
+
.where(name: ->(n) { n.length > 5 }) # Changed to > 5 to exclude "Delta"
|
|
188
|
+
.order(:score, :desc)
|
|
189
|
+
.all
|
|
190
|
+
|
|
191
|
+
expect(results.map { |r| r["name"] }).to eq(["Epsilon"])
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it "handles empty result sets gracefully" do
|
|
195
|
+
results = MudisQL.from("items")
|
|
196
|
+
.where(score: ->(s) { s > 100 })
|
|
197
|
+
.all
|
|
198
|
+
|
|
199
|
+
expect(results).to be_empty
|
|
200
|
+
expect(MudisQL.from("items").where(score: 999).exists?).to be false
|
|
201
|
+
expect(MudisQL.from("items").where(score: 999).count).to eq(0)
|
|
202
|
+
expect(MudisQL.from("items").where(score: 999).first).to be_nil
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it "plucks multiple fields with complex queries" do
|
|
206
|
+
results = MudisQL.from("items")
|
|
207
|
+
.where(score: ->(s) { s >= 85 })
|
|
208
|
+
.order(:score, :desc)
|
|
209
|
+
.pluck(:name, :score, :tags)
|
|
210
|
+
|
|
211
|
+
expect(results.size).to eq(3)
|
|
212
|
+
expect(results.first[0]).to eq("Alpha")
|
|
213
|
+
expect(results.first[1]).to eq(95)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
describe "Edge cases and nil handling" do
|
|
218
|
+
before do
|
|
219
|
+
data = [
|
|
220
|
+
{ id: "1", name: "Complete", value: 100, status: "active", metadata: { key: "val" } },
|
|
221
|
+
{ id: "2", name: nil, value: 50, status: "pending", metadata: nil },
|
|
222
|
+
{ id: "3", name: "Partial", value: nil, status: "active", metadata: { key: "val" } },
|
|
223
|
+
{ id: "4", name: "", value: 0, status: nil, metadata: {} }
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
data.each { |d| Mudis.write(d[:id], d, namespace: "edge_cases") }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "handles nil values in comparisons" do
|
|
230
|
+
results = MudisQL.from("edge_cases")
|
|
231
|
+
.where(status: "active")
|
|
232
|
+
.all
|
|
233
|
+
|
|
234
|
+
expect(results.size).to eq(2)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it "handles nil values in ordering" do
|
|
238
|
+
results = MudisQL.from("edge_cases")
|
|
239
|
+
.order(:value)
|
|
240
|
+
.pluck(:id, :value)
|
|
241
|
+
|
|
242
|
+
# Nil values should be pushed to end
|
|
243
|
+
expect(results.last[1]).to be_nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it "handles proc conditions with nil values" do
|
|
247
|
+
results = MudisQL.from("edge_cases")
|
|
248
|
+
.where(value: ->(v) { !v.nil? && v > 0 })
|
|
249
|
+
.all
|
|
250
|
+
|
|
251
|
+
expect(results.size).to eq(2)
|
|
252
|
+
expect(results.map { |r| r["id"] }).to contain_exactly("1", "2")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it "handles empty strings" do
|
|
256
|
+
results = MudisQL.from("edge_cases")
|
|
257
|
+
.where(name: "")
|
|
258
|
+
.all
|
|
259
|
+
|
|
260
|
+
expect(results.size).to eq(1)
|
|
261
|
+
expect(results.first["id"]).to eq("4")
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
describe "Performance and scale tests" do
|
|
266
|
+
it "handles larger datasets efficiently" do
|
|
267
|
+
# Create 100 records
|
|
268
|
+
100.times do |i|
|
|
269
|
+
Mudis.write(
|
|
270
|
+
"item_#{i}",
|
|
271
|
+
{ id: i, value: rand(1..1000), category: ["A", "B", "C"].sample },
|
|
272
|
+
namespace: "large_set"
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Complex query
|
|
277
|
+
results = MudisQL.from("large_set")
|
|
278
|
+
.where(category: "A")
|
|
279
|
+
.where(value: ->(v) { v > 500 })
|
|
280
|
+
.order(:value, :desc)
|
|
281
|
+
.limit(10)
|
|
282
|
+
.all
|
|
283
|
+
|
|
284
|
+
expect(results.size).to be <= 10
|
|
285
|
+
expect(results).to all(have_key("category"))
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it "handles rapid consecutive queries" do
|
|
289
|
+
Mudis.write("test1", { value: 1 }, namespace: "rapid")
|
|
290
|
+
Mudis.write("test2", { value: 2 }, namespace: "rapid")
|
|
291
|
+
|
|
292
|
+
10.times do
|
|
293
|
+
result = MudisQL.from("rapid").where(value: 1).first
|
|
294
|
+
expect(result).not_to be_nil
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
describe "Scope reusability" do
|
|
300
|
+
before do
|
|
301
|
+
5.times do |i|
|
|
302
|
+
Mudis.write("p#{i}", { price: (i + 1) * 100, active: i.even? }, namespace: "products")
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
it "allows building reusable scope objects" do
|
|
307
|
+
# Each scope should be independent
|
|
308
|
+
cheap_active = MudisQL.from("products")
|
|
309
|
+
.where(active: true)
|
|
310
|
+
.where(price: ->(p) { p < 300 })
|
|
311
|
+
.all
|
|
312
|
+
|
|
313
|
+
expensive_active = MudisQL.from("products")
|
|
314
|
+
.where(active: true)
|
|
315
|
+
.where(price: ->(p) { p >= 300 })
|
|
316
|
+
.all
|
|
317
|
+
|
|
318
|
+
# With 5 products at 100, 200, 300, 400, 500
|
|
319
|
+
# Active are at indices 0, 2, 4 = 100, 300, 500
|
|
320
|
+
expect(cheap_active.size).to eq(1) # 100
|
|
321
|
+
expect(expensive_active.size).to eq(2) # 300, 500
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it "maintains query independence" do
|
|
325
|
+
scope1 = MudisQL.from("products")
|
|
326
|
+
scope2 = MudisQL.from("products")
|
|
327
|
+
|
|
328
|
+
scope1.where(active: true)
|
|
329
|
+
scope2.where(active: false)
|
|
330
|
+
|
|
331
|
+
# Both queries should work independently
|
|
332
|
+
expect(scope1.count).to eq(3)
|
|
333
|
+
expect(scope2.count).to eq(2)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|