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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e53524562d8d882f297dfff646ee905a2cba7fc887b00640ab80bed510de8c1c
4
- data.tar.gz: 823ee307f5b4ce938a3970c4872b45351d87f7af8225d8bd98820f8f73eb4229
3
+ metadata.gz: a908aad71096ba6a650fa67545aad0a2f891be6472fc6cb4ed067977c4edd706
4
+ data.tar.gz: 01d676ccdf7a783d585053be2861319785a8aa5c33298848607f589b3dfa6aaf
5
5
  SHA512:
6
- metadata.gz: 51bb9a24cb6d83caa6ca1574358a0c3a93bc6735f65294a45132e0d415c1494e97c092f9263457f49e2895dc56120ba0f384f1df7b29713784b2cd6fd1d4b8f5
7
- data.tar.gz: 7fd5600d88b2ae5fd6971002d57f16094e59f4813b8a720cdc75fcdaac77e225384b0458c4a40b89bdc396e688ff677f86c75dfa19fc0e523fbd276369d2493f
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
@@ -13,6 +13,7 @@ group :test do
13
13
  end
14
14
 
15
15
  group :development, :test do
16
+ gem 'benchmark', '~> 0.4', require: false
16
17
  gem 'debug', require: false
17
18
  gem 'irb', '~> 1.15.2', require: false
18
19
  gem 'redcarpet', require: false
data/Gemfile.lock CHANGED
@@ -1,8 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (2.0.0.pre26)
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.4+ (3.4+ recommended)
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#encrypted-fields-feature-v200-pre5)** - Implementation details and advanced patterns
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#expiration-feature-v200-pre5)** - Implementation details and advanced patterns
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#quantization-feature-v200-pre7)** - Implementation details and advanced patterns
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#quantization-feature-v200-pre7).
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#object-identifier-feature-v200-pre7).
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#external-identifier-feature-v200-pre7).
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#relationships-feature-v200-pre7).
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#transient-fields-feature-v200-pre5).
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#encrypted-fields-feature-v200-pre5).
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-v200-pre).
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 (v2.0.0-pre5+)
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.0-pre
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.0-pre syntax
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.pre6 (as of version.rb)
1308
- - **Target Version**: v2.0.0.pre7 (relationships release)
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.0-pre series. For complete API documentation, see the generated YARD docs and wiki guides.
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.3.6')
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.