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
@@ -0,0 +1,281 @@
1
+ # try/unit/horreum/unique_index_guard_validation_try.rb
2
+
3
+ #
4
+ # Unique index guard validation tests
5
+ # Tests the guard_unique_*! methods for both class-level and instance-scoped indexes
6
+ #
7
+
8
+ require_relative '../../support/helpers/test_helpers'
9
+
10
+ # Test classes for unique index guard validation
11
+ class ::GuardUser < Familia::Horreum
12
+ feature :relationships
13
+
14
+ identifier_field :user_id
15
+ field :user_id
16
+ field :email
17
+ field :username
18
+
19
+ # Class-level unique indexes (auto-validated on save)
20
+ unique_index :email, :email_index
21
+ unique_index :username, :username_index
22
+ end
23
+
24
+ class ::GuardCompany < Familia::Horreum
25
+ feature :relationships
26
+
27
+ identifier_field :company_id
28
+ field :company_id
29
+ field :name
30
+ end
31
+
32
+ class ::GuardEmployee < Familia::Horreum
33
+ feature :relationships
34
+
35
+ identifier_field :emp_id
36
+ field :emp_id
37
+ field :badge_number
38
+ field :email
39
+
40
+ # Instance-scoped unique index (manually validated)
41
+ unique_index :badge_number, :badge_index, within: GuardCompany
42
+
43
+ # Class-level unique index (auto-validated)
44
+ unique_index :email, :email_index
45
+ end
46
+
47
+ # Setup
48
+ @user_id1 = "user_#{rand(1000000)}"
49
+ @user_id2 = "user_#{rand(1000000)}"
50
+ @company_id = "comp_#{rand(1000000)}"
51
+ @emp_id1 = "emp_#{rand(1000000)}"
52
+ @emp_id2 = "emp_#{rand(1000000)}"
53
+
54
+ @company = GuardCompany.new(company_id: @company_id, name: 'Test Corp')
55
+ @company.save
56
+
57
+ # =============================================
58
+ # 1. Class-Level Unique Index Guard Methods
59
+ # =============================================
60
+
61
+ ## Guard method exists for class-level unique index
62
+ @user1 = GuardUser.new(user_id: @user_id1, email: 'test@example.com', username: 'testuser')
63
+ @user1.respond_to?(:guard_unique_email_index!)
64
+ #=> true
65
+
66
+ ## Guard passes when no conflict exists
67
+ @user1.guard_unique_email_index!
68
+ #=> nil
69
+
70
+ ## Save succeeds after guard passes
71
+ @user1.save
72
+ #=> true
73
+
74
+ ## Guard fails when duplicate email exists
75
+ @user2 = GuardUser.new(user_id: @user_id2, email: 'test@example.com', username: 'different')
76
+ begin
77
+ @user2.guard_unique_email_index!
78
+ false
79
+ rescue Familia::RecordExistsError => e
80
+ e.message.include?('GuardUser exists email=test@example.com')
81
+ end
82
+ #=> true
83
+
84
+ ## Save automatically calls guard and raises error
85
+ begin
86
+ @user2.save
87
+ false
88
+ rescue Familia::RecordExistsError
89
+ true
90
+ end
91
+ #=> true
92
+
93
+ ## Guard allows same identifier (updating existing record)
94
+ @user1_copy = GuardUser.new(user_id: @user_id1, email: 'test@example.com', username: 'testuser')
95
+ @user1_copy.guard_unique_email_index!
96
+ #=> nil
97
+
98
+ ## Guard handles nil field values gracefully
99
+ @user_nil = GuardUser.new(user_id: "user_nil_#{rand(1000000)}", email: nil, username: 'niluser')
100
+ @user_nil.guard_unique_email_index!
101
+ #=> nil
102
+
103
+ ## Guard handles empty string field values
104
+ @user_empty1 = GuardUser.new(user_id: "user_empty1_#{rand(1000000)}", email: '', username: 'empty1')
105
+ @user_empty1.save
106
+ @user_empty2 = GuardUser.new(user_id: "user_empty2_#{rand(1000000)}", email: '', username: 'empty2')
107
+ begin
108
+ @user_empty2.save
109
+ false
110
+ rescue Familia::RecordExistsError => e
111
+ e.message.include?('GuardUser exists email=')
112
+ end
113
+ #=> true
114
+
115
+ # =============================================
116
+ # 2. Instance-Scoped Unique Index Guard Methods
117
+ # =============================================
118
+
119
+ ## Guard method exists for instance-scoped unique index
120
+ @emp1 = GuardEmployee.new(emp_id: @emp_id1, badge_number: 'BADGE123', email: 'emp1@example.com')
121
+ @emp1.respond_to?(:guard_unique_guard_company_badge_index!)
122
+ #=> true
123
+
124
+ ## Guard method requires parent instance parameter
125
+ @emp1.method(:guard_unique_guard_company_badge_index!).arity
126
+ #=> 1
127
+
128
+ ## Guard passes when no conflict exists in parent's index
129
+ @emp1.guard_unique_guard_company_badge_index!(@company)
130
+ #=> nil
131
+
132
+ ## Can add to index after guard passes
133
+ @emp1.add_to_guard_company_badge_index(@company)
134
+ @company.badge_index.has_key?('BADGE123')
135
+ #=> true
136
+
137
+ ## Guard fails when duplicate badge exists in same company
138
+ @emp2 = GuardEmployee.new(emp_id: @emp_id2, badge_number: 'BADGE123', email: 'emp2@example.com')
139
+ begin
140
+ @emp2.guard_unique_guard_company_badge_index!(@company)
141
+ false
142
+ rescue Familia::RecordExistsError => e
143
+ e.message.include?('GuardEmployee exists in GuardCompany with badge_number=BADGE123')
144
+ end
145
+ #=> true
146
+
147
+ ## Guard allows same employee to re-add (idempotent)
148
+ @emp1.guard_unique_guard_company_badge_index!(@company)
149
+ #=> nil
150
+
151
+ ## Guard passes for different company (different scope)
152
+ @company2_id = "comp_#{rand(1000000)}"
153
+ @company2 = GuardCompany.new(company_id: @company2_id, name: 'Other Corp')
154
+ @company2.save
155
+ @emp2.guard_unique_guard_company_badge_index!(@company2)
156
+ #=> nil
157
+
158
+ ## Can add same badge to different company
159
+ @emp2.add_to_guard_company_badge_index(@company2)
160
+ @company2.badge_index.has_key?('BADGE123')
161
+ #=> true
162
+
163
+ ## Guard handles nil parent instance gracefully
164
+ @emp3 = GuardEmployee.new(emp_id: "emp_#{rand(1000000)}", badge_number: 'BADGE456', email: 'emp3@example.com')
165
+ @emp3.guard_unique_guard_company_badge_index!(nil)
166
+ #=> nil
167
+
168
+ ## Guard handles nil badge_number gracefully
169
+ @emp_nil = GuardEmployee.new(emp_id: "emp_nil_#{rand(1000000)}", badge_number: nil, email: 'empnil@example.com')
170
+ @emp_nil.guard_unique_guard_company_badge_index!(@company)
171
+ #=> nil
172
+
173
+ # =============================================
174
+ # 3. Mixed Class and Instance-Scoped Validation
175
+ # =============================================
176
+
177
+ ## Employee has both class-level and instance-scoped indexes
178
+ @emp4_id = "emp_#{rand(1000000)}"
179
+ @emp4 = GuardEmployee.new(emp_id: @emp4_id, badge_number: 'BADGE789', email: 'unique@example.com')
180
+ @emp4.class
181
+ #=> GuardEmployee
182
+
183
+ ## Class-level email index auto-validates on save
184
+ @emp4.save
185
+ GuardEmployee.find_by_email('unique@example.com')&.emp_id
186
+ #=> @emp4_id
187
+
188
+ ## Instance-scoped badge index must be manually validated and added
189
+ @emp4.guard_unique_guard_company_badge_index!(@company)
190
+ @emp4.add_to_guard_company_badge_index(@company)
191
+ @company.badge_index.has_key?('BADGE789')
192
+ #=> true
193
+
194
+ ## Duplicate class-level index caught by save
195
+ @emp5_id = "emp_#{rand(1000000)}"
196
+ @emp5 = GuardEmployee.new(emp_id: @emp5_id, badge_number: 'BADGE999', email: 'unique@example.com')
197
+ begin
198
+ @emp5.save
199
+ false
200
+ rescue Familia::RecordExistsError => e
201
+ e.message.include?('GuardEmployee exists email=unique@example.com')
202
+ end
203
+ #=> true
204
+
205
+ ## Duplicate instance-scoped index requires manual guard
206
+ @emp6_id = "emp_#{rand(1000000)}"
207
+ @emp6 = GuardEmployee.new(emp_id: @emp6_id, badge_number: 'BADGE789', email: 'emp6@example.com')
208
+ @emp6.save # Succeeds - no auto-validation of instance-scoped indexes
209
+ begin
210
+ @emp6.guard_unique_guard_company_badge_index!(@company)
211
+ false
212
+ rescue Familia::RecordExistsError => e
213
+ e.message.include?('GuardEmployee exists in GuardCompany with badge_number=BADGE789')
214
+ end
215
+ #=> true
216
+
217
+ # =============================================
218
+ # 4. Guard Method Error Messages
219
+ # =============================================
220
+
221
+ ## Class-level guard error includes class and field
222
+ @user_dup = GuardUser.new(user_id: "user_dup_#{rand(1000000)}", email: 'test@example.com', username: 'dupuser')
223
+ begin
224
+ @user_dup.guard_unique_email_index!
225
+ rescue Familia::RecordExistsError => e
226
+ [e.message.include?('GuardUser'), e.message.include?('email=test@example.com')]
227
+ end
228
+ #=> [true, true]
229
+
230
+ ## Instance-scoped guard error includes both classes and field
231
+ begin
232
+ @emp2.guard_unique_guard_company_badge_index!(@company)
233
+ rescue Familia::RecordExistsError => e
234
+ [e.message.include?('GuardEmployee'), e.message.include?('GuardCompany'), e.message.include?('badge_number=BADGE123')]
235
+ end
236
+ #=> [true, true, true]
237
+
238
+ ## RecordExistsError is correct type
239
+ begin
240
+ @emp2.guard_unique_guard_company_badge_index!(@company)
241
+ rescue => e
242
+ e.class
243
+ end
244
+ #=> Familia::RecordExistsError
245
+
246
+ # =============================================
247
+ # 5. Transaction Context Behavior
248
+ # =============================================
249
+
250
+ ## Guard works outside transaction
251
+ @user_tx = GuardUser.new(user_id: "user_tx_#{rand(1000000)}", email: 'tx@example.com', username: 'txuser')
252
+ @user_tx.guard_unique_email_index!
253
+ #=> nil
254
+
255
+ ## Guard must be called outside transaction (new rule)
256
+ unique_timestamp = Time.now.to_i
257
+ unique_rand = rand(1000000)
258
+ email = "tx_unique_#{unique_timestamp}_#{unique_rand}@example.com"
259
+ @user_tx_unique = GuardUser.new(user_id: "user_tx_unique_#{unique_rand}", email: email, username: "txuser_#{unique_rand}")
260
+
261
+ # Guards should be called outside transactions
262
+ @user_tx_unique.send(:guard_unique_indexes!)
263
+ #=> nil
264
+
265
+ # Teardown - clean up test objects
266
+ [@user1, @user2, @user_nil, @user_empty1, @user_empty2, @user_dup, @user_tx, @user_tx_unique].each do |obj|
267
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
268
+ end
269
+
270
+ [@emp1, @emp2, @emp3, @emp_nil, @emp4, @emp5, @emp6].each do |obj|
271
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
272
+ end
273
+
274
+ [@company, @company2].each do |obj|
275
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
276
+ end
277
+
278
+ # Clean up class-level indexes
279
+ [GuardUser.email_index, GuardUser.username_index, GuardEmployee.email_index].each do |index|
280
+ index.delete! if index.respond_to?(:delete!) && index.respond_to?(:exists?) && index.exists?
281
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: familia
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre18
4
+ version: 2.0.0.pre19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -153,6 +153,10 @@ files:
153
153
  - LICENSE.txt
