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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c8ca84afb158da1ef68705edf7990481b4248356318f76cb591ae6a640f6a3fd
4
+ data.tar.gz: 0c50f8055c99f60448003e67812d8b5b0ec9c630f7a4f981fddee572678354be
5
+ SHA512:
6
+ metadata.gz: aa080b7e1efdd7ab898550b1261e9af054c3f40848285b1a77a5f9f56f15cb38790eabf1be32e425e35ba5058ee49a302e714ebe677ed2824205b36d55497db2
7
+ data.tar.gz: '084565dea25c30c6e206238e61d5b9e184be59fef8e84135ec5c5a9eb726876c141b6308d1c09ce1e23b240fc839dd2def97eea3123a578d0bcd757fe30db63e'
data/README.md ADDED
@@ -0,0 +1,596 @@
1
+ # Mudis-QL
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/mudis-ql.svg)](https://badge.fury.io/rb/mudis-ql)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A simple query DSL for [mudis](https://github.com/kiebor81/mudis) cache. Mudis-QL extends mudis by providing a SQL-like query interface for data stored in the cache, enabling you to filter, sort, and paginate cached data without needing a full database.
7
+
8
+ ## Why Mudis-QL?
9
+
10
+ **Mudis** is an excellent in-memory cache, but it only supports key-value retrieval. From the mudis documentation:
11
+
12
+ > "No SQL or equivalent query interface for cached data. Data is per Key retrieval only."
13
+
14
+ **Mudis-QL** solves this limitation by providing a chainable query DSL that allows you to:
15
+ - Filter cached data with `where` conditions
16
+ - Sort results with `order`
17
+ - Paginate with `limit` and `offset`
18
+ - Count and check existence
19
+ - Pluck specific fields
20
+
21
+ All while maintaining mudis's speed, thread-safety, and in-memory efficiency.
22
+
23
+ ### Best Practice: Use Namespaces
24
+
25
+ **Important**: Mudis-QL is designed to query collections of related data in mudis. For optimal functionality, **always use namespaces** when storing data you intend to query:
26
+
27
+ ```ruby
28
+ # Recommended - enables full query functionality
29
+ Mudis.write('user1', { name: 'Alice' }, namespace: 'users')
30
+ Mudis.write('user2', { name: 'Bob' }, namespace: 'users')
31
+ MudisQL.from('users').where(name: /^A/).all
32
+
33
+ # Limited - namespace-less keys can't be listed/queried as a collection
34
+ Mudis.write('user:1', { name: 'Alice' }) # individual key access only
35
+ ```
36
+
37
+ Namespaces provide logical separation and enable Mudis-QL to retrieve all keys in a collection for filtering, sorting, and pagination. Without namespaces, Mudis-QL can only perform individual key operations.
38
+
39
+ ## Design
40
+
41
+ ```mermaid
42
+ flowchart TD
43
+ Start([User Code]) --> Entry{Entry Point}
44
+
45
+ Entry -->|mudis-ql.from| CreateScope[Create Scope Instance]
46
+ Entry -->|mudis-ql.metrics| CreateMetrics[Create MetricsScope]
47
+
48
+ CreateScope --> InitStore[Initialize Store with namespace]
49
+ InitStore --> Scope[Scope Object]
50
+
51
+ CreateMetrics --> MetricsObj[MetricsScope Object]
52
+
53
+ Scope --> Chain{Chain Operations?}
54
+ Chain -->|where| WhereOp[Apply Conditions<br/>Hash/Proc/Regex/Range]
55
+ Chain -->|order| OrderOp[Apply Sorting<br/>Handle nil/mixed types]
56
+ Chain -->|limit| LimitOp[Apply Row Limit]
57
+ Chain -->|offset| OffsetOp[Apply Row Offset]
58
+
59
+ WhereOp --> Chain
60
+ OrderOp --> Chain
61
+ LimitOp --> Chain
62
+ OffsetOp --> Chain
63
+
64
+ Chain -->|Terminal Method| Execute{Execution Type}
65
+
66
+ Execute -->|all| FetchAll[Store.all<br/>Get all namespace keys]
67
+ Execute -->|first| FetchFirst[Apply filters & get first]
68
+ Execute -->|last| FetchLast[Apply filters & get last]
69
+ Execute -->|count| FetchCount[Apply filters & count]
70
+ Execute -->|exists?| FetchExists[Apply filters & check any?]
71
+ Execute -->|pluck| FetchPluck[Apply filters & extract fields]
72
+
73
+ FetchAll --> MudisRead[Mudis.keys + Mudis.read]
74
+ FetchFirst --> MudisRead
75
+ FetchLast --> MudisRead
76
+ FetchCount --> MudisRead
77
+ FetchExists --> MudisRead
78
+ FetchPluck --> MudisRead
79
+
80
+ MudisRead --> Transform[Transform to Hash<br/>Add _key field]
81
+ Transform --> Filter[Apply where conditions]
82
+ Filter --> Sort[Apply order]
83
+ Sort --> Paginate[Apply limit/offset]
84
+ Paginate --> Result([Return Results])
85
+
86
+ MetricsObj --> MetricsChain{Metrics Operations}
87
+ MetricsChain -->|summary| GetSummary[Mudis.metrics<br/>Return summary hash]
88
+ MetricsChain -->|hit_rate| CalcHitRate[Calculate hits/total %]
89
+ MetricsChain -->|efficiency| CalcEfficiency[Calculate efficiency score]
90
+ MetricsChain -->|least_touched| ReturnScope1[Return Scope for<br/>least accessed keys]
91
+ MetricsChain -->|buckets| ReturnScope2[Return Scope for<br/>bucket metrics]
92
+
93
+ GetSummary --> MetricsResult([Return Metrics])
94
+ CalcHitRate --> MetricsResult
95
+ CalcEfficiency --> MetricsResult
96
+ ReturnScope1 --> Scope
97
+ ReturnScope2 --> Scope
98
+
99
+ style Start fill:#e1f5ff
100
+ style Result fill:#c8e6c9
101
+ style MetricsResult fill:#c8e6c9
102
+ style MudisRead fill:#fff3e0
103
+ style Scope fill:#f3e5f5
104
+ style MetricsObj fill:#f3e5f5
105
+ ```
106
+
107
+ ## Installation
108
+
109
+ Add this line to your application's Gemfile:
110
+
111
+ ```ruby
112
+ gem 'mudis-ql'
113
+ ```
114
+
115
+ And then execute:
116
+
117
+ ```bash
118
+ $ bundle install
119
+ ```
120
+
121
+ Or install it yourself as:
122
+
123
+ ```bash
124
+ $ gem install mudis-ql
125
+ ```
126
+
127
+ ## Requirements
128
+
129
+ - Ruby >= 2.7.0
130
+ - mudis gem
131
+
132
+ ## Usage
133
+
134
+ ### Basic Setup
135
+
136
+ ```ruby
137
+ require 'mudis'
138
+ require 'mudis-ql'
139
+
140
+ # Configure mudis first
141
+ Mudis.configure do |c|
142
+ c.serializer = JSON
143
+ c.compress = true
144
+ end
145
+
146
+ # Store some data in mudis
147
+ Mudis.write('user1', { name: 'Alice', age: 30, status: 'active' }, namespace: 'users')
148
+ Mudis.write('user2', { name: 'Bob', age: 25, status: 'active' }, namespace: 'users')
149
+ Mudis.write('user3', { name: 'Charlie', age: 35, status: 'inactive' }, namespace: 'users')
150
+ ```
151
+
152
+ ### Query Examples
153
+
154
+ #### Simple Queries
155
+
156
+ ```ruby
157
+ # Get all users from a namespace
158
+ users = mudis-ql.from('users').all
159
+ # => [{"name"=>"Alice", "age"=>30, "status"=>"active", "_key"=>"user1"}, ...]
160
+
161
+ # Filter by exact match
162
+ active_users = mudis-ql.from('users')
163
+ .where(status: 'active')
164
+ .all
165
+
166
+ # Chain multiple conditions
167
+ result = mudis-ql.from('users')
168
+ .where(status: 'active')
169
+ .where(age: ->(v) { v >= 25 })
170
+ .all
171
+ ```
172
+
173
+ #### Advanced Filtering
174
+
175
+ ```ruby
176
+ # Use proc for custom conditions
177
+ adults = mudis-ql.from('users')
178
+ .where(age: ->(age) { age >= 18 })
179
+ .all
180
+
181
+ # Use regex for pattern matching
182
+ a_names = mudis-ql.from('users')
183
+ .where(name: /^A/i)
184
+ .all
185
+
186
+ # Use ranges
187
+ young_adults = mudis-ql.from('users')
188
+ .where(age: 18..25)
189
+ .all
190
+ ```
191
+
192
+ #### Ordering and Pagination
193
+
194
+ ```ruby
195
+ # Order by field (ascending by default)
196
+ sorted_users = mudis-ql.from('users')
197
+ .order(:age)
198
+ .all
199
+
200
+ # Order descending
201
+ sorted_desc = mudis-ql.from('users')
202
+ .order(:age, :desc)
203
+ .all
204
+
205
+ # Limit results
206
+ top_5 = mudis-ql.from('users')
207
+ .order(:age, :desc)
208
+ .limit(5)
209
+ .all
210
+
211
+ # Pagination with offset
212
+ page_2 = mudis-ql.from('users')
213
+ .order(:name)
214
+ .limit(10)
215
+ .offset(10)
216
+ .all
217
+ ```
218
+
219
+ #### Utility Methods
220
+
221
+ ```ruby
222
+ # Get first matching record
223
+ first_active = mudis-ql.from('users')
224
+ .where(status: 'active')
225
+ .first
226
+
227
+ # Get last matching record
228
+ last_user = mudis-ql.from('users')
229
+ .order(:age)
230
+ .last
231
+
232
+ # Count matching records
233
+ count = mudis-ql.from('users')
234
+ .where(status: 'active')
235
+ .count
236
+
237
+ # Check if any records match
238
+ has_inactive = mudis-ql.from('users')
239
+ .where(status: 'inactive')
240
+ .exists?
241
+
242
+ # Pluck specific fields
243
+ names = mudis-ql.from('users').pluck(:name)
244
+ # => ["Alice", "Bob", "Charlie"]
245
+
246
+ name_age_pairs = mudis-ql.from('users').pluck(:name, :age)
247
+ # => [["Alice", 30], ["Bob", 25], ["Charlie", 35]]
248
+ ```
249
+
250
+ #### Complete Example
251
+
252
+ ```ruby
253
+ # Complex query combining multiple operations
254
+ result = mudis-ql.from('users')
255
+ .where(status: 'active')
256
+ .where(age: ->(age) { age >= 25 })
257
+ .order(:age, :desc)
258
+ .limit(10)
259
+ .offset(0)
260
+ .all
261
+
262
+ # Or using method chaining for pagination
263
+ def get_active_users(page: 1, per_page: 10)
264
+ mudis-ql.from('users')
265
+ .where(status: 'active')
266
+ .order(:name)
267
+ .limit(per_page)
268
+ .offset((page - 1) * per_page)
269
+ .all
270
+ end
271
+ ```
272
+
273
+ ## How It Works
274
+
275
+ mudis-ql works by:
276
+
277
+ 1. **Retrieving all keys** from a mudis namespace using `Mudis.keys(namespace:)`
278
+ 2. **Loading values** for each key using `Mudis.read(key, namespace:)`
279
+ 3. **Applying filters** in memory using Ruby's enumerable methods
280
+ 4. **Sorting and paginating** the results
281
+
282
+ This approach is efficient for moderate-sized datasets (thousands of records) that are already cached in memory. For very large datasets, consider using a proper database.
283
+
284
+ ## Integration with Rails
285
+
286
+ ```ruby
287
+ # app/services/user_cache_service.rb
288
+ class UserCacheService
289
+ NAMESPACE = 'users'
290
+
291
+ def self.cache_user(user)
292
+ Mudis.write(
293
+ user.id.to_s,
294
+ user.attributes.slice('name', 'email', 'status', 'created_at'),
295
+ expires_in: 3600,
296
+ namespace: NAMESPACE
297
+ )
298
+ end
299
+
300
+ def self.active_users(limit: 50)
301
+ mudis-ql.from(NAMESPACE)
302
+ .where(status: 'active')
303
+ .order(:created_at, :desc)
304
+ .limit(limit)
305
+ .all
306
+ end
307
+
308
+ def self.search_by_name(pattern)
309
+ mudis-ql.from(NAMESPACE)
310
+ .where(name: /#{Regexp.escape(pattern)}/i)
311
+ .all
312
+ end
313
+ end
314
+ ```
315
+
316
+ ## Integration with Hanami
317
+
318
+ ```ruby
319
+ # lib/my_app/repos/user_cache_repo.rb
320
+ module MyApp
321
+ module Repos
322
+ class UserCacheRepo
323
+ NAMESPACE = 'users'
324
+
325
+ def find_active(limit: 50)
326
+ mudis-ql.from(NAMESPACE)
327
+ .where(status: 'active')
328
+ .limit(limit)
329
+ .all
330
+ end
331
+
332
+ def find_by_age_range(min:, max:)
333
+ mudis-ql.from(NAMESPACE)
334
+ .where(age: min..max)
335
+ .order(:age)
336
+ .all
337
+ end
338
+ end
339
+ end
340
+ end
341
+ ```
342
+
343
+ ## Querying Mudis Metrics
344
+
345
+ mudis-ql provides a powerful interface for querying mudis cache metrics:
346
+
347
+ ### Basic Metrics
348
+
349
+ ```ruby
350
+ # Get a metrics scope
351
+ metrics = mudis-ql.metrics
352
+
353
+ # Top-level metrics summary
354
+ summary = metrics.summary
355
+ # => { hits: 150, misses: 20, evictions: 5, rejected: 0, total_memory: 45678 }
356
+
357
+ # Cache hit rate
358
+ hit_rate = metrics.hit_rate
359
+ # => 88.24 (percentage)
360
+
361
+ # Overall efficiency
362
+ efficiency = metrics.efficiency
363
+ # => { hit_rate: 88.24, miss_rate: 11.76, eviction_rate: 2.94, rejection_rate: 0.0 }
364
+
365
+ # Total keys and memory
366
+ metrics.total_keys # => 1000
367
+ metrics.total_memory # => 2048576 (bytes)
368
+ ```
369
+
370
+ ### Querying Least Touched Keys
371
+
372
+ ```ruby
373
+ # Get least accessed keys (returns a Scope)
374
+ least_touched = metrics.least_touched
375
+
376
+ # Find never-accessed keys
377
+ never_used = least_touched
378
+ .where(access_count: 0)
379
+ .pluck(:key)
380
+
381
+ # Find keys accessed less than 5 times
382
+ rarely_used = least_touched
383
+ .where(access_count: ->(count) { count < 5 })
384
+ .order(:access_count)
385
+ .all
386
+
387
+ # Identify hotspots (most accessed)
388
+ hotspots = least_touched
389
+ .order(:access_count, :desc)
390
+ .limit(10)
391
+ .pluck(:key, :access_count)
392
+ # => [["user:123", 450], ["product:456", 380], ...]
393
+
394
+ # Quick helper for never-accessed keys
395
+ cold_keys = metrics.never_accessed_keys
396
+ # => ["temp:old_session", "cache:expired_data", ...]
397
+ ```
398
+
399
+ ### Querying Bucket Metrics
400
+
401
+ ```ruby
402
+ # Get bucket metrics (returns a Scope)
403
+ buckets = metrics.buckets
404
+
405
+ # Find buckets with high memory usage
406
+ high_memory = buckets
407
+ .where(memory_bytes: ->(m) { m > 1_000_000 })
408
+ .order(:memory_bytes, :desc)
409
+ .all
410
+
411
+ # Find imbalanced buckets (many keys)
412
+ busy_buckets = buckets
413
+ .where(keys: ->(k) { k > 50 })
414
+ .pluck(:index, :keys, :memory_bytes)
415
+
416
+ # Analyze specific bucket
417
+ bucket_5 = buckets.where(index: 5).first
418
+
419
+ # Distribution statistics
420
+ dist = metrics.bucket_distribution
421
+ # => {
422
+ # total_buckets: 32,
423
+ # avg_keys_per_bucket: 31.25,
424
+ # max_keys_per_bucket: 45,
425
+ # min_keys_per_bucket: 18,
426
+ # avg_memory_per_bucket: 65536.5,
427
+ # max_memory_per_bucket: 98304,
428
+ # min_memory_per_bucket: 32768
429
+ # }
430
+ ```
431
+
432
+ ### Advanced Metrics Queries
433
+
434
+ ```ruby
435
+ # Find buckets needing rebalancing
436
+ avg_keys = metrics.bucket_distribution[:avg_keys_per_bucket]
437
+ unbalanced = metrics.buckets
438
+ .where(keys: ->(k) { k > avg_keys * 1.5 })
439
+ .order(:keys, :desc)
440
+ .pluck(:index, :keys)
441
+
442
+ # Monitor memory hotspots
443
+ memory_threshold = 5_000_000
444
+ hot_buckets = metrics.high_memory_buckets(memory_threshold)
445
+
446
+ # Find buckets with many keys
447
+ key_threshold = 100
448
+ busy_buckets = metrics.high_key_buckets(key_threshold)
449
+
450
+ # Cache health monitoring
451
+ health_report = {
452
+ hit_rate: metrics.hit_rate,
453
+ total_keys: metrics.total_keys,
454
+ memory_usage: metrics.total_memory,
455
+ cold_keys_count: metrics.never_accessed_keys.size,
456
+ efficiency: metrics.efficiency,
457
+ distribution: metrics.bucket_distribution
458
+ }
459
+ ```
460
+
461
+ ### Real-time Monitoring
462
+
463
+ ```ruby
464
+ # Refresh metrics to get latest data
465
+ current_metrics = metrics.refresh
466
+
467
+ # Monitor cache performance over time
468
+ def cache_health_check
469
+ m = mudis-ql.metrics
470
+
471
+ {
472
+ timestamp: Time.now,
473
+ hit_rate: m.hit_rate,
474
+ total_keys: m.total_keys,
475
+ memory_mb: (m.total_memory / 1024.0 / 1024.0).round(2),
476
+ cold_keys: m.never_accessed_keys.size,
477
+ hottest_keys: m.least_touched.order(:access_count, :desc).limit(5).pluck(:key),
478
+ memory_hotspots: m.high_memory_buckets(1_000_000).size
479
+ }
480
+ end
481
+
482
+ # Create a dashboard endpoint
483
+ class MetricsController < ApplicationController
484
+ def show
485
+ 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
490
+ }
491
+ end
492
+ end
493
+ ```
494
+
495
+ ## API Reference
496
+
497
+ ### mudis-ql.from(namespace)
498
+
499
+ Creates a new scope for the specified mudis namespace.
500
+
501
+ **Returns:** `mudis-ql::Scope`
502
+
503
+ ### mudis-ql.metrics
504
+
505
+ Access mudis metrics with a queryable interface.
506
+
507
+ **Returns:** `mudis-ql::MetricsScope`
508
+
509
+ ### Scope Methods
510
+
511
+ | Method | Description | Returns |
512
+ |--------|-------------|---------|
513
+ | `where(conditions)` | Filter by hash of conditions | `Scope` (chainable) |
514
+ | `order(field, direction)` | Sort by field (:asc or :desc) | `Scope` (chainable) |
515
+ | `limit(n)` | Limit results to n records | `Scope` (chainable) |
516
+ | `offset(n)` | Skip first n records | `Scope` (chainable) |
517
+ | `all` | Execute query, return all results | `Array<Hash>` |
518
+ | `first` | Return first matching record | `Hash` or `nil` |
519
+ | `last` | Return last matching record | `Hash` or `nil` |
520
+ | `count` | Count matching records | `Integer` |
521
+ | `exists?` | Check if any records match | `Boolean` |
522
+ | `pluck(*fields)` | Extract specific fields | `Array` |
523
+
524
+ ### MetricsScope Methods
525
+
526
+ | Method | Description | Returns |
527
+ |--------|-------------|---------|
528
+ | `summary` | Top-level metrics (hits, misses, etc.) | `Hash` |
529
+ | `least_touched` | Query least accessed keys | `Scope` |
530
+ | `buckets` | Query bucket metrics | `Scope` |
531
+ | `total_keys` | Sum of keys across all buckets | `Integer` |
532
+ | `total_memory` | Total memory usage in bytes | `Integer` |
533
+ | `hit_rate` | Cache hit rate percentage | `Float` |
534
+ | `efficiency` | Hit/miss/eviction/rejection rates | `Hash` |
535
+ | `high_memory_buckets(threshold)` | Buckets exceeding memory threshold | `Array<Hash>` |
536
+ | `high_key_buckets(threshold)` | Buckets with many keys | `Array<Hash>` |
537
+ | `bucket_distribution` | Distribution statistics | `Hash` |
538
+ | `never_accessed_keys` | Keys with 0 access count | `Array<String>` |
539
+ | `refresh` | Reload metrics data | `MetricsScope` |
540
+
541
+ ### Condition Matchers
542
+
543
+ mudis-ql supports multiple types of matchers in `where` conditions:
544
+
545
+ ```ruby
546
+ # Exact match
547
+ .where(status: 'active')
548
+
549
+ # Proc/Lambda for custom logic
550
+ .where(age: ->(v) { v >= 18 })
551
+
552
+ # Regex for pattern matching
553
+ .where(name: /^A/i)
554
+
555
+ # Range for inclusive matching
556
+ .where(age: 18..65)
557
+ ```
558
+
559
+ ## Performance Considerations
560
+
561
+ - **Best for**: Small to medium datasets (hundreds to thousands of records)
562
+ - **Memory**: All matching keys are loaded into memory for filtering
563
+ - **Speed**: Fast for cached data, but involves full table scan
564
+ - **Use Case**: Perfect for frequently accessed, relatively static data that benefits from caching
565
+
566
+ For very large datasets or complex queries, consider using a proper database alongside mudis for caching.
567
+
568
+ ## Known Limitations
569
+
570
+ 1. **Full scan required**: Unlike databases with indexes, mudis-ql must load all records from a namespace to filter them
571
+ 2. **In-memory processing**: All filtering happens in Ruby memory, not at the storage layer
572
+ 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).
575
+
576
+ These limitations are by design to maintain simplicity and compatibility with mudis's key-value architecture.
577
+
578
+ ## Contributing
579
+
580
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kiebor81/mudisql.
581
+
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
+ ## See Also
594
+
595
+ - [Mudis](https://github.com/kiebor81/mudis)
596
+