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
@@ -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.
|
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/
|
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
|