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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +58 -6
  3. data/CLAUDE.md +34 -9
  4. data/Gemfile +2 -2
  5. data/Gemfile.lock +9 -47
  6. data/README.md +39 -0
  7. data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
  8. data/changelog.d/20251011_203905_delano_next.rst +30 -0
  9. data/changelog.d/20251011_212633_delano_next.rst +13 -0
  10. data/changelog.d/20251011_221253_delano_next.rst +26 -0
  11. data/docs/guides/feature-expiration.md +18 -18
  12. data/docs/migrating/v2.0.0-pre19.md +197 -0
  13. data/examples/datatype_standalone.rb +281 -0
  14. data/lib/familia/connection/behavior.rb +252 -0
  15. data/lib/familia/connection/handlers.rb +95 -0
  16. data/lib/familia/connection/operation_core.rb +1 -1
  17. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
  18. data/lib/familia/connection/transaction_core.rb +7 -9
  19. data/lib/familia/connection.rb +3 -2
  20. data/lib/familia/data_type/connection.rb +151 -7
  21. data/lib/familia/data_type/database_commands.rb +7 -4
  22. data/lib/familia/data_type/serialization.rb +4 -0
  23. data/lib/familia/data_type/types/hashkey.rb +1 -1
  24. data/lib/familia/errors.rb +51 -14
  25. data/lib/familia/features/expiration/extensions.rb +8 -10
  26. data/lib/familia/features/expiration.rb +19 -19
  27. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +39 -38
  28. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +115 -43
  29. data/lib/familia/features/relationships/indexing.rb +37 -42
  30. data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
  31. data/lib/familia/field_type.rb +2 -1
  32. data/lib/familia/horreum/connection.rb +11 -35
  33. data/lib/familia/horreum/database_commands.rb +129 -10
  34. data/lib/familia/horreum/definition.rb +2 -1
  35. data/lib/familia/horreum/management.rb +21 -15
  36. data/lib/familia/horreum/persistence.rb +190 -66
  37. data/lib/familia/horreum/serialization.rb +3 -0
  38. data/lib/familia/horreum/utils.rb +0 -8
  39. data/lib/familia/horreum.rb +31 -12
  40. data/lib/familia/logging.rb +2 -5
  41. data/lib/familia/settings.rb +7 -7
  42. data/lib/familia/version.rb +1 -1
  43. data/lib/middleware/database_logger.rb +76 -5
  44. data/try/edge_cases/string_coercion_try.rb +4 -4
  45. data/try/features/expiration/expiration_try.rb +1 -1
  46. data/try/features/relationships/indexing_try.rb +28 -4
  47. data/try/features/relationships/relationships_api_changes_try.rb +4 -4
  48. data/try/integration/connection/fiber_context_preservation_try.rb +3 -3
  49. data/try/integration/connection/operation_mode_guards_try.rb +1 -1
  50. data/try/integration/connection/pipeline_fallback_integration_try.rb +12 -12
  51. data/try/integration/create_method_try.rb +22 -22
  52. data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
  53. data/try/integration/data_types/datatype_transactions_try.rb +247 -0
  54. data/try/integration/models/customer_safe_dump_try.rb +5 -1
  55. data/try/integration/models/familia_object_try.rb +1 -1
  56. data/try/integration/persistence_operations_try.rb +162 -10
  57. data/try/unit/data_types/boolean_try.rb +1 -1
  58. data/try/unit/data_types/string_try.rb +1 -1
  59. data/try/unit/horreum/auto_indexing_on_save_try.rb +32 -16
  60. data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
  61. data/try/unit/horreum/base_try.rb +1 -1
  62. data/try/unit/horreum/class_methods_try.rb +2 -2
  63. data/try/unit/horreum/initialization_try.rb +1 -1
  64. data/try/unit/horreum/relations_try.rb +4 -4
  65. data/try/unit/horreum/serialization_try.rb +2 -2
  66. data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
  67. data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
  68. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5bbf0ac2fc6a1243d74f679c7027040be00783af625dd90d376637e32417394
4
- data.tar.gz: 4b354347a0c2403490dc20fdc12f93b3e71b826cf0b2b08638f6fd884a883630
3
+ metadata.gz: 4250fd7b94da275c6cfe9ebc7515e14fc6bead4bb25597aa5e433bf6c9368727
4
+ data.tar.gz: 50818f7fce2464d3a4349d4de6a1fd7992900e45d8774f6d6d40d047d71a1842
5
5
  SHA512:
6
- metadata.gz: 221116e1aa14bc7114cb51164727587dcb0e8435dd8b85bb7d14618393ef446446376fd7febf6dbc9993098e2c819b3b0aa6f3c8bedce1c965b99ec4c8832a7b
7
- data.tar.gz: 457b0e5bfef1f203c8f2edf934d1ee37915df04c49b0cb8b097ab9732738186ba9e8e92496851699f0952426971d2d3d244e61e594719439672996a0ccde2050
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
- All notable changes to Familia are documented here.
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 instead:**
108
+ **Good - use the `init` hook to apply defaults (use `||=` not `=`):**
109
109
  ```ruby
110
110
  class User < Familia::Horreum
111
- def init(email = nil)
112
- @email = email # Called after super, related fields work
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
- **Good - call super explicitly:**
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(**kwargs) # Related fields initialized
122
- @email = 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` to set up DataType objects (lists, sets, etc.). Without calling `super`, these objects remain nil and you'll get helpful errors pointing to the missing super call.
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** (preferred): For simple initialization logic that doesn't need to intercept constructor arguments. The `init` method is called automatically after `super` with the same arguments passed to `new`.
131
- - **Use explicit `super`**: When you need full control over initialization order or need to transform arguments before passing to parent. Remember to pass `**kwargs` to preserve keyword argument handling.
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 'solargraph', require: false
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.pre18)
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
- jaro_winkler (1.6.1)
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.1)
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(default_expiration: 5.minutes)
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(default_expiration: 12.hours)
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(default_expiration: 1.hour)
140
+ update_expiration(expiration: 1.hour)
141
141
  else
142
- update_expiration(default_expiration: 30.days)
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(default_expiration: 30.minutes)
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(default_expiration: 30.minutes)
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(default_expiration: new_ttl)
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(default_expiration: 5.minutes)
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(default_expiration: ttl)
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(default_expiration: 1.hour)
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(default_expiration: new_ttl)
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(default_expiration: 5.minutes)
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(default_expiration: 1.hour)
391
+ session.update_expiration(expiration: 1.hour)
392
392
 
393
393
  # ✅ Correct - save first, then expire
394
394
  session.save
395
- session.update_expiration(default_expiration: 1.hour)
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(default_expiration: 10.minutes)
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(default_expiration: 5.minutes)
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(default_expiration: 30.minutes) if session.active?
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(default_expiration: 30.minutes)
568
+ session.update_expiration(expiration: 30.minutes)
569
569
  session
570
570
  else
571
571
  # Create new session if old one expired