mudis-ql 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.
- checksums.yaml +4 -4
- data/README.md +69 -51
- data/lib/mudis-ql/scope.rb +76 -7
- data/lib/mudis-ql/version.rb +1 -1
- data/sig/mudis-ql/metrics_scope.rbs +54 -0
- data/sig/mudis-ql/scope.rbs +60 -0
- data/sig/mudis-ql/store.rbs +23 -0
- data/sig/mudis-ql.rbs +25 -0
- data/spec/mudis-ql/scope_spec.rb +91 -0
- metadata +13 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca08d464b1ccfec7dcca637fa7fcf3e80e59fc5175ad1875ae785a6e6e204f9a
|
|
4
|
+
data.tar.gz: a00f60cf704c7b7b7df56570c2addbb926beb31599c411047d209dea47d3b4f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 934c5dc7d6bfd8fd3dd71dc875a0cc967b33c618cc1ca2b47602462ef31db0d09dc40d8a6d67e93cd3472a2d78cc2a9d52aa583b620c6d95e4ca2278b5b9ab2f
|
|
7
|
+
data.tar.gz: bb29da619a4be61e0090955cab91ae96cdfd0824a65b3becce4694ae23aa87c37b00ea873f2a4cc897f3f02851b403b24c748fc861a7544566b8fc29a75dc9d3
|
data/README.md
CHANGED
|
@@ -7,7 +7,7 @@ A simple query DSL for [mudis](https://github.com/kiebor81/mudis) cache. Mudis-Q
|
|
|
7
7
|
|
|
8
8
|
## Why Mudis-QL?
|
|
9
9
|
|
|
10
|
-
**Mudis**
|
|
10
|
+
**Mudis** has been a great in-memory cache for most of my needs, but it was only ever designed to support simple key-value retrieval. From the documentation:
|
|
11
11
|
|
|
12
12
|
> "No SQL or equivalent query interface for cached data. Data is per Key retrieval only."
|
|
13
13
|
|
|
@@ -18,7 +18,7 @@ A simple query DSL for [mudis](https://github.com/kiebor81/mudis) cache. Mudis-Q
|
|
|
18
18
|
- Count and check existence
|
|
19
19
|
- Pluck specific fields
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
The goal is to retain Mudis's speed, thread-safety, and in-memory efficiency, but provide an opt-in gem fro advanced retrieval options if needed.
|
|
22
22
|
|
|
23
23
|
### Best Practice: Use Namespaces
|
|
24
24
|
|
|
@@ -42,8 +42,8 @@ Namespaces provide logical separation and enable Mudis-QL to retrieve all keys i
|
|
|
42
42
|
flowchart TD
|
|
43
43
|
Start([User Code]) --> Entry{Entry Point}
|
|
44
44
|
|
|
45
|
-
Entry -->|
|
|
46
|
-
Entry -->|
|
|
45
|
+
Entry -->|MudisQL.from| CreateScope[Create Scope Instance]
|
|
46
|
+
Entry -->|MudisQL.metrics| CreateMetrics[Create MetricsScope]
|
|
47
47
|
|
|
48
48
|
CreateScope --> InitStore[Initialize Store with namespace]
|
|
49
49
|
InitStore --> Scope[Scope Object]
|
|
@@ -126,7 +126,7 @@ $ gem install mudis-ql
|
|
|
126
126
|
|
|
127
127
|
## Requirements
|
|
128
128
|
|
|
129
|
-
- Ruby >=
|
|
129
|
+
- Ruby >= 3.0.0
|
|
130
130
|
- mudis gem
|
|
131
131
|
|
|
132
132
|
## Usage
|
|
@@ -155,16 +155,16 @@ Mudis.write('user3', { name: 'Charlie', age: 35, status: 'inactive' }, namespace
|
|
|
155
155
|
|
|
156
156
|
```ruby
|
|
157
157
|
# Get all users from a namespace
|
|
158
|
-
users =
|
|
158
|
+
users = MudisQL.from('users').all
|
|
159
159
|
# => [{"name"=>"Alice", "age"=>30, "status"=>"active", "_key"=>"user1"}, ...]
|
|
160
160
|
|
|
161
161
|
# Filter by exact match
|
|
162
|
-
active_users =
|
|
162
|
+
active_users = MudisQL.from('users')
|
|
163
163
|
.where(status: 'active')
|
|
164
164
|
.all
|
|
165
165
|
|
|
166
166
|
# Chain multiple conditions
|
|
167
|
-
result =
|
|
167
|
+
result = MudisQL.from('users')
|
|
168
168
|
.where(status: 'active')
|
|
169
169
|
.where(age: ->(v) { v >= 25 })
|
|
170
170
|
.all
|
|
@@ -174,17 +174,17 @@ result = mudis-ql.from('users')
|
|
|
174
174
|
|
|
175
175
|
```ruby
|
|
176
176
|
# Use proc for custom conditions
|
|
177
|
-
adults =
|
|
177
|
+
adults = MudisQL.from('users')
|
|
178
178
|
.where(age: ->(age) { age >= 18 })
|
|
179
179
|
.all
|
|
180
180
|
|
|
181
181
|
# Use regex for pattern matching
|
|
182
|
-
a_names =
|
|
182
|
+
a_names = MudisQL.from('users')
|
|
183
183
|
.where(name: /^A/i)
|
|
184
184
|
.all
|
|
185
185
|
|
|
186
186
|
# Use ranges
|
|
187
|
-
young_adults =
|
|
187
|
+
young_adults = MudisQL.from('users')
|
|
188
188
|
.where(age: 18..25)
|
|
189
189
|
.all
|
|
190
190
|
```
|
|
@@ -193,23 +193,23 @@ young_adults = mudis-ql.from('users')
|
|
|
193
193
|
|
|
194
194
|
```ruby
|
|
195
195
|
# Order by field (ascending by default)
|
|
196
|
-
sorted_users =
|
|
196
|
+
sorted_users = MudisQL.from('users')
|
|
197
197
|
.order(:age)
|
|
198
198
|
.all
|
|
199
199
|
|
|
200
200
|
# Order descending
|
|
201
|
-
sorted_desc =
|
|
201
|
+
sorted_desc = MudisQL.from('users')
|
|
202
202
|
.order(:age, :desc)
|
|
203
203
|
.all
|
|
204
204
|
|
|
205
205
|
# Limit results
|
|
206
|
-
top_5 =
|
|
206
|
+
top_5 = MudisQL.from('users')
|
|
207
207
|
.order(:age, :desc)
|
|
208
208
|
.limit(5)
|
|
209
209
|
.all
|
|
210
210
|
|
|
211
211
|
# Pagination with offset
|
|
212
|
-
page_2 =
|
|
212
|
+
page_2 = MudisQL.from('users')
|
|
213
213
|
.order(:name)
|
|
214
214
|
.limit(10)
|
|
215
215
|
.offset(10)
|
|
@@ -220,30 +220,30 @@ page_2 = mudis-ql.from('users')
|
|
|
220
220
|
|
|
221
221
|
```ruby
|
|
222
222
|
# Get first matching record
|
|
223
|
-
first_active =
|
|
223
|
+
first_active = MudisQL.from('users')
|
|
224
224
|
.where(status: 'active')
|
|
225
225
|
.first
|
|
226
226
|
|
|
227
227
|
# Get last matching record
|
|
228
|
-
last_user =
|
|
228
|
+
last_user = MudisQL.from('users')
|
|
229
229
|
.order(:age)
|
|
230
230
|
.last
|
|
231
231
|
|
|
232
232
|
# Count matching records
|
|
233
|
-
count =
|
|
233
|
+
count = MudisQL.from('users')
|
|
234
234
|
.where(status: 'active')
|
|
235
235
|
.count
|
|
236
236
|
|
|
237
237
|
# Check if any records match
|
|
238
|
-
has_inactive =
|
|
238
|
+
has_inactive = MudisQL.from('users')
|
|
239
239
|
.where(status: 'inactive')
|
|
240
240
|
.exists?
|
|
241
241
|
|
|
242
242
|
# Pluck specific fields
|
|
243
|
-
names =
|
|
243
|
+
names = MudisQL.from('users').pluck(:name)
|
|
244
244
|
# => ["Alice", "Bob", "Charlie"]
|
|
245
245
|
|
|
246
|
-
name_age_pairs =
|
|
246
|
+
name_age_pairs = MudisQL.from('users').pluck(:name, :age)
|
|
247
247
|
# => [["Alice", 30], ["Bob", 25], ["Charlie", 35]]
|
|
248
248
|
```
|
|
249
249
|
|
|
@@ -251,7 +251,7 @@ name_age_pairs = mudis-ql.from('users').pluck(:name, :age)
|
|
|
251
251
|
|
|
252
252
|
```ruby
|
|
253
253
|
# Complex query combining multiple operations
|
|
254
|
-
result =
|
|
254
|
+
result = MudisQL.from('users')
|
|
255
255
|
.where(status: 'active')
|
|
256
256
|
.where(age: ->(age) { age >= 25 })
|
|
257
257
|
.order(:age, :desc)
|
|
@@ -261,7 +261,7 @@ result = mudis-ql.from('users')
|
|
|
261
261
|
|
|
262
262
|
# Or using method chaining for pagination
|
|
263
263
|
def get_active_users(page: 1, per_page: 10)
|
|
264
|
-
|
|
264
|
+
MudisQL.from('users')
|
|
265
265
|
.where(status: 'active')
|
|
266
266
|
.order(:name)
|
|
267
267
|
.limit(per_page)
|
|
@@ -270,6 +270,31 @@ def get_active_users(page: 1, per_page: 10)
|
|
|
270
270
|
end
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
+
#### Aggregation
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
# Sum numeric values
|
|
277
|
+
total_salary = MudisQL.from('users')
|
|
278
|
+
.where(status: 'active')
|
|
279
|
+
.sum(:salary)
|
|
280
|
+
|
|
281
|
+
# Calculate average
|
|
282
|
+
avg_age = MudisQL.from('users')
|
|
283
|
+
.average(:age)
|
|
284
|
+
|
|
285
|
+
# Group by field value
|
|
286
|
+
by_department = MudisQL.from('users')
|
|
287
|
+
.where(status: 'active')
|
|
288
|
+
.group_by(:department)
|
|
289
|
+
# => { "engineering" => [...], "sales" => [...], "marketing" => [...] }
|
|
290
|
+
|
|
291
|
+
# Aggregation with complex filtering
|
|
292
|
+
avg_high_earners = MudisQL.from('users')
|
|
293
|
+
.where(status: 'active')
|
|
294
|
+
.where(salary: ->(s) { s > 100000 })
|
|
295
|
+
.average(:salary)
|
|
296
|
+
```
|
|
297
|
+
|
|
273
298
|
## How It Works
|
|
274
299
|
|
|
275
300
|
mudis-ql works by:
|
|
@@ -298,7 +323,7 @@ class UserCacheService
|
|
|
298
323
|
end
|
|
299
324
|
|
|
300
325
|
def self.active_users(limit: 50)
|
|
301
|
-
|
|
326
|
+
MudisQL.from(NAMESPACE)
|
|
302
327
|
.where(status: 'active')
|
|
303
328
|
.order(:created_at, :desc)
|
|
304
329
|
.limit(limit)
|
|
@@ -306,7 +331,7 @@ class UserCacheService
|
|
|
306
331
|
end
|
|
307
332
|
|
|
308
333
|
def self.search_by_name(pattern)
|
|
309
|
-
|
|
334
|
+
MudisQL.from(NAMESPACE)
|
|
310
335
|
.where(name: /#{Regexp.escape(pattern)}/i)
|
|
311
336
|
.all
|
|
312
337
|
end
|
|
@@ -323,14 +348,14 @@ module MyApp
|
|
|
323
348
|
NAMESPACE = 'users'
|
|
324
349
|
|
|
325
350
|
def find_active(limit: 50)
|
|
326
|
-
|
|
351
|
+
MudisQL.from(NAMESPACE)
|
|
327
352
|
.where(status: 'active')
|
|
328
353
|
.limit(limit)
|
|
329
354
|
.all
|
|
330
355
|
end
|
|
331
356
|
|
|
332
357
|
def find_by_age_range(min:, max:)
|
|
333
|
-
|
|
358
|
+
MudisQL.from(NAMESPACE)
|
|
334
359
|
.where(age: min..max)
|
|
335
360
|
.order(:age)
|
|
336
361
|
.all
|
|
@@ -348,7 +373,7 @@ mudis-ql provides a powerful interface for querying mudis cache metrics:
|
|
|
348
373
|
|
|
349
374
|
```ruby
|
|
350
375
|
# Get a metrics scope
|
|
351
|
-
metrics =
|
|
376
|
+
metrics = MudisQL.metrics
|
|
352
377
|
|
|
353
378
|
# Top-level metrics summary
|
|
354
379
|
summary = metrics.summary
|
|
@@ -466,7 +491,7 @@ current_metrics = metrics.refresh
|
|
|
466
491
|
|
|
467
492
|
# Monitor cache performance over time
|
|
468
493
|
def cache_health_check
|
|
469
|
-
m =
|
|
494
|
+
m = MudisQL.metrics
|
|
470
495
|
|
|
471
496
|
{
|
|
472
497
|
timestamp: Time.now,
|
|
@@ -483,10 +508,10 @@ end
|
|
|
483
508
|
class MetricsController < ApplicationController
|
|
484
509
|
def show
|
|
485
510
|
render json: {
|
|
486
|
-
summary:
|
|
487
|
-
efficiency:
|
|
488
|
-
distribution:
|
|
489
|
-
top_keys:
|
|
511
|
+
summary: MudisQL.metrics.summary,
|
|
512
|
+
efficiency: MudisQL.metrics.efficiency,
|
|
513
|
+
distribution: MudisQL.metrics.bucket_distribution,
|
|
514
|
+
top_keys: MudisQL.metrics.least_touched.order(:access_count, :desc).limit(10).all
|
|
490
515
|
}
|
|
491
516
|
end
|
|
492
517
|
end
|
|
@@ -494,17 +519,17 @@ end
|
|
|
494
519
|
|
|
495
520
|
## API Reference
|
|
496
521
|
|
|
497
|
-
###
|
|
522
|
+
### MudisQL.from(namespace)
|
|
498
523
|
|
|
499
524
|
Creates a new scope for the specified mudis namespace.
|
|
500
525
|
|
|
501
|
-
**Returns:** `
|
|
526
|
+
**Returns:** `MudisQL::Scope`
|
|
502
527
|
|
|
503
|
-
###
|
|
528
|
+
### MudisQL.metrics
|
|
504
529
|
|
|
505
530
|
Access mudis metrics with a queryable interface.
|
|
506
531
|
|
|
507
|
-
**Returns:** `
|
|
532
|
+
**Returns:** `MudisQL::MetricsScope`
|
|
508
533
|
|
|
509
534
|
### Scope Methods
|
|
510
535
|
|
|
@@ -520,6 +545,11 @@ Access mudis metrics with a queryable interface.
|
|
|
520
545
|
| `count` | Count matching records | `Integer` |
|
|
521
546
|
| `exists?` | Check if any records match | `Boolean` |
|
|
522
547
|
| `pluck(*fields)` | Extract specific fields | `Array` |
|
|
548
|
+
| `sum(field)` | Sum numeric values of a field | `Integer` or `Float` |
|
|
549
|
+
| `average(field)` | Average numeric values of a field | `Float` |
|
|
550
|
+
| `group_by(field)` | Group records by field value | `Hash` |
|
|
551
|
+
| `exists?` | Check if any records match | `Boolean` |
|
|
552
|
+
| `pluck(*fields)` | Extract specific fields | `Array` |
|
|
523
553
|
|
|
524
554
|
### MetricsScope Methods
|
|
525
555
|
|
|
@@ -540,7 +570,7 @@ Access mudis metrics with a queryable interface.
|
|
|
540
570
|
|
|
541
571
|
### Condition Matchers
|
|
542
572
|
|
|
543
|
-
|
|
573
|
+
Mudis-QL supports multiple types of matchers in `where` conditions:
|
|
544
574
|
|
|
545
575
|
```ruby
|
|
546
576
|
# Exact match
|
|
@@ -570,8 +600,7 @@ For very large datasets or complex queries, consider using a proper database alo
|
|
|
570
600
|
1. **Full scan required**: Unlike databases with indexes, mudis-ql must load all records from a namespace to filter them
|
|
571
601
|
2. **In-memory processing**: All filtering happens in Ruby memory, not at the storage layer
|
|
572
602
|
3. **No joins**: Cannot join data across namespaces (each query targets one namespace)
|
|
573
|
-
4. **
|
|
574
|
-
5. **Namespaces required for queries**: mudis-ql requires mudis namespaces to list and query collections. Keys stored without namespaces cannot be queried as a collection (individual key access still works via mudis directly).
|
|
603
|
+
4. **Namespaces required for queries**: mudis-ql requires mudis namespaces to list and query collections. Keys stored without namespaces cannot be queried as a collection (individual key access still works via mudis directly).
|
|
575
604
|
|
|
576
605
|
These limitations are by design to maintain simplicity and compatibility with mudis's key-value architecture.
|
|
577
606
|
|
|
@@ -579,17 +608,6 @@ These limitations are by design to maintain simplicity and compatibility with mu
|
|
|
579
608
|
|
|
580
609
|
Bug reports and pull requests are welcome on GitHub at https://github.com/kiebor81/mudisql.
|
|
581
610
|
|
|
582
|
-
## Roadmap
|
|
583
|
-
|
|
584
|
-
Future enhancements under consideration:
|
|
585
|
-
|
|
586
|
-
- [ ] Index support for common query patterns
|
|
587
|
-
- [ ] Aggregation methods (sum, average, group_by)
|
|
588
|
-
- [ ] Multi-namespace queries
|
|
589
|
-
- [ ] Query result caching
|
|
590
|
-
- [ ] Bulk operations support
|
|
591
|
-
- [ ] Custom serialization per namespace
|
|
592
|
-
|
|
593
611
|
## See Also
|
|
594
612
|
|
|
595
613
|
- [Mudis](https://github.com/kiebor81/mudis)
|
data/lib/mudis-ql/scope.rb
CHANGED
|
@@ -108,6 +108,48 @@ module MudisQL
|
|
|
108
108
|
count > 0
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
# Sum numeric values of a field across matching records
|
|
112
|
+
#
|
|
113
|
+
# @param field [Symbol, String] the field to sum
|
|
114
|
+
# @return [Integer, Float] sum of numeric values, 0 if none found
|
|
115
|
+
def sum(field)
|
|
116
|
+
field_str = field.to_s
|
|
117
|
+
apply_conditions(store.all).sum do |record|
|
|
118
|
+
value = record[field_str]
|
|
119
|
+
value.is_a?(Numeric) ? value : 0
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Calculate average of numeric values for a field across matching records
|
|
124
|
+
#
|
|
125
|
+
# @param field [Symbol, String] the field to average
|
|
126
|
+
# @return [Float] average of numeric values, 0.0 if no records
|
|
127
|
+
def average(field)
|
|
128
|
+
field_str = field.to_s
|
|
129
|
+
results = apply_conditions(store.all)
|
|
130
|
+
return 0.0 if results.empty?
|
|
131
|
+
|
|
132
|
+
total = results.sum do |record|
|
|
133
|
+
value = record[field_str]
|
|
134
|
+
value.is_a?(Numeric) ? value : 0
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
(total.to_f / results.size).round(2)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Group matching records by the value of a field
|
|
141
|
+
#
|
|
142
|
+
# @param field [Symbol, String] the field to group by
|
|
143
|
+
# @return [Hash] hash of field_value => array of records
|
|
144
|
+
def group_by(field)
|
|
145
|
+
field_str = field.to_s
|
|
146
|
+
filtered = apply_conditions(store.all)
|
|
147
|
+
|
|
148
|
+
filtered.group_by do |record|
|
|
149
|
+
record[field_str]
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
111
153
|
# Pluck specific fields from results
|
|
112
154
|
#
|
|
113
155
|
# @param fields [Array<Symbol, String>] fields to extract
|
|
@@ -125,6 +167,9 @@ module MudisQL
|
|
|
125
167
|
|
|
126
168
|
private
|
|
127
169
|
|
|
170
|
+
# Apply where conditions to results
|
|
171
|
+
# @param results [Array<Hash>] array of records
|
|
172
|
+
# @return [Array<Hash>] filtered records
|
|
128
173
|
def apply_conditions(results)
|
|
129
174
|
@conditions.reduce(results) do |filtered, condition|
|
|
130
175
|
filtered.select do |record|
|
|
@@ -138,6 +183,9 @@ module MudisQL
|
|
|
138
183
|
end
|
|
139
184
|
end
|
|
140
185
|
|
|
186
|
+
# Check if a value matches a condition
|
|
187
|
+
# @param value [Object] the field value
|
|
188
|
+
# @param matcher [Object, Proc, Regexp, Range] the condition to match
|
|
141
189
|
def match_condition?(value, matcher)
|
|
142
190
|
case matcher
|
|
143
191
|
when Proc
|
|
@@ -151,29 +199,50 @@ module MudisQL
|
|
|
151
199
|
end
|
|
152
200
|
end
|
|
153
201
|
|
|
202
|
+
# Apply ordering to results
|
|
203
|
+
# @param results [Array<Hash>] array of records
|
|
204
|
+
# @return [Array<Hash>] ordered records
|
|
154
205
|
def apply_order(results)
|
|
155
206
|
return results unless @order_by
|
|
156
207
|
|
|
157
208
|
begin
|
|
158
|
-
|
|
209
|
+
# Separate nil and non-nil values
|
|
210
|
+
non_nil_results = []
|
|
211
|
+
nil_results = []
|
|
212
|
+
|
|
213
|
+
results.each do |record|
|
|
159
214
|
val = record[@order_by]
|
|
160
|
-
# Handle nil values and type-safe comparison
|
|
161
215
|
if val.nil?
|
|
162
|
-
|
|
163
|
-
elsif val.is_a?(Numeric)
|
|
164
|
-
[0, val]
|
|
216
|
+
nil_results << record
|
|
165
217
|
else
|
|
166
|
-
|
|
218
|
+
non_nil_results << record
|
|
167
219
|
end
|
|
168
220
|
end
|
|
169
221
|
|
|
170
|
-
|
|
222
|
+
# Sort non-nil values
|
|
223
|
+
sorted = non_nil_results.sort_by do |record|
|
|
224
|
+
val = record[@order_by]
|
|
225
|
+
if val.is_a?(Numeric)
|
|
226
|
+
val
|
|
227
|
+
else
|
|
228
|
+
val.to_s
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Reverse if descending
|
|
233
|
+
sorted = sorted.reverse if @order_direction == :desc
|
|
234
|
+
|
|
235
|
+
# Append nil values at the end
|
|
236
|
+
sorted + nil_results
|
|
171
237
|
rescue ArgumentError => e
|
|
172
238
|
# If sorting fails, return unsorted results
|
|
173
239
|
results
|
|
174
240
|
end
|
|
175
241
|
end
|
|
176
242
|
|
|
243
|
+
# Apply limit and offset to results
|
|
244
|
+
# @param results [Array<Hash>] array of records
|
|
245
|
+
# @return [Array<Hash>] paginated records
|
|
177
246
|
def apply_pagination(results)
|
|
178
247
|
offset = [@offset_value, 0].max
|
|
179
248
|
results = results.drop(offset) if offset > 0
|
data/lib/mudis-ql/version.rb
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module MudisQL
|
|
2
|
+
# MetricsScope provides queryable access to mudis metrics data
|
|
3
|
+
class MetricsScope
|
|
4
|
+
attr_reader metrics_data: Hash[Symbol, untyped]
|
|
5
|
+
|
|
6
|
+
def initialize: () -> void
|
|
7
|
+
|
|
8
|
+
# Get top-level metrics (hits, misses, evictions, etc.)
|
|
9
|
+
def summary: () -> Hash[Symbol, untyped]
|
|
10
|
+
|
|
11
|
+
# Query least touched keys
|
|
12
|
+
def least_touched: () -> Scope
|
|
13
|
+
|
|
14
|
+
# Query bucket metrics
|
|
15
|
+
def buckets: () -> Scope
|
|
16
|
+
|
|
17
|
+
# Get total number of keys across all buckets
|
|
18
|
+
def total_keys: () -> Integer
|
|
19
|
+
|
|
20
|
+
# Get total memory across all buckets
|
|
21
|
+
def total_memory: () -> Integer
|
|
22
|
+
|
|
23
|
+
# Get hit rate as a percentage
|
|
24
|
+
def hit_rate: () -> Float
|
|
25
|
+
|
|
26
|
+
# Get cache efficiency metrics
|
|
27
|
+
def efficiency: () -> Hash[Symbol, Float]
|
|
28
|
+
|
|
29
|
+
# Find buckets with high memory usage
|
|
30
|
+
def high_memory_buckets: (Integer threshold) -> Array[Hash[Symbol, untyped]]
|
|
31
|
+
|
|
32
|
+
# Find buckets with many keys
|
|
33
|
+
def high_key_buckets: (Integer threshold) -> Array[Hash[Symbol, untyped]]
|
|
34
|
+
|
|
35
|
+
# Get bucket distribution statistics
|
|
36
|
+
def bucket_distribution: () -> Hash[Symbol, untyped]
|
|
37
|
+
|
|
38
|
+
# Get keys that have never been accessed
|
|
39
|
+
def never_accessed_keys: () -> Array[String]
|
|
40
|
+
|
|
41
|
+
# Refresh metrics data
|
|
42
|
+
def refresh: () -> MetricsScope
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def eviction_rate: () -> Float
|
|
47
|
+
|
|
48
|
+
def rejection_rate: () -> Float
|
|
49
|
+
|
|
50
|
+
def create_scope_for_array: (Array[Hash[Symbol, untyped]] data) -> Scope
|
|
51
|
+
|
|
52
|
+
def default_distribution: () -> Hash[Symbol, untyped]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module MudisQL
|
|
2
|
+
# Scope provides a chainable DSL for querying mudis cache data
|
|
3
|
+
class Scope
|
|
4
|
+
attr_reader store: Store
|
|
5
|
+
|
|
6
|
+
def initialize: (Store store) -> void
|
|
7
|
+
|
|
8
|
+
# Create a new independent scope instance
|
|
9
|
+
def clone: () -> Scope
|
|
10
|
+
|
|
11
|
+
# Filter records by conditions
|
|
12
|
+
def where: (Hash[String | Symbol, untyped] conditions) -> Scope
|
|
13
|
+
|
|
14
|
+
# Order results by a field
|
|
15
|
+
def order: (String | Symbol field, ?Symbol direction) -> Scope
|
|
16
|
+
|
|
17
|
+
# Limit the number of results
|
|
18
|
+
def limit: (Integer value) -> Scope
|
|
19
|
+
|
|
20
|
+
# Skip the first N results
|
|
21
|
+
def offset: (Integer value) -> Scope
|
|
22
|
+
|
|
23
|
+
# Execute the query and return all matching records
|
|
24
|
+
def all: () -> Array[Hash[String, untyped]]
|
|
25
|
+
|
|
26
|
+
# Execute query and return first result
|
|
27
|
+
def first: () -> Hash[String, untyped] | nil
|
|
28
|
+
|
|
29
|
+
# Execute query and return last result
|
|
30
|
+
def last: () -> Hash[String, untyped] | nil
|
|
31
|
+
|
|
32
|
+
# Count matching records
|
|
33
|
+
def count: () -> Integer
|
|
34
|
+
|
|
35
|
+
# Check if any records match
|
|
36
|
+
def exists?: () -> bool
|
|
37
|
+
|
|
38
|
+
# Pluck specific fields from results
|
|
39
|
+
def pluck: (*String | Symbol fields) -> Array[untyped]
|
|
40
|
+
|
|
41
|
+
# Sum numeric values of a field
|
|
42
|
+
def sum: (String | Symbol field) -> Integer | Float
|
|
43
|
+
|
|
44
|
+
# Calculate average of numeric values for a field
|
|
45
|
+
def average: (String | Symbol field) -> Float
|
|
46
|
+
|
|
47
|
+
# Group matching records by field value
|
|
48
|
+
def group_by: (String | Symbol field) -> Hash[untyped, Array[Hash[String, untyped]]]
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def apply_conditions: (Array[Hash[String, untyped]] results) -> Array[Hash[String, untyped]]
|
|
53
|
+
|
|
54
|
+
def match_condition?: (untyped value, untyped matcher) -> bool
|
|
55
|
+
|
|
56
|
+
def apply_order: (Array[Hash[String, untyped]] results) -> Array[Hash[String, untyped]]
|
|
57
|
+
|
|
58
|
+
def apply_pagination: (Array[Hash[String, untyped]] results) -> Array[Hash[String, untyped]]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module MudisQL
|
|
2
|
+
# Store wraps mudis operations for a specific namespace
|
|
3
|
+
class Store
|
|
4
|
+
attr_reader namespace: String?
|
|
5
|
+
|
|
6
|
+
def initialize: (String? namespace) -> void
|
|
7
|
+
|
|
8
|
+
# Retrieve all keys from the namespace
|
|
9
|
+
def keys: () -> Array[String]
|
|
10
|
+
|
|
11
|
+
# Read a value from the cache
|
|
12
|
+
def read: (String key) -> untyped
|
|
13
|
+
|
|
14
|
+
# Write a value to the cache
|
|
15
|
+
def write: (String key, untyped value, ?expires_in: Integer?) -> untyped
|
|
16
|
+
|
|
17
|
+
# Delete a key from the cache
|
|
18
|
+
def delete: (String key) -> untyped
|
|
19
|
+
|
|
20
|
+
# Retrieve all records from the namespace
|
|
21
|
+
def all: () -> Array[Hash[String, untyped]]
|
|
22
|
+
end
|
|
23
|
+
end
|
data/sig/mudis-ql.rbs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module MudisQL
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# Create a new scope for the given namespace
|
|
8
|
+
def self.from: (String? namespace) -> Scope
|
|
9
|
+
|
|
10
|
+
# Access mudis metrics with queryable interface
|
|
11
|
+
def self.metrics: () -> MetricsScope
|
|
12
|
+
|
|
13
|
+
# Configure MudisQL defaults
|
|
14
|
+
def self.configure: () { (Configuration) -> void } -> void
|
|
15
|
+
|
|
16
|
+
# Get the current configuration
|
|
17
|
+
def self.configuration: () -> Configuration
|
|
18
|
+
|
|
19
|
+
# Configuration class for MudisQL
|
|
20
|
+
class Configuration
|
|
21
|
+
attr_accessor default_limit: Integer
|
|
22
|
+
|
|
23
|
+
def initialize: () -> void
|
|
24
|
+
end
|
|
25
|
+
end
|
data/spec/mudis-ql/scope_spec.rb
CHANGED
|
@@ -165,5 +165,96 @@ RSpec.describe MudisQL::Scope do
|
|
|
165
165
|
expect(results.map { |r| r["name"] }).to eq(["Mouse", "Keyboard"])
|
|
166
166
|
end
|
|
167
167
|
end
|
|
168
|
+
|
|
169
|
+
describe "#sum" do
|
|
170
|
+
it "sums numeric values of a field" do
|
|
171
|
+
total = scope.sum(:price)
|
|
172
|
+
# Laptop (1200) + Mouse (25) + Desk (300) + Chair (150) + Keyboard (75) = 1750
|
|
173
|
+
expect(total).to eq(1750)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it "sums only matching records" do
|
|
177
|
+
# Desk (300) + Chair (150) = 450
|
|
178
|
+
total = scope.where(category: "furniture").sum(:price)
|
|
179
|
+
expect(total).to eq(450)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "ignores non-numeric values" do
|
|
183
|
+
total = scope.sum(:name)
|
|
184
|
+
expect(total).to eq(0)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "returns 0 when no records" do
|
|
188
|
+
total = scope.where(name: "nonexistent").sum(:price)
|
|
189
|
+
expect(total).to eq(0)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
describe "#average" do
|
|
194
|
+
it "calculates average of numeric values" do
|
|
195
|
+
# (1200 + 25 + 300 + 150 + 75) / 5 = 1750 / 5 = 350.0
|
|
196
|
+
avg = scope.average(:price)
|
|
197
|
+
expect(avg).to eq(350.0)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "averages only matching records" do
|
|
201
|
+
# Electronics: (1200 + 25 + 75) / 3 = 1300 / 3 = 433.33
|
|
202
|
+
avg = scope.where(category: "electronics").average(:price)
|
|
203
|
+
expect(avg).to eq(433.33)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "ignores non-numeric values" do
|
|
207
|
+
avg = scope.average(:name)
|
|
208
|
+
expect(avg).to eq(0.0)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "returns 0.0 when no records" do
|
|
212
|
+
avg = scope.where(name: "nonexistent").average(:price)
|
|
213
|
+
expect(avg).to eq(0.0)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it "rounds to 2 decimal places" do
|
|
217
|
+
# Furniture average: (300 + 150) / 2 = 225.0
|
|
218
|
+
avg = scope.where(category: "furniture").average(:price)
|
|
219
|
+
expect(avg).to eq(225.0)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
describe "#group_by" do
|
|
224
|
+
it "groups records by field value" do
|
|
225
|
+
grouped = scope.group_by(:category)
|
|
226
|
+
expect(grouped.keys).to contain_exactly("electronics", "furniture")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "includes all records in correct groups" do
|
|
230
|
+
grouped = scope.group_by(:category)
|
|
231
|
+
expect(grouped["electronics"].size).to eq(3)
|
|
232
|
+
expect(grouped["furniture"].size).to eq(2)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "groups correctly with filters applied" do
|
|
236
|
+
# Only stock >= 30: Mouse (50), Keyboard (30)
|
|
237
|
+
grouped = scope.where(stock: ->(s) { s >= 30 }).group_by(:category)
|
|
238
|
+
expect(grouped["electronics"].size).to eq(2)
|
|
239
|
+
expect(grouped["furniture"]).to be_nil
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it "groups by non-string fields" do
|
|
243
|
+
grouped = scope.group_by(:stock)
|
|
244
|
+
# stock values: 5, 50, 10, 20, 30
|
|
245
|
+
expect(grouped.keys).to contain_exactly(5, 50, 10, 20, 30)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it "includes nil as a group key" do
|
|
249
|
+
Mudis.write("item_nil_cat", { name: "Unknown", price: 50 }, namespace: namespace)
|
|
250
|
+
grouped = scope.group_by(:category)
|
|
251
|
+
expect(grouped).to have_key(nil)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it "returns empty hash when no records match" do
|
|
255
|
+
grouped = scope.where(name: "nonexistent").group_by(:category)
|
|
256
|
+
expect(grouped).to eq({})
|
|
257
|
+
end
|
|
258
|
+
end
|
|
168
259
|
end
|
|
169
260
|
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mudis-ql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kiebor81
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: mudis
|
|
@@ -65,11 +65,15 @@ dependencies:
|
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '0.22'
|
|
68
|
-
description: Mudis-QL extends Mudis by providing a SQL-like query interface
|
|
69
|
-
stored in the mudis cache
|
|
68
|
+
description: Mudis-QL extends Mudis by providing a fluent SQL-like query interface
|
|
69
|
+
for data stored in the mudis cache
|
|
70
70
|
executables: []
|
|
71
71
|
extensions: []
|
|
72
|
-
extra_rdoc_files:
|
|
72
|
+
extra_rdoc_files:
|
|
73
|
+
- sig/mudis-ql/metrics_scope.rbs
|
|
74
|
+
- sig/mudis-ql/scope.rbs
|
|
75
|
+
- sig/mudis-ql/store.rbs
|
|
76
|
+
- sig/mudis-ql.rbs
|
|
73
77
|
files:
|
|
74
78
|
- README.md
|
|
75
79
|
- lib/mudis-ql.rb
|
|
@@ -77,6 +81,10 @@ files:
|
|
|
77
81
|
- lib/mudis-ql/scope.rb
|
|
78
82
|
- lib/mudis-ql/store.rb
|
|
79
83
|
- lib/mudis-ql/version.rb
|
|
84
|
+
- sig/mudis-ql.rbs
|
|
85
|
+
- sig/mudis-ql/metrics_scope.rbs
|
|
86
|
+
- sig/mudis-ql/scope.rbs
|
|
87
|
+
- sig/mudis-ql/store.rbs
|
|
80
88
|
- spec/mudis-ql/error_handling_spec.rb
|
|
81
89
|
- spec/mudis-ql/integration_spec.rb
|
|
82
90
|
- spec/mudis-ql/metrics_scope_spec.rb
|