familia 2.0.0.pre18 → 2.0.0.pre19
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 +58 -6
- data/CLAUDE.md +34 -9
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +39 -0
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +3 -2
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/database_commands.rb +7 -4
- data/lib/familia/data_type/serialization.rb +4 -0
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +39 -38
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +115 -43
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +129 -10
- data/lib/familia/horreum/definition.rb +2 -1
- data/lib/familia/horreum/management.rb +21 -15
- data/lib/familia/horreum/persistence.rb +190 -66
- data/lib/familia/horreum/serialization.rb +3 -0
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +31 -12
- data/lib/familia/logging.rb +2 -5
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/middleware/database_logger.rb +76 -5
- data/try/edge_cases/string_coercion_try.rb +4 -4
- data/try/features/expiration/expiration_try.rb +1 -1
- data/try/features/relationships/indexing_try.rb +28 -4
- data/try/features/relationships/relationships_api_changes_try.rb +4 -4
- data/try/integration/connection/fiber_context_preservation_try.rb +3 -3
- data/try/integration/connection/operation_mode_guards_try.rb +1 -1
- data/try/integration/connection/pipeline_fallback_integration_try.rb +12 -12
- data/try/integration/create_method_try.rb +22 -22
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/integration/models/customer_safe_dump_try.rb +5 -1
- data/try/integration/models/familia_object_try.rb +1 -1
- data/try/integration/persistence_operations_try.rb +162 -10
- data/try/unit/data_types/boolean_try.rb +1 -1
- data/try/unit/data_types/string_try.rb +1 -1
- data/try/unit/horreum/auto_indexing_on_save_try.rb +32 -16
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/unit/horreum/base_try.rb +1 -1
- data/try/unit/horreum/class_methods_try.rb +2 -2
- data/try/unit/horreum/initialization_try.rb +1 -1
- data/try/unit/horreum/relations_try.rb +4 -4
- data/try/unit/horreum/serialization_try.rb +2 -2
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- metadata +14 -2
@@ -16,6 +16,10 @@ module Familia
|
|
16
16
|
# just load the object again.
|
17
17
|
#
|
18
18
|
module DatabaseCommands
|
19
|
+
# Moves the object's key to a different logical database.
|
20
|
+
#
|
21
|
+
# @param logical_database [Integer] The target database number
|
22
|
+
# @return [Boolean] true if the key was moved successfully
|
19
23
|
def move(logical_database)
|
20
24
|
dbclient.move dbkey, logical_database
|
21
25
|
end
|
@@ -40,7 +44,18 @@ module Familia
|
|
40
44
|
key_exists = self.class.exists?(identifier)
|
41
45
|
return key_exists unless check_size
|
42
46
|
|
43
|
-
|
47
|
+
# Handle Redis::Future in transactions - skip size check
|
48
|
+
if key_exists.is_a?(Redis::Future)
|
49
|
+
return key_exists
|
50
|
+
end
|
51
|
+
|
52
|
+
current_size = size
|
53
|
+
# Handle Redis::Future from size call too
|
54
|
+
if current_size.is_a?(Redis::Future)
|
55
|
+
return current_size
|
56
|
+
end
|
57
|
+
|
58
|
+
key_exists && !current_size.zero?
|
44
59
|
end
|
45
60
|
|
46
61
|
# Returns the number of fields in the main object hash
|
@@ -55,6 +70,8 @@ module Familia
|
|
55
70
|
# automatically be deleted. Returns 1 if the timeout was set, 0 if key
|
56
71
|
# does not exist or the timeout could not be set.
|
57
72
|
#
|
73
|
+
# @param default_expiration [Integer] TTL in seconds (uses class default if nil)
|
74
|
+
# @return [Integer] 1 if timeout was set, 0 otherwise
|
58
75
|
def expire(default_expiration = nil)
|
59
76
|
default_expiration ||= self.class.default_expiration
|
60
77
|
Familia.trace :EXPIRE, nil, default_expiration if Familia.debug?
|
@@ -69,7 +86,7 @@ module Familia
|
|
69
86
|
# @return [Integer] The TTL of the key in seconds. Returns -1 if the key does not exist
|
70
87
|
# or has no associated expire time.
|
71
88
|
def current_expiration
|
72
|
-
Familia.trace :CURRENT_EXPIRATION, nil, uri if Familia.debug?
|
89
|
+
Familia.trace :CURRENT_EXPIRATION, nil, self.class.uri if Familia.debug?
|
73
90
|
dbclient.ttl dbkey
|
74
91
|
end
|
75
92
|
|
@@ -83,24 +100,38 @@ module Familia
|
|
83
100
|
end
|
84
101
|
alias remove remove_field # deprecated
|
85
102
|
|
103
|
+
# Returns the Redis data type of the key.
|
104
|
+
#
|
105
|
+
# @return [String] The data type (e.g., 'hash', 'string', 'list')
|
86
106
|
def data_type
|
87
|
-
Familia.trace :DATATYPE, nil, uri if Familia.debug?
|
107
|
+
Familia.trace :DATATYPE, nil, self.class.uri if Familia.debug?
|
88
108
|
dbclient.type dbkey(suffix)
|
89
109
|
end
|
90
110
|
|
91
|
-
#
|
111
|
+
# Returns all fields and values in the hash.
|
112
|
+
#
|
113
|
+
# @return [Hash] All field-value pairs in the hash
|
114
|
+
# @note For parity with DataType#hgetall
|
92
115
|
def hgetall
|
93
|
-
Familia.trace :HGETALL, nil, uri if Familia.debug?
|
116
|
+
Familia.trace :HGETALL, nil, self.class.uri if Familia.debug?
|
94
117
|
dbclient.hgetall dbkey(suffix)
|
95
118
|
end
|
96
119
|
alias all hgetall
|
97
120
|
|
121
|
+
# Gets the value of a hash field.
|
122
|
+
#
|
123
|
+
# @param field [String] The field name
|
124
|
+
# @return [String, nil] The value of the field, or nil if field doesn't exist
|
98
125
|
def hget(field)
|
99
126
|
Familia.trace :HGET, nil, field if Familia.debug?
|
100
127
|
dbclient.hget dbkey(suffix), field
|
101
128
|
end
|
102
129
|
|
103
|
-
#
|
130
|
+
# Sets the value of a hash field.
|
131
|
+
#
|
132
|
+
# @param field [String] The field name
|
133
|
+
# @param value [String] The value to set
|
134
|
+
# @return [Integer] The number of fields that were added to the hash. If the
|
104
135
|
# field already exists, this will return 0.
|
105
136
|
def hset(field, value)
|
106
137
|
Familia.trace :HSET, nil, field if Familia.debug?
|
@@ -120,51 +151,92 @@ module Familia
|
|
120
151
|
dbclient.hsetnx dbkey, field, value
|
121
152
|
end
|
122
153
|
|
154
|
+
# Sets multiple hash fields to multiple values.
|
155
|
+
#
|
156
|
+
# @param hsh [Hash] Hash of field-value pairs to set
|
157
|
+
# @return [String] 'OK' on success
|
123
158
|
def hmset(hsh = {})
|
124
159
|
hsh ||= to_h_for_storage
|
125
160
|
Familia.trace :HMSET, nil, hsh if Familia.debug?
|
126
161
|
dbclient.hmset dbkey(suffix), hsh
|
127
162
|
end
|
128
163
|
|
164
|
+
# Returns all field names in the hash.
|
165
|
+
#
|
166
|
+
# @return [Array<String>] Array of field names
|
129
167
|
def hkeys
|
130
|
-
Familia.trace :HKEYS, nil,
|
168
|
+
Familia.trace :HKEYS, nil, self.class.uri if Familia.debug?
|
131
169
|
dbclient.hkeys dbkey(suffix)
|
132
170
|
end
|
133
171
|
|
172
|
+
# Returns all values in the hash.
|
173
|
+
#
|
174
|
+
# @return [Array<String>] Array of values
|
134
175
|
def hvals
|
135
176
|
dbclient.hvals dbkey(suffix)
|
136
177
|
end
|
137
178
|
|
179
|
+
# Increments the integer value of a hash field by 1.
|
180
|
+
#
|
181
|
+
# @param field [String] The field name
|
182
|
+
# @return [Integer] The value after incrementing
|
138
183
|
def incr(field)
|
139
184
|
dbclient.hincrby dbkey(suffix), field, 1
|
140
185
|
end
|
141
186
|
alias increment incr
|
142
187
|
|
188
|
+
# Increments the integer value of a hash field by the given amount.
|
189
|
+
#
|
190
|
+
# @param field [String] The field name
|
191
|
+
# @param increment [Integer] The increment value
|
192
|
+
# @return [Integer] The value after incrementing
|
143
193
|
def incrby(field, increment)
|
144
194
|
dbclient.hincrby dbkey(suffix), field, increment
|
145
195
|
end
|
146
196
|
alias incrementby incrby
|
147
197
|
|
198
|
+
# Increments the float value of a hash field by the given amount.
|
199
|
+
#
|
200
|
+
# @param field [String] The field name
|
201
|
+
# @param increment [Float] The increment value
|
202
|
+
# @return [Float] The value after incrementing
|
148
203
|
def incrbyfloat(field, increment)
|
149
204
|
dbclient.hincrbyfloat dbkey(suffix), field, increment
|
150
205
|
end
|
151
206
|
alias incrementbyfloat incrbyfloat
|
152
207
|
|
208
|
+
# Decrements the integer value of a hash field by the given amount.
|
209
|
+
#
|
210
|
+
# @param field [String] The field name
|
211
|
+
# @param decrement [Integer] The decrement value
|
212
|
+
# @return [Integer] The value after decrementing
|
153
213
|
def decrby(field, decrement)
|
154
214
|
dbclient.decrby dbkey(suffix), field, decrement
|
155
215
|
end
|
156
216
|
alias decrementby decrby
|
157
217
|
|
218
|
+
# Decrements the integer value of a hash field by 1.
|
219
|
+
#
|
220
|
+
# @param field [String] The field name
|
221
|
+
# @return [Integer] The value after decrementing
|
158
222
|
def decr(field)
|
159
223
|
dbclient.hdecr field
|
160
224
|
end
|
161
225
|
alias decrement decr
|
162
226
|
|
227
|
+
# Returns the string length of the value associated with field in the hash.
|
228
|
+
#
|
229
|
+
# @param field [String] The field name
|
230
|
+
# @return [Integer] The string length of the field value, or 0 if field doesn't exist
|
163
231
|
def hstrlen(field)
|
164
232
|
dbclient.hstrlen dbkey(suffix), field
|
165
233
|
end
|
166
234
|
alias hstrlength hstrlen
|
167
235
|
|
236
|
+
# Determines if a hash field exists.
|
237
|
+
#
|
238
|
+
# @param field [String] The field name
|
239
|
+
# @return [Boolean] true if the field exists, false otherwise
|
168
240
|
def key?(field)
|
169
241
|
dbclient.hexists dbkey(suffix), field
|
170
242
|
end
|
@@ -176,14 +248,61 @@ module Familia
|
|
176
248
|
#
|
177
249
|
# @return [Boolean] true if the key was deleted, false otherwise
|
178
250
|
def delete!
|
179
|
-
Familia.trace :DELETE!, nil, uri if Familia.debug?
|
251
|
+
Familia.trace :DELETE!, nil, self.class.uri if Familia.debug?
|
180
252
|
|
181
253
|
# Delete the main object key
|
182
|
-
|
183
|
-
ret.positive?
|
254
|
+
dbclient.del dbkey
|
184
255
|
end
|
185
256
|
alias clear delete!
|
186
257
|
|
258
|
+
# Watches the key for changes during a MULTI/EXEC transaction.
|
259
|
+
#
|
260
|
+
# Decision Matrix:
|
261
|
+
#
|
262
|
+
# | Scenario | Use | Why |
|
263
|
+
# |----------|-----|-----|
|
264
|
+
# | Check if exists, then create | WATCH | Must prevent duplicate creation |
|
265
|
+
# | Read value, update conditionally | WATCH | Decision depends on current state |
|
266
|
+
# | Compare-and-swap operations | WATCH | Need optimistic locking |
|
267
|
+
# | Version-based updates | WATCH | Must detect concurrent changes |
|
268
|
+
# | Batch field updates | MULTI only | No conditional logic |
|
269
|
+
# | Increment + timestamp together | MULTI only | Concurrent increments OK |
|
270
|
+
# | Save object atomically | MULTI only | Just need atomicity |
|
271
|
+
# | Update indexes with save | MULTI only | No state checking needed |
|
272
|
+
#
|
273
|
+
# @param suffix_override [String, nil] Optional suffix override
|
274
|
+
# @return [String] 'OK' on success
|
275
|
+
def watch(...)
|
276
|
+
raise ArgumentError, 'Block required' unless block_given?
|
277
|
+
|
278
|
+
# Forward all arguments including the block to the watch command
|
279
|
+
dbclient.watch(dbkey, ...)
|
280
|
+
|
281
|
+
rescue Redis::BaseError => e
|
282
|
+
raise OptimisticLockError, "Redis error: #{e.message}"
|
283
|
+
end
|
284
|
+
|
285
|
+
# Flushes all the previously watched keys for a transaction.
|
286
|
+
#
|
287
|
+
# If a transaction completes successfully or discard is called, there's
|
288
|
+
# no need to manually call unwatch.
|
289
|
+
#
|
290
|
+
# NOTE: This command operates on the connection itself; not a specific key
|
291
|
+
#
|
292
|
+
# @return [String] 'OK' always, regardless of whether the key was watched or not
|
293
|
+
def unwatch(...) = dbclient.unwatch(...)
|
294
|
+
|
295
|
+
# Flushes all previously queued commands in a transaction and all watched keys
|
296
|
+
#
|
297
|
+
# NOTE: This command operates on the connection itself; not a specific key
|
298
|
+
#
|
299
|
+
# @return [String] 'OK' always
|
300
|
+
def discard(...) = dbclient.discard(...)
|
301
|
+
|
302
|
+
# Echoes a message through the Redis connection.
|
303
|
+
#
|
304
|
+
# @param args [Array] Arguments to join and echo
|
305
|
+
# @return [String] The echoed message
|
187
306
|
def echo(*args)
|
188
307
|
dbclient.echo "[#{self.class}] #{args.join(' ')}"
|
189
308
|
end
|
@@ -467,7 +467,8 @@ module Familia
|
|
467
467
|
|
468
468
|
# If no value is provided to this fast attribute method, make a call
|
469
469
|
# to the db to return the current stored value of the hash field.
|
470
|
-
|
470
|
+
# Handle Redis::Future objects during transactions
|
471
|
+
return hget field_name if val.nil? || val.is_a?(Redis::Future)
|
471
472
|
|
472
473
|
begin
|
473
474
|
# Trace the operation if debugging is enabled.
|
@@ -23,7 +23,7 @@ module Familia
|
|
23
23
|
# to the constructor.
|
24
24
|
# @param kwargs [Hash] Keyword arguments to be passed to the constructor.
|
25
25
|
# @return [Object] The newly created and persisted instance.
|
26
|
-
# @raise [Familia::
|
26
|
+
# @raise [Familia::RecordExistsError] If an instance with the same identifier already
|
27
27
|
# exists.
|
28
28
|
#
|
29
29
|
# This method serves as a factory method for creating and persisting new
|
@@ -35,7 +35,7 @@ module Familia
|
|
35
35
|
# - Keyword arguments (**kwargs) are passed as a hash to the constructor.
|
36
36
|
#
|
37
37
|
# After instantiation, the method checks if an object with the same
|
38
|
-
# identifier already exists. If it does, a Familia::
|
38
|
+
# identifier already exists. If it does, a Familia::RecordExistsError exception is
|
39
39
|
# raised to prevent overwriting existing data.
|
40
40
|
#
|
41
41
|
# Finally, the method saves the new instance returns it.
|
@@ -52,24 +52,27 @@ module Familia
|
|
52
52
|
# @see #new
|
53
53
|
# @see #exists?
|
54
54
|
# @see #save
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
55
|
+
def create!(...)
|
56
|
+
hobj = new(...)
|
57
|
+
hobj.save_if_not_exists!
|
58
|
+
|
59
|
+
# If a block is given, yield the created object
|
60
|
+
# This allows for additional operations on successful creation
|
61
|
+
yield hobj if block_given?
|
62
|
+
|
63
|
+
hobj
|
60
64
|
end
|
61
65
|
|
62
|
-
def multiget(
|
63
|
-
|
64
|
-
ids.filter_map { |json| from_json(json) }
|
66
|
+
def multiget(...)
|
67
|
+
rawmultiget(...).filter_map { |json| Familia::JsonSerializer.parse(json) }
|
65
68
|
end
|
66
69
|
|
67
|
-
def rawmultiget(*
|
68
|
-
|
69
|
-
return [] if
|
70
|
+
def rawmultiget(*hids)
|
71
|
+
hids.collect! { |hobjid| dbkey(hobjid) }
|
72
|
+
return [] if hids.compact.empty?
|
70
73
|
|
71
|
-
Familia.trace :MULTIGET, nil, "#{
|
72
|
-
dbclient.mget(*
|
74
|
+
Familia.trace :MULTIGET, nil, "#{hids.size}: #{hids}" if Familia.debug?
|
75
|
+
dbclient.mget(*hids)
|
73
76
|
end
|
74
77
|
|
75
78
|
# Converts the class name into a string that can be used to look up
|
@@ -209,6 +212,9 @@ module Familia
|
|
209
212
|
ret = dbclient.exists objkey
|
210
213
|
Familia.trace :EXISTS, nil, "#{objkey} #{ret.inspect}" if Familia.debug?
|
211
214
|
|
215
|
+
# Handle Redis::Future objects during transactions
|
216
|
+
return ret if ret.is_a?(Redis::Future)
|
217
|
+
|
212
218
|
ret.positive? # differs from Valkey API but I think it's okay bc `exists?` is a predicate method.
|
213
219
|
end
|
214
220
|
|