154
154
  - README.md
155
155
  - bin/irb
156
+ - changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst
157
+ - changelog.d/20251011_203905_delano_next.rst
158
+ - changelog.d/20251011_212633_delano_next.rst
159
+ - changelog.d/20251011_221253_delano_next.rst
156
160
  - changelog.d/README.md
157
161
  - changelog.d/scriv.ini
158
162
  - docs/archive/.gitignore
@@ -185,6 +189,7 @@ files:
185
189
  - docs/migrating/v2.0.0-pre13.md
186
190
  - docs/migrating/v2.0.0-pre14.md
187
191
  - docs/migrating/v2.0.0-pre18.md
192
+ - docs/migrating/v2.0.0-pre19.md
188
193
  - docs/migrating/v2.0.0-pre5.md
189
194
  - docs/migrating/v2.0.0-pre6.md
190
195
  - docs/migrating/v2.0.0-pre7.md
@@ -194,6 +199,7 @@ files:
194
199
  - examples/autoloader/mega_customer.rb
195
200
  - examples/autoloader/mega_customer/features/deprecated_fields.rb
196
201
  - examples/autoloader/mega_customer/safe_dump_fields.rb
202
+ - examples/datatype_standalone.rb
197
203
  - examples/encrypted_fields.rb
198
204
  - examples/json_usage_patterns.rb
199
205
  - examples/relationships.rb
@@ -203,12 +209,13 @@ files:
203
209
  - lib/familia.rb
204
210
  - lib/familia/base.rb
205
211
  - lib/familia/connection.rb
212
+ - lib/familia/connection/behavior.rb
206
213
  - lib/familia/connection/handlers.rb
207
214
  - lib/familia/connection/individual_command_proxy.rb
208
215
  - lib/familia/connection/middleware.rb
209
216
  - lib/familia/connection/operation_core.rb
210
217
  - lib/familia/connection/operations.rb
211
- - lib/familia/connection/pipeline_core.rb
218
+ - lib/familia/connection/pipelined_core.rb
212
219
  - lib/familia/connection/transaction_core.rb
213
220
  - lib/familia/data_type.rb
214
221
  - lib/familia/data_type/class_methods.rb
@@ -366,6 +373,8 @@ files:
366
373
  - try/integration/conventional_inheritance_try.rb
367
374
  - try/integration/create_method_try.rb
368
375
  - try/integration/cross_component_try.rb
376
+ - try/integration/data_types/datatype_pipelines_try.rb
377
+ - try/integration/data_types/datatype_transactions_try.rb
369
378
  - try/integration/database_consistency_try.rb
370
379
  - try/integration/familia_extended_try.rb
371
380
  - try/integration/familia_members_methods_try.rb
@@ -451,6 +460,7 @@ files:
451
460
  - try/unit/data_types/string_try.rb
452
461
  - try/unit/data_types/unsortedset_try.rb
453
462
  - try/unit/horreum/auto_indexing_on_save_try.rb
463
+ - try/unit/horreum/automatic_index_validation_try.rb
454
464
  - try/unit/horreum/base_try.rb
455
465
  - try/unit/horreum/class_methods_try.rb
456
466
  - try/unit/horreum/commands_try.rb
@@ -465,6 +475,8 @@ files:
465
475
  - try/unit/horreum/serialization_persistent_fields_try.rb
466
476
  - try/unit/horreum/serialization_try.rb
467
477
  - try/unit/horreum/settings_try.rb
478
+ - try/unit/horreum/unique_index_edge_cases_try.rb
479
+ - try/unit/horreum/unique_index_guard_validation_try.rb
468
480
  - try/unit/refinements/dear_json_array_methods_try.rb
469
481
  - try/unit/refinements/dear_json_hash_methods_try.rb
470
482
  - try/unit/refinements/time_literals_numeric_methods_try.rb