familia 2.9.1 → 2.10.1
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 +1 -1
- data/.gitignore +1 -1
- data/AGENTS.md +198 -0
- data/CHANGELOG.rst +218 -0
- data/Gemfile.lock +45 -42
- data/README.md +2 -2
- data/changelog.d/20260605_220911_anthropic_sleepy-allen.rst +35 -0
- data/docs/guides/datatype-collections.md +31 -3
- data/docs/guides/feature-migrations.md +45 -0
- data/docs/guides/feature-relationships-indexing.md +10 -2
- data/docs/guides/feature-relationships-methods.md +41 -0
- data/docs/guides/feature-relationships-participation.md +43 -2
- data/docs/guides/feature-relationships.md +162 -1
- data/docs/guides/getting-started.md +87 -0
- data/docs/guides/index.md +4 -0
- data/docs/migrating/v2.10.md +294 -0
- data/examples/encrypted_fields.rb +43 -29
- data/examples/relationships.rb +66 -36
- data/examples/safe_dump.rb +7 -5
- data/familia.gemspec +0 -1
- data/lib/familia/connection/operations.rb +194 -0
- data/lib/familia/connection/transaction_core.rb +51 -0
- data/lib/familia/data_type/collection_base.rb +26 -9
- data/lib/familia/data_type/serialization.rb +33 -3
- data/lib/familia/data_type.rb +164 -8
- data/lib/familia/encryption/encrypted_data.rb +27 -2
- data/lib/familia/encryption/manager.rb +17 -2
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -1
- data/lib/familia/encryption/request_cache.rb +4 -28
- data/lib/familia/encryption.rb +1 -0
- data/lib/familia/features/encrypted_fields/concealed_string.rb +12 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +50 -67
- data/lib/familia/features/encrypted_fields.rb +14 -0
- data/lib/familia/features/relationships/collection_operations.rb +23 -2
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +8 -9
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +23 -6
- data/lib/familia/features/relationships/indexing.rb +6 -2
- data/lib/familia/features/relationships/participation/target_methods.rb +23 -5
- data/lib/familia/features/relationships/participation.rb +6 -1
- data/lib/familia/horreum/atomic_write.rb +97 -22
- data/lib/familia/horreum/definition.rb +51 -0
- data/lib/familia/horreum/dirty_tracking.rb +28 -0
- data/lib/familia/horreum/management.rb +117 -3
- data/lib/familia/horreum/persistence.rb +71 -42
- data/lib/familia/horreum/related_fields.rb +2 -0
- data/lib/familia/horreum.rb +1 -0
- data/lib/familia/index_descriptor.rb +258 -0
- data/lib/familia/instrumentation.rb +22 -0
- data/lib/familia/logging.rb +24 -3
- data/lib/familia/migration/base.rb +1 -1
- data/lib/familia/migration/errors.rb +2 -0
- data/lib/familia/migration/model.rb +1 -1
- data/lib/familia/migration/pipeline.rb +1 -1
- data/lib/familia/migration/rake_tasks.rb +11 -17
- data/lib/familia/migration/registry.rb +4 -0
- data/lib/familia/migration/runner.rb +2 -0
- data/lib/familia/migration/script.rb +2 -0
- data/lib/familia/migration.rb +2 -0
- data/lib/familia/settings.rb +79 -1
- data/lib/familia/utils.rb +15 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -0
- data/lib/middleware/database_logger.rb +208 -128
- data/try/audit/audit_cross_references_try.rb +6 -6
- data/try/audit/audit_unique_indexes_try.rb +3 -2
- data/try/audit/repair_all_integration_try.rb +2 -1
- data/try/audit/repair_indexes_try.rb +2 -1
- data/try/features/atomic_write_watch_try.rb +254 -0
- data/try/features/build_block_try.rb +191 -0
- data/try/features/create_block_try.rb +58 -0
- data/try/features/cross_model_atomic_write_try.rb +402 -0
- data/try/features/dirty_write_new_object_try.rb +181 -0
- data/try/features/dirty_write_warnings_try.rb +456 -0
- data/try/features/encrypted_fields/aad_transient_fix_try.rb +164 -0
- data/try/features/encrypted_fields/aad_transient_proof_try.rb +253 -0
- data/try/features/encrypted_fields/concealed_string_core_try.rb +6 -4
- data/try/features/encrypted_fields/encrypted_data_try.rb +151 -0
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -0
- data/try/features/encrypted_fields/envelope_version_branching_try.rb +106 -0
- data/try/features/encrypted_fields/envelope_version_try.rb +171 -0
- data/try/features/encrypted_fields/key_material_try.rb +205 -0
- data/try/features/encryption/request_cache_try.rb +88 -0
- data/try/features/relationships/index_introspection_try.rb +304 -0
- data/try/features/relationships/multi_index_each_record_try.rb +211 -0
- data/try/features/relationships/participation_each_record_try.rb +247 -0
- data/try/features/relationships/participation_reverse_methods_try.rb +7 -4
- data/try/features/relationships/unique_index_each_record_try.rb +143 -0
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/integration/examples/encrypted_fields_example_try.rb +67 -0
- data/try/integration/examples/relationships_example_try.rb +69 -0
- data/try/integration/examples/safe_dump_example_try.rb +60 -0
- data/try/investigation/cross_model_atomic_poc_try.rb +130 -0
- data/try/unit/core/trace_caching_try.rb +58 -0
- data/try/unit/data_types/each_record_try.rb +1 -1
- data/try/unit/data_types/stringkey_extended_try.rb +1 -1
- data/try/unit/horreum/relations_try.rb +5 -0
- data/try/unit/middleware/database_logger_capture_toggle_try.rb +278 -0
- metadata +30 -3
- data/CLAUDE.md +0 -322
- data/docs/guides/writing-migrations.md +0 -345
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7903e14486c85385ad0b682c009b1f7d182e0cff24b107aa0264692529bb8dfd
|
|
4
|
+
data.tar.gz: 445219dfcd2df1cf2054b07d90e33902fc21532279730bc274629a2082a02f7f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 635d35b86d7c6a85332517e3b238b8b932d4823b0cfad2f58b7c349a530de44ae02b91930c9ddac7ed538c619cea3a6f8355eeaba20ebf52358ebd45f4547067
|
|
7
|
+
data.tar.gz: d96538d6fbb486fcd92bc329acd52cba07008e40db5df059f338e060cb6dbb4789c8fe0e8e52ee69891662b1b2353eec3832032aa3ccda50848a986d06ea6a4d
|
|
@@ -43,7 +43,7 @@ jobs:
|
|
|
43
43
|
- Security concerns
|
|
44
44
|
- Test coverage
|
|
45
45
|
|
|
46
|
-
Use the repository's
|
|
46
|
+
Use the repository's AGENTS.md for guidance on style and conventions. Be constructive and helpful in your feedback.
|
|
47
47
|
|
|
48
48
|
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
|
|
49
49
|
|
data/.gitignore
CHANGED
data/AGENTS.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for AI coding agents working in this repository.
|
|
4
|
+
|
|
5
|
+
## Development Commands
|
|
6
|
+
|
|
7
|
+
- **Install**: `bundle install`
|
|
8
|
+
- **Docs**: `bundle exec yard`
|
|
9
|
+
- **Lint**: `bundle exec rubocop`
|
|
10
|
+
- **Test**: `bundle exec try` (auto-discovers `*_try.rb` / `*.try.rb`)
|
|
11
|
+
|
|
12
|
+
### Testing (Tryouts v3)
|
|
13
|
+
|
|
14
|
+
Each file has optional setup, testcases, and optional teardown. A testcase is a
|
|
15
|
+
`##` description line, Ruby code, then one or more expectation comments
|
|
16
|
+
(`#=>`, `#==>`, `#=:>`, `#=!>`, ...). The last expression is the result.
|
|
17
|
+
Instance variables (`@var`) persist across sections; locals do not. Write plain
|
|
18
|
+
realistic code; avoid mocks and test DSL.
|
|
19
|
+
|
|
20
|
+
Run with `--agent` for token-efficient output (`--agent-focus summary|first-failure|critical`).
|
|
21
|
+
See `bundle exec try --help` for the full CLI, framework integration (`--rspec`,
|
|
22
|
+
`--minitest`), and debugging flags.
|
|
23
|
+
|
|
24
|
+
### Changelog
|
|
25
|
+
|
|
26
|
+
Add a changelog fragment (RST) with each user-facing change. See @changelog.d/README.md
|
|
27
|
+
|
|
28
|
+
### Known Issues & Quirks
|
|
29
|
+
|
|
30
|
+
- **Reserved field names**: `ttl`, `db`, `valkey`, `redis` cannot be field names — use prefixed alternatives.
|
|
31
|
+
- **Empty identifiers**: Cause a stack overflow in key generation — validate before operations.
|
|
32
|
+
- **Lazy initialization**: Connection chains and field collections initialize lazily without synchronization (generally safe under the GIL, not guaranteed).
|
|
33
|
+
|
|
34
|
+
### Debugging
|
|
35
|
+
|
|
36
|
+
Ask the user for real-time database command monitoring (commands with timestamps
|
|
37
|
+
and database numbers, live) when debugging multi/exec, pipelining, or
|
|
38
|
+
`logical_database` issues.
|
|
39
|
+
|
|
40
|
+
## Architecture
|
|
41
|
+
|
|
42
|
+
**Familia** is a Valkey-compatible ORM providing Ruby object storage with
|
|
43
|
+
expiration, safe dumping, and quantization.
|
|
44
|
+
|
|
45
|
+
### Core Classes
|
|
46
|
+
|
|
47
|
+
- **`Familia::Horreum`** (`lib/familia/horreum.rb`) — base class for Valkey-backed objects (ActiveRecord-like). Field definitions, data type relationships, lifecycle.
|
|
48
|
+
- **`Familia::DataType`** (`lib/familia/data_type.rb`) — base for type wrappers (String, JsonStringKey, List, UnsortedSet, SortedSet, HashKey). Each type in `lib/familia/data_type/types/`.
|
|
49
|
+
- **`Familia::Base`** (`lib/familia/base.rb`) — shared module for both, hosts the feature system.
|
|
50
|
+
|
|
51
|
+
Features (Expiration, SafeDump, Relationships, ...) are modules mixed into
|
|
52
|
+
classes via `Familia::Base`. See `lib/familia/features/`.
|
|
53
|
+
|
|
54
|
+
### Defining a Model
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
class User < Familia::Horreum
|
|
58
|
+
field :email # scalar field
|
|
59
|
+
list :sessions # Valkey/Redis list
|
|
60
|
+
set :tags # set
|
|
61
|
+
zset :metrics # sorted set
|
|
62
|
+
hashkey :settings # hash
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Identifier strategies:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
identifier_field :email # symbol
|
|
70
|
+
identifier ->(user) { "user:#{user.email}" } # proc
|
|
71
|
+
identifier [:type, :email] # array
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Connection handling lives in `lib/familia/connection.rb` and `lib/familia/settings.rb`;
|
|
75
|
+
select databases with the `logical_database` class method (URI configuration supported).
|
|
76
|
+
|
|
77
|
+
### Initialization: do not override `initialize` without `super`
|
|
78
|
+
|
|
79
|
+
Familia's `initialize` sets fields from kwargs, then sets up DataType objects,
|
|
80
|
+
then calls your `init` hook. Overriding `initialize` without `super` breaks
|
|
81
|
+
related-field setup.
|
|
82
|
+
|
|
83
|
+
Apply defaults in the `init` hook with `||=` (never `=`, which would overwrite
|
|
84
|
+
values Horreum already set from kwargs):
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
class User < Familia::Horreum
|
|
88
|
+
field :objid
|
|
89
|
+
field :email
|
|
90
|
+
|
|
91
|
+
def init
|
|
92
|
+
@objid ||= SecureRandom.uuid
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
User.new(email: 'test@example.com').objid # => generated UUID
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Only override `initialize` (with `super`) when you must transform arguments
|
|
100
|
+
before Horreum processes them.
|
|
101
|
+
|
|
102
|
+
## Serialization
|
|
103
|
+
|
|
104
|
+
Horreum fields are JSON-encoded for storage and JSON-decoded on load, preserving
|
|
105
|
+
Ruby types (Integer, Boolean, String, Float, Hash, Array, nil). `false` and `0`
|
|
106
|
+
are preserved; only `nil` values are omitted from storage.
|
|
107
|
+
|
|
108
|
+
| Context | Serialize | Ruby `"UK"` stored as | Ruby `123` stored as |
|
|
109
|
+
|---|---|---|---|
|
|
110
|
+
| Horreum `field` | `serialize_value` (JSON) | `"\"UK\""` | `"123"` |
|
|
111
|
+
| `StringKey` | `.to_s` (raw) | `"UK"` | `"123"` |
|
|
112
|
+
| `JsonStringKey` | JSON dump | `"\"UK\""` | `"123"` |
|
|
113
|
+
| List/Set/SortedSet/HashKey values | `serialize_value` (JSON) | `"\"UK\""` | `"123"` |
|
|
114
|
+
|
|
115
|
+
`StringKey` uses raw `.to_s` (not JSON) to support `INCR`/`DECR`/`APPEND`; a
|
|
116
|
+
Horreum string field stores `"UK"` as `"\"UK\""` while a `StringKey` stores it as
|
|
117
|
+
`"UK"`. Use `instance.debug_fields` to compare Ruby values vs stored JSON.
|
|
118
|
+
|
|
119
|
+
Database keys are generated as `classname:identifier:fieldname` (aka dbkey).
|
|
120
|
+
DataType instances are frozen after instantiation.
|
|
121
|
+
|
|
122
|
+
## Write Model: Deferred vs Immediate
|
|
123
|
+
|
|
124
|
+
**Scalar fields** (`field`) use deferred writes: normal setters
|
|
125
|
+
(`user.name = "Alice"`) only touch memory until `save`/`commit_fields`/`batch_update`.
|
|
126
|
+
Fast writers (`user.name! "Alice"`) do an immediate `HSET`.
|
|
127
|
+
|
|
128
|
+
**Collection fields** (`list`, `set`, `zset`, `hashkey`) use immediate writes:
|
|
129
|
+
every mutator (`add`, `push`, `remove`, `clear`, `[]=`) hits Redis right away.
|
|
130
|
+
Collections live on separate keys from the object hash.
|
|
131
|
+
|
|
132
|
+
**Safe pattern — scalars first, then collections:**
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
# Option A: explicit save, then mutate collections directly
|
|
136
|
+
plan.name = "Premium"
|
|
137
|
+
plan.save # HMSET for scalar fields
|
|
138
|
+
plan.features.clear
|
|
139
|
+
plan.features.add("sso")
|
|
140
|
+
|
|
141
|
+
# Option B: convenience wrapper (calls save internally, then yields the block)
|
|
142
|
+
plan.name = "Premium"
|
|
143
|
+
plan.save_with_collections do
|
|
144
|
+
plan.features.clear
|
|
145
|
+
plan.features.add("sso")
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Mutating collections before `save` is unsafe: if `save` raises, the collections
|
|
150
|
+
are already mutated.
|
|
151
|
+
|
|
152
|
+
**Atomic pattern — scalars and collections in one MULTI/EXEC:**
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
plan.atomic_write do
|
|
156
|
+
plan.name = "Premium" # deferred: queued as HMSET
|
|
157
|
+
plan.features.clear # immediate: queued as DEL in the open MULTI
|
|
158
|
+
plan.features.add("sso")
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`atomic_write` composes the `transaction` infrastructure so every command lands
|
|
163
|
+
in one MULTI/EXEC; collection mutations auto-route into the open transaction via
|
|
164
|
+
`Fiber[:familia_transaction]`. Constraints:
|
|
165
|
+
|
|
166
|
+
- All related DataTypes must share the parent's `logical_database`, else `Familia::CrossDatabaseError` (fall back to `save_with_collections`). MULTI/EXEC is single-database only.
|
|
167
|
+
- Cannot nest inside another `transaction`/`atomic_write` (`Familia::OperationModeError`).
|
|
168
|
+
- Collection return values inside the block are `Redis::Future` — do not inspect before EXEC.
|
|
169
|
+
|
|
170
|
+
**Factory — `build` for create-and-populate:**
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
user = User.build(email: "alice@example.com") do |u|
|
|
174
|
+
u.name = "Alice" # deferred scalar
|
|
175
|
+
u.tags.add("admin") # folded into the same MULTI
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`build` is class-level sugar over `new` + `atomic_write` with create-only
|
|
180
|
+
semantics: raises `RecordExistsError` if the identifier exists, same
|
|
181
|
+
single-database constraint. Without a block it degenerates to `new(...).save`.
|
|
182
|
+
For upsert, use `save`/`save_with_collections`.
|
|
183
|
+
|
|
184
|
+
## Instances Timeline
|
|
185
|
+
|
|
186
|
+
Every Horreum subclass has a class-level `instances` sorted set — a timeline of
|
|
187
|
+
last-write timestamps (ZADD score), not a registry.
|
|
188
|
+
|
|
189
|
+
- **Touch** (`touch_instances!`): `save`/`save_if_not_exists!` (via `persist_to_storage`), `commit_fields`, `batch_update`, `save_fields`, fast writers.
|
|
190
|
+
- **Remove**: instance `destroy!` (`remove_from_instances!`), class `destroy!(id)`, lazy `cleanup_stale_instance_entry` in `find_by_dbkey`.
|
|
191
|
+
- **Ghosts**: a hash key expiring via TTL leaves a stale identifier in `instances`. `find_by_dbkey` prunes on access; raw enumeration (`instances.members`) still sees ghosts.
|
|
192
|
+
- **`in_instances?(id)`** — fast O(log N), may report ghosts or miss non-Familia objects. **`exists?(id)`** — authoritative hash-key check (round-trip). `load`/`find_by_id` read the hash key directly and bypass `instances`.
|
|
193
|
+
|
|
194
|
+
## Thread Safety
|
|
195
|
+
|
|
196
|
+
DataType instances are frozen (immutable). Configure module-level settings once
|
|
197
|
+
at startup, before threads spawn. `Familia.start_monitoring!` tracks contention.
|
|
198
|
+
Tests and contention patterns live in `try/thread_safety/`.
|
data/CHANGELOG.rst
CHANGED
|
@@ -7,6 +7,224 @@ 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.10.1:
|
|
11
|
+
|
|
12
|
+
2.10.1 — 2026-06-06
|
|
13
|
+
===================
|
|
14
|
+
|
|
15
|
+
Added
|
|
16
|
+
-----
|
|
17
|
+
|
|
18
|
+
- ``record_class:`` option for collection DataTypes (``list``/``set``/
|
|
19
|
+
``sorted_set``/``hashkey``). This loading-only hint tells ``each_record`` which
|
|
20
|
+
class to hydrate via ``load_multi`` without changing how the collection
|
|
21
|
+
serializes or deserializes reads. Use this when you want ``each_record`` lookup
|
|
22
|
+
behavior but no changes to read behavior. Issue #297
|
|
23
|
+
|
|
24
|
+
- ``Familia.atomic_write(*instances)`` persists multiple Horreum instances in a
|
|
25
|
+
single ``MULTI/EXEC``. Includes an optional ``watch_keys:``/``pre_check:``
|
|
26
|
+
variant for race-safe, write-once semantics. All participating instances must
|
|
27
|
+
resolve to the same logical database (raising ``Familia::CrossDatabaseError``
|
|
28
|
+
otherwise) and must share a hash slot on Redis Cluster. #296
|
|
29
|
+
|
|
30
|
+
Changed
|
|
31
|
+
-------
|
|
32
|
+
|
|
33
|
+
- ``participates_in`` / ``class_participates_in`` collections now default to
|
|
34
|
+
using ``record_class:``. This change requires **no data migration and causes no
|
|
35
|
+
behavior changes**: existing collections already stored raw identifiers, and
|
|
36
|
+
read operations (``members``, ``to_a``, ``member?``, ``score``) behave exactly
|
|
37
|
+
as before. The only difference is that ``each_record`` is now supported. Pre-
|
|
38
|
+
declared collections are left untouched. Issue #297
|
|
39
|
+
|
|
40
|
+
Fixed
|
|
41
|
+
-----
|
|
42
|
+
|
|
43
|
+
- Enabled ``each_record`` on ``participates_in`` and ``class_participates_in``
|
|
44
|
+
collections by automatically declaring them with ``record_class: <participant
|
|
45
|
+
class>``. This resolves ``Familia::Problem`` exceptions and loads participant
|
|
46
|
+
records via ``load_multi`` across all collection types. Issue #297
|
|
47
|
+
|
|
48
|
+
- Suppressed per-member ``[deserialize] Raw fallback`` warning storm when
|
|
49
|
+
iterating ``record_class`` collections with non-JSON identifiers (such as UUIDs
|
|
50
|
+
or prefixed IDs). These expected raw values are now logged at the debug level
|
|
51
|
+
instead of warnings. Issue #297
|
|
52
|
+
|
|
53
|
+
- Resolved a connection-pooling bug where the ``WATCH``-based optimistic lock
|
|
54
|
+
in ``atomic_write(watch_keys:)``, ``save_if_not_exists!``, and ``create!`` was
|
|
55
|
+
silent/inert. The ``WATCH`` and ``MULTI/EXEC`` commands are now driven through
|
|
56
|
+
the same connection, ensuring concurrent modifications correctly abort and raise
|
|
57
|
+
as
|
|
58
|
+
documented. #296
|
|
59
|
+
|
|
60
|
+
AI Assistance
|
|
61
|
+
-------------
|
|
62
|
+
|
|
63
|
+
- AI diagnosed the participation iteration bug and identified that ``reference: true``
|
|
64
|
+
introduced unintended read-behavior changes. Designed and implemented the
|
|
65
|
+
``record_class:`` option to decouple ``each_record`` lookup from read deserialization,
|
|
66
|
+
suppressed a resulting per-member deserialize warning storm, kept intentional
|
|
67
|
+
raw-string semantics on ``instances`` and ``unique_index``, updated stale
|
|
68
|
+
flowcharts in ``datatype-collections.md``, and added regression coverage. Issue #297
|
|
69
|
+
|
|
70
|
+
- Root-caused and fixed a split-connection defect with Claude Code: implemented a
|
|
71
|
+
single-connection ``execute_watched_transaction`` primitive (avoiding fiber-pinning
|
|
72
|
+
that degrades atomic commands) and added real concurrent-modification tests to
|
|
73
|
+
replace simulated aborts. #296
|
|
74
|
+
|
|
75
|
+
- Designed and built multi-model atomic writes on top of the new ``WATCH`` primitive:
|
|
76
|
+
implemented the same-database guard, orchestration logic, and a test suite covering
|
|
77
|
+
two-model commits, rollback on error, cross-database rejection, and race conditions. #296
|
|
78
|
+
|
|
79
|
+
.. _changelog-2.10.0:
|
|
80
|
+
|
|
81
|
+
2.10.0 — 2026-06-04
|
|
82
|
+
====================
|
|
83
|
+
|
|
84
|
+
Added
|
|
85
|
+
-----
|
|
86
|
+
|
|
87
|
+
- ``Horreum.build``: A factory block that yields a new instance, then commits
|
|
88
|
+
all scalar and collection changes in a single ``MULTI/EXEC`` upon exit.
|
|
89
|
+
This avoids sequencing ``save`` before collection writes. Raises
|
|
90
|
+
``Familia::RecordExistsError`` if the identifier exists (create-only).
|
|
91
|
+
Without a block, it behaves as ``new(...).save``. #279
|
|
92
|
+
|
|
93
|
+
- ``atomic_write`` now supports ``watch_keys:`` (keys to watch) and
|
|
94
|
+
``pre_check:`` (a callable run between ``WATCH`` and ``MULTI``) to enable
|
|
95
|
+
optimistic locking. Retries with exponential backoff on abort. #288
|
|
96
|
+
|
|
97
|
+
- ``encrypted_field`` now accepts a ``key_material:`` proc. This mixes
|
|
98
|
+
additional entropy into key derivation (separate from AAD), requiring
|
|
99
|
+
the correct material at decryption to avoid producing garbage output. PR #280
|
|
100
|
+
|
|
101
|
+
- Encrypted-field envelopes now store their own ``envelope_version`` and
|
|
102
|
+
``aad_fields`` list. Decryption rebuilds AAD from these stored fields
|
|
103
|
+
rather than the active class declaration, preventing breakage when model
|
|
104
|
+
definitions change. PR #280
|
|
105
|
+
|
|
106
|
+
- ``DatabaseLogger.capture_enabled`` (Boolean, default ``true``) controls
|
|
107
|
+
in-memory buffer capturing. Disabling it bypasses clock checks, message
|
|
108
|
+
allocations, and buffer appends, offering a zero-overhead production path. Issue #233
|
|
109
|
+
|
|
110
|
+
- ``Familia::Instrumentation.hooks?(type)`` reports whether hooks are
|
|
111
|
+
registered for a given event type (e.g. ``:command``, ``:pipeline``). Issue #233
|
|
112
|
+
|
|
113
|
+
- ``Familia.reset_trace!`` clears the cached trace environment lookup. Issue #233
|
|
114
|
+
|
|
115
|
+
- ``dirty_write_warnings`` class method configures write-order warnings per
|
|
116
|
+
class (inheritable). Accepts ``:strict``, ``:warn``, ``:once``, or ``:off``. Issue #277
|
|
117
|
+
|
|
118
|
+
- ``Familia.dirty_write_warnings`` global setting providing the default mode for
|
|
119
|
+
classes that do not set their own. Issue #277
|
|
120
|
+
|
|
121
|
+
- ``Familia.raise_on_unsaved_parent_write`` (default ``true``) controls whether a
|
|
122
|
+
collection write on a new, unsaved, dirty parent raises or warns. Issue #278
|
|
123
|
+
|
|
124
|
+
Changed
|
|
125
|
+
-------
|
|
126
|
+
|
|
127
|
+
- Mutating a collection on a *new, unsaved* parent Horreum now **raises**
|
|
128
|
+
``Familia::Problem`` by default. The guard fires *before* the command runs,
|
|
129
|
+
preventing orphaned data. Save the parent first, or set
|
|
130
|
+
``Familia.raise_on_unsaved_parent_write = false`` to restore warnings. Issue #278
|
|
131
|
+
|
|
132
|
+
- Dirty-write warnings are now **deduplicated per dirty window** (mode ``:once``).
|
|
133
|
+
Writing to a collection on a parent with unsaved scalar fields warns once per
|
|
134
|
+
distinct set of unsaved fields instead of on every write. Set
|
|
135
|
+
``dirty_write_warnings :warn`` to restore the old behavior. Issue #277
|
|
136
|
+
|
|
137
|
+
- Dirty-write warnings and strict raises now append the hint:
|
|
138
|
+
``(call #save first or wrap in atomic_write)``. Issue #277
|
|
139
|
+
|
|
140
|
+
- ``trace_enabled?`` now caches the ``FAMILIA_TRACE`` lookup. Use
|
|
141
|
+
``Familia.reset_trace!`` to force a re-read of the environment. Issue #233
|
|
142
|
+
|
|
143
|
+
- ``unique_index`` hashkeys now store identifiers as raw strings rather than
|
|
144
|
+
JSON-encoded strings. Rebuild existing unique indexes to convert legacy entries,
|
|
145
|
+
e.g., via ``User.rebuild_email_lookup`` or ``company.rebuild_badge_index``. Issue #276
|
|
146
|
+
|
|
147
|
+
Fixed
|
|
148
|
+
-----
|
|
149
|
+
|
|
150
|
+
- ``Horreum.build`` with a block no longer has a TOCTOU race between the
|
|
151
|
+
``exists?`` check and the ``atomic_write`` commit. The block path now uses
|
|
152
|
+
``atomic_write(watch_keys:, pre_check:)`` so the existence check runs between
|
|
153
|
+
``WATCH`` and ``MULTI``. #288
|
|
154
|
+
|
|
155
|
+
- ``aad_fields`` containing a ``transient_field`` now bind to the field's real
|
|
156
|
+
value. Previously ``build_aad`` called ``RedactedString#to_s``, which returns
|
|
157
|
+
``"[REDACTED]"`` for every value -- so all passphrases produced identical AAD
|
|
158
|
+
and the binding was defeated. PR #280
|
|
159
|
+
|
|
160
|
+
- ``each_record`` now works on ``unique_index`` hashkeys. Previously it raised
|
|
161
|
+
``Familia::Problem`` because ``unique_index`` created its backing hashkey
|
|
162
|
+
without the ``class:`` option. Issue #276
|
|
163
|
+
|
|
164
|
+
- ``each_record`` extracts the stored identifier (the hash *value*) from a
|
|
165
|
+
HashKey instead of the indexed field (the hash *key*). Issue #276
|
|
166
|
+
|
|
167
|
+
- The unguarded ``Familia.trace`` sites in ``Horreum#destroy!`` and
|
|
168
|
+
``find_by_dbkey`` now carry an inline ``if Familia.debug?`` guard. Issue #233
|
|
169
|
+
|
|
170
|
+
- Two latent encryption bugs surfaced while repairing the examples (issue #250):
|
|
171
|
+
|
|
172
|
+
- ``Familia::Encryption.with_request_cache`` and ``clear_request_cache!``
|
|
173
|
+
were unreachable. The implementation lived in
|
|
174
|
+
``lib/familia/encryption/request_cache.rb``, which was never ``require``\ d.
|
|
175
|
+
The file is now loaded with the rest of the encryption stack.
|
|
176
|
+
|
|
177
|
+
- The XChaCha20-Poly1305 provider derived keys with
|
|
178
|
+
``context.force_encoding('BINARY')``, mutating the caller's string. A
|
|
179
|
+
frozen context raised ``FrozenError``. It now uses ``context.b``.
|
|
180
|
+
|
|
181
|
+
Security
|
|
182
|
+
--------
|
|
183
|
+
|
|
184
|
+
- The ``aad_fields`` transient-field fix changes AAD output for any field that
|
|
185
|
+
lists a ``transient_field``. Values encrypted by an earlier release using a
|
|
186
|
+
transient field in ``aad_fields`` were bound to ``"[REDACTED]"`` and will no
|
|
187
|
+
longer decrypt after upgrading. Re-encrypt affected values if any exist.
|
|
188
|
+
PR #280
|
|
189
|
+
|
|
190
|
+
Documentation
|
|
191
|
+
-------------
|
|
192
|
+
|
|
193
|
+
- Repaired every script in ``examples/`` so each runs top-to-bottom and is
|
|
194
|
+
re-runnable (issue #250). Added ``try/integration/examples/`` with one
|
|
195
|
+
subprocess-driven tryouts file per example script for automated regression
|
|
196
|
+
coverage.
|
|
197
|
+
|
|
198
|
+
- ``Horreum.create!``: added ``@yield``, ``@yieldparam``, and
|
|
199
|
+
``@yieldreturn`` YARD tags documenting the post-success block semantics. #286
|
|
200
|
+
|
|
201
|
+
- ``Horreum#save``: added ``@example`` tags showing idiomatic Ruby patterns
|
|
202
|
+
for post-save callbacks (``if save`` and ``&&`` short-circuit). #286
|
|
203
|
+
|
|
204
|
+
- Renamed ``CLAUDE.md`` to ``AGENTS.md`` and pruned it to remove volatile
|
|
205
|
+
content better served by its source of truth. Kept the non-obvious behavioral
|
|
206
|
+
contracts like deferred-vs-immediate write model and the serialization table.
|
|
207
|
+
|
|
208
|
+
AI Assistance
|
|
209
|
+
-------------
|
|
210
|
+
|
|
211
|
+
- AI implemented ``build`` factory block (#279) and WATCH composition in
|
|
212
|
+
``atomic_write`` (#288), including tryouts for both.
|
|
213
|
+
|
|
214
|
+
- AI refactored encryption envelope handling (#280): unified AAD construction
|
|
215
|
+
through ``EncryptedData``, added envelope versioning, and fixed the
|
|
216
|
+
transient-field AAD bypass.
|
|
217
|
+
|
|
218
|
+
- AI implemented ``DatabaseLogger.capture_enabled`` toggle and middleware
|
|
219
|
+
consolidation (#233), per-class ``dirty_write_warnings`` (#277), and
|
|
220
|
+
unsaved-parent guard (#278) with tryouts for each.
|
|
221
|
+
|
|
222
|
+
- AI diagnosed and fixed ``each_record`` on ``unique_index`` hashkeys (#276)
|
|
223
|
+
and repaired all example scripts with regression tryouts (#250).
|
|
224
|
+
|
|
225
|
+
- AI evaluated and rejected ``save_and_then`` (#286) after cross-ORM analysis;
|
|
226
|
+
added YARD docs and ``create_block_try.rb`` instead.
|
|
227
|
+
|
|
10
228
|
.. _changelog-2.9.1:
|
|
11
229
|
|
|
12
230
|
2.9.1 — 2026-05-18
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
familia (2.
|
|
4
|
+
familia (2.10.1)
|
|
5
5
|
concurrent-ruby (~> 1.3)
|
|
6
6
|
connection_pool (>= 2.4, < 4.0)
|
|
7
7
|
csv (~> 3.3)
|
|
@@ -15,59 +15,59 @@ PATH
|
|
|
15
15
|
GEM
|
|
16
16
|
remote: https://rubygems.org/
|
|
17
17
|
specs:
|
|
18
|
-
addressable (2.
|
|
18
|
+
addressable (2.9.0)
|
|
19
19
|
public_suffix (>= 2.0.2, < 8.0)
|
|
20
20
|
ast (2.4.3)
|
|
21
21
|
base64 (0.3.0)
|
|
22
22
|
benchmark (0.5.0)
|
|
23
|
-
bigdecimal (
|
|
23
|
+
bigdecimal (4.1.2)
|
|
24
24
|
concurrent-ruby (1.3.6)
|
|
25
25
|
connection_pool (3.0.2)
|
|
26
26
|
csv (3.3.5)
|
|
27
|
-
date (3.5.
|
|
28
|
-
debug (1.11.
|
|
27
|
+
date (3.5.1)
|
|
28
|
+
debug (1.11.1)
|
|
29
29
|
irb (~> 1.10)
|
|
30
30
|
reline (>= 0.3.8)
|
|
31
31
|
diff-lcs (1.6.2)
|
|
32
|
-
dry-configurable (1.
|
|
33
|
-
dry-core (~> 1.
|
|
32
|
+
dry-configurable (1.4.0)
|
|
33
|
+
dry-core (~> 1.0)
|
|
34
34
|
zeitwerk (~> 2.6)
|
|
35
|
-
dry-core (1.
|
|
35
|
+
dry-core (1.2.0)
|
|
36
36
|
concurrent-ruby (~> 1.0)
|
|
37
37
|
logger
|
|
38
38
|
zeitwerk (~> 2.6)
|
|
39
|
-
dry-inflector (1.
|
|
39
|
+
dry-inflector (1.3.1)
|
|
40
40
|
dry-initializer (3.2.0)
|
|
41
41
|
dry-logic (1.6.0)
|
|
42
42
|
bigdecimal
|
|
43
43
|
concurrent-ruby (~> 1.0)
|
|
44
44
|
dry-core (~> 1.1)
|
|
45
45
|
zeitwerk (~> 2.6)
|
|
46
|
-
dry-schema (1.
|
|
46
|
+
dry-schema (1.16.0)
|
|
47
47
|
concurrent-ruby (~> 1.0)
|
|
48
48
|
dry-configurable (~> 1.0, >= 1.0.1)
|
|
49
49
|
dry-core (~> 1.1)
|
|
50
50
|
dry-initializer (~> 3.2)
|
|
51
|
-
dry-logic (~> 1.
|
|
52
|
-
dry-types (~> 1.
|
|
51
|
+
dry-logic (~> 1.6)
|
|
52
|
+
dry-types (~> 1.9, >= 1.9.1)
|
|
53
53
|
zeitwerk (~> 2.6)
|
|
54
|
-
dry-types (1.
|
|
55
|
-
bigdecimal (
|
|
54
|
+
dry-types (1.9.1)
|
|
55
|
+
bigdecimal (>= 3.0)
|
|
56
56
|
concurrent-ruby (~> 1.0)
|
|
57
57
|
dry-core (~> 1.0)
|
|
58
58
|
dry-inflector (~> 1.0)
|
|
59
59
|
dry-logic (~> 1.4)
|
|
60
60
|
zeitwerk (~> 2.6)
|
|
61
|
-
erb (
|
|
62
|
-
ffi (1.17.
|
|
63
|
-
ffi (1.17.
|
|
61
|
+
erb (6.0.4)
|
|
62
|
+
ffi (1.17.4)
|
|
63
|
+
ffi (1.17.4-arm64-darwin)
|
|
64
64
|
hana (1.3.7)
|
|
65
|
-
io-console (0.8.
|
|
65
|
+
io-console (0.8.2)
|
|
66
66
|
irb (1.15.3)
|
|
67
67
|
pp (>= 0.6.0)
|
|
68
68
|
rdoc (>= 4.0.0)
|
|
69
69
|
reline (>= 0.4.2)
|
|
70
|
-
json (2.
|
|
70
|
+
json (2.19.8)
|
|
71
71
|
json-schema (6.2.0)
|
|
72
72
|
addressable (~> 2.8)
|
|
73
73
|
bigdecimal (>= 3.1, < 5)
|
|
@@ -79,15 +79,15 @@ GEM
|
|
|
79
79
|
language_server-protocol (3.17.0.5)
|
|
80
80
|
lint_roller (1.1.0)
|
|
81
81
|
logger (1.7.0)
|
|
82
|
-
mcp (0.
|
|
82
|
+
mcp (0.18.0)
|
|
83
83
|
json-schema (>= 4.1)
|
|
84
84
|
minitest (5.27.0)
|
|
85
|
-
oj (3.
|
|
85
|
+
oj (3.17.3)
|
|
86
86
|
bigdecimal (>= 3.0)
|
|
87
87
|
ostruct (>= 0.2)
|
|
88
88
|
ostruct (0.6.3)
|
|
89
|
-
parallel (1.
|
|
90
|
-
parser (3.3.
|
|
89
|
+
parallel (1.28.0)
|
|
90
|
+
parser (3.3.11.1)
|
|
91
91
|
ast (~> 2.4.1)
|
|
92
92
|
racc
|
|
93
93
|
pastel (0.8.0)
|
|
@@ -96,25 +96,27 @@ GEM
|
|
|
96
96
|
prettyprint
|
|
97
97
|
prettyprint (0.2.0)
|
|
98
98
|
prism (1.9.0)
|
|
99
|
-
psych (5.
|
|
99
|
+
psych (5.4.0)
|
|
100
100
|
date
|
|
101
101
|
stringio
|
|
102
102
|
public_suffix (7.0.5)
|
|
103
103
|
racc (1.8.1)
|
|
104
104
|
rainbow (3.1.1)
|
|
105
|
-
rake (13.
|
|
105
|
+
rake (13.4.2)
|
|
106
106
|
rbnacl (7.1.2)
|
|
107
107
|
ffi (~> 1)
|
|
108
|
-
rbs (
|
|
108
|
+
rbs (4.0.2)
|
|
109
109
|
logger
|
|
110
|
-
|
|
110
|
+
prism (>= 1.6.0)
|
|
111
|
+
tsort
|
|
112
|
+
rdoc (7.2.0)
|
|
111
113
|
erb
|
|
112
114
|
psych (>= 4.0.0)
|
|
113
115
|
tsort
|
|
114
116
|
redcarpet (3.6.1)
|
|
115
117
|
redis (5.4.1)
|
|
116
118
|
redis-client (>= 0.22.0)
|
|
117
|
-
redis-client (0.
|
|
119
|
+
redis-client (0.29.0)
|
|
118
120
|
connection_pool
|
|
119
121
|
reek (6.5.0)
|
|
120
122
|
dry-schema (~> 1.13)
|
|
@@ -122,10 +124,10 @@ GEM
|
|
|
122
124
|
parser (~> 3.3.0)
|
|
123
125
|
rainbow (>= 2.0, < 4.0)
|
|
124
126
|
rexml (~> 3.1)
|
|
125
|
-
regexp_parser (2.
|
|
126
|
-
reline (0.6.
|
|
127
|
+
regexp_parser (2.12.0)
|
|
128
|
+
reline (0.6.3)
|
|
127
129
|
io-console (~> 0.5)
|
|
128
|
-
rexml (3.4.
|
|
130
|
+
rexml (3.4.4)
|
|
129
131
|
rspec (3.13.2)
|
|
130
132
|
rspec-core (~> 3.13.0)
|
|
131
133
|
rspec-expectations (~> 3.13.0)
|
|
@@ -135,10 +137,10 @@ GEM
|
|
|
135
137
|
rspec-expectations (3.13.5)
|
|
136
138
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
137
139
|
rspec-support (~> 3.13.0)
|
|
138
|
-
rspec-mocks (3.13.
|
|
140
|
+
rspec-mocks (3.13.8)
|
|
139
141
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
140
142
|
rspec-support (~> 3.13.0)
|
|
141
|
-
rspec-support (3.13.
|
|
143
|
+
rspec-support (3.13.7)
|
|
142
144
|
rubocop (1.85.1)
|
|
143
145
|
json (~> 2.3)
|
|
144
146
|
language_server-protocol (~> 3.17.0.2)
|
|
@@ -151,28 +153,29 @@ GEM
|
|
|
151
153
|
rubocop-ast (>= 1.49.0, < 2.0)
|
|
152
154
|
ruby-progressbar (~> 1.7)
|
|
153
155
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
154
|
-
rubocop-ast (1.49.
|
|
156
|
+
rubocop-ast (1.49.1)
|
|
155
157
|
parser (>= 3.3.7.2)
|
|
156
158
|
prism (~> 1.7)
|
|
157
|
-
rubocop-performance (1.
|
|
159
|
+
rubocop-performance (1.26.1)
|
|
158
160
|
lint_roller (~> 1.1)
|
|
159
161
|
rubocop (>= 1.75.0, < 2.0)
|
|
160
|
-
rubocop-ast (>= 1.
|
|
162
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
161
163
|
rubocop-thread_safety (0.7.3)
|
|
162
164
|
lint_roller (~> 1.1)
|
|
163
165
|
rubocop (~> 1.72, >= 1.72.1)
|
|
164
166
|
rubocop-ast (>= 1.44.0, < 2.0)
|
|
165
|
-
ruby-lsp (0.26.
|
|
167
|
+
ruby-lsp (0.26.9)
|
|
166
168
|
language_server-protocol (~> 3.17.0)
|
|
167
169
|
prism (>= 1.2, < 2.0)
|
|
168
170
|
rbs (>= 3, < 5)
|
|
169
|
-
ruby-prof (
|
|
171
|
+
ruby-prof (2.0.4)
|
|
170
172
|
base64
|
|
173
|
+
ostruct
|
|
171
174
|
ruby-progressbar (1.13.0)
|
|
172
175
|
simpleidn (0.2.3)
|
|
173
|
-
stackprof (0.2.
|
|
176
|
+
stackprof (0.2.28)
|
|
174
177
|
stringio (3.1.9)
|
|
175
|
-
timecop (0.9.
|
|
178
|
+
timecop (0.9.11)
|
|
176
179
|
tryouts (3.7.1)
|
|
177
180
|
concurrent-ruby (~> 1.0, < 2)
|
|
178
181
|
irb
|
|
@@ -190,8 +193,8 @@ GEM
|
|
|
190
193
|
unicode-emoji (~> 4.1)
|
|
191
194
|
unicode-emoji (4.2.0)
|
|
192
195
|
uri-valkey (1.4.0)
|
|
193
|
-
yard (0.9.
|
|
194
|
-
zeitwerk (2.
|
|
196
|
+
yard (0.9.44)
|
|
197
|
+
zeitwerk (2.8.2)
|
|
195
198
|
|
|
196
199
|
PLATFORMS
|
|
197
200
|
arm64-darwin-24
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Familia -
|
|
1
|
+
# Familia - v2
|
|
2
2
|
|
|
3
3
|
**Organize and store Ruby objects in Valkey/Redis using native database types (an ORM of sorts).**
|
|
4
4
|
|
|
@@ -56,7 +56,7 @@ The performance characteristics you rely on in Valkey/Redis remain unchanged. Se
|
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
58
|
# Add to Gemfile
|
|
59
|
-
gem 'familia', '~> 2.
|
|
59
|
+
gem 'familia', '~> 2.10'
|
|
60
60
|
|
|
61
61
|
# Or install directly
|
|
62
62
|
gem install familia
|