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.
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe MudisQL::MetricsScope do
4
+ before do
5
+ Mudis.serializer = JSON
6
+ Mudis.reset!
7
+ Mudis.reset_metrics!
8
+
9
+ # Create some cache activity to generate metrics
10
+ 10.times do |i|
11
+ Mudis.write("key#{i}", { value: i }, namespace: "test")
12
+ end
13
+
14
+ # Create some reads for hits
15
+ 5.times { |i| Mudis.read("key#{i}", namespace: "test") }
16
+
17
+ # Try to read non-existent keys for misses
18
+ 3.times { |i| Mudis.read("missing#{i}", namespace: "test") }
19
+ end
20
+
21
+ let(:metrics_scope) { described_class.new }
22
+
23
+ describe "#summary" do
24
+ it "returns top-level metrics without arrays" do
25
+ summary = metrics_scope.summary
26
+
27
+ expect(summary).to have_key(:hits)
28
+ expect(summary).to have_key(:misses)
29
+ expect(summary).to have_key(:evictions)
30
+ expect(summary).not_to have_key(:least_touched)
31
+ expect(summary).not_to have_key(:buckets)
32
+ end
33
+
34
+ it "shows correct hit and miss counts" do
35
+ summary = metrics_scope.summary
36
+
37
+ expect(summary[:hits]).to eq(5)
38
+ expect(summary[:misses]).to eq(3)
39
+ end
40
+ end
41
+
42
+ describe "#least_touched" do
43
+ it "returns a Scope object" do
44
+ result = metrics_scope.least_touched
45
+
46
+ expect(result).to be_a(MudisQL::Scope)
47
+ end
48
+
49
+ it "can be queried with where" do
50
+ result = metrics_scope.least_touched
51
+ .where(access_count: 0)
52
+ .all
53
+
54
+ expect(result).to be_an(Array)
55
+ end
56
+
57
+ it "can be ordered by access count" do
58
+ result = metrics_scope.least_touched
59
+ .order(:access_count)
60
+ .limit(5)
61
+ .all
62
+
63
+ expect(result.size).to be <= 5
64
+ end
65
+
66
+ it "can pluck just keys" do
67
+ keys = metrics_scope.least_touched
68
+ .where(access_count: 0)
69
+ .pluck(:key)
70
+
71
+ expect(keys).to be_an(Array)
72
+ expect(keys).to all(be_a(String))
73
+ end
74
+ end
75
+
76
+ describe "#buckets" do
77
+ it "returns a Scope object" do
78
+ result = metrics_scope.buckets
79
+
80
+ expect(result).to be_a(MudisQL::Scope)
81
+ end
82
+
83
+ it "can query buckets with conditions" do
84
+ result = metrics_scope.buckets
85
+ .where(keys: ->(k) { k && k > 0 })
86
+ .all
87
+
88
+ expect(result).to be_an(Array)
89
+ result.each do |bucket|
90
+ expect(bucket[:keys]).to be > 0
91
+ end
92
+ end
93
+
94
+ it "can order buckets by memory" do
95
+ result = metrics_scope.buckets
96
+ .order(:memory_bytes, :desc)
97
+ .all
98
+
99
+ if result.size > 1
100
+ expect(result.first[:memory_bytes]).to be >= result.last[:memory_bytes]
101
+ end
102
+ end
103
+
104
+ it "can find buckets by index" do
105
+ result = metrics_scope.buckets
106
+ .where(index: 0)
107
+ .first
108
+
109
+ expect(result).to be_a(Hash) if result
110
+ end
111
+ end
112
+
113
+ describe "#total_keys" do
114
+ it "returns the sum of keys across all buckets" do
115
+ total = metrics_scope.total_keys
116
+
117
+ expect(total).to eq(10)
118
+ end
119
+ end
120
+
121
+ describe "#total_memory" do
122
+ it "returns total memory usage" do
123
+ memory = metrics_scope.total_memory
124
+
125
+ expect(memory).to be_a(Integer)
126
+ expect(memory).to be > 0
127
+ end
128
+ end
129
+
130
+ describe "#hit_rate" do
131
+ it "calculates hit rate percentage" do
132
+ rate = metrics_scope.hit_rate
133
+
134
+ expect(rate).to be_a(Float)
135
+ expect(rate).to be_between(0, 100)
136
+ # 5 hits, 3 misses = 5/8 = 62.5%
137
+ expect(rate).to eq(62.5)
138
+ end
139
+
140
+ it "returns 0 when no operations" do
141
+ Mudis.reset_metrics!
142
+ rate = metrics_scope.refresh.hit_rate
143
+
144
+ expect(rate).to eq(0.0)
145
+ end
146
+ end
147
+
148
+ describe "#efficiency" do
149
+ it "returns efficiency metrics" do
150
+ eff = metrics_scope.efficiency
151
+
152
+ expect(eff).to have_key(:hit_rate)
153
+ expect(eff).to have_key(:miss_rate)
154
+ expect(eff).to have_key(:eviction_rate)
155
+ expect(eff).to have_key(:rejection_rate)
156
+ end
157
+
158
+ it "has hit_rate and miss_rate that sum to 100" do
159
+ eff = metrics_scope.efficiency
160
+
161
+ expect(eff[:hit_rate] + eff[:miss_rate]).to eq(100.0)
162
+ end
163
+ end
164
+
165
+ describe "#high_memory_buckets" do
166
+ it "finds buckets exceeding memory threshold" do
167
+ buckets = metrics_scope.high_memory_buckets(1000)
168
+
169
+ expect(buckets).to be_an(Array)
170
+ buckets.each do |bucket|
171
+ expect(bucket[:memory_bytes]).to be > 1000
172
+ end
173
+ end
174
+
175
+ it "returns empty array when threshold too high" do
176
+ buckets = metrics_scope.high_memory_buckets(999_999_999)
177
+
178
+ expect(buckets).to be_empty
179
+ end
180
+ end
181
+
182
+ describe "#high_key_buckets" do
183
+ it "finds buckets with many keys" do
184
+ buckets = metrics_scope.high_key_buckets(0)
185
+
186
+ expect(buckets).to be_an(Array)
187
+ buckets.each do |bucket|
188
+ expect(bucket[:keys]).to be > 0
189
+ end
190
+ end
191
+
192
+ it "filters by threshold" do
193
+ buckets = metrics_scope.high_key_buckets(5)
194
+
195
+ buckets.each do |bucket|
196
+ expect(bucket[:keys]).to be > 5
197
+ end
198
+ end
199
+ end
200
+
201
+ describe "#bucket_distribution" do
202
+ it "returns distribution statistics" do
203
+ dist = metrics_scope.bucket_distribution
204
+
205
+ expect(dist).to have_key(:total_buckets)
206
+ expect(dist).to have_key(:avg_keys_per_bucket)
207
+ expect(dist).to have_key(:max_keys_per_bucket)
208
+ expect(dist).to have_key(:min_keys_per_bucket)
209
+ expect(dist).to have_key(:avg_memory_per_bucket)
210
+ end
211
+
212
+ it "calculates correct totals" do
213
+ dist = metrics_scope.bucket_distribution
214
+
215
+ expect(dist[:total_buckets]).to be > 0
216
+ expect(dist[:max_keys_per_bucket]).to be >= dist[:min_keys_per_bucket]
217
+ end
218
+ end
219
+
220
+ describe "#never_accessed_keys" do
221
+ it "returns keys with zero access count" do
222
+ keys = metrics_scope.never_accessed_keys
223
+
224
+ expect(keys).to be_an(Array)
225
+ expect(keys.size).to be > 0
226
+ end
227
+
228
+ it "returns only string keys" do
229
+ keys = metrics_scope.never_accessed_keys
230
+
231
+ expect(keys).to all(be_a(String))
232
+ end
233
+ end
234
+
235
+ describe "#refresh" do
236
+ it "updates metrics data" do
237
+ old_hits = metrics_scope.summary[:hits]
238
+
239
+ # Generate more activity
240
+ Mudis.read("key0", namespace: "test")
241
+
242
+ new_hits = metrics_scope.refresh.summary[:hits]
243
+
244
+ expect(new_hits).to be > old_hits
245
+ end
246
+
247
+ it "returns self for chaining" do
248
+ result = metrics_scope.refresh
249
+
250
+ expect(result).to eq(metrics_scope)
251
+ end
252
+ end
253
+
254
+ describe "integration scenarios" do
255
+ it "identifies hotspots - most accessed keys" do
256
+ # Access some keys multiple times
257
+ 10.times { Mudis.read("key0", namespace: "test") }
258
+ 5.times { Mudis.read("key1", namespace: "test") }
259
+
260
+ most_accessed = MudisQL.metrics.refresh.least_touched
261
+ .order(:access_count, :desc)
262
+ .limit(5)
263
+ .all
264
+
265
+ expect(most_accessed).to be_an(Array)
266
+ if most_accessed.size > 1 && most_accessed.first[:access_count] && most_accessed.last[:access_count]
267
+ expect(most_accessed.first[:access_count]).to be >= most_accessed.last[:access_count]
268
+ end
269
+ end
270
+
271
+ it "finds unbalanced buckets" do
272
+ dist = MudisQL.metrics.bucket_distribution
273
+ avg_keys = dist[:avg_keys_per_bucket]
274
+
275
+ unbalanced = MudisQL.metrics.buckets
276
+ .where(keys: ->(k) { k && k > avg_keys * 1.5 })
277
+ .all
278
+
279
+ expect(unbalanced).to be_an(Array)
280
+ end
281
+
282
+ it "monitors cache health" do
283
+ health = {
284
+ hit_rate: MudisQL.metrics.hit_rate,
285
+ total_keys: MudisQL.metrics.total_keys,
286
+ memory: MudisQL.metrics.total_memory,
287
+ efficiency: MudisQL.metrics.efficiency
288
+ }
289
+
290
+ expect(health[:hit_rate]).to be_a(Float)
291
+ expect(health[:total_keys]).to be_a(Integer)
292
+ expect(health[:memory]).to be_a(Integer)
293
+ expect(health[:efficiency]).to be_a(Hash)
294
+ end
295
+
296
+ it "analyzes memory distribution" do
297
+ buckets_by_memory = MudisQL.metrics.buckets
298
+ .order(:memory_bytes, :desc)
299
+ .pluck(:index, :memory_bytes)
300
+
301
+ expect(buckets_by_memory).to be_an(Array)
302
+ expect(buckets_by_memory).not_to be_empty
303
+ end
304
+ end
305
+
306
+ describe "edge cases" do
307
+ it "handles empty cache" do
308
+ Mudis.reset!
309
+ Mudis.reset_metrics!
310
+
311
+ metrics = MudisQL.metrics
312
+
313
+ expect(metrics.total_keys).to eq(0)
314
+ expect(metrics.hit_rate).to eq(0.0)
315
+ expect(metrics.never_accessed_keys).to be_an(Array)
316
+ end
317
+
318
+ it "handles metrics without least_touched data" do
319
+ allow(Mudis).to receive(:metrics).and_return({
320
+ hits: 10,
321
+ misses: 5,
322
+ buckets: []
323
+ })
324
+
325
+ metrics = MudisQL.metrics
326
+
327
+ expect { metrics.least_touched.all }.not_to raise_error
328
+ expect { metrics.never_accessed_keys }.not_to raise_error
329
+ end
330
+ end
331
+ end
332
+
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe "MudisQL Performance Tests" do
4
+ before do
5
+ Mudis.serializer = JSON
6
+ end
7
+
8
+ describe "query optimization" do
9
+ let(:namespace) { "perf_test" }
10
+
11
+ before do
12
+ # Create 1000 records
13
+ 1000.times do |i|
14
+ Mudis.write(
15
+ "record_#{i}",
16
+ {
17
+ id: i,
18
+ category: ["A", "B", "C", "D", "E"][i % 5],
19
+ score: rand(1..100),
20
+ active: i.even?,
21
+ name: "Record #{i}"
22
+ },
23
+ namespace: namespace
24
+ )
25
+ end
26
+ end
27
+
28
+ it "executes complex queries efficiently" do
29
+ start_time = Time.now
30
+
31
+ results = MudisQL.from(namespace)
32
+ .where(category: "A")
33
+ .where(score: ->(s) { s > 50 })
34
+ .where(active: true)
35
+ .order(:score, :desc)
36
+ .limit(20)
37
+ .all
38
+
39
+ duration = Time.now - start_time
40
+
41
+ expect(results.size).to be <= 20
42
+ expect(duration).to be < 1.0 # Should complete in under 1 second
43
+ end
44
+
45
+ it "handles multiple sequential queries efficiently" do
46
+ start_time = Time.now
47
+
48
+ 10.times do
49
+ MudisQL.from(namespace)
50
+ .where(active: true)
51
+ .order(:score)
52
+ .limit(10)
53
+ .all
54
+ end
55
+
56
+ duration = Time.now - start_time
57
+
58
+ expect(duration).to be < 2.0 # 10 queries in under 2 seconds
59
+ end
60
+
61
+ it "counts large result sets efficiently" do
62
+ start_time = Time.now
63
+
64
+ count = MudisQL.from(namespace)
65
+ .where(active: true)
66
+ .count
67
+
68
+ duration = Time.now - start_time
69
+
70
+ expect(count).to eq(500)
71
+ expect(duration).to be < 0.5
72
+ end
73
+
74
+ it "handles pluck on large datasets efficiently" do
75
+ start_time = Time.now
76
+
77
+ ids = MudisQL.from(namespace)
78
+ .where(category: "B")
79
+ .pluck(:id)
80
+
81
+ duration = Time.now - start_time
82
+
83
+ expect(ids.size).to eq(200)
84
+ expect(duration).to be < 0.5
85
+ end
86
+ end
87
+
88
+ describe "memory efficiency" do
89
+ let(:namespace) { "memory_test" }
90
+
91
+ it "handles queries without loading all data unnecessarily" do
92
+ 100.times do |i|
93
+ Mudis.write("m#{i}", { value: i }, namespace: namespace)
94
+ end
95
+
96
+ # first should be more efficient than all.first
97
+ result = MudisQL.from(namespace)
98
+ .where(value: ->(v) { v > 50 })
99
+ .order(:value)
100
+ .first
101
+
102
+ expect(result["value"]).to eq(51)
103
+ end
104
+
105
+ it "handles pagination without memory bloat" do
106
+ 500.times do |i|
107
+ Mudis.write("p#{i}", { seq: i }, namespace: namespace)
108
+ end
109
+
110
+ # Simulate pagination through large dataset
111
+ 5.times do |page|
112
+ results = MudisQL.from(namespace)
113
+ .order(:seq)
114
+ .limit(10)
115
+ .offset(page * 10)
116
+ .all
117
+
118
+ expect(results.size).to eq(10)
119
+ end
120
+ end
121
+ end
122
+
123
+ describe "cache interaction patterns" do
124
+ let(:namespace) { "cache_pattern" }
125
+
126
+ it "efficiently handles repeated queries with cache hits" do
127
+ 20.times { |i| Mudis.write("item#{i}", { value: i }, namespace: namespace) }
128
+
129
+ # Warm up
130
+ MudisQL.from(namespace).all
131
+
132
+ start_time = Time.now
133
+
134
+ # Run same query 100 times
135
+ 100.times do
136
+ MudisQL.from(namespace)
137
+ .where(value: ->(v) { v < 10 })
138
+ .all
139
+ end
140
+
141
+ duration = Time.now - start_time
142
+
143
+ # Should benefit from cache, complete quickly
144
+ expect(duration).to be < 2.0
145
+ end
146
+
147
+ it "handles rapid creation and querying" do
148
+ start_time = Time.now
149
+
150
+ 100.times do |i|
151
+ Mudis.write("rapid#{i}", { num: i }, namespace: namespace)
152
+
153
+ # Query immediately after write
154
+ result = MudisQL.from(namespace)
155
+ .where(num: i)
156
+ .first
157
+
158
+ expect(result).not_to be_nil
159
+ end
160
+
161
+ duration = Time.now - start_time
162
+
163
+ expect(duration).to be < 3.0
164
+ end
165
+ end
166
+
167
+ describe "query complexity scaling" do
168
+ let(:namespace) { "scaling" }
169
+
170
+ before do
171
+ 200.times do |i|
172
+ Mudis.write(
173
+ "s#{i}",
174
+ {
175
+ a: rand(100),
176
+ b: rand(100),
177
+ c: rand(100),
178
+ d: ["x", "y", "z"].sample,
179
+ e: i.even?
180
+ },
181
+ namespace: namespace
182
+ )
183
+ end
184
+ end
185
+
186
+ it "handles queries with multiple conditions" do
187
+ start_time = Time.now
188
+
189
+ results = MudisQL.from(namespace)
190
+ .where(a: ->(v) { v > 25 })
191
+ .where(b: ->(v) { v < 75 })
192
+ .where(c: 40..60)
193
+ .where(d: "x")
194
+ .where(e: true)
195
+ .all
196
+
197
+ duration = Time.now - start_time
198
+
199
+ expect(duration).to be < 0.5
200
+ expect(results).to be_an(Array)
201
+ end
202
+
203
+ it "handles complex ordering scenarios" do
204
+ start_time = Time.now
205
+
206
+ results = MudisQL.from(namespace)
207
+ .where(a: ->(v) { v > 50 })
208
+ .order(:b, :desc)
209
+ .limit(50)
210
+ .all
211
+
212
+ duration = Time.now - start_time
213
+
214
+ expect(duration).to be < 0.5
215
+ expect(results.size).to be <= 50
216
+ end
217
+ end
218
+
219
+ describe "real-world simulation" do
220
+ it "simulates an e-commerce search with filters" do
221
+ # Setup product catalog
222
+ 500.times do |i|
223
+ Mudis.write(
224
+ "product_#{i}",
225
+ {
226
+ name: "Product #{i}",
227
+ price: rand(10..1000),
228
+ category: ["Electronics", "Clothing", "Home", "Sports"][i % 4],
229
+ rating: rand(1.0..5.0).round(1),
230
+ in_stock: [true, false].sample,
231
+ brand: ["BrandA", "BrandB", "BrandC"][i % 3]
232
+ },
233
+ namespace: "products"
234
+ )
235
+ end
236
+
237
+ # Simulate user search
238
+ start_time = Time.now
239
+
240
+ results = MudisQL.from("products")
241
+ .where(category: "Electronics")
242
+ .where(price: 100..500)
243
+ .where(rating: ->(r) { r >= 4.0 })
244
+ .where(in_stock: true)
245
+ .order(:price, :asc)
246
+ .limit(25)
247
+ .all
248
+
249
+ duration = Time.now - start_time
250
+
251
+ expect(results.size).to be <= 25
252
+ expect(duration).to be < 0.8
253
+ end
254
+
255
+ it "simulates user analytics dashboard" do
256
+ # Setup user data
257
+ 300.times do |i|
258
+ Mudis.write(
259
+ "user_#{i}",
260
+ {
261
+ id: i,
262
+ signup_date: "2025-#{rand(1..12).to_s.rjust(2, '0')}-01",
263
+ lifetime_value: rand(0..5000),
264
+ orders: rand(0..50),
265
+ status: ["active", "inactive", "suspended"][i % 3]
266
+ },
267
+ namespace: "users"
268
+ )
269
+ end
270
+
271
+ start_time = Time.now
272
+
273
+ # Dashboard queries
274
+ active_users = MudisQL.from("users").where(status: "active").count
275
+ high_value = MudisQL.from("users")
276
+ .where(lifetime_value: ->(v) { v > 1000 })
277
+ .where(status: "active")
278
+ .count
279
+
280
+ top_customers = MudisQL.from("users")
281
+ .where(status: "active")
282
+ .order(:lifetime_value, :desc)
283
+ .limit(10)
284
+ .pluck(:id, :lifetime_value)
285
+
286
+ duration = Time.now - start_time
287
+
288
+ expect(active_users).to be > 0
289
+ expect(high_value).to be >= 0
290
+ expect(top_customers.size).to eq(10)
291
+ expect(duration).to be < 1.0
292
+ end
293
+ end
294
+ end
295
+