familia 2.0.0.pre21 → 2.0.0.pre23
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/.github/workflows/claude-code-review.yml +8 -5
- data/.talismanrc +5 -1
- data/CHANGELOG.rst +76 -0
- data/Gemfile.lock +8 -8
- data/docs/1106-participates_in-bidirectional-solution.md +201 -58
- data/examples/through_relationships.rb +275 -0
- data/lib/familia/connection/operation_core.rb +1 -2
- data/lib/familia/connection/pipelined_core.rb +1 -3
- data/lib/familia/connection/transaction_core.rb +1 -2
- data/lib/familia/data_type/serialization.rb +76 -51
- data/lib/familia/data_type/types/sorted_set.rb +5 -10
- data/lib/familia/data_type/types/stringkey.rb +22 -0
- data/lib/familia/features/external_identifier.rb +29 -0
- data/lib/familia/features/object_identifier.rb +47 -0
- data/lib/familia/features/relationships/README.md +1 -1
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +15 -15
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +8 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +59 -10
- data/lib/familia/features/relationships/participation/target_methods.rb +51 -7
- data/lib/familia/features/relationships/participation/through_model_operations.rb +150 -0
- data/lib/familia/features/relationships/participation.rb +39 -15
- data/lib/familia/features/relationships/participation_relationship.rb +19 -1
- data/lib/familia/features/relationships.rb +1 -1
- data/lib/familia/horreum/database_commands.rb +6 -1
- data/lib/familia/horreum/management.rb +141 -10
- data/lib/familia/horreum/persistence.rb +3 -0
- data/lib/familia/identifier_extractor.rb +1 -1
- data/lib/familia/version.rb +1 -1
- data/lib/multi_result.rb +59 -31
- data/pr_agent.toml +6 -1
- data/try/features/count_any_edge_cases_try.rb +486 -0
- data/try/features/count_any_methods_try.rb +197 -0
- data/try/features/external_identifier/external_identifier_try.rb +134 -0
- data/try/features/object_identifier/object_identifier_try.rb +138 -0
- data/try/features/relationships/indexing_rebuild_try.rb +6 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +1 -1
- data/try/features/relationships/participation_method_prefix_try.rb +133 -0
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/{participation_bidirectional_try.rb → participation_reverse_methods_try.rb} +6 -6
- data/try/features/relationships/participation_through_try.rb +173 -0
- data/try/integration/data_types/datatype_pipelines_try.rb +5 -3
- data/try/integration/data_types/datatype_transactions_try.rb +13 -7
- data/try/integration/models/customer_try.rb +3 -3
- data/try/unit/data_types/boolean_try.rb +35 -22
- data/try/unit/data_types/hash_try.rb +2 -2
- data/try/unit/data_types/serialization_try.rb +386 -0
- data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +2 -1
- metadata +9 -8
- data/changelog.d/20251105_flexible_external_identifier_format.rst +0 -66
- data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +0 -44
- data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +0 -20
- data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +0 -91
- data/changelog.d/20251107_optimized_redis_exists_checks.rst +0 -94
- data/changelog.d/20251108_frozen_string_literal_pragma.rst +0 -44
|
@@ -81,10 +81,12 @@ result = @user.scores.pipelined { |pipe| }
|
|
|
81
81
|
#=> [true, true]
|
|
82
82
|
|
|
83
83
|
## Multiple DataType operations in single pipeline
|
|
84
|
+
# Note: Raw Redis commands bypass Familia's JSON serialization.
|
|
85
|
+
# Use serialize_value for values that will be looked up via Familia methods.
|
|
84
86
|
result = @user.scores.pipelined do |pipe|
|
|
85
|
-
pipe.zadd(@user.scores.dbkey, 500, 'multi')
|
|
86
|
-
pipe.hset(@user.profile.dbkey, 'multi', 'pipeline')
|
|
87
|
-
pipe.sadd(@user.tags.dbkey, 'multi_tag')
|
|
87
|
+
pipe.zadd(@user.scores.dbkey, 500, @user.scores.serialize_value('multi'))
|
|
88
|
+
pipe.hset(@user.profile.dbkey, 'multi', @user.profile.serialize_value('pipeline'))
|
|
89
|
+
pipe.sadd(@user.tags.dbkey, @user.tags.serialize_value('multi_tag'))
|
|
88
90
|
end
|
|
89
91
|
[
|
|
90
92
|
result.is_a?(MultiResult),
|
|
@@ -152,6 +152,8 @@ conn_class
|
|
|
152
152
|
#=> "Redis::MultiConnection"
|
|
153
153
|
|
|
154
154
|
## Transaction with direct_access works correctly
|
|
155
|
+
# Note: direct_access bypasses serialize_value, so raw 'true' string
|
|
156
|
+
# gets parsed as JSON boolean true on retrieval (Issue #190 behavior)
|
|
155
157
|
result = @user.profile.transaction do |trans_conn|
|
|
156
158
|
trans_conn.hset(@user.profile.dbkey, 'status', 'active')
|
|
157
159
|
|
|
@@ -162,7 +164,7 @@ result = @user.profile.transaction do |trans_conn|
|
|
|
162
164
|
end
|
|
163
165
|
end
|
|
164
166
|
[@user.profile['status'], @user.profile['verified']]
|
|
165
|
-
#=> ["active",
|
|
167
|
+
#=> ["active", true]
|
|
166
168
|
|
|
167
169
|
## Transaction atomicity - all commands succeed or none
|
|
168
170
|
test_zset = Familia::SortedSet.new('atomic:test')
|
|
@@ -182,11 +184,13 @@ end
|
|
|
182
184
|
#=> ["initial"]
|
|
183
185
|
|
|
184
186
|
## Nested transactions with parent-owned DataTypes work
|
|
187
|
+
# Note: Raw Redis commands bypass Familia's JSON serialization.
|
|
188
|
+
# Use serialize_value or check raw members for consistency.
|
|
185
189
|
outer_result = @user.scores.transaction do |outer_conn|
|
|
186
|
-
outer_conn.zadd(@user.scores.dbkey, 999, 'outer_member')
|
|
190
|
+
outer_conn.zadd(@user.scores.dbkey, 999, @user.scores.serialize_value('outer_member'))
|
|
187
191
|
|
|
188
192
|
inner_result = @user.tags.transaction do |inner_conn|
|
|
189
|
-
inner_conn.sadd(@user.tags.dbkey, 'nested_tag')
|
|
193
|
+
inner_conn.sadd(@user.tags.dbkey, @user.tags.serialize_value('nested_tag'))
|
|
190
194
|
end
|
|
191
195
|
|
|
192
196
|
inner_result.is_a?(MultiResult)
|
|
@@ -231,12 +235,14 @@ TransactionTestUser.logical_database
|
|
|
231
235
|
#=> 2
|
|
232
236
|
|
|
233
237
|
## Multiple DataType types in single transaction
|
|
238
|
+
# Note: Raw Redis commands bypass Familia's JSON serialization.
|
|
239
|
+
# Use serialize_value for values that will be looked up via Familia methods.
|
|
234
240
|
result = @user.scores.transaction do |conn|
|
|
235
241
|
# Can operate on different DataTypes using same connection
|
|
236
|
-
conn.zadd(@user.scores.dbkey, 777, 'multi_test')
|
|
237
|
-
conn.hset(@user.profile.dbkey, 'multi', 'yes')
|
|
238
|
-
conn.sadd(@user.tags.dbkey, 'multi_tag')
|
|
239
|
-
conn.rpush(@user.activity.dbkey, 'multi_action')
|
|
242
|
+
conn.zadd(@user.scores.dbkey, 777, @user.scores.serialize_value('multi_test'))
|
|
243
|
+
conn.hset(@user.profile.dbkey, 'multi', @user.profile.serialize_value('yes'))
|
|
244
|
+
conn.sadd(@user.tags.dbkey, @user.tags.serialize_value('multi_tag'))
|
|
245
|
+
conn.rpush(@user.activity.dbkey, @user.activity.serialize_value('multi_action'))
|
|
240
246
|
end
|
|
241
247
|
[
|
|
242
248
|
result.is_a?(MultiResult),
|
|
@@ -73,7 +73,8 @@ Customer.find_by_id(ident).planid
|
|
|
73
73
|
#=> true
|
|
74
74
|
|
|
75
75
|
## Customer can be added to class-level sorted set
|
|
76
|
-
|
|
76
|
+
# Note: Add the object directly so identifier extraction is consistent
|
|
77
|
+
Customer.instances.add(@customer)
|
|
77
78
|
Customer.instances.member?(@customer)
|
|
78
79
|
#=> true
|
|
79
80
|
|
|
@@ -97,11 +98,10 @@ multi_result = @customer.destroy!
|
|
|
97
98
|
cust = Customer.find_by_id('test@example.com')
|
|
98
99
|
exists = Customer.exists?('test@example.com')
|
|
99
100
|
[multi_result.results, cust.nil?, exists]
|
|
100
|
-
#=> [[1, 0, 1, 1, 1, 1, 1], true, false]
|
|
101
|
+
#=> [[1, 0, 1, 1, 1, 1, 1, true], true, false]
|
|
101
102
|
|
|
102
103
|
## Customer.destroy! can be called on an already destroyed object
|
|
103
104
|
@customer.destroy!
|
|
104
|
-
#=:> MultiResult
|
|
105
105
|
#==> result.successful?
|
|
106
106
|
#=*> result.results
|
|
107
107
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
5
|
# try/data_types/boolean_try.rb
|
|
6
|
+
# Issue #190: Updated to reflect JSON serialization with type preservation
|
|
6
7
|
|
|
7
8
|
require_relative '../../support/helpers/test_helpers'
|
|
8
9
|
|
|
@@ -10,37 +11,49 @@ Familia.debug = false
|
|
|
10
11
|
|
|
11
12
|
@hashkey = Familia::HashKey.new 'key'
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## String 'true' is stored and returned as string
|
|
14
15
|
@hashkey['test'] = 'true'
|
|
15
16
|
#=> "true"
|
|
16
17
|
|
|
17
|
-
##
|
|
18
|
+
## String values are returned as strings
|
|
18
19
|
@hashkey['test']
|
|
19
20
|
#=> "true"
|
|
20
21
|
|
|
21
|
-
##
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
rescue Familia::NotDistinguishableError => e
|
|
25
|
-
e.message
|
|
26
|
-
end
|
|
27
|
-
#=> "Cannot represent true<TrueClass> as a string"
|
|
22
|
+
## Boolean true is now stored with type preservation (Issue #190)
|
|
23
|
+
@hashkey['bool_true'] = true
|
|
24
|
+
#=> true
|
|
28
25
|
|
|
29
|
-
## Boolean
|
|
30
|
-
@hashkey['
|
|
31
|
-
#=>
|
|
26
|
+
## Boolean true is returned as TrueClass
|
|
27
|
+
@hashkey['bool_true']
|
|
28
|
+
#=> true
|
|
32
29
|
|
|
33
|
-
##
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
rescue Familia::NotDistinguishableError => e
|
|
37
|
-
e.message
|
|
38
|
-
end
|
|
39
|
-
#=> "Cannot represent <NilClass> as a string"
|
|
30
|
+
## Boolean true has correct class
|
|
31
|
+
@hashkey['bool_true'].class
|
|
32
|
+
#=> TrueClass
|
|
40
33
|
|
|
41
|
-
##
|
|
42
|
-
@hashkey['
|
|
43
|
-
#=>
|
|
34
|
+
## Boolean false is stored with type preservation
|
|
35
|
+
@hashkey['bool_false'] = false
|
|
36
|
+
#=> false
|
|
37
|
+
|
|
38
|
+
## Boolean false is returned as FalseClass
|
|
39
|
+
@hashkey['bool_false']
|
|
40
|
+
#=> false
|
|
41
|
+
|
|
42
|
+
## Boolean false has correct class
|
|
43
|
+
@hashkey['bool_false'].class
|
|
44
|
+
#=> FalseClass
|
|
45
|
+
|
|
46
|
+
## nil is stored with type preservation
|
|
47
|
+
@hashkey['nil_value'] = nil
|
|
48
|
+
#=> nil
|
|
49
|
+
|
|
50
|
+
## nil is returned as nil
|
|
51
|
+
@hashkey['nil_value']
|
|
52
|
+
#=> nil
|
|
53
|
+
|
|
54
|
+
## nil has correct class
|
|
55
|
+
@hashkey['nil_value'].class
|
|
56
|
+
#=> NilClass
|
|
44
57
|
|
|
45
58
|
## Clear the hash key
|
|
46
59
|
@hashkey.delete!
|
|
@@ -50,8 +50,8 @@ require_relative '../../support/helpers/test_helpers'
|
|
|
50
50
|
@a.props.decrement 'counter', 60
|
|
51
51
|
#=> 40
|
|
52
52
|
|
|
53
|
-
## Familia::HashKey#values_at
|
|
53
|
+
## Familia::HashKey#values_at (counter is integer from HINCRBY, others are strings)
|
|
54
54
|
@a.props.values_at 'fieldA', 'counter', 'fieldC'
|
|
55
|
-
#=> ['1',
|
|
55
|
+
#=> ['1', 40, '3']
|
|
56
56
|
|
|
57
57
|
@a.props.delete!
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# try/unit/data_types/serialization_try.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Test coverage for DataType serialization/deserialization behavior
|
|
6
|
+
# Issue #190: Unify DataType and Horreum serialization for type preservation
|
|
7
|
+
|
|
8
|
+
require_relative '../../support/helpers/test_helpers'
|
|
9
|
+
|
|
10
|
+
Familia.debug = false
|
|
11
|
+
|
|
12
|
+
# Create test instances
|
|
13
|
+
@bone = Bone.new('serialize_test_token')
|
|
14
|
+
|
|
15
|
+
# ========================================
|
|
16
|
+
# DataType Serialization Behavior (Issue #190)
|
|
17
|
+
# Now uses JSON serialization for type preservation
|
|
18
|
+
# ========================================
|
|
19
|
+
|
|
20
|
+
## HashKey stores string values correctly
|
|
21
|
+
@bone.props['string_field'] = 'hello'
|
|
22
|
+
@bone.props['string_field']
|
|
23
|
+
#=> 'hello'
|
|
24
|
+
|
|
25
|
+
## HashKey stores integer with type preservation
|
|
26
|
+
@bone.props['int_field'] = 42
|
|
27
|
+
@bone.props['int_field']
|
|
28
|
+
#=> 42
|
|
29
|
+
|
|
30
|
+
## HashKey stores float with type preservation
|
|
31
|
+
@bone.props['float_field'] = 3.14
|
|
32
|
+
@bone.props['float_field']
|
|
33
|
+
#=> 3.14
|
|
34
|
+
|
|
35
|
+
## HashKey stores symbol as string (symbols serialize to strings in JSON)
|
|
36
|
+
@bone.props['symbol_field'] = :active
|
|
37
|
+
@bone.props['symbol_field']
|
|
38
|
+
#=> 'active'
|
|
39
|
+
|
|
40
|
+
## HashKey stores boolean true with type preservation
|
|
41
|
+
@bone.props['bool_true'] = true
|
|
42
|
+
@bone.props['bool_true']
|
|
43
|
+
#=> true
|
|
44
|
+
|
|
45
|
+
## HashKey stores boolean true as TrueClass
|
|
46
|
+
@bone.props['bool_true'].class
|
|
47
|
+
#=> TrueClass
|
|
48
|
+
|
|
49
|
+
## HashKey stores boolean false with type preservation
|
|
50
|
+
@bone.props['bool_false'] = false
|
|
51
|
+
@bone.props['bool_false']
|
|
52
|
+
#=> false
|
|
53
|
+
|
|
54
|
+
## HashKey stores boolean false as FalseClass
|
|
55
|
+
@bone.props['bool_false'].class
|
|
56
|
+
#=> FalseClass
|
|
57
|
+
|
|
58
|
+
## HashKey stores nil with type preservation
|
|
59
|
+
@bone.props['nil_field'] = nil
|
|
60
|
+
@bone.props['nil_field']
|
|
61
|
+
#=> nil
|
|
62
|
+
|
|
63
|
+
## HashKey stores hash with type preservation
|
|
64
|
+
@bone.props['hash_field'] = { 'key' => 'value' }
|
|
65
|
+
@bone.props['hash_field']
|
|
66
|
+
#=> {'key'=>'value'}
|
|
67
|
+
|
|
68
|
+
## HashKey stores array with type preservation
|
|
69
|
+
@bone.props['array_field'] = [1, 2, 3]
|
|
70
|
+
@bone.props['array_field']
|
|
71
|
+
#=> [1, 2, 3]
|
|
72
|
+
|
|
73
|
+
# ========================================
|
|
74
|
+
# List Serialization Behavior
|
|
75
|
+
# ========================================
|
|
76
|
+
|
|
77
|
+
## List stores string values correctly
|
|
78
|
+
@bone.owners.delete!
|
|
79
|
+
@bone.owners.push('owner1')
|
|
80
|
+
@bone.owners.first
|
|
81
|
+
#=> 'owner1'
|
|
82
|
+
|
|
83
|
+
## List stores integer with type preservation
|
|
84
|
+
@bone.owners.delete!
|
|
85
|
+
@bone.owners.push(123)
|
|
86
|
+
@bone.owners.first
|
|
87
|
+
#=> 123
|
|
88
|
+
|
|
89
|
+
## List stores boolean with type preservation
|
|
90
|
+
@bone.owners.delete!
|
|
91
|
+
@bone.owners.push(true)
|
|
92
|
+
@bone.owners.first
|
|
93
|
+
#=> true
|
|
94
|
+
|
|
95
|
+
## List stores nil with type preservation
|
|
96
|
+
@bone.owners.delete!
|
|
97
|
+
@bone.owners.push(nil)
|
|
98
|
+
@bone.owners.first
|
|
99
|
+
#=> nil
|
|
100
|
+
|
|
101
|
+
# ========================================
|
|
102
|
+
# Set Serialization Behavior
|
|
103
|
+
# ========================================
|
|
104
|
+
|
|
105
|
+
## Set stores string values correctly
|
|
106
|
+
@bone.tags.delete!
|
|
107
|
+
@bone.tags.add('tag1')
|
|
108
|
+
@bone.tags.members.include?('tag1')
|
|
109
|
+
#=> true
|
|
110
|
+
|
|
111
|
+
## Set stores integer with type preservation
|
|
112
|
+
@bone.tags.delete!
|
|
113
|
+
@bone.tags.add(42)
|
|
114
|
+
@bone.tags.members.include?(42)
|
|
115
|
+
#=> true
|
|
116
|
+
|
|
117
|
+
## Set stores boolean with type preservation
|
|
118
|
+
@bone.tags.delete!
|
|
119
|
+
@bone.tags.add(true)
|
|
120
|
+
@bone.tags.members.include?(true)
|
|
121
|
+
#=> true
|
|
122
|
+
|
|
123
|
+
# ========================================
|
|
124
|
+
# SortedSet Serialization Behavior
|
|
125
|
+
# ========================================
|
|
126
|
+
|
|
127
|
+
## SortedSet stores string values correctly
|
|
128
|
+
@bone.metrics.delete!
|
|
129
|
+
@bone.metrics.add('metric1', 1.0)
|
|
130
|
+
@bone.metrics.members.include?('metric1')
|
|
131
|
+
#=> true
|
|
132
|
+
|
|
133
|
+
## SortedSet stores integer member with type preservation
|
|
134
|
+
@bone.metrics.delete!
|
|
135
|
+
@bone.metrics.add(999, 1.0)
|
|
136
|
+
@bone.metrics.members.include?(999)
|
|
137
|
+
#=> true
|
|
138
|
+
|
|
139
|
+
## SortedSet stores boolean member with type preservation
|
|
140
|
+
@bone.metrics.delete!
|
|
141
|
+
@bone.metrics.add(true, 1.0)
|
|
142
|
+
@bone.metrics.members.include?(true)
|
|
143
|
+
#=> true
|
|
144
|
+
|
|
145
|
+
# ========================================
|
|
146
|
+
# Horreum Field Serialization (for comparison)
|
|
147
|
+
# Uses JSON encoding - type preserved
|
|
148
|
+
# ========================================
|
|
149
|
+
|
|
150
|
+
## Horreum field stores string with JSON encoding
|
|
151
|
+
@customer = Customer.new
|
|
152
|
+
@customer.custid = 'serialization_test'
|
|
153
|
+
@customer.role = 'admin'
|
|
154
|
+
@customer.save
|
|
155
|
+
loaded = Customer.find_by_id('serialization_test')
|
|
156
|
+
loaded.role
|
|
157
|
+
#=> 'admin'
|
|
158
|
+
|
|
159
|
+
## Horreum field stores boolean true (JSON encoded)
|
|
160
|
+
@customer.verified = true
|
|
161
|
+
@customer.save
|
|
162
|
+
@loaded_customer = Customer.find_by_id('serialization_test')
|
|
163
|
+
@loaded_customer.verified
|
|
164
|
+
#=> true
|
|
165
|
+
|
|
166
|
+
## Horreum verified field is actually boolean, not string
|
|
167
|
+
@loaded_customer.verified.class
|
|
168
|
+
#=> TrueClass
|
|
169
|
+
|
|
170
|
+
## Horreum field stores boolean false (JSON encoded)
|
|
171
|
+
@customer.reset_requested = false
|
|
172
|
+
@customer.save
|
|
173
|
+
@loaded_customer2 = Customer.find_by_id('serialization_test')
|
|
174
|
+
@loaded_customer2.reset_requested
|
|
175
|
+
#=> false
|
|
176
|
+
|
|
177
|
+
## Horreum reset_requested field is actually boolean, not string
|
|
178
|
+
@loaded_customer2.reset_requested.class
|
|
179
|
+
#=> FalseClass
|
|
180
|
+
|
|
181
|
+
# ========================================
|
|
182
|
+
# Type Round-Trip Comparison (Unified Behavior)
|
|
183
|
+
# ========================================
|
|
184
|
+
|
|
185
|
+
## Integer round-trip in HashKey now preserves type (Issue #190)
|
|
186
|
+
@bone.props['roundtrip_int'] = 100
|
|
187
|
+
retrieved = @bone.props['roundtrip_int']
|
|
188
|
+
retrieved.class
|
|
189
|
+
#=> Integer
|
|
190
|
+
|
|
191
|
+
## Boolean round-trip in HashKey preserves type
|
|
192
|
+
@bone.props['roundtrip_bool'] = true
|
|
193
|
+
@bone.props['roundtrip_bool'].class
|
|
194
|
+
#=> TrueClass
|
|
195
|
+
|
|
196
|
+
## DataType and Horreum now use same JSON serialization
|
|
197
|
+
@session = Session.new
|
|
198
|
+
@session.sessid = 'roundtrip_test'
|
|
199
|
+
# Both DataType and Horreum fields now preserve types consistently
|
|
200
|
+
|
|
201
|
+
# ========================================
|
|
202
|
+
# Horreum serialize_value Comprehensive Tests
|
|
203
|
+
# (Issue #190: Document behavior for unification)
|
|
204
|
+
# ========================================
|
|
205
|
+
|
|
206
|
+
## Horreum serialize_value: string gets JSON encoded with quotes
|
|
207
|
+
@customer.serialize_value('hello')
|
|
208
|
+
#=> '"hello"'
|
|
209
|
+
|
|
210
|
+
## Horreum serialize_value: empty string gets JSON encoded
|
|
211
|
+
@customer.serialize_value('')
|
|
212
|
+
#=> '""'
|
|
213
|
+
|
|
214
|
+
## Horreum serialize_value: integer becomes JSON number (no quotes)
|
|
215
|
+
@customer.serialize_value(42)
|
|
216
|
+
#=> '42'
|
|
217
|
+
|
|
218
|
+
## Horreum serialize_value: zero becomes JSON number
|
|
219
|
+
@customer.serialize_value(0)
|
|
220
|
+
#=> '0'
|
|
221
|
+
|
|
222
|
+
## Horreum serialize_value: negative integer
|
|
223
|
+
@customer.serialize_value(-99)
|
|
224
|
+
#=> '-99'
|
|
225
|
+
|
|
226
|
+
## Horreum serialize_value: float becomes JSON number
|
|
227
|
+
@customer.serialize_value(3.14159)
|
|
228
|
+
#=> '3.14159'
|
|
229
|
+
|
|
230
|
+
## Horreum serialize_value: boolean true becomes JSON true
|
|
231
|
+
@customer.serialize_value(true)
|
|
232
|
+
#=> 'true'
|
|
233
|
+
|
|
234
|
+
## Horreum serialize_value: boolean false becomes JSON false
|
|
235
|
+
@customer.serialize_value(false)
|
|
236
|
+
#=> 'false'
|
|
237
|
+
|
|
238
|
+
## Horreum serialize_value: nil becomes JSON null
|
|
239
|
+
@customer.serialize_value(nil)
|
|
240
|
+
#=> 'null'
|
|
241
|
+
|
|
242
|
+
## Horreum serialize_value: symbol becomes JSON string
|
|
243
|
+
@customer.serialize_value(:active)
|
|
244
|
+
#=> '"active"'
|
|
245
|
+
|
|
246
|
+
## Horreum serialize_value: hash becomes JSON object
|
|
247
|
+
@customer.serialize_value({ name: 'test', count: 5 })
|
|
248
|
+
#=> '{"name":"test","count":5}'
|
|
249
|
+
|
|
250
|
+
## Horreum serialize_value: array becomes JSON array
|
|
251
|
+
@customer.serialize_value([1, 'two', true, nil])
|
|
252
|
+
#=> '[1,"two",true,null]'
|
|
253
|
+
|
|
254
|
+
## Horreum serialize_value: nested structures work
|
|
255
|
+
@customer.serialize_value({ users: [{ id: 1 }, { id: 2 }] })
|
|
256
|
+
#=> '{"users":[{"id":1},{"id":2}]}'
|
|
257
|
+
|
|
258
|
+
# ========================================
|
|
259
|
+
# Horreum deserialize_value Comprehensive Tests
|
|
260
|
+
# ========================================
|
|
261
|
+
|
|
262
|
+
## Horreum deserialize_value: JSON string becomes Ruby string
|
|
263
|
+
@customer.deserialize_value('"hello"')
|
|
264
|
+
#=> 'hello'
|
|
265
|
+
|
|
266
|
+
## Horreum deserialize_value: JSON number becomes Ruby integer
|
|
267
|
+
@customer.deserialize_value('42')
|
|
268
|
+
#=> 42
|
|
269
|
+
|
|
270
|
+
## Horreum deserialize_value: JSON number is actually Integer class
|
|
271
|
+
@customer.deserialize_value('42').class
|
|
272
|
+
#=> Integer
|
|
273
|
+
|
|
274
|
+
## Horreum deserialize_value: JSON float becomes Ruby float
|
|
275
|
+
@customer.deserialize_value('3.14159')
|
|
276
|
+
#=> 3.14159
|
|
277
|
+
|
|
278
|
+
## Horreum deserialize_value: JSON float is actually Float class
|
|
279
|
+
@customer.deserialize_value('3.14159').class
|
|
280
|
+
#=> Float
|
|
281
|
+
|
|
282
|
+
## Horreum deserialize_value: JSON true becomes Ruby true
|
|
283
|
+
@customer.deserialize_value('true')
|
|
284
|
+
#=> true
|
|
285
|
+
|
|
286
|
+
## Horreum deserialize_value: JSON true is TrueClass
|
|
287
|
+
@customer.deserialize_value('true').class
|
|
288
|
+
#=> TrueClass
|
|
289
|
+
|
|
290
|
+
## Horreum deserialize_value: JSON false becomes Ruby false
|
|
291
|
+
@customer.deserialize_value('false')
|
|
292
|
+
#=> false
|
|
293
|
+
|
|
294
|
+
## Horreum deserialize_value: JSON false is FalseClass
|
|
295
|
+
@customer.deserialize_value('false').class
|
|
296
|
+
#=> FalseClass
|
|
297
|
+
|
|
298
|
+
## Horreum deserialize_value: JSON null becomes Ruby nil
|
|
299
|
+
@customer.deserialize_value('null')
|
|
300
|
+
#=> nil
|
|
301
|
+
|
|
302
|
+
## Horreum deserialize_value: JSON object becomes Ruby hash
|
|
303
|
+
@customer.deserialize_value('{"name":"test","count":5}')
|
|
304
|
+
#=> {"name"=>"test", "count"=>5}
|
|
305
|
+
|
|
306
|
+
## Horreum deserialize_value: JSON array becomes Ruby array
|
|
307
|
+
@customer.deserialize_value('[1,"two",true,null]')
|
|
308
|
+
#=> [1, "two", true, nil]
|
|
309
|
+
|
|
310
|
+
## Horreum deserialize_value: nil input returns nil
|
|
311
|
+
@customer.deserialize_value(nil)
|
|
312
|
+
#=> nil
|
|
313
|
+
|
|
314
|
+
## Horreum deserialize_value: empty string returns nil
|
|
315
|
+
@customer.deserialize_value('')
|
|
316
|
+
#=> nil
|
|
317
|
+
|
|
318
|
+
## Horreum deserialize_value: plain unquoted string (legacy data) returns as-is
|
|
319
|
+
# This handles data stored before JSON encoding was used
|
|
320
|
+
@customer.deserialize_value('plain string without quotes')
|
|
321
|
+
#=> 'plain string without quotes'
|
|
322
|
+
|
|
323
|
+
# ========================================
|
|
324
|
+
# Familia Object Serialization (shared behavior)
|
|
325
|
+
# ========================================
|
|
326
|
+
|
|
327
|
+
## Horreum serialize_value: string value gets JSON encoded
|
|
328
|
+
# When storing a value from another Familia object's field
|
|
329
|
+
@ref_customer = Customer.new('reference_test@example.com')
|
|
330
|
+
@ref_customer.custid = 'reference_test@example.com'
|
|
331
|
+
# Note: Horreum.serialize_value uses JsonSerializer.dump, which JSON-encodes
|
|
332
|
+
# all values, including strings. This is different from DataType#serialize_value,
|
|
333
|
+
# which has special handling for Familia objects.
|
|
334
|
+
@customer.serialize_value(@ref_customer.custid)
|
|
335
|
+
#=> '"reference_test@example.com"'
|
|
336
|
+
|
|
337
|
+
# ========================================
|
|
338
|
+
# Round-trip Type Preservation Tests
|
|
339
|
+
# ========================================
|
|
340
|
+
|
|
341
|
+
## Round-trip: integer preserves type through Horreum serialization
|
|
342
|
+
serialized = @customer.serialize_value(42)
|
|
343
|
+
@customer.deserialize_value(serialized)
|
|
344
|
+
#=> 42
|
|
345
|
+
|
|
346
|
+
## Round-trip: integer class preserved
|
|
347
|
+
serialized = @customer.serialize_value(42)
|
|
348
|
+
@customer.deserialize_value(serialized).class
|
|
349
|
+
#=> Integer
|
|
350
|
+
|
|
351
|
+
## Round-trip: boolean true preserves type
|
|
352
|
+
serialized = @customer.serialize_value(true)
|
|
353
|
+
@customer.deserialize_value(serialized)
|
|
354
|
+
#=> true
|
|
355
|
+
|
|
356
|
+
## Round-trip: boolean true class preserved
|
|
357
|
+
serialized = @customer.serialize_value(true)
|
|
358
|
+
@customer.deserialize_value(serialized).class
|
|
359
|
+
#=> TrueClass
|
|
360
|
+
|
|
361
|
+
## Round-trip: boolean false preserves type
|
|
362
|
+
serialized = @customer.serialize_value(false)
|
|
363
|
+
@customer.deserialize_value(serialized)
|
|
364
|
+
#=> false
|
|
365
|
+
|
|
366
|
+
## Round-trip: nil preserves type
|
|
367
|
+
serialized = @customer.serialize_value(nil)
|
|
368
|
+
@customer.deserialize_value(serialized)
|
|
369
|
+
#=> nil
|
|
370
|
+
|
|
371
|
+
## Round-trip: hash preserves structure
|
|
372
|
+
serialized = @customer.serialize_value({ active: true, count: 10 })
|
|
373
|
+
@customer.deserialize_value(serialized)
|
|
374
|
+
#=> {"active"=>true, "count"=>10}
|
|
375
|
+
|
|
376
|
+
## Round-trip: array preserves structure and types
|
|
377
|
+
serialized = @customer.serialize_value([1, 'two', true, nil, 3.5])
|
|
378
|
+
@customer.deserialize_value(serialized)
|
|
379
|
+
#=> [1, "two", true, nil, 3.5]
|
|
380
|
+
|
|
381
|
+
# Cleanup
|
|
382
|
+
@bone.props.delete!
|
|
383
|
+
@bone.owners.delete!
|
|
384
|
+
@bone.tags.delete!
|
|
385
|
+
@bone.metrics.delete!
|
|
386
|
+
@customer.destroy! rescue nil
|
|
@@ -259,8 +259,9 @@ destroy_result = model.destroy!
|
|
|
259
259
|
|
|
260
260
|
# Should result in success and also complete in a reasonable amount of
|
|
261
261
|
# time (under 100ms for this test). I acknowledge this is flaky.
|
|
262
|
+
# Note: 42 results = 40 related field DELs + 1 main object DEL + 1 instances ZREM
|
|
262
263
|
[destroy_result.class, destroy_result.successful?, destroy_result.results.size]
|
|
263
|
-
#=> [MultiResult, true,
|
|
264
|
+
#=> [MultiResult, true, 42]
|
|
264
265
|
#=%> 100
|
|
265
266
|
|
|
266
267
|
## Verify transaction_fallback_integration_try.rb bug is fixed
|
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.pre23
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Delano Mandelbaum
|
|
@@ -168,12 +168,6 @@ files:
|
|
|
168
168
|
- bin/irb
|
|
169
169
|
- bin/try
|
|
170
170
|
- bin/tryouts
|
|
171
|
-
- changelog.d/20251105_flexible_external_identifier_format.rst
|
|
172
|
-
- changelog.d/20251107_112554_delano_179_participation_asymmetry.rst
|
|
173
|
-
- changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst
|
|
174
|
-
- changelog.d/20251107_fix_participates_in_symbol_resolution.rst
|
|
175
|
-
- changelog.d/20251107_optimized_redis_exists_checks.rst
|
|
176
|
-
- changelog.d/20251108_frozen_string_literal_pragma.rst
|
|
177
171
|
- changelog.d/README.md
|
|
178
172
|
- changelog.d/scriv.ini
|
|
179
173
|
- docs/1106-participates_in-bidirectional-solution.md
|
|
@@ -224,6 +218,7 @@ files:
|
|
|
224
218
|
- examples/safe_dump.rb
|
|
225
219
|
- examples/sampling_demo.rb
|
|
226
220
|
- examples/single_connection_transaction_confusions.rb
|
|
221
|
+
- examples/through_relationships.rb
|
|
227
222
|
- familia.gemspec
|
|
228
223
|
- lib/familia.rb
|
|
229
224
|
- lib/familia/base.rb
|
|
@@ -281,6 +276,7 @@ files:
|
|
|
281
276
|
- lib/familia/features/relationships/participation/participant_methods.rb
|
|
282
277
|
- lib/familia/features/relationships/participation/rebuild_strategies.md
|
|
283
278
|
- lib/familia/features/relationships/participation/target_methods.rb
|
|
279
|
+
- lib/familia/features/relationships/participation/through_model_operations.rb
|
|
284
280
|
- lib/familia/features/relationships/participation_membership.rb
|
|
285
281
|
- lib/familia/features/relationships/participation_relationship.rb
|
|
286
282
|
- lib/familia/features/relationships/score_encoding.rb
|
|
@@ -328,6 +324,8 @@ files:
|
|
|
328
324
|
- try/edge_cases/reserved_keywords_try.rb
|
|
329
325
|
- try/edge_cases/string_coercion_try.rb
|
|
330
326
|
- try/edge_cases/ttl_side_effects_try.rb
|
|
327
|
+
- try/features/count_any_edge_cases_try.rb
|
|
328
|
+
- try/features/count_any_methods_try.rb
|
|
331
329
|
- try/features/encrypted_fields/aad_protection_try.rb
|
|
332
330
|
- try/features/encrypted_fields/concealed_string_core_try.rb
|
|
333
331
|
- try/features/encrypted_fields/context_isolation_try.rb
|
|
@@ -365,12 +363,14 @@ files:
|
|
|
365
363
|
- try/features/relationships/indexing_commands_verification_try.rb
|
|
366
364
|
- try/features/relationships/indexing_rebuild_try.rb
|
|
367
365
|
- try/features/relationships/indexing_try.rb
|
|
368
|
-
- try/features/relationships/participation_bidirectional_try.rb
|
|
369
366
|
- try/features/relationships/participation_commands_verification_spec.rb
|
|
370
367
|
- try/features/relationships/participation_commands_verification_try.rb
|
|
368
|
+
- try/features/relationships/participation_method_prefix_try.rb
|
|
371
369
|
- try/features/relationships/participation_performance_improvements_try.rb
|
|
372
370
|
- try/features/relationships/participation_reverse_index_try.rb
|
|
371
|
+
- try/features/relationships/participation_reverse_methods_try.rb
|
|
373
372
|
- try/features/relationships/participation_target_class_resolution_try.rb
|
|
373
|
+
- try/features/relationships/participation_through_try.rb
|
|
374
374
|
- try/features/relationships/participation_unresolved_target_try.rb
|
|
375
375
|
- try/features/relationships/relationships_api_changes_try.rb
|
|
376
376
|
- try/features/relationships/relationships_edge_cases_try.rb
|
|
@@ -505,6 +505,7 @@ files:
|
|
|
505
505
|
- try/unit/data_types/hash_try.rb
|
|
506
506
|
- try/unit/data_types/list_try.rb
|
|
507
507
|
- try/unit/data_types/lock_try.rb
|
|
508
|
+
- try/unit/data_types/serialization_try.rb
|
|
508
509
|
- try/unit/data_types/sorted_set_try.rb
|
|
509
510
|
- try/unit/data_types/sorted_set_zadd_options_try.rb
|
|
510
511
|
- try/unit/data_types/string_try.rb
|