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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4250fd7b94da275c6cfe9ebc7515e14fc6bead4bb25597aa5e433bf6c9368727
|
4
|
+
data.tar.gz: 50818f7fce2464d3a4349d4de6a1fd7992900e45d8774f6d6d40d047d71a1842
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf19202fe2ae0176698fa3ed5b5bd5cb4c1536de2e091a0978d75d8d9964c05cf2285cbcbbdf2d8deb20500a00b83f4fc4d7091e80f08fcaa29840053788da4f
|
7
|
+
data.tar.gz: c7a5810d3c84cca3c8f51e7449d0049c13eae86300f4703b5af9a492329a1a1c6ff01142529351e705f527c4b4f1ad8d07b15e5280b956a3c5a4fa2971ff2b08
|
data/CHANGELOG.rst
CHANGED
@@ -1,17 +1,69 @@
|
|
1
1
|
CHANGELOG.rst
|
2
2
|
=============
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
The format is based on `Keep a
|
7
|
-
Changelog <https://keepachangelog.com/en/1.1.0/>`__, and this project
|
8
|
-
adheres to `Semantic
|
9
|
-
Versioning <https://semver.org/spec/v2.0.0.html>`__.
|
4
|
+
The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`__, and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`__.
|
10
5
|
|
11
6
|
.. raw:: html
|
12
7
|
|
13
8
|
<!--scriv-insert-here-->
|
14
9
|
|
10
|
+
.. _changelog-2.0.0.pre19:
|
11
|
+
|
12
|
+
2.0.0.pre19 — 2025-10-11
|
13
|
+
=========================
|
14
|
+
|
15
|
+
Added
|
16
|
+
-----
|
17
|
+
|
18
|
+
- **DataType Transaction and Pipeline Support** - DataType objects can now initiate transactions and pipelines independently, enabling atomic operations and batch command execution for both parent-owned and standalone DataType objects. `PR #160 <https://github.com/familia/familia/pull/160>`__. Key capabilities added:
|
19
|
+
|
20
|
+
* ``transaction`` and ``pipelined`` methods for atomic MULTI/EXEC operations and batched command execution on all DataType classes
|
21
|
+
* Connection chain pattern with Chain of Responsibility for DataType objects
|
22
|
+
* Two new connection handlers: ``ParentDelegationHandler`` for owned DataTypes and ``StandaloneConnectionHandler`` for independent DataTypes
|
23
|
+
* Enhanced ``direct_access`` method with automatic transaction/pipeline context detection
|
24
|
+
* Shared ``Familia::Connection::Behavior`` module extracting common connection functionality
|
25
|
+
|
26
|
+
- New error hierarchy with ``PersistenceError``, ``HorreumError``, ``CreationError``, and ``OptimisticLockError`` classes for better error categorization and handling
|
27
|
+
- ``watch``, ``unwatch``, and ``discard`` Redis commands for optimistic locking support
|
28
|
+
- Enhanced database command logging with structured format for pipelined and transaction operations
|
29
|
+
- ``save_fields`` method in Persistence module for selective field updates
|
30
|
+
|
31
|
+
Changed
|
32
|
+
-------
|
33
|
+
|
34
|
+
- **Connection Architecture Refactored** - The ``Horreum::Connection`` module now includes ``Familia::Connection::Behavior``, eliminating code duplication by sharing URI normalization and connection creation methods between Horreum and DataType. DataType objects with ``logical_database`` settings now return clean URIs without custom port information (e.g., ``redis://127.0.0.1/3`` instead of ``redis://127.0.0.1:2525/3``), ensuring consistent URI representation across the library.
|
35
|
+
|
36
|
+
- **BREAKING**: Renamed ``Management.create`` to ``create!`` to follow Rails conventions and indicate potential exceptions
|
37
|
+
- **BREAKING**: Updated ``save_if_not_exists`` to ``save_if_not_exists!`` with optimistic locking and automatic retry logic (up to 3 attempts)
|
38
|
+
- Improved ``save`` method to use single atomic transaction encompassing field updates, expiration setting, index updates, and instance collection management
|
39
|
+
- Enhanced ``delete!`` methods to work correctly within Redis transactions
|
40
|
+
- Updated timestamp fields (``created``, ``updated``) to use float values instead of integers for higher precision
|
41
|
+
- Refined log message formatting for better readability and debugging
|
42
|
+
- Removed deprecated Connection instance methods for Horreum models in favor of class-level database operations
|
43
|
+
- Clarified "pipelined" terminology throughout codebase (renamed from "pipeline" for consistency with Redis documentation)
|
44
|
+
|
45
|
+
Fixed
|
46
|
+
-----
|
47
|
+
|
48
|
+
- Resolved atomicity issues and race conditions in save operations by consolidating all related operations into single Redis transaction with proper watch/multi/exec pattern and optimistic locking
|
49
|
+
- Corrected transaction handling to ensure proper cleanup and error propagation
|
50
|
+
|
51
|
+
Documentation
|
52
|
+
-------------
|
53
|
+
|
54
|
+
- Added comprehensive parameter documentation for database command methods including return value specifications and usage examples
|
55
|
+
|
56
|
+
AI Assistance
|
57
|
+
-------------
|
58
|
+
|
59
|
+
This feature was implemented with AI assistance from Claude Sonnet 4.5, Opus 4.1 (Anthropic).
|
60
|
+
|
61
|
+
* Architectural design of the connection chain pattern and shared Behavior module
|
62
|
+
* Implementation of DataType-specific connection handlers (ParentDelegationHandler, StandaloneConnectionHandler) and comprehensive test coverage
|
63
|
+
* Error hierarchy design and transaction atomicity optimization
|
64
|
+
* Documentation enhancement and URI formatting debugging
|
65
|
+
|
66
|
+
|
15
67
|
.. _changelog-2.0.0.pre18:
|
16
68
|
|
17
69
|
2.0.0.pre18 — 2025-10-05
|
data/CLAUDE.md
CHANGED
@@ -105,30 +105,55 @@ class User < Familia::Horreum
|
|
105
105
|
end
|
106
106
|
```
|
107
107
|
|
108
|
-
**Good - use the `init` hook
|
108
|
+
**Good - use the `init` hook to apply defaults (use `||=` not `=`):**
|
109
109
|
```ruby
|
110
110
|
class User < Familia::Horreum
|
111
|
-
|
112
|
-
|
111
|
+
field :objid
|
112
|
+
field :email
|
113
|
+
|
114
|
+
# Called after Horreum sets fields from kwargs
|
115
|
+
# IMPORTANT: Use ||= to apply defaults, not = to override
|
116
|
+
def init
|
117
|
+
@objid ||= SecureRandom.uuid # Apply default only if not already set
|
118
|
+
_run_post_init_hooks # Additional setup logic
|
113
119
|
end
|
114
120
|
end
|
121
|
+
|
122
|
+
# This works correctly:
|
123
|
+
user = User.new(email: 'test@example.com')
|
124
|
+
user.objid # → generated UUID (applied by init)
|
125
|
+
user.email # → 'test@example.com' (set by Horreum from kwargs)
|
115
126
|
```
|
116
127
|
|
117
|
-
**
|
128
|
+
**Okay - if absolutely necessary, override and call super explicitly:**
|
118
129
|
```ruby
|
119
130
|
class User < Familia::Horreum
|
120
131
|
def initialize(email = nil, **kwargs)
|
121
|
-
super
|
122
|
-
@email
|
132
|
+
super # Initializes related fields here and also calls init
|
133
|
+
@email ||= generate_email if email.nil?
|
123
134
|
end
|
124
135
|
end
|
125
136
|
```
|
126
137
|
|
127
|
-
**Why this matters**: Familia's `initialize` method calls `initialize_relatives`
|
138
|
+
**Why this matters**: Familia's `initialize` method processes kwargs FIRST (setting fields), then calls `initialize_relatives` (setting up DataType objects), then calls your `init` hook. By the time `init` runs, kwargs have already been consumed and fields are set.
|
139
|
+
|
140
|
+
**The ||= Pattern Explained**:
|
141
|
+
```ruby
|
142
|
+
# WRONG - overwrites what Horreum already set
|
143
|
+
def init
|
144
|
+
@email = generate_email # Overwrites the correct value
|
145
|
+
end
|
146
|
+
|
147
|
+
# RIGHT - applies default only if not already set
|
148
|
+
def init
|
149
|
+
@email ||= email # Preserves value Horreum set from kwargs
|
150
|
+
@email ||= 'default@example.com' # Apply fallback default if still nil
|
151
|
+
end
|
152
|
+
```
|
128
153
|
|
129
154
|
**When to use each approach:**
|
130
|
-
- **Use `init` hook
|
131
|
-
- **Use explicit `super`**:
|
155
|
+
- **Use `init` hook with `||=`** (preferred): Apply defaults, run validations, setup callbacks - any logic that should run after field initialization. Follows standard ORM lifecycle hook patterns.
|
156
|
+
- **Use explicit `super`**: Only when you need to intercept or transform arguments before Horreum processes them (rare).
|
132
157
|
|
133
158
|
**DataType Definition**: Use class methods to define keystore database-backed attributes:
|
134
159
|
```ruby
|
data/Gemfile
CHANGED
@@ -17,10 +17,10 @@ group :development, :test do
|
|
17
17
|
gem 'irb', '~> 1.15.2', require: false
|
18
18
|
gem 'redcarpet', require: false
|
19
19
|
gem 'reek', require: false
|
20
|
-
gem 'rubocop', require: false
|
20
|
+
gem 'rubocop', '~> 1.81.1', require: false
|
21
21
|
gem 'rubocop-performance', require: false
|
22
22
|
gem 'rubocop-thread_safety', require: false
|
23
|
-
gem '
|
23
|
+
gem 'ruby-lsp', require: false
|
24
24
|
gem 'yard', '~> 0.9', require: false
|
25
25
|
end
|
26
26
|
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
familia (2.0.0.
|
4
|
+
familia (2.0.0.pre19)
|
5
5
|
benchmark (~> 0.4)
|
6
6
|
connection_pool (~> 2.5)
|
7
7
|
csv (~> 3.3)
|
@@ -15,7 +15,6 @@ GEM
|
|
15
15
|
remote: https://rubygems.org/
|
16
16
|
specs:
|
17
17
|
ast (2.4.3)
|
18
|
-
backport (1.2.0)
|
19
18
|
base64 (0.3.0)
|
20
19
|
benchmark (0.4.1)
|
21
20
|
bigdecimal (3.2.3)
|
@@ -64,23 +63,11 @@ GEM
|
|
64
63
|
pp (>= 0.6.0)
|
65
64
|
rdoc (>= 4.0.0)
|
66
65
|
reline (>= 0.4.2)
|
67
|
-
|
68
|
-
json (2.15.0)
|
69
|
-
kramdown (2.5.1)
|
70
|
-
rexml (>= 3.3.9)
|
71
|
-
kramdown-parser-gfm (1.1.0)
|
72
|
-
kramdown (~> 2.0)
|
66
|
+
json (2.15.1)
|
73
67
|
language_server-protocol (3.17.0.5)
|
74
68
|
lint_roller (1.1.0)
|
75
69
|
logger (1.7.0)
|
76
|
-
mini_portile2 (2.8.9)
|
77
70
|
minitest (5.25.5)
|
78
|
-
nokogiri (1.18.10)
|
79
|
-
mini_portile2 (~> 2.8.2)
|
80
|
-
racc (~> 1.4)
|
81
|
-
nokogiri (1.18.10-arm64-darwin)
|
82
|
-
racc (~> 1.4)
|
83
|
-
observer (0.1.2)
|
84
71
|
oj (3.16.11)
|
85
72
|
bigdecimal (>= 3.0)
|
86
73
|
ostruct (>= 0.2)
|
@@ -94,7 +81,7 @@ GEM
|
|
94
81
|
pp (0.6.2)
|
95
82
|
prettyprint
|
96
83
|
prettyprint (0.2.0)
|
97
|
-
prism (1.5.
|
84
|
+
prism (1.5.2)
|
98
85
|
psych (5.2.6)
|
99
86
|
date
|
100
87
|
stringio
|
@@ -121,8 +108,6 @@ GEM
|
|
121
108
|
regexp_parser (2.11.3)
|
122
109
|
reline (0.6.2)
|
123
110
|
io-console (~> 0.5)
|
124
|
-
reverse_markdown (3.0.0)
|
125
|
-
nokogiri
|
126
111
|
rexml (3.4.1)
|
127
112
|
rspec (3.13.1)
|
128
113
|
rspec-core (~> 3.13.0)
|
@@ -159,34 +144,15 @@ GEM
|
|
159
144
|
lint_roller (~> 1.1)
|
160
145
|
rubocop (~> 1.72, >= 1.72.1)
|
161
146
|
rubocop-ast (>= 1.44.0, < 2.0)
|
147
|
+
ruby-lsp (0.26.1)
|
148
|
+
language_server-protocol (~> 3.17.0)
|
149
|
+
prism (>= 1.2, < 2.0)
|
150
|
+
rbs (>= 3, < 5)
|
162
151
|
ruby-prof (1.7.2)
|
163
152
|
base64
|
164
153
|
ruby-progressbar (1.13.0)
|
165
|
-
solargraph (0.57.0)
|
166
|
-
backport (~> 1.2)
|
167
|
-
benchmark (~> 0.4)
|
168
|
-
bundler (~> 2.0)
|
169
|
-
diff-lcs (~> 1.4)
|
170
|
-
jaro_winkler (~> 1.6, >= 1.6.1)
|
171
|
-
kramdown (~> 2.3)
|
172
|
-
kramdown-parser-gfm (~> 1.1)
|
173
|
-
logger (~> 1.6)
|
174
|
-
observer (~> 0.1)
|
175
|
-
ostruct (~> 0.6)
|
176
|
-
parser (~> 3.0)
|
177
|
-
prism (~> 1.4)
|
178
|
-
rbs (>= 3.6.1, <= 4.0.0.dev.4)
|
179
|
-
reverse_markdown (~> 3.0)
|
180
|
-
rubocop (~> 1.76)
|
181
|
-
thor (~> 1.0)
|
182
|
-
tilt (~> 2.0)
|
183
|
-
yard (~> 0.9, >= 0.9.24)
|
184
|
-
yard-activesupport-concern (~> 0.0)
|
185
|
-
yard-solargraph (~> 0.1)
|
186
154
|
stackprof (0.2.27)
|
187
155
|
stringio (3.1.7)
|
188
|
-
thor (1.4.0)
|
189
|
-
tilt (2.6.1)
|
190
156
|
timecop (0.9.10)
|
191
157
|
tryouts (3.6.0)
|
192
158
|
concurrent-ruby (~> 1.0)
|
@@ -205,10 +171,6 @@ GEM
|
|
205
171
|
unicode-emoji (4.1.0)
|
206
172
|
uri-valkey (1.4.0)
|
207
173
|
yard (0.9.37)
|
208
|
-
yard-activesupport-concern (0.0.1)
|
209
|
-
yard (>= 0.8)
|
210
|
-
yard-solargraph (0.1.0)
|
211
|
-
yard (~> 0.9)
|
212
174
|
zeitwerk (2.7.3)
|
213
175
|
|
214
176
|
PLATFORMS
|
@@ -223,11 +185,11 @@ DEPENDENCIES
|
|
223
185
|
rbnacl (~> 7.1, >= 7.1.1)
|
224
186
|
redcarpet
|
225
187
|
reek
|
226
|
-
rubocop
|
188
|
+
rubocop (~> 1.81.1)
|
227
189
|
rubocop-performance
|
228
190
|
rubocop-thread_safety
|
191
|
+
ruby-lsp
|
229
192
|
ruby-prof
|
230
|
-
solargraph
|
231
193
|
stackprof
|
232
194
|
timecop
|
233
195
|
tryouts (~> 3.6.0)
|
data/README.md
CHANGED
@@ -280,6 +280,7 @@ Flower.multiget("prose", "tulip", "daisy")
|
|
280
280
|
|
281
281
|
### Transactional Operations
|
282
282
|
|
283
|
+
**Horreum Model Transactions:**
|
283
284
|
```ruby
|
284
285
|
user.transaction do |conn|
|
285
286
|
conn.set("user:#{user.id}:status", "active")
|
@@ -287,6 +288,44 @@ user.transaction do |conn|
|
|
287
288
|
end
|
288
289
|
```
|
289
290
|
|
291
|
+
**DataType Transactions** (standalone or parent-owned):
|
292
|
+
```ruby
|
293
|
+
# Recommended: Use DataType methods for clean, automatic key handling
|
294
|
+
user.scores.transaction do
|
295
|
+
user.scores.add('level1', 100)
|
296
|
+
user.scores.add('level2', 200)
|
297
|
+
end
|
298
|
+
|
299
|
+
# Standalone DataType transaction (e.g., session storage)
|
300
|
+
session_key = Familia::StringKey.new('session:abc123')
|
301
|
+
session_key.transaction do
|
302
|
+
session_key.set(session_data)
|
303
|
+
session_key.expire(3600) # Atomic: both succeed or both fail
|
304
|
+
end
|
305
|
+
|
306
|
+
# Advanced: Connection available for low-level Redis commands
|
307
|
+
user.scores.transaction do |conn|
|
308
|
+
conn.zadd(user.scores.dbkey, 100, 'level1')
|
309
|
+
conn.hset(user.profile.dbkey, 'status', 'active')
|
310
|
+
end
|
311
|
+
```
|
312
|
+
|
313
|
+
**Pipeline Operations** (batch commands for performance):
|
314
|
+
```ruby
|
315
|
+
# Recommended: Use DataType methods
|
316
|
+
leaderboard.pipelined do
|
317
|
+
leaderboard.add('player1', 500)
|
318
|
+
leaderboard.add('player2', 600)
|
319
|
+
leaderboard.size
|
320
|
+
end
|
321
|
+
|
322
|
+
# Advanced: Raw Redis commands for fine-grained control
|
323
|
+
leaderboard.pipelined do |conn|
|
324
|
+
conn.zadd(leaderboard.dbkey, 500, 'player1')
|
325
|
+
conn.zadd(leaderboard.dbkey, 600, 'player2')
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
290
329
|
### Advanced Patterns
|
291
330
|
|
292
331
|
**Time-based Expiration:**
|
@@ -0,0 +1,91 @@
|
|
1
|
+
.. Added
|
2
|
+
.. -----
|
3
|
+
.. New features and capabilities that have been added.
|
4
|
+
|
5
|
+
.. Changed
|
6
|
+
.. -------
|
7
|
+
.. Changes to existing functionality.
|
8
|
+
|
9
|
+
.. Deprecated
|
10
|
+
.. ----------
|
11
|
+
.. Soon-to-be removed features.
|
12
|
+
|
13
|
+
.. Removed
|
14
|
+
.. -------
|
15
|
+
.. Now removed features.
|
16
|
+
|
17
|
+
.. Fixed
|
18
|
+
.. -----
|
19
|
+
.. Bug fixes.
|
20
|
+
|
21
|
+
.. Security
|
22
|
+
.. --------
|
23
|
+
.. Security-related improvements.
|
24
|
+
|
25
|
+
Added
|
26
|
+
-----
|
27
|
+
|
28
|
+
- **DataType Transaction and Pipeline Support** - DataType objects can now initiate transactions and pipelines independently, enabling atomic operations and batch command execution for both parent-owned and standalone DataType objects. `PR #159 <https://github.com/familia/familia/pull/159>`_
|
29
|
+
|
30
|
+
Key capabilities added:
|
31
|
+
|
32
|
+
* ``transaction`` method for atomic MULTI/EXEC operations on all DataType classes
|
33
|
+
* ``pipelined`` method for batched command execution on all DataType classes
|
34
|
+
* Connection chain pattern with Chain of Responsibility for DataType objects
|
35
|
+
* Two new connection handlers: ``ParentDelegationHandler`` for owned DataTypes and ``StandaloneConnectionHandler`` for independent DataTypes
|
36
|
+
* Enhanced ``direct_access`` method with automatic transaction/pipeline context detection
|
37
|
+
* Shared ``Familia::Connection::Behavior`` module extracting common connection functionality
|
38
|
+
|
39
|
+
This enhancement addresses a critical gap where standalone DataType objects could not guarantee atomicity across multiple operations. A prime example is session storage implementations (similar to Rack::Session stores) where setting session data and expiration must be atomic to prevent memory leaks or security issues. Both parent-owned DataTypes (delegating to parent Horreum objects) and standalone DataTypes now support the full transaction and pipeline API.
|
40
|
+
|
41
|
+
Example usage:
|
42
|
+
|
43
|
+
.. code-block:: ruby
|
44
|
+
|
45
|
+
# Recommended: Use DataType methods for clean, key-free syntax
|
46
|
+
# Parent-owned DataType transaction
|
47
|
+
user.scores.transaction do
|
48
|
+
user.scores.add('level1', 100)
|
49
|
+
user.scores.add('level2', 200)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Standalone DataType transaction (e.g., session storage)
|
53
|
+
session_store = Familia::StringKey.new('session:abc123')
|
54
|
+
session_store.transaction do
|
55
|
+
session_store.set(session_data)
|
56
|
+
session_store.update_expiration(expiration: 3600)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Pipeline for performance optimization
|
60
|
+
leaderboard.pipelined do
|
61
|
+
leaderboard.add('player1', 500)
|
62
|
+
leaderboard.add('player2', 600)
|
63
|
+
leaderboard.size
|
64
|
+
end
|
65
|
+
|
66
|
+
# Advanced: Connection available for low-level Redis commands when needed
|
67
|
+
user.scores.transaction do |conn|
|
68
|
+
conn.zadd(user.scores.dbkey, 100, 'level1')
|
69
|
+
conn.hset(user.profile.dbkey, 'status', 'active')
|
70
|
+
end
|
71
|
+
|
72
|
+
Changed
|
73
|
+
-------
|
74
|
+
|
75
|
+
- **DataType URI Construction** - DataType objects with ``logical_database`` settings now return clean URIs without custom port information (e.g., ``redis://127.0.0.1/3`` instead of ``redis://127.0.0.1:2525/3``), ensuring consistent URI representation across the library.
|
76
|
+
|
77
|
+
- **Horreum::Connection Refactored** - The ``Horreum::Connection`` module now includes ``Familia::Connection::Behavior``, eliminating code duplication by sharing URI normalization and connection creation methods between Horreum and DataType. This refactoring improves maintainability while preserving all existing functionality.
|
78
|
+
|
79
|
+
AI Assistance
|
80
|
+
-------------
|
81
|
+
|
82
|
+
This feature was implemented with significant AI assistance from Claude (Anthropic). The AI helped with:
|
83
|
+
|
84
|
+
* Architectural design of the connection chain pattern for DataType objects
|
85
|
+
* Implementation of the shared Behavior module to extract common functionality
|
86
|
+
* Creation of DataType-specific connection handlers (ParentDelegationHandler, StandaloneConnectionHandler)
|
87
|
+
* Comprehensive test coverage including transaction and pipeline integration tests
|
88
|
+
* Documentation and changelog preparation
|
89
|
+
* Debugging and fixing URI formatting edge cases
|
90
|
+
|
91
|
+
The implementation preserves backward compatibility (all 2,216 existing tests pass) while adding 27 new tests specifically for DataType transaction and pipeline support.
|
@@ -0,0 +1,30 @@
|
|
1
|
+
.. A new scriv changelog fragment.
|
2
|
+
..
|
3
|
+
.. Uncomment the section that is right (remove the leading dots).
|
4
|
+
.. For top level release notes, leave all the headers commented out.
|
5
|
+
..
|
6
|
+
Added
|
7
|
+
-----
|
8
|
+
|
9
|
+
- Automatic validation in ``add_to_*`` methods for instance-scoped unique indexes. Previously required manual ``guard_unique_*!`` call before adding to index; now validation happens automatically with clear error messages on duplicate detection.
|
10
|
+
|
11
|
+
- Transaction detection in ``save()`` method. Raises ``Familia::OperationModeError`` when ``save()`` is called within an existing transaction, since unique index guards need to read current values which is not possible inside MULTI/EXEC blocks.
|
12
|
+
|
13
|
+
Changed
|
14
|
+
-------
|
15
|
+
|
16
|
+
- Instance-scoped unique index ``add_to_*`` methods now automatically validate uniqueness before adding to parent's index. This matches modern ORM expectations where constraint validation happens implicitly during mutation operations.
|
17
|
+
|
18
|
+
Documentation
|
19
|
+
-------------
|
20
|
+
|
21
|
+
- Enhanced ``save()`` method documentation to explain transaction restrictions and unique index validation flow.
|
22
|
+
|
23
|
+
- Updated ``UniqueIndexGenerators`` documentation to clarify that ``add_to_*`` methods perform automatic validation.
|
24
|
+
|
25
|
+
- Added comprehensive test suite (21 test cases) demonstrating automatic validation behavior, transaction detection, and error handling patterns.
|
26
|
+
|
27
|
+
AI Assistance
|
28
|
+
-------------
|
29
|
+
|
30
|
+
- Claude Sonnet 4.5 assisted with implementation design, test coverage, and documentation for automatic unique index validation and transaction detection features.
|
@@ -0,0 +1,13 @@
|
|
1
|
+
|
2
|
+
Changed
|
3
|
+
-------
|
4
|
+
|
5
|
+
- **IndexingRelationship**: Added explicit ``:within`` field to preserve the original DSL parameter, replacing brittle ``target_class`` equality checks with clearer ``within.nil?`` checks. This makes the distinction between class-level and instance-scoped indexes more explicit and prevents potential issues with inheritance scenarios.
|
6
|
+
|
7
|
+
AI Assistance
|
8
|
+
-------------
|
9
|
+
|
10
|
+
- Design review and architectural analysis by Claude Code (Sonnet 4.5) via second-opinion agent, identifying brittleness in class comparison logic and recommending explicit storage of the ``within`` parameter.
|
11
|
+
- Implementation of the ``within`` field addition across IndexingRelationship, generators, and usage sites by Claude Code.
|
12
|
+
- All tests verified passing with no behavioral changes.
|
13
|
+
..
|
@@ -0,0 +1,26 @@
|
|
1
|
+
.. Internal terminology refactoring for indexing relationships
|
2
|
+
|
3
|
+
Changed
|
4
|
+
-------
|
5
|
+
|
6
|
+
- **Indexing terminology refactoring**: Renamed internal field ``target_class`` to ``scope_class`` throughout the indexing system to better reflect the semantic role. The ``within:`` parameter in index declarations refers to a "scope" that provides a uniqueness boundary, not a "target" or "parent" relationship. This change affects internal code, comments, and documentation but has no user-facing API impact.
|
7
|
+
|
8
|
+
- Renamed ``IndexingRelationship.target_class`` to ``scope_class``
|
9
|
+
- Updated method parameter names from ``target_instance`` to ``scope_instance``
|
10
|
+
- Replaced "parent" terminology with "scope" in comments and documentation
|
11
|
+
- Updated cheatsheets to reflect correct terminology
|
12
|
+
|
13
|
+
**Rationale**: The term "target" created semantic confusion because it has different meanings in participation relationships (where objects target a collection owner) versus indexing relationships (where objects use a scope for uniqueness). The term "parent" was misleading because it implied an ownership relationship that doesn't exist. "Scope" accurately describes the role: Company provides the scope within which badge_number must be unique.
|
14
|
+
|
15
|
+
Documentation
|
16
|
+
-------------
|
17
|
+
|
18
|
+
- Updated indexing and relationships cheatsheets with improved terminology explanations
|
19
|
+
- Added explicit clarification of scope vs target vs parent semantics
|
20
|
+
|
21
|
+
AI Assistance
|
22
|
+
-------------
|
23
|
+
|
24
|
+
- Claude Code (Sonnet 4.5) provided second-opinion analysis on terminology confusion
|
25
|
+
- Assisted with systematic refactoring of variable names, comments, and documentation
|
26
|
+
- Helped identify all occurrences requiring updates across codebase and tests
|
@@ -66,7 +66,7 @@ session.default_expiration # => 900.0
|
|
66
66
|
session.update_expiration # Uses instance expiration (15 minutes)
|
67
67
|
|
68
68
|
# Or specify expiration inline
|
69
|
-
session.update_expiration(
|
69
|
+
session.update_expiration(expiration: 5.minutes)
|
70
70
|
```
|
71
71
|
|
72
72
|
## Advanced Usage
|
@@ -112,7 +112,7 @@ customer = Customer.new(customer_id: 'cust_123')
|
|
112
112
|
customer.save
|
113
113
|
|
114
114
|
# This will set TTL on the main object AND all related fields
|
115
|
-
customer.update_expiration(
|
115
|
+
customer.update_expiration(expiration: 12.hours)
|
116
116
|
# Sets expiration on:
|
117
117
|
# - customer:cust_123 (main hash)
|
118
118
|
# - customer:cust_123:recent_orders (list)
|
@@ -137,9 +137,9 @@ class AnalyticsEvent < Familia::Horreum
|
|
137
137
|
save
|
138
138
|
|
139
139
|
if should_expire?
|
140
|
-
update_expiration(
|
140
|
+
update_expiration(expiration: 1.hour)
|
141
141
|
else
|
142
|
-
update_expiration(
|
142
|
+
update_expiration(expiration: 30.days)
|
143
143
|
end
|
144
144
|
end
|
145
145
|
end
|
@@ -200,7 +200,7 @@ class SessionCleanupJob
|
|
200
200
|
# Extend expiration for active sessions
|
201
201
|
UserSession.all.each do |session|
|
202
202
|
if session.recently_active?
|
203
|
-
session.update_expiration(
|
203
|
+
session.update_expiration(expiration: 30.minutes)
|
204
204
|
end
|
205
205
|
end
|
206
206
|
end
|
@@ -225,7 +225,7 @@ class SessionExpirationMiddleware
|
|
225
225
|
session = UserSession.find(session_token)
|
226
226
|
|
227
227
|
# Extend session TTL on each request
|
228
|
-
session&.update_expiration(
|
228
|
+
session&.update_expiration(expiration: 30.minutes)
|
229
229
|
end
|
230
230
|
|
231
231
|
@app.call(env)
|
@@ -268,14 +268,14 @@ end
|
|
268
268
|
class SessionManager
|
269
269
|
def self.extend_all_sessions(new_ttl)
|
270
270
|
UserSession.all.each do |session|
|
271
|
-
session.update_expiration(
|
271
|
+
session.update_expiration(expiration: new_ttl)
|
272
272
|
end
|
273
273
|
end
|
274
274
|
|
275
275
|
def self.expire_inactive_sessions
|
276
276
|
UserSession.all.select(&:inactive?).each do |session|
|
277
277
|
# Set very short TTL for inactive sessions
|
278
|
-
session.update_expiration(
|
278
|
+
session.update_expiration(expiration: 5.minutes)
|
279
279
|
end
|
280
280
|
end
|
281
281
|
|
@@ -306,7 +306,7 @@ class DataRetentionService
|
|
306
306
|
model_class = data_type.to_s.pascalize.constantize
|
307
307
|
|
308
308
|
model_class.all.each do |record|
|
309
|
-
record.update_expiration(
|
309
|
+
record.update_expiration(expiration: ttl)
|
310
310
|
end
|
311
311
|
end
|
312
312
|
end
|
@@ -323,7 +323,7 @@ DataRetentionService.apply_retention_policies
|
|
323
323
|
```ruby
|
324
324
|
# ❌ Inefficient: Multiple round trips
|
325
325
|
sessions.each do |session|
|
326
|
-
session.update_expiration(
|
326
|
+
session.update_expiration(expiration: 1.hour)
|
327
327
|
end
|
328
328
|
|
329
329
|
# ✅ Efficient: Batch operations
|
@@ -357,7 +357,7 @@ class ResilientSession < Familia::Horreum
|
|
357
357
|
return unless exists?
|
358
358
|
|
359
359
|
begin
|
360
|
-
update_expiration(
|
360
|
+
update_expiration(expiration: new_ttl)
|
361
361
|
rescue => e
|
362
362
|
# Log error but don't crash the application
|
363
363
|
Familia.logger.warn "Failed to update expiration for #{dbkey}: #{e.message}"
|
@@ -377,7 +377,7 @@ Familia.debug = true
|
|
377
377
|
|
378
378
|
session = UserSession.new(session_token: 'debug_session')
|
379
379
|
session.save
|
380
|
-
session.update_expiration(
|
380
|
+
session.update_expiration(expiration: 5.minutes)
|
381
381
|
# Logs will show:
|
382
382
|
# [update_expiration] Expires session:debug_session in 300.0 seconds
|
383
383
|
```
|
@@ -388,11 +388,11 @@ session.update_expiration(default_expiration: 5.minutes)
|
|
388
388
|
```ruby
|
389
389
|
session = UserSession.new
|
390
390
|
# ❌ Won't work - object must be saved first
|
391
|
-
session.update_expiration(
|
391
|
+
session.update_expiration(expiration: 1.hour)
|
392
392
|
|
393
393
|
# ✅ Correct - save first, then expire
|
394
394
|
session.save
|
395
|
-
session.update_expiration(
|
395
|
+
session.update_expiration(expiration: 1.hour)
|
396
396
|
```
|
397
397
|
|
398
398
|
**2. Related Fields Not Expiring**
|
@@ -459,7 +459,7 @@ RSpec.describe UserSession do
|
|
459
459
|
|
460
460
|
it "applies TTL to database key" do
|
461
461
|
session.save
|
462
|
-
session.update_expiration(
|
462
|
+
session.update_expiration(expiration: 10.minutes)
|
463
463
|
|
464
464
|
ttl = session.ttl
|
465
465
|
expect(ttl).to be > 500 # Should be close to 600 seconds
|
@@ -470,7 +470,7 @@ RSpec.describe UserSession do
|
|
470
470
|
session.save
|
471
471
|
session.activity_log.push('login') # Assume activity_log is a list
|
472
472
|
|
473
|
-
session.update_expiration(
|
473
|
+
session.update_expiration(expiration: 5.minutes)
|
474
474
|
|
475
475
|
# Both main object and related fields should have TTL
|
476
476
|
expect(session.ttl).to be > 250
|
@@ -542,7 +542,7 @@ class TTLHealthCheck
|
|
542
542
|
expired_count += 1
|
543
543
|
elsif ttl < 300 # Less than 5 minutes remaining
|
544
544
|
# Extend TTL for active sessions
|
545
|
-
session.update_expiration(
|
545
|
+
session.update_expiration(expiration: 30.minutes) if session.active?
|
546
546
|
end
|
547
547
|
end
|
548
548
|
|
@@ -565,7 +565,7 @@ class RobustSessionManager
|
|
565
565
|
# Check if session exists and hasn't expired
|
566
566
|
if session&.ttl&.positive?
|
567
567
|
# Extend TTL on access
|
568
|
-
session.update_expiration(
|
568
|
+
session.update_expiration(expiration: 30.minutes)
|
569
569
|
session
|
570
570
|
else
|
571
571
|
# Create new session if old one expired
|