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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8ca84afb158da1ef68705edf7990481b4248356318f76cb591ae6a640f6a3fd
4
- data.tar.gz: 0c50f8055c99f60448003e67812d8b5b0ec9c630f7a4f981fddee572678354be
3
+ metadata.gz: ca08d464b1ccfec7dcca637fa7fcf3e80e59fc5175ad1875ae785a6e6e204f9a
4
+ data.tar.gz: a00f60cf704c7b7b7df56570c2addbb926beb31599c411047d209dea47d3b4f5
5
5
  SHA512:
6
- metadata.gz: aa080b7e1efdd7ab898550b1261e9af054c3f40848285b1a77a5f9f56f15cb38790eabf1be32e425e35ba5058ee49a302e714ebe677ed2824205b36d55497db2
7
- data.tar.gz: '084565dea25c30c6e206238e61d5b9e184be59fef8e84135ec5c5a9eb726876c141b6308d1c09ce1e23b240fc839dd2def97eea3123a578d0bcd757fe30db63e'
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** is an excellent in-memory cache, but it only supports key-value retrieval. From the mudis documentation:
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
- All while maintaining mudis's speed, thread-safety, and in-memory efficiency.
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 -->|mudis-ql.from| CreateScope[Create Scope Instance]
46
- Entry -->|mudis-ql.metrics| CreateMetrics[Create MetricsScope]
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 >= 2.7.0
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 = mudis-ql.from('users').all
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 = mudis-ql.from('users')
162
+ active_users = MudisQL.from('users')
163
163
  .where(status: 'active')
164
164
  .all
165
165
 
166
166
  # Chain multiple conditions
167
- result = mudis-ql.from('users')
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 = mudis-ql.from('users')
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 = mudis-ql.from('users')
182
+ a_names = MudisQL.from('users')
183
183
  .where(name: /^A/i)
184
184
  .all
185
185
 
186
186
  # Use ranges
187
- young_adults = mudis-ql.from('users')
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 = mudis-ql.from('users')
196
+ sorted_users = MudisQL.from('users')
197
197
  .order(:age)
198
198
  .all
199
199
 
200
200
  # Order descending
201
- sorted_desc = mudis-ql.from('users')
201
+ sorted_desc = MudisQL.from('users')
202
202
  .order(:age, :desc)
203
203
  .all
204
204
 
205
205
  # Limit results
206
- top_5 = mudis-ql.from('users')
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 = mudis-ql.from('users')
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 = mudis-ql.from('users')
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 = mudis-ql.from('users')
228
+ last_user = MudisQL.from('users')
229
229
  .order(:age)
230
230
  .last
231
231
 
232
232
  # Count matching records
233
- count = mudis-ql.from('users')
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 = mudis-ql.from('users')
238
+ has_inactive = MudisQL.from('users')
239
239
  .where(status: 'inactive')
240
240
  .exists?
241
241
 
242
242
  # Pluck specific fields
243
- names = mudis-ql.from('users').pluck(:name)
243
+ names = MudisQL.from('users').pluck(:name)
244
244
  # => ["Alice", "Bob", "Charlie"]
245
245
 
246
- name_age_pairs = mudis-ql.from('users').pluck(:name, :age)
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 = mudis-ql.from('users')
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
- mudis-ql.from('users')
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
- mudis-ql.from(NAMESPACE)
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
- mudis-ql.from(NAMESPACE)
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
- mudis-ql.from(NAMESPACE)
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
- mudis-ql.from(NAMESPACE)
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 = mudis-ql.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 = mudis-ql.metrics
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: mudis-ql.metrics.summary,
487
- efficiency: mudis-ql.metrics.efficiency,
488
- distribution: mudis-ql.metrics.bucket_distribution,
489
- top_keys: mudis-ql.metrics.least_touched.order(:access_count, :desc).limit(10).all
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
- ### mudis-ql.from(namespace)
522
+ ### MudisQL.from(namespace)
498
523
 
499
524
  Creates a new scope for the specified mudis namespace.
500
525
 
501
- **Returns:** `mudis-ql::Scope`
526
+ **Returns:** `MudisQL::Scope`
502
527
 
503
- ### mudis-ql.metrics
528
+ ### MudisQL.metrics
504
529
 
505
530
  Access mudis metrics with a queryable interface.
506
531
 
507
- **Returns:** `mudis-ql::MetricsScope`
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
- mudis-ql supports multiple types of matchers in `where` conditions:
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. **No aggregations**: No built-in support for GROUP BY, SUM, AVG, etc.
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)
@@ -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
- sorted = results.sort_by do |record|
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
- [@order_direction == :asc ? 1 : -1, ""]
163
- elsif val.is_a?(Numeric)
164
- [0, val]
216
+ nil_results << record
165
217
  else
166
- [0, val.to_s]
218
+ non_nil_results << record
167
219
  end
168
220
  end
169
221
 
170
- @order_direction == :desc ? sorted.reverse : sorted
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MudisQL
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -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
@@ -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.1.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: 2025-12-07 00:00:00.000000000 Z
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 for data
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