familia 2.1.1 → 2.2.0
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 +48 -0
- data/Gemfile.lock +1 -1
- data/docs/guides/feature-object-identifiers.md +4 -0
- data/docs/guides/feature-relationships-indexing.md +4 -0
- data/docs/guides/feature-relationships.md +4 -0
- data/docs/guides/field-system.md +37 -0
- data/lib/familia/data_type/serialization.rb +22 -5
- data/lib/familia/data_type/types/sorted_set.rb +1 -1
- data/lib/familia/data_type/types/unsorted_set.rb +2 -2
- data/lib/familia/data_type.rb +1 -1
- data/lib/familia/horreum/persistence.rb +1 -1
- data/lib/familia/horreum.rb +1 -1
- data/lib/familia/version.rb +1 -1
- data/try/integration/models/customer_try.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5608f472e264e8cbf6228e398d4a25517173e55ad0d7d10a2b74de31121b18f
|
|
4
|
+
data.tar.gz: 665996a35ca541c9884453c4c664d197b865bdba888e424cdd8a5a555df96a37
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22c1c274df0683053f768b418e7bc239fbc31fce4decf405af2e87c0b60f6f0c365c7dd88ba7a897746fffd785bdf15cfba71549ea8f32a0cdbf054eeb2467fc
|
|
7
|
+
data.tar.gz: b8bbe2ec2153c5de43b98bf9e03e80e94d494d056bbb0b65711f20b76fb77b99f5847be4b5420ccce5497ef18fb29dd38fd7f8329b67ce4928dff6d6b43546d4
|
data/CHANGELOG.rst
CHANGED
|
@@ -7,6 +7,54 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`
|
|
|
7
7
|
|
|
8
8
|
<!--scriv-insert-here-->
|
|
9
9
|
|
|
10
|
+
.. _changelog-2.2.0:
|
|
11
|
+
|
|
12
|
+
2.2.0 — 2026-02-23
|
|
13
|
+
==================
|
|
14
|
+
|
|
15
|
+
Added
|
|
16
|
+
-----
|
|
17
|
+
|
|
18
|
+
- Introduced ``reference: true`` option for DataType collection declarations.
|
|
19
|
+
Collections with this option store member identifiers raw instead of
|
|
20
|
+
JSON-encoding them, resolving the semantic mismatch between field storage
|
|
21
|
+
(type-preserving JSON) and collection member storage (identity references).
|
|
22
|
+
|
|
23
|
+
Fixed
|
|
24
|
+
-----
|
|
25
|
+
|
|
26
|
+
- Fixed serialization mismatch in ``instances`` sorted set where
|
|
27
|
+
``persist_to_storage`` passed a string identifier (JSON-encoded as
|
|
28
|
+
``"\"abc-123\""``), while direct calls passed Familia objects (stored raw as
|
|
29
|
+
``abc-123``). Now passes ``self`` to ``instances.add`` and declares
|
|
30
|
+
``reference: true`` on the collection, ensuring consistent storage.
|
|
31
|
+
(`#215 <https://github.com/delano/familia/issues/215>`_)
|
|
32
|
+
|
|
33
|
+
- Fixed ``UnsortedSet#pop`` returning raw Redis strings instead of deserialized
|
|
34
|
+
values.
|
|
35
|
+
|
|
36
|
+
- Fixed ``UnsortedSet#move`` passing raw values to Redis instead of serializing
|
|
37
|
+
them.
|
|
38
|
+
|
|
39
|
+
- Fixed ``SortedSet#increment`` truncating scores to integer (``.to_i``) instead
|
|
40
|
+
of preserving float precision (``.to_f``).
|
|
41
|
+
|
|
42
|
+
Documentation
|
|
43
|
+
-------------
|
|
44
|
+
|
|
45
|
+
- Added collection member serialization guide to ``docs/guides/field-system.md``
|
|
46
|
+
explaining the distinction between field serialization (JSON for type
|
|
47
|
+
preservation) and collection member serialization (raw identifiers for
|
|
48
|
+
reference collections).
|
|
49
|
+
|
|
50
|
+
AI Assistance
|
|
51
|
+
-------------
|
|
52
|
+
|
|
53
|
+
- Claude assisted with systematic audit of all ``.add()`` call sites and
|
|
54
|
+
collection declarations across the codebase, identifying the root cause of the
|
|
55
|
+
serialization mismatch and the three additional DataType method bugs discovered
|
|
56
|
+
during the audit.
|
|
57
|
+
|
|
10
58
|
.. _changelog-2.1.1:
|
|
11
59
|
|
|
12
60
|
2.1.1 — 2026-02-02
|
data/Gemfile.lock
CHANGED
|
@@ -193,6 +193,10 @@ custom.objid_generator_used # => nil (unknown provenance)
|
|
|
193
193
|
- Debugging and auditing benefit from generator tracking
|
|
194
194
|
- Format validation can be performed based on expected generator
|
|
195
195
|
|
|
196
|
+
## Identifiers in Collections
|
|
197
|
+
|
|
198
|
+
When object identifiers are stored in DataType collections (sorted sets, sets, lists), they are stored as **raw strings** — not JSON-encoded. This is critical for consistent membership checks and lookups. Collections that hold object references must be declared with `class:` and `reference: true` options so that `serialize_value` treats both Familia objects and plain identifier strings identically. See [Collection Member Serialization](field-system.md#collection-member-serialization) for the full explanation of why this distinction exists.
|
|
199
|
+
|
|
196
200
|
## Lookup Management
|
|
197
201
|
|
|
198
202
|
### Automatic Mapping
|
|
@@ -366,6 +366,10 @@ company.badge_index.to_h.each do |badge, emp_id|
|
|
|
366
366
|
end
|
|
367
367
|
```
|
|
368
368
|
|
|
369
|
+
## Index Storage Format
|
|
370
|
+
|
|
371
|
+
Index values (the object identifiers stored in hash keys and sets) are raw strings, not JSON-encoded. This is a deliberate design choice shared across all Familia collections that store object references — it ensures that lookups, membership checks, and key construction all operate on the same byte representation. See [Collection Member Serialization](field-system.md#collection-member-serialization) for the underlying serialization rules.
|
|
372
|
+
|
|
369
373
|
## Redis Key Patterns
|
|
370
374
|
|
|
371
375
|
| Type | Pattern | Example |
|
|
@@ -167,6 +167,10 @@ team.members.to_a # Just IDs
|
|
|
167
167
|
team.member_instances # Load objects
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
## Serialization of Collection Members
|
|
171
|
+
|
|
172
|
+
Relationship collections (participation sorted sets, index hash keys, instance-scoped sets) store object identifiers as raw strings. When adding objects to these collections, `serialize_value` extracts the `.identifier` from Familia objects and stores it without JSON encoding. This ensures consistent membership checks regardless of whether code passes an object reference or a string identifier. See [Collection Member Serialization](field-system.md#collection-member-serialization) for the authoritative explanation of why reference collections use raw identifiers while value fields use JSON.
|
|
173
|
+
|
|
170
174
|
## Best Practices
|
|
171
175
|
|
|
172
176
|
1. **Use bulk methods** for multiple additions: `add_domains([d1, d2, d3])`
|
data/docs/guides/field-system.md
CHANGED
|
@@ -146,6 +146,43 @@ user.preferences["theme"] = "dark"
|
|
|
146
146
|
user.login_count.increment
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
+
## Collection Member Serialization
|
|
150
|
+
|
|
151
|
+
DataType collections (lists, sets, sorted sets, hash keys) and hashkey fields use different serialization strategies based on what they store.
|
|
152
|
+
|
|
153
|
+
### Fields: JSON Serialization for Type Preservation
|
|
154
|
+
|
|
155
|
+
Hashkey fields store arbitrary Ruby values — integers, booleans, hashes, nil. All values are JSON-encoded so types survive the Redis round-trip. An Integer `35` stores as `"35"` and loads back as Integer `35`, not String `"35"`.
|
|
156
|
+
|
|
157
|
+
### Collections: Raw Identifiers for Object References
|
|
158
|
+
|
|
159
|
+
When a collection's members represent references to Familia objects, those members must be stored as **raw identifier strings** — not JSON-encoded. Identifiers are lookup keys: they're matched against, compared with, and used to construct Redis keys (e.g. `customer:abc-def-123:object`). JSON-encoding an identifier produces a different byte sequence (`"\"abc-def-123\""` vs `abc-def-123`), which causes silent duplicates and broken membership checks.
|
|
160
|
+
|
|
161
|
+
The `class:` and `reference: true` options on a collection declaration tell `serialize_value` that members are object references, not arbitrary values:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
class Customer < Familia::Horreum
|
|
165
|
+
# Members are object references — stored as raw identifiers
|
|
166
|
+
class_sorted_set :instances, class: self, reference: true
|
|
167
|
+
|
|
168
|
+
# Members are arbitrary values — stored as JSON
|
|
169
|
+
list :activity_log
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
With these options set, `serialize_value` normalizes both code paths:
|
|
174
|
+
- Passing a Familia object extracts `.identifier` and stores it raw
|
|
175
|
+
- Passing a String identifier for a Familia class stores it raw (same result)
|
|
176
|
+
- Passing any other value JSON-encodes it for type preservation
|
|
177
|
+
|
|
178
|
+
Without `class:` metadata, a collection has no way to distinguish "this string is an identifier" from "this string is an arbitrary value" — and the two paths silently diverge.
|
|
179
|
+
|
|
180
|
+
### The `instances` Sorted Set
|
|
181
|
+
|
|
182
|
+
Every Horreum subclass automatically gets a `class_sorted_set :instances` — a class-level registry of persisted objects. Members are raw identifier strings; scores are timestamps of when each object was last saved. This is the index used to enumerate all known instances of a class, check persistence, or clean up stale entries.
|
|
183
|
+
|
|
184
|
+
Because `instances` stores object references, it is declared with `class:` and `reference: true` to ensure consistent serialization regardless of whether callers pass an object or a string identifier.
|
|
185
|
+
|
|
149
186
|
## Advanced Field Types
|
|
150
187
|
|
|
151
188
|
### Creating Custom Field Types
|
|
@@ -38,6 +38,15 @@ module Familia
|
|
|
38
38
|
return prepared
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# Priority 1b: If this collection stores object references (reference: true)
|
|
42
|
+
# and the value is a String, treat it as a raw identifier. This prevents
|
|
43
|
+
# mismatches when callers pass identifier strings directly instead of
|
|
44
|
+
# Familia objects.
|
|
45
|
+
if val.is_a?(String) && opts[:reference]
|
|
46
|
+
Familia.debug " String identifier (reference): #{val}"
|
|
47
|
+
return val
|
|
48
|
+
end
|
|
49
|
+
|
|
41
50
|
# Priority 2: Everything else gets JSON serialized for type preservation
|
|
42
51
|
# This unifies behavior with Horreum fields (Issue #190)
|
|
43
52
|
prepared = Familia::JsonSerializer.dump(val)
|
|
@@ -80,6 +89,11 @@ module Familia
|
|
|
80
89
|
Familia.debug "deserialize_values: (#{@opts}) #{values}"
|
|
81
90
|
return [] if values.empty?
|
|
82
91
|
|
|
92
|
+
# Reference collections store raw identifiers — return as-is
|
|
93
|
+
if @opts[:reference]
|
|
94
|
+
return values.flatten
|
|
95
|
+
end
|
|
96
|
+
|
|
83
97
|
# If a class option is specified, use class-based deserialization
|
|
84
98
|
if @opts[:class]
|
|
85
99
|
unless @opts[:class].respond_to?(:from_json)
|
|
@@ -94,9 +108,9 @@ module Familia
|
|
|
94
108
|
|
|
95
109
|
val
|
|
96
110
|
rescue StandardError => e
|
|
97
|
-
Familia.
|
|
98
|
-
Familia.
|
|
99
|
-
Familia.
|
|
111
|
+
Familia.debug "[deserialize] from_json error in #{dbkey}: #{e.message}"
|
|
112
|
+
Familia.debug " raw value: #{obj.inspect[0..80]}"
|
|
113
|
+
Familia.trace :DESERIALIZE_ERROR, dbkey, e.message if Familia.debug?
|
|
100
114
|
nil
|
|
101
115
|
end
|
|
102
116
|
|
|
@@ -110,7 +124,7 @@ module Familia
|
|
|
110
124
|
begin
|
|
111
125
|
Familia::JsonSerializer.parse(obj)
|
|
112
126
|
rescue Familia::SerializerError
|
|
113
|
-
|
|
127
|
+
Familia.debug "[deserialize] Raw fallback in #{dbkey}: #{obj.inspect[0..80]}"
|
|
114
128
|
obj
|
|
115
129
|
end
|
|
116
130
|
end
|
|
@@ -138,6 +152,9 @@ module Familia
|
|
|
138
152
|
|
|
139
153
|
return @opts[:default] if val.nil?
|
|
140
154
|
|
|
155
|
+
# Reference collections store raw identifiers — return as-is
|
|
156
|
+
return val if @opts[:reference]
|
|
157
|
+
|
|
141
158
|
# If a class option is specified, use the existing class-based deserialization
|
|
142
159
|
if @opts[:class]
|
|
143
160
|
ret = deserialize_values val
|
|
@@ -149,7 +166,7 @@ module Familia
|
|
|
149
166
|
begin
|
|
150
167
|
Familia::JsonSerializer.parse(val)
|
|
151
168
|
rescue Familia::SerializerError
|
|
152
|
-
|
|
169
|
+
Familia.debug "[deserialize] Raw fallback in #{dbkey}: #{val.inspect[0..80]}"
|
|
153
170
|
val
|
|
154
171
|
end
|
|
155
172
|
end
|
|
@@ -92,11 +92,11 @@ module Familia
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
def pop
|
|
95
|
-
dbclient.spop
|
|
95
|
+
deserialize_value(dbclient.spop(dbkey))
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
def move(dstkey, val)
|
|
99
|
-
dbclient.smove dbkey, dstkey, val
|
|
99
|
+
dbclient.smove dbkey, dstkey, serialize_value(val)
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
# Get one or more random members from the set
|
data/lib/familia/data_type.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Familia
|
|
|
25
25
|
using Familia::Refinements::TimeLiterals
|
|
26
26
|
|
|
27
27
|
@registered_types = {}
|
|
28
|
-
@valid_options = %i[class parent default_expiration default logical_database dbkey dbclient suffix prefix].freeze
|
|
28
|
+
@valid_options = %i[class parent default_expiration default logical_database dbkey dbclient suffix prefix reference].freeze
|
|
29
29
|
@logical_database = nil
|
|
30
30
|
|
|
31
31
|
feature :expiration
|
|
@@ -643,7 +643,7 @@ module Familia
|
|
|
643
643
|
auto_update_class_indexes
|
|
644
644
|
|
|
645
645
|
# 4. Add to instances collection if available
|
|
646
|
-
self.class.instances.add(
|
|
646
|
+
self.class.instances.add(self, Familia.now) if self.class.respond_to?(:instances)
|
|
647
647
|
|
|
648
648
|
hmset_result
|
|
649
649
|
end
|
data/lib/familia/horreum.rb
CHANGED
|
@@ -169,7 +169,7 @@ module Familia
|
|
|
169
169
|
Familia.members << member
|
|
170
170
|
|
|
171
171
|
# Set up automatic instance tracking using built-in class_sorted_set
|
|
172
|
-
member.class_sorted_set :instances
|
|
172
|
+
member.class_sorted_set :instances, class: member, reference: true
|
|
173
173
|
|
|
174
174
|
super
|
|
175
175
|
end
|
data/lib/familia/version.rb
CHANGED
|
@@ -98,7 +98,7 @@ multi_result = @customer.destroy!
|
|
|
98
98
|
cust = Customer.find_by_id('test@example.com')
|
|
99
99
|
exists = Customer.exists?('test@example.com')
|
|
100
100
|
[multi_result.results, cust.nil?, exists]
|
|
101
|
-
#=> [[1, 0, 1, 1, 1, 1, 1,
|
|
101
|
+
#=> [[1, 0, 1, 1, 1, 1, 1, false], true, false]
|
|
102
102
|
|
|
103
103
|
## Customer.destroy! can be called on an already destroyed object
|
|
104
104
|
@customer.destroy!
|