familia 2.0.0.pre26 → 2.0.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/CHANGELOG.rst +49 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +2 -2
- data/README.md +1 -3
- data/docs/guides/feature-encrypted-fields.md +1 -1
- data/docs/guides/feature-expiration.md +1 -1
- data/docs/guides/feature-quantization.md +1 -1
- data/docs/overview.md +7 -7
- data/docs/reference/api-technical.md +103 -7
- data/familia.gemspec +1 -2
- data/lib/familia/data_type/types/hashkey.rb +238 -0
- data/lib/familia/data_type/types/listkey.rb +110 -4
- data/lib/familia/data_type/types/sorted_set.rb +365 -0
- data/lib/familia/data_type/types/stringkey.rb +139 -0
- data/lib/familia/data_type/types/unsorted_set.rb +122 -2
- data/lib/familia/version.rb +1 -1
- metadata +2 -27
- data/docs/migrating/v2.0.0-pre.md +0 -84
- data/docs/migrating/v2.0.0-pre11.md +0 -253
- data/docs/migrating/v2.0.0-pre12.md +0 -306
- data/docs/migrating/v2.0.0-pre13.md +0 -95
- data/docs/migrating/v2.0.0-pre14.md +0 -37
- data/docs/migrating/v2.0.0-pre18.md +0 -58
- data/docs/migrating/v2.0.0-pre19.md +0 -197
- data/docs/migrating/v2.0.0-pre22.md +0 -241
- data/docs/migrating/v2.0.0-pre5.md +0 -131
- data/docs/migrating/v2.0.0-pre6.md +0 -154
- data/docs/migrating/v2.0.0-pre7.md +0 -222
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a908aad71096ba6a650fa67545aad0a2f891be6472fc6cb4ed067977c4edd706
|
|
4
|
+
data.tar.gz: 01d676ccdf7a783d585053be2861319785a8aa5c33298848607f589b3dfa6aaf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6eb5d61cb2255e5901969ae607b324a1b396f819562815d9f7f57a851b433b6cd4c695d339978de0a3341f92ddb909c8c4878a3580d442cf651449b6ac6d3be8
|
|
7
|
+
data.tar.gz: 96f2e0a1bdefbd12e6332aefcdf189ae8279d07adaa05348d839ebccf40a9e306bdd18fc21a5a9af478dab94e6b5de71fd6e86ac7cbdd264a99a26f502d598d8
|
data/CHANGELOG.rst
CHANGED
|
@@ -7,6 +7,55 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`
|
|
|
7
7
|
|
|
8
8
|
<!--scriv-insert-here-->
|
|
9
9
|
|
|
10
|
+
.. _changelog-2.0.0:
|
|
11
|
+
|
|
12
|
+
2.0.0 — 2026-01-19
|
|
13
|
+
==================
|
|
14
|
+
|
|
15
|
+
Familia 2.0.0 represents a complete rewrite of the library with 26 pre-release
|
|
16
|
+
iterations incorporating community feedback and production testing.
|
|
17
|
+
|
|
18
|
+
Added
|
|
19
|
+
-----
|
|
20
|
+
|
|
21
|
+
- **Modular Feature System**: Autoloading features with ancestry chain traversal
|
|
22
|
+
(``feature :expiration``, ``feature :relationships``, etc.)
|
|
23
|
+
- **Unified Relationships API**: ``participates_in`` replaces ``tracked_in``/``member_of``
|
|
24
|
+
with bidirectional reverse lookups (``_instances`` suffix methods)
|
|
25
|
+
- **Type-Safe Serialization**: JSON encoding preserves Integer, Boolean, Float,
|
|
26
|
+
Hash, Array types across Redis boundary
|
|
27
|
+
- **Performance Optimizations**: Pipelined bulk loading (``load_multi``),
|
|
28
|
+
optional EXISTS check (``check_exists: false``), OJ JSON for 2-5× faster operations
|
|
29
|
+
- **Security Features**: VerifiableIdentifier with HMAC signatures,
|
|
30
|
+
ExternalIdentifier with format flexibility, encrypted fields with key rotation
|
|
31
|
+
- **Thread Safety**: Mutex initialization fixes, 56-test thread safety suite
|
|
32
|
+
- **Instrumentation**: ``Familia.on_command``, ``Familia.on_pipeline``,
|
|
33
|
+
``Familia.on_lifecycle`` hooks for monitoring
|
|
34
|
+
|
|
35
|
+
Changed
|
|
36
|
+
-------
|
|
37
|
+
|
|
38
|
+
- **BREAKING**: DataType class renaming to avoid Ruby namespace conflicts
|
|
39
|
+
(``Familia::String`` → ``Familia::StringKey``, etc.)
|
|
40
|
+
- **BREAKING**: Removed ``dump_method``/``load_method`` - JSON serialization is now standard
|
|
41
|
+
- **BREAKING**: Indexing API renamed (``class_indexed_by`` → ``unique_index``,
|
|
42
|
+
``indexed_by`` → ``multi_index``)
|
|
43
|
+
|
|
44
|
+
Documentation
|
|
45
|
+
-------------
|
|
46
|
+
|
|
47
|
+
- Archived 11 pre-release migration guides to ``docs/.archive/``
|
|
48
|
+
- Enhanced ``api-technical.md`` with bulk loading, EXISTS optimization,
|
|
49
|
+
per-class feature registration, and index rebuilding documentation
|
|
50
|
+
- Updated version references and fixed broken anchor links throughout docs
|
|
51
|
+
|
|
52
|
+
AI Assistance
|
|
53
|
+
-------------
|
|
54
|
+
|
|
55
|
+
- Claude Opus 4.5 coordinated 11 parallel code-explorer agents to evaluate
|
|
56
|
+
migration docs, identifying unique content to preserve before archiving.
|
|
57
|
+
Assisted with release statistics gathering and documentation consolidation.
|
|
58
|
+
|
|
10
59
|
.. _changelog-2.0.0.pre26:
|
|
11
60
|
|
|
12
61
|
2.0.0.pre26 — 2026-01-19
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
familia (2.0.0
|
|
5
|
-
benchmark (~> 0.4)
|
|
4
|
+
familia (2.0.0)
|
|
6
5
|
concurrent-ruby (~> 1.3)
|
|
7
6
|
connection_pool (~> 2.5)
|
|
8
7
|
csv (~> 3.3)
|
|
@@ -181,6 +180,7 @@ PLATFORMS
|
|
|
181
180
|
ruby
|
|
182
181
|
|
|
183
182
|
DEPENDENCIES
|
|
183
|
+
benchmark (~> 0.4)
|
|
184
184
|
concurrent-ruby (~> 1.3.5)
|
|
185
185
|
debug
|
|
186
186
|
familia!
|
data/README.md
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
Familia provides object-oriented access to Valkey/Redis using their database types. Unlike traditional ORMs that map objects to relational tables, Familia maps Ruby objects directly to Valkey's native data structures (strings, lists, sets, sorted sets, hashes) as instance variables.
|
|
6
6
|
|
|
7
|
-
> [!CAUTION]
|
|
8
|
-
> Familia 2 is in pre-release and not ready for production use. (October 2025)
|
|
9
7
|
## Traditional ORM vs Familia
|
|
10
8
|
|
|
11
9
|
**Traditional ORMs** convert your objects to SQL tables. A product with categories becomes two tables with a join table. Checking if a tag exists requires a query with joins.
|
|
@@ -140,7 +138,7 @@ user.tags? # Check if it's a Set type
|
|
|
140
138
|
|
|
141
139
|
## Prerequisites
|
|
142
140
|
|
|
143
|
-
- **Ruby**: 3.
|
|
141
|
+
- **Ruby**: 3.2+
|
|
144
142
|
- **Valkey/Redis**: 6.0+
|
|
145
143
|
- **Gems**: `redis` (automatically installed)
|
|
146
144
|
|
|
@@ -895,7 +895,7 @@ Familia::Encryption.validate_configuration!
|
|
|
895
895
|
## See Also
|
|
896
896
|
|
|
897
897
|
- **[Overview](../overview.md#encrypted-fields)** - Conceptual introduction to encrypted fields
|
|
898
|
-
- **[Technical Reference](../reference/api-technical.md#
|
|
898
|
+
- **[Technical Reference](../reference/api-technical.md#feature-system)** - Implementation details and advanced patterns
|
|
899
899
|
- **[Security Model Guide](security-model.md)** - Cryptographic design and threat model considerations
|
|
900
900
|
- **[Feature System Guide](feature-system.md)** - Understanding Familia's feature architecture
|
|
901
901
|
- **[Implementation Guide](implementation.md)** - Production deployment and configuration patterns
|
|
@@ -641,7 +641,7 @@ Get/set class-level default expiration.
|
|
|
641
641
|
|
|
642
642
|
## See Also
|
|
643
643
|
|
|
644
|
-
- **[Technical Reference](../reference/api-technical.md#
|
|
644
|
+
- **[Technical Reference](../reference/api-technical.md#feature-system)** - Implementation details and advanced patterns
|
|
645
645
|
- **[Overview](../overview.md#automatic-expiration)** - Conceptual introduction to expiration
|
|
646
646
|
- **[Feature System Guide](feature-system.md)** - Understanding Familia's feature architecture
|
|
647
647
|
- **[Implementation Guide](implementation.md)** - Production deployment and configuration patterns
|
|
@@ -891,7 +891,7 @@ BasicModel.qstamp() # Uses 10.minutes fallback (600 seconds)
|
|
|
891
891
|
|
|
892
892
|
## See Also
|
|
893
893
|
|
|
894
|
-
- **[Technical Reference](../reference/api-technical.md#
|
|
894
|
+
- **[Technical Reference](../reference/api-technical.md#feature-system)** - Implementation details and advanced patterns
|
|
895
895
|
- **[Overview](../overview.md#time-based-quantization)** - Conceptual introduction to quantization
|
|
896
896
|
- **[Time Utilities Guide](time-utilities.md)** - Time manipulation and formatting utilities
|
|
897
897
|
- **[Feature System Guide](feature-system.md)** - Understanding Familia's feature architecture
|
data/docs/overview.md
CHANGED
|
@@ -242,7 +242,7 @@ This automatically groups metrics into 10-minute intervals formatted as "HH:MM",
|
|
|
242
242
|
- **Reduced Storage**: Aggregate similar data points to optimize memory usage
|
|
243
243
|
- **Analytics Ready**: Perfect for dashboards and time-series data visualization
|
|
244
244
|
|
|
245
|
-
> For advanced quantization strategies, value bucketing, geographic quantization, and performance patterns, see the [Technical Reference](reference/api-technical.md#
|
|
245
|
+
> For advanced quantization strategies, value bucketing, geographic quantization, and performance patterns, see the [Technical Reference](reference/api-technical.md#feature-system).
|
|
246
246
|
|
|
247
247
|
### Object Identifiers
|
|
248
248
|
|
|
@@ -273,7 +273,7 @@ session.objid # => "a1b2c3d4e5f6" (hex)
|
|
|
273
273
|
- `:uuid_v4` - Standard UUID format for global uniqueness
|
|
274
274
|
- `:hex` - Compact hexadecimal identifiers for internal use
|
|
275
275
|
|
|
276
|
-
> For custom generators, collision detection, and advanced identifier patterns, see the [Technical Reference](reference/api-technical.md#
|
|
276
|
+
> For custom generators, collision detection, and advanced identifier patterns, see the [Technical Reference](reference/api-technical.md#feature-system).
|
|
277
277
|
|
|
278
278
|
### Specialized Field Types
|
|
279
279
|
|
|
@@ -354,7 +354,7 @@ user = ExternalUser.create(external_id: "ext_12345", name: "Alice")
|
|
|
354
354
|
|
|
355
355
|
This feature helps maintain consistency when integrating with external APIs or legacy systems.
|
|
356
356
|
|
|
357
|
-
> For advanced external identifier patterns, batch operations, and sync status management, see the [Technical Reference](reference/api-technical.md#
|
|
357
|
+
> For advanced external identifier patterns, batch operations, and sync status management, see the [Technical Reference](reference/api-technical.md#feature-system).
|
|
358
358
|
|
|
359
359
|
### Relationships
|
|
360
360
|
|
|
@@ -435,7 +435,7 @@ Team.email_index_for("alice@example.com") # Direct index access
|
|
|
435
435
|
- **Automatic Indexing**: Efficient O(1) lookups with automatic index maintenance
|
|
436
436
|
- **Performance Optimized**: Bulk operations and efficient sorted set operations
|
|
437
437
|
|
|
438
|
-
> For advanced relationship patterns, permission-encoded relationships, time-series tracking, and performance optimization, see the [Technical Reference](reference/api-technical.md#
|
|
438
|
+
> For advanced relationship patterns, permission-encoded relationships, time-series tracking, and performance optimization, see the [Technical Reference](reference/api-technical.md#feature-system).
|
|
439
439
|
|
|
440
440
|
### Transient Fields
|
|
441
441
|
|
|
@@ -472,7 +472,7 @@ attempt.security_token.reveal # => "sensitive_data"
|
|
|
472
472
|
- **Transient Fields**: Exist only in memory, never persisted
|
|
473
473
|
- **Redacted Fields**: Return `[REDACTED]` when converted to strings for logging safety
|
|
474
474
|
|
|
475
|
-
> For RedactedString implementation details, single-use patterns, and security considerations, see the [Technical Reference](reference/api-technical.md#
|
|
475
|
+
> For RedactedString implementation details, single-use patterns, and security considerations, see the [Technical Reference](reference/api-technical.md#feature-system).
|
|
476
476
|
|
|
477
477
|
### Permission Management
|
|
478
478
|
|
|
@@ -638,7 +638,7 @@ user.encrypted_fields_status # Check encryption status
|
|
|
638
638
|
- **Key Rotation**: Seamless updates with backward compatibility
|
|
639
639
|
- **Multiple Algorithms**: XChaCha20-Poly1305 (preferred) with AES-256-GCM fallback
|
|
640
640
|
|
|
641
|
-
> For advanced encryption configuration, multiple providers, request caching, and key rotation procedures, see the [Technical Reference](reference/api-technical.md#
|
|
641
|
+
> For advanced encryption configuration, multiple providers, request caching, and key rotation procedures, see the [Technical Reference](reference/api-technical.md#feature-system).
|
|
642
642
|
|
|
643
643
|
### Open-ended Serialization
|
|
644
644
|
|
|
@@ -772,7 +772,7 @@ Familia.configure do |config|
|
|
|
772
772
|
end
|
|
773
773
|
```
|
|
774
774
|
|
|
775
|
-
> For production configuration patterns, advanced connection pooling, multi-database setup, and environment-based configuration, see the [Technical Reference](reference/api-technical.md#connection-management
|
|
775
|
+
> For production configuration patterns, advanced connection pooling, multi-database setup, and environment-based configuration, see the [Technical Reference](reference/api-technical.md#connection-management).
|
|
776
776
|
|
|
777
777
|
## Common Patterns
|
|
778
778
|
|
|
@@ -48,7 +48,7 @@ Base class for Valkey/Redis data type implementations.
|
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
51
|
-
## Feature System
|
|
51
|
+
## Feature System
|
|
52
52
|
|
|
53
53
|
### Feature Architecture
|
|
54
54
|
Modular system for extending Horreum classes with reusable functionality.
|
|
@@ -500,6 +500,39 @@ Familia::Base.add_feature ExternalIdentifier, :external_identifier, depends_on:
|
|
|
500
500
|
end
|
|
501
501
|
```
|
|
502
502
|
|
|
503
|
+
### Per-Class Feature Registration
|
|
504
|
+
|
|
505
|
+
Register custom features for specific model classes with ancestry chain lookup.
|
|
506
|
+
|
|
507
|
+
```ruby
|
|
508
|
+
# Define a custom feature module
|
|
509
|
+
module CustomerAnalytics
|
|
510
|
+
def track_purchase(amount)
|
|
511
|
+
purchases.increment(amount)
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Register feature only for Customer and its subclasses
|
|
516
|
+
Customer.add_feature CustomerAnalytics, :customer_analytics
|
|
517
|
+
|
|
518
|
+
class Customer < Familia::Horreum
|
|
519
|
+
feature :customer_analytics # Available via Customer's registry
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
class PremiumCustomer < Customer
|
|
523
|
+
feature :customer_analytics # Inherited via ancestry chain
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
class Session < Familia::Horreum
|
|
527
|
+
# feature :customer_analytics # Not available - would raise error
|
|
528
|
+
end
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Benefits:**
|
|
532
|
+
- Features can have the same name across different model hierarchies
|
|
533
|
+
- Natural inheritance through Ruby's class hierarchy
|
|
534
|
+
- Better namespace management for large applications
|
|
535
|
+
|
|
503
536
|
### Per-Class Feature Configuration Isolation
|
|
504
537
|
Each class maintains independent feature options.
|
|
505
538
|
|
|
@@ -970,6 +1003,43 @@ end
|
|
|
970
1003
|
|
|
971
1004
|
## Performance Optimization
|
|
972
1005
|
|
|
1006
|
+
### Pipelined Bulk Loading
|
|
1007
|
+
|
|
1008
|
+
Load multiple objects efficiently with a single pipelined Redis batch.
|
|
1009
|
+
|
|
1010
|
+
```ruby
|
|
1011
|
+
# Before: N×2 commands (EXISTS + HGETALL per object)
|
|
1012
|
+
users = ids.map { |id| User.find_by_id(id) }
|
|
1013
|
+
# For 14 objects: 28 Redis commands
|
|
1014
|
+
|
|
1015
|
+
# After: 1 pipelined batch
|
|
1016
|
+
users = User.load_multi(ids)
|
|
1017
|
+
# For 14 objects: 1 batch with 14 HGETALL commands (2× faster)
|
|
1018
|
+
|
|
1019
|
+
# Load by full dbkeys
|
|
1020
|
+
users = User.load_multi_by_keys(['user:123:object', 'user:456:object'])
|
|
1021
|
+
|
|
1022
|
+
# Filter out nils for missing objects
|
|
1023
|
+
existing_users = User.load_multi(ids).compact
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
### Optional EXISTS Check Optimization
|
|
1027
|
+
|
|
1028
|
+
Skip the EXISTS check for 50% reduction in Redis commands when keys are known to exist.
|
|
1029
|
+
|
|
1030
|
+
```ruby
|
|
1031
|
+
# Default behavior (2 commands: EXISTS + HGETALL)
|
|
1032
|
+
user = User.find_by_id(123)
|
|
1033
|
+
|
|
1034
|
+
# Optimized (1 command: HGETALL only)
|
|
1035
|
+
user = User.find_by_id(123, check_exists: false)
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
**When to use `check_exists: false`:**
|
|
1039
|
+
- Loading from sorted set results (keys guaranteed to exist)
|
|
1040
|
+
- High-throughput API endpoints
|
|
1041
|
+
- Bulk operations with known-existing keys
|
|
1042
|
+
|
|
973
1043
|
### Batch Operations
|
|
974
1044
|
Minimize Valkey/Redis round trips with batch operations.
|
|
975
1045
|
|
|
@@ -990,6 +1060,33 @@ User.pipelined do
|
|
|
990
1060
|
end
|
|
991
1061
|
```
|
|
992
1062
|
|
|
1063
|
+
### Index Rebuilding
|
|
1064
|
+
|
|
1065
|
+
Auto-generated rebuild methods for unique and multi indexes with zero downtime.
|
|
1066
|
+
|
|
1067
|
+
```ruby
|
|
1068
|
+
class User < Familia::Horreum
|
|
1069
|
+
feature :relationships
|
|
1070
|
+
unique_index :email, :email_lookup
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
# Rebuild class-level unique index
|
|
1074
|
+
User.rebuild_email_lookup
|
|
1075
|
+
|
|
1076
|
+
# With progress tracking
|
|
1077
|
+
User.rebuild_email_lookup(batch_size: 100) do |progress|
|
|
1078
|
+
puts "#{progress[:completed]}/#{progress[:total]}"
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
# Instance-scoped index rebuild
|
|
1082
|
+
company.rebuild_badge_index
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
**When to use:**
|
|
1086
|
+
- After data migrations or bulk imports
|
|
1087
|
+
- Recovering from index corruption
|
|
1088
|
+
- Adding indexes to existing data
|
|
1089
|
+
|
|
993
1090
|
### Memory Optimization
|
|
994
1091
|
Efficient memory usage patterns.
|
|
995
1092
|
|
|
@@ -1040,7 +1137,7 @@ end
|
|
|
1040
1137
|
|
|
1041
1138
|
## Migration and Upgrading
|
|
1042
1139
|
|
|
1043
|
-
### From v1.x to v2.0
|
|
1140
|
+
### From v1.x to v2.0
|
|
1044
1141
|
Key changes and migration steps.
|
|
1045
1142
|
|
|
1046
1143
|
```ruby
|
|
@@ -1051,7 +1148,7 @@ class User < Familia
|
|
|
1051
1148
|
list :sessions
|
|
1052
1149
|
end
|
|
1053
1150
|
|
|
1054
|
-
# NEW v2.0
|
|
1151
|
+
# NEW v2.0 syntax
|
|
1055
1152
|
class User < Familia::Horreum
|
|
1056
1153
|
identifier_field :email # Updated method name
|
|
1057
1154
|
field :name # Generic field method
|
|
@@ -1304,9 +1401,8 @@ end
|
|
|
1304
1401
|
- [Connection Pooling Guide](../guides/Connection-Pooling-Guide.md)
|
|
1305
1402
|
|
|
1306
1403
|
### Version Information
|
|
1307
|
-
- **Current Version**: v2.0.0
|
|
1308
|
-
- **
|
|
1309
|
-
- **Ruby Compatibility**: 3.0+ (3.4+ recommended for optimal threading)
|
|
1404
|
+
- **Current Version**: v2.0.0
|
|
1405
|
+
- **Ruby Compatibility**: 3.2+
|
|
1310
1406
|
- **Redis Compatibility**: 6.0+ (Valkey compatible)
|
|
1311
1407
|
|
|
1312
|
-
This technical reference covers the major components and usage patterns available in Familia v2.0.
|
|
1408
|
+
This technical reference covers the major components and usage patterns available in Familia v2.0. For complete API documentation, see the generated YARD docs and wiki guides.
|
data/familia.gemspec
CHANGED
|
@@ -17,9 +17,8 @@ Gem::Specification.new do |spec|
|
|
|
17
17
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
18
18
|
spec.require_paths = ['lib']
|
|
19
19
|
|
|
20
|
-
spec.required_ruby_version = Gem::Requirement.new('>= 3.
|
|
20
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 3.2')
|
|
21
21
|
|
|
22
|
-
spec.add_dependency 'benchmark', '~> 0.4'
|
|
23
22
|
spec.add_dependency 'concurrent-ruby', '~> 1.3'
|
|
24
23
|
spec.add_dependency 'connection_pool', '~> 2.5'
|
|
25
24
|
spec.add_dependency 'csv', '~> 3.3'
|
|
@@ -129,6 +129,244 @@ module Familia
|
|
|
129
129
|
deserialize_values(*elements)
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
+
# Incrementally iterates over fields in the hash using cursor-based iteration.
|
|
133
|
+
# This is more memory-efficient than `hgetall` for large hashes.
|
|
134
|
+
#
|
|
135
|
+
# @param cursor [Integer] The cursor position to start from (0 for initial call)
|
|
136
|
+
# @param match [String, nil] Optional glob-style pattern to filter field names
|
|
137
|
+
# @param count [Integer, nil] Optional hint for number of elements to return per call
|
|
138
|
+
# @return [Array<String, Hash>] A two-element array: [new_cursor, {field => value, ...}]
|
|
139
|
+
# When new_cursor is "0", iteration is complete.
|
|
140
|
+
#
|
|
141
|
+
# @example Basic iteration
|
|
142
|
+
# cursor = 0
|
|
143
|
+
# loop do
|
|
144
|
+
# cursor, results = my_hash.scan(cursor)
|
|
145
|
+
# results.each { |field, value| puts "#{field}: #{value}" }
|
|
146
|
+
# break if cursor == "0"
|
|
147
|
+
# end
|
|
148
|
+
#
|
|
149
|
+
# @example With pattern matching
|
|
150
|
+
# cursor, results = my_hash.scan(0, match: "user:*", count: 100)
|
|
151
|
+
def scan(cursor = 0, match: nil, count: nil)
|
|
152
|
+
args = [dbkey, cursor]
|
|
153
|
+
args += ['MATCH', match] if match
|
|
154
|
+
args += ['COUNT', count] if count
|
|
155
|
+
|
|
156
|
+
new_cursor, pairs = dbclient.hscan(*args)
|
|
157
|
+
|
|
158
|
+
# pairs is an array of [field, value] pairs, convert to hash with deserialization
|
|
159
|
+
result_hash = pairs.to_h.transform_values { |v| deserialize_value(v) }
|
|
160
|
+
|
|
161
|
+
[new_cursor, result_hash]
|
|
162
|
+
end
|
|
163
|
+
alias hscan scan
|
|
164
|
+
|
|
165
|
+
# Increments the float value of a hash field by the given amount.
|
|
166
|
+
#
|
|
167
|
+
# @param field [String] The field name
|
|
168
|
+
# @param by [Float, Integer] The amount to increment by (can be negative)
|
|
169
|
+
# @return [Float] The new value after incrementing
|
|
170
|
+
#
|
|
171
|
+
# @example
|
|
172
|
+
# my_hash.incrbyfloat('temperature', 0.5) #=> 23.5
|
|
173
|
+
# my_hash.incrbyfloat('temperature', -1.2) #=> 22.3
|
|
174
|
+
def incrbyfloat(field, by)
|
|
175
|
+
dbclient.hincrbyfloat(dbkey, field.to_s, by).to_f
|
|
176
|
+
end
|
|
177
|
+
alias incrfloat incrbyfloat
|
|
178
|
+
|
|
179
|
+
# Returns the string length of the value associated with field.
|
|
180
|
+
#
|
|
181
|
+
# @param field [String] The field name
|
|
182
|
+
# @return [Integer] The length of the value in bytes, or 0 if field does not exist
|
|
183
|
+
#
|
|
184
|
+
# @example
|
|
185
|
+
# my_hash['name'] = 'Alice'
|
|
186
|
+
# my_hash.strlen('name') #=> 7 (includes JSON quotes: "Alice")
|
|
187
|
+
def strlen(field)
|
|
188
|
+
dbclient.hstrlen(dbkey, field.to_s)
|
|
189
|
+
end
|
|
190
|
+
alias hstrlen strlen
|
|
191
|
+
|
|
192
|
+
# Returns one or more random fields from the hash.
|
|
193
|
+
#
|
|
194
|
+
# @param count [Integer, nil] Number of fields to return. If nil, returns a single field.
|
|
195
|
+
# If positive, returns distinct fields. If negative, allows duplicates.
|
|
196
|
+
# @param withvalues [Boolean] If true, returns fields with their values
|
|
197
|
+
# @return [String, Array<String>, Array<Array>] Depending on arguments:
|
|
198
|
+
# - No count: single field name (or nil if hash is empty)
|
|
199
|
+
# - With count: array of field names
|
|
200
|
+
# - With count and withvalues: array of [field, value] pairs
|
|
201
|
+
#
|
|
202
|
+
# @example Get a single random field
|
|
203
|
+
# my_hash.randfield #=> "some_field"
|
|
204
|
+
#
|
|
205
|
+
# @example Get 3 distinct random fields
|
|
206
|
+
# my_hash.randfield(3) #=> ["field1", "field2", "field3"]
|
|
207
|
+
#
|
|
208
|
+
# @example Get 2 random fields with values
|
|
209
|
+
# my_hash.randfield(2, withvalues: true) #=> [["field1", value1], ["field2", value2]]
|
|
210
|
+
def randfield(count = nil, withvalues: false)
|
|
211
|
+
if count.nil?
|
|
212
|
+
dbclient.hrandfield(dbkey)
|
|
213
|
+
elsif withvalues
|
|
214
|
+
pairs = dbclient.hrandfield(dbkey, count, 'WITHVALUES')
|
|
215
|
+
# pairs is array of [field, value, field, value, ...]
|
|
216
|
+
# Convert to array of [field, deserialized_value] pairs
|
|
217
|
+
pairs.each_slice(2).map { |field, val| [field, deserialize_value(val)] }
|
|
218
|
+
else
|
|
219
|
+
dbclient.hrandfield(dbkey, count)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
alias hrandfield randfield
|
|
223
|
+
|
|
224
|
+
# -----------------------------------------------------------------------
|
|
225
|
+
# Field-Level Expiration Methods (Redis 7.4+)
|
|
226
|
+
#
|
|
227
|
+
# These methods require Redis/Valkey 7.4 or later. They allow setting
|
|
228
|
+
# TTL on individual hash fields rather than the entire key.
|
|
229
|
+
# -----------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
# Sets expiration time in seconds on one or more hash fields.
|
|
232
|
+
# @note Requires Redis 7.4+
|
|
233
|
+
#
|
|
234
|
+
# @param seconds [Integer] TTL in seconds
|
|
235
|
+
# @param fields [Array<String>] One or more field names
|
|
236
|
+
# @return [Array<Integer>] Array of results for each field:
|
|
237
|
+
# -2 if field does not exist, 1 if expiration was set,
|
|
238
|
+
# 0 if expiration was not set (e.g., field has no expiration)
|
|
239
|
+
#
|
|
240
|
+
# @example Set 1 hour TTL on specific fields
|
|
241
|
+
# my_hash.expire_fields(3600, 'session_token', 'temp_data')
|
|
242
|
+
def expire_fields(seconds, *fields)
|
|
243
|
+
string_fields = fields.flatten.compact.map(&:to_s)
|
|
244
|
+
dbclient.call('HEXPIRE', dbkey, seconds, 'FIELDS', string_fields.size, *string_fields)
|
|
245
|
+
end
|
|
246
|
+
alias hexpire expire_fields
|
|
247
|
+
|
|
248
|
+
# Sets expiration time in milliseconds on one or more hash fields.
|
|
249
|
+
# @note Requires Redis 7.4+
|
|
250
|
+
#
|
|
251
|
+
# @param milliseconds [Integer] TTL in milliseconds
|
|
252
|
+
# @param fields [Array<String>] One or more field names
|
|
253
|
+
# @return [Array<Integer>] Array of results for each field
|
|
254
|
+
#
|
|
255
|
+
# @example Set 500ms TTL on a field
|
|
256
|
+
# my_hash.pexpire_fields(500, 'rate_limit_counter')
|
|
257
|
+
def pexpire_fields(milliseconds, *fields)
|
|
258
|
+
string_fields = fields.flatten.compact.map(&:to_s)
|
|
259
|
+
dbclient.call('HPEXPIRE', dbkey, milliseconds, 'FIELDS', string_fields.size, *string_fields)
|
|
260
|
+
end
|
|
261
|
+
alias hpexpire pexpire_fields
|
|
262
|
+
|
|
263
|
+
# Sets absolute expiration time (Unix timestamp in seconds) on hash fields.
|
|
264
|
+
# @note Requires Redis 7.4+
|
|
265
|
+
#
|
|
266
|
+
# @param unix_time [Integer] Absolute Unix timestamp in seconds
|
|
267
|
+
# @param fields [Array<String>] One or more field names
|
|
268
|
+
# @return [Array<Integer>] Array of results for each field
|
|
269
|
+
#
|
|
270
|
+
# @example Expire fields at midnight tonight
|
|
271
|
+
# midnight = Time.now.to_i + (24 * 60 * 60)
|
|
272
|
+
# my_hash.expireat_fields(midnight, 'daily_counter')
|
|
273
|
+
def expireat_fields(unix_time, *fields)
|
|
274
|
+
string_fields = fields.flatten.compact.map(&:to_s)
|
|
275
|
+
dbclient.call('HEXPIREAT', dbkey, unix_time, 'FIELDS', string_fields.size, *string_fields)
|
|
276
|
+
end
|
|
277
|
+
alias hexpireat expireat_fields
|
|
278
|
+
|
|
279
|
+
# Sets absolute expiration time (Unix timestamp in milliseconds) on hash fields.
|
|
280
|
+
# @note Requires Redis 7.4+
|
|
281
|
+
#
|
|
282
|
+
# @param unix_time_ms [Integer] Absolute Unix timestamp in milliseconds
|
|
283
|
+
# @param fields [Array<String>] One or more field names
|
|
284
|
+
# @return [Array<Integer>] Array of results for each field
|
|
285
|
+
#
|
|
286
|
+
# @example Expire field at a precise millisecond
|
|
287
|
+
# my_hash.pexpireat_fields(1700000000000, 'precise_data')
|
|
288
|
+
def pexpireat_fields(unix_time_ms, *fields)
|
|
289
|
+
string_fields = fields.flatten.compact.map(&:to_s)
|
|
290
|
+
dbclient.call('HPEXPIREAT', dbkey, unix_time_ms, 'FIELDS', string_fields.size, *string_fields)
|
|
291
|
+
end
|
|
292
|
+
alias hpexpireat pexpireat_fields
|
|
293
|
+
|
|
294
|
+
# Returns the remaining TTL in seconds for one or more hash fields.
|
|
295
|
+
# @note Requires Redis 7.4+
|
|
296
|
+
#
|
|
297
|
+
# @param fields [Array<String>] One or more field names
|
|
298
|
+
# @return [Array<Integer>] Array of TTL values for each field:
|
|
299
|
+
# -2 if field does not exist, -1 if field has no expiration,
|
|
300
|
+
# otherwise the TTL in seconds
|
|
301
|
+
#
|
|
302
|
+
# @example Check remaining TTL on fields
|
|
303
|
+
# my_hash.ttl_fields('session_token', 'temp_data') #=> [3600, -1]
|
|
304
|
+
def ttl_fields(*fields)
|
|
305
|
+
string_fields = fields.flatten.compact.map(&:to_s)
|
|
306
|
+
dbclient.call('HTTL', dbkey, 'FIELDS', string_fields.size, *string_fields)
|
|
307
|
+
end
|
|
308
|
+
alias httl ttl_fields
|
|
309
|
+
|
|
310
|
+
# Returns the remaining TTL in milliseconds for one or more hash fields.
|
|
311
|
+
# @note Requires Redis 7.4+
|
|
312
|
+
#
|
|
313
|
+
# @param fields [Array<String>] One or more field names
|
|
314
|
+
# @return [Array<Integer>] Array of TTL values in milliseconds
|
|
315
|
+
#
|
|
316
|
+
# @example Check remaining TTL in milliseconds
|
|
317
|
+
# my_hash.pttl_fields('rate_limit') #=> [450]
|
|
318
|
+
def pttl_fields(*fields)
|
|
319
|
+
string_fields = fields.flatten.compact.map(&:to_s)
|
|
320
|
+
dbclient.call('HPTTL', dbkey, 'FIELDS', string_fields.size, *string_fields)
|
|
321
|
+
end
|
|
322
|
+
alias hpttl pttl_fields
|
|
323
|
+
|
|
324
|
+
# Removes expiration from one or more hash fields.
|
|
325
|
+
# @note Requires Redis 7.4+
|
|
326
|
+
#
|
|
327
|
+
# @param fields [Array<String>] One or more field names
|
|
328
|
+
# @return [Array<Integer>] Array of results for each field:
|
|
329
|
+
# -2 if field does not exist, -1 if field has no expiration,
|
|
330
|
+
# 1 if expiration was removed
|
|
331
|
+
#
|
|
332
|
+
# @example Remove expiration from fields
|
|
333
|
+
# my_hash.persist_fields('important_data') #=> [1]
|
|
334
|
+
def persist_fields(*fields)
|
|
335
|
+
string_fields = fields.flatten.compact.map(&:to_s)
|
|
336
|
+
dbclient.call('HPERSIST', dbkey, 'FIELDS', string_fields.size, *string_fields)
|
|
337
|
+
end
|
|
338
|
+
alias hpersist persist_fields
|
|
339
|
+
|
|
340
|
+
# Returns the absolute Unix expiration timestamp in seconds for hash fields.
|
|
341
|
+
# @note Requires Redis 7.4+
|
|
342
|
+
#
|
|
343
|
+
# @param fields [Array<String>] One or more field names
|
|
344
|
+
# @return [Array<Integer>] Array of timestamps for each field:
|
|
345
|
+
# -2 if field does not exist, -1 if field has no expiration,
|
|
346
|
+
# otherwise the absolute Unix timestamp in seconds
|
|
347
|
+
#
|
|
348
|
+
# @example Get expiration timestamp
|
|
349
|
+
# my_hash.expiretime_fields('session') #=> [1700000000]
|
|
350
|
+
def expiretime_fields(*fields)
|
|
351
|
+
string_fields = fields.flatten.compact.map(&:to_s)
|
|
352
|
+
dbclient.call('HEXPIRETIME', dbkey, 'FIELDS', string_fields.size, *string_fields)
|
|
353
|
+
end
|
|
354
|
+
alias hexpiretime expiretime_fields
|
|
355
|
+
|
|
356
|
+
# Returns the absolute Unix expiration timestamp in milliseconds for hash fields.
|
|
357
|
+
# @note Requires Redis 7.4+
|
|
358
|
+
#
|
|
359
|
+
# @param fields [Array<String>] One or more field names
|
|
360
|
+
# @return [Array<Integer>] Array of timestamps in milliseconds
|
|
361
|
+
#
|
|
362
|
+
# @example Get precise expiration timestamp
|
|
363
|
+
# my_hash.pexpiretime_fields('session') #=> [1700000000000]
|
|
364
|
+
def pexpiretime_fields(*fields)
|
|
365
|
+
string_fields = fields.flatten.compact.map(&:to_s)
|
|
366
|
+
dbclient.call('HPEXPIRETIME', dbkey, 'FIELDS', string_fields.size, *string_fields)
|
|
367
|
+
end
|
|
368
|
+
alias hpexpiretime pexpiretime_fields
|
|
369
|
+
|
|
132
370
|
# The Great Database Refresh-o-matic 3000 for HashKey!
|
|
133
371
|
#
|
|
134
372
|
# This method performs a complete refresh of the hash's state from the database.
|