familia 2.9.1 → 2.10.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/.github/workflows/claude-code-review.yml +1 -1
- data/.gitignore +1 -1
- data/AGENTS.md +198 -0
- data/CHANGELOG.rst +149 -0
- data/Gemfile.lock +2 -2
- data/README.md +2 -2
- data/docs/guides/datatype-collections.md +1 -1
- data/docs/guides/getting-started.md +87 -0
- data/docs/guides/index.md +4 -0
- data/docs/migrating/v2.10.0.md +167 -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/data_type/collection_base.rb +4 -2
- data/lib/familia/data_type/serialization.rb +15 -2
- data/lib/familia/data_type.rb +163 -7
- 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/indexing/unique_index_generators.rb +18 -3
- data/lib/familia/features/relationships/indexing.rb +6 -2
- data/lib/familia/horreum/atomic_write.rb +107 -22
- data/lib/familia/horreum/definition.rb +51 -0
- data/lib/familia/horreum/dirty_tracking.rb +28 -0
- data/lib/familia/horreum/management.rb +115 -3
- data/lib/familia/horreum/persistence.rb +17 -4
- data/lib/familia/horreum/related_fields.rb +2 -0
- data/lib/familia/horreum.rb +1 -0
- data/lib/familia/instrumentation.rb +22 -0
- data/lib/familia/logging.rb +24 -3
- data/lib/familia/settings.rb +79 -1
- data/lib/familia/version.rb +1 -1
- 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 +164 -0
- data/try/features/build_block_try.rb +191 -0
- data/try/features/create_block_try.rb +58 -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/participation_reverse_methods_try.rb +3 -2
- 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/unit/core/trace_caching_try.rb +58 -0
- 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 +22 -2
- data/CLAUDE.md +0 -322
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab706939f766966f0471ff1285db5def4d14bfc9b0d428c5b0caf481595d57c2
|
|
4
|
+
data.tar.gz: 6d08805e52f5acd4a90fc117bb8a6b2e352c036daa9916c0a19079af827ddfec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b328108ed4793a4c94ddd1ef1447e759bd369647fc81aca87bafe92eb1f23a71a53ca1d763726da5a631af19e520966075bb65ab40ff29d4a4563ac1a4b1e34a
|
|
7
|
+
data.tar.gz: 652d0a36301b9321eba9225ec658f3d72b0aa449bc0133b038055900475fe546fca36e2df2133680f69a938409afd6b637d0541be44dd956f51cf2cb50b205b2
|
|
@@ -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,155 @@ 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.0:
|
|
11
|
+
|
|
12
|
+
2.10.0 — 2026-06-04
|
|
13
|
+
====================
|
|
14
|
+
|
|
15
|
+
Added
|
|
16
|
+
-----
|
|
17
|
+
|
|
18
|
+
- ``Horreum.build``: A factory block that yields a new instance, then commits
|
|
19
|
+
all scalar and collection changes in a single ``MULTI/EXEC`` upon exit.
|
|
20
|
+
This avoids sequencing ``save`` before collection writes. Raises
|
|
21
|
+
``Familia::RecordExistsError`` if the identifier exists (create-only).
|
|
22
|
+
Without a block, it behaves as ``new(...).save``. #279
|
|
23
|
+
|
|
24
|
+
- ``atomic_write`` now supports ``watch_keys:`` (keys to watch) and
|
|
25
|
+
``pre_check:`` (a callable run between ``WATCH`` and ``MULTI``) to enable
|
|
26
|
+
optimistic locking. Retries with exponential backoff on abort. #288
|
|
27
|
+
|
|
28
|
+
- ``encrypted_field`` now accepts a ``key_material:`` proc. This mixes
|
|
29
|
+
additional entropy into key derivation (separate from AAD), requiring
|
|
30
|
+
the correct material at decryption to avoid producing garbage output. PR #280
|
|
31
|
+
|
|
32
|
+
- Encrypted-field envelopes now store their own ``envelope_version`` and
|
|
33
|
+
``aad_fields`` list. Decryption rebuilds AAD from these stored fields
|
|
34
|
+
rather than the active class declaration, preventing breakage when model
|
|
35
|
+
definitions change. PR #280
|
|
36
|
+
|
|
37
|
+
- ``DatabaseLogger.capture_enabled`` (Boolean, default ``true``) controls
|
|
38
|
+
in-memory buffer capturing. Disabling it bypasses clock checks, message
|
|
39
|
+
allocations, and buffer appends, offering a zero-overhead production path. Issue #233
|
|
40
|
+
|
|
41
|
+
- ``Familia::Instrumentation.hooks?(type)`` reports whether hooks are
|
|
42
|
+
registered for a given event type (e.g. ``:command``, ``:pipeline``). Issue #233
|
|
43
|
+
|
|
44
|
+
- ``Familia.reset_trace!`` clears the cached trace environment lookup. Issue #233
|
|
45
|
+
|
|
46
|
+
- ``dirty_write_warnings`` class method configures write-order warnings per
|
|
47
|
+
class (inheritable). Accepts ``:strict``, ``:warn``, ``:once``, or ``:off``. Issue #277
|
|
48
|
+
|
|
49
|
+
- ``Familia.dirty_write_warnings`` global setting providing the default mode for
|
|
50
|
+
classes that do not set their own. Issue #277
|
|
51
|
+
|
|
52
|
+
- ``Familia.raise_on_unsaved_parent_write`` (default ``true``) controls whether a
|
|
53
|
+
collection write on a new, unsaved, dirty parent raises or warns. Issue #278
|
|
54
|
+
|
|
55
|
+
Changed
|
|
56
|
+
-------
|
|
57
|
+
|
|
58
|
+
- Mutating a collection on a *new, unsaved* parent Horreum now **raises**
|
|
59
|
+
``Familia::Problem`` by default. The guard fires *before* the command runs,
|
|
60
|
+
preventing orphaned data. Save the parent first, or set
|
|
61
|
+
``Familia.raise_on_unsaved_parent_write = false`` to restore warnings. Issue #278
|
|
62
|
+
|
|
63
|
+
- Dirty-write warnings are now **deduplicated per dirty window** (mode ``:once``).
|
|
64
|
+
Writing to a collection on a parent with unsaved scalar fields warns once per
|
|
65
|
+
distinct set of unsaved fields instead of on every write. Set
|
|
66
|
+
``dirty_write_warnings :warn`` to restore the old behavior. Issue #277
|
|
67
|
+
|
|
68
|
+
- Dirty-write warnings and strict raises now append the hint:
|
|
69
|
+
``(call #save first or wrap in atomic_write)``. Issue #277
|
|
70
|
+
|
|
71
|
+
- ``trace_enabled?`` now caches the ``FAMILIA_TRACE`` lookup. Use
|
|
72
|
+
``Familia.reset_trace!`` to force a re-read of the environment. Issue #233
|
|
73
|
+
|
|
74
|
+
- ``unique_index`` hashkeys now store identifiers as raw strings rather than
|
|
75
|
+
JSON-encoded strings. Rebuild existing unique indexes to convert legacy entries,
|
|
76
|
+
e.g., via ``User.rebuild_email_lookup`` or ``company.rebuild_badge_index``. Issue #276
|
|
77
|
+
|
|
78
|
+
Fixed
|
|
79
|
+
-----
|
|
80
|
+
|
|
81
|
+
- ``Horreum.build`` with a block no longer has a TOCTOU race between the
|
|
82
|
+
``exists?`` check and the ``atomic_write`` commit. The block path now uses
|
|
83
|
+
``atomic_write(watch_keys:, pre_check:)`` so the existence check runs between
|
|
84
|
+
``WATCH`` and ``MULTI``. #288
|
|
85
|
+
|
|
86
|
+
- ``aad_fields`` containing a ``transient_field`` now bind to the field's real
|
|
87
|
+
value. Previously ``build_aad`` called ``RedactedString#to_s``, which returns
|
|
88
|
+
``"[REDACTED]"`` for every value -- so all passphrases produced identical AAD
|
|
89
|
+
and the binding was defeated. PR #280
|
|
90
|
+
|
|
91
|
+
- ``each_record`` now works on ``unique_index`` hashkeys. Previously it raised
|
|
92
|
+
``Familia::Problem`` because ``unique_index`` created its backing hashkey
|
|
93
|
+
without the ``class:`` option. Issue #276
|
|
94
|
+
|
|
95
|
+
- ``each_record`` extracts the stored identifier (the hash *value*) from a
|
|
96
|
+
HashKey instead of the indexed field (the hash *key*). Issue #276
|
|
97
|
+
|
|
98
|
+
- The unguarded ``Familia.trace`` sites in ``Horreum#destroy!`` and
|
|
99
|
+
``find_by_dbkey`` now carry an inline ``if Familia.debug?`` guard. Issue #233
|
|
100
|
+
|
|
101
|
+
- Two latent encryption bugs surfaced while repairing the examples (issue #250):
|
|
102
|
+
|
|
103
|
+
- ``Familia::Encryption.with_request_cache`` and ``clear_request_cache!``
|
|
104
|
+
were unreachable. The implementation lived in
|
|
105
|
+
``lib/familia/encryption/request_cache.rb``, which was never ``require``\ d.
|
|
106
|
+
The file is now loaded with the rest of the encryption stack.
|
|
107
|
+
|
|
108
|
+
- The XChaCha20-Poly1305 provider derived keys with
|
|
109
|
+
``context.force_encoding('BINARY')``, mutating the caller's string. A
|
|
110
|
+
frozen context raised ``FrozenError``. It now uses ``context.b``.
|
|
111
|
+
|
|
112
|
+
Security
|
|
113
|
+
--------
|
|
114
|
+
|
|
115
|
+
- The ``aad_fields`` transient-field fix changes AAD output for any field that
|
|
116
|
+
lists a ``transient_field``. Values encrypted by an earlier release using a
|
|
117
|
+
transient field in ``aad_fields`` were bound to ``"[REDACTED]"`` and will no
|
|
118
|
+
longer decrypt after upgrading. Re-encrypt affected values if any exist.
|
|
119
|
+
PR #280
|
|
120
|
+
|
|
121
|
+
Documentation
|
|
122
|
+
-------------
|
|
123
|
+
|
|
124
|
+
- Repaired every script in ``examples/`` so each runs top-to-bottom and is
|
|
125
|
+
re-runnable (issue #250). Added ``try/integration/examples/`` with one
|
|
126
|
+
subprocess-driven tryouts file per example script for automated regression
|
|
127
|
+
coverage.
|
|
128
|
+
|
|
129
|
+
- ``Horreum.create!``: added ``@yield``, ``@yieldparam``, and
|
|
130
|
+
``@yieldreturn`` YARD tags documenting the post-success block semantics. #286
|
|
131
|
+
|
|
132
|
+
- ``Horreum#save``: added ``@example`` tags showing idiomatic Ruby patterns
|
|
133
|
+
for post-save callbacks (``if save`` and ``&&`` short-circuit). #286
|
|
134
|
+
|
|
135
|
+
- Renamed ``CLAUDE.md`` to ``AGENTS.md`` and pruned it to remove volatile
|
|
136
|
+
content better served by its source of truth. Kept the non-obvious behavioral
|
|
137
|
+
contracts like deferred-vs-immediate write model and the serialization table.
|
|
138
|
+
|
|
139
|
+
AI Assistance
|
|
140
|
+
-------------
|
|
141
|
+
|
|
142
|
+
- AI implemented ``build`` factory block (#279) and WATCH composition in
|
|
143
|
+
``atomic_write`` (#288), including tryouts for both.
|
|
144
|
+
|
|
145
|
+
- AI refactored encryption envelope handling (#280): unified AAD construction
|
|
146
|
+
through ``EncryptedData``, added envelope versioning, and fixed the
|
|
147
|
+
transient-field AAD bypass.
|
|
148
|
+
|
|
149
|
+
- AI implemented ``DatabaseLogger.capture_enabled`` toggle and middleware
|
|
150
|
+
consolidation (#233), per-class ``dirty_write_warnings`` (#277), and
|
|
151
|
+
unsaved-parent guard (#278) with tryouts for each.
|
|
152
|
+
|
|
153
|
+
- AI diagnosed and fixed ``each_record`` on ``unique_index`` hashkeys (#276)
|
|
154
|
+
and repaired all example scripts with regression tryouts (#250).
|
|
155
|
+
|
|
156
|
+
- AI evaluated and rejected ``save_and_then`` (#286) after cross-ORM analysis;
|
|
157
|
+
added YARD docs and ``create_block_try.rb`` instead.
|
|
158
|
+
|
|
10
159
|
.. _changelog-2.9.1:
|
|
11
160
|
|
|
12
161
|
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.0)
|
|
5
5
|
concurrent-ruby (~> 1.3)
|
|
6
6
|
connection_pool (>= 2.4, < 4.0)
|
|
7
7
|
csv (~> 3.3)
|
|
@@ -67,7 +67,7 @@ GEM
|
|
|
67
67
|
pp (>= 0.6.0)
|
|
68
68
|
rdoc (>= 4.0.0)
|
|
69
69
|
reline (>= 0.4.2)
|
|
70
|
-
json (2.15.1)
|
|
70
|
+
json (2.15.2.1)
|
|
71
71
|
json-schema (6.2.0)
|
|
72
72
|
addressable (~> 2.8)
|
|
73
73
|
bigdecimal (>= 3.1, < 5)
|
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
|
|
@@ -7,7 +7,7 @@ UnsortedSet, Sorted Set, List, and Hash data types all include the `Collection`
|
|
|
7
7
|
|
|
8
8
|
## Bulk writes — single round-trip mutations
|
|
9
9
|
|
|
10
|
-
Collection mutations are **immediate** — every call hits Valkey/Redis right away, unlike scalar `field` setters which are deferred until `save`. Each call also runs `warn_if_dirty!` and cascades expiration. (See the write-model notes in `
|
|
10
|
+
Collection mutations are **immediate** — every call hits Valkey/Redis right away, unlike scalar `field` setters which are deferred until `save`. Each call also runs `warn_if_dirty!` and cascades expiration. (See the write-model notes in `AGENTS.md` for the deferred-vs-immediate split.)
|
|
11
11
|
|
|
12
12
|
Multi-element adds issue **one** command for the whole batch, not one per element. Populating a large collection is therefore a single round-trip even without an explicit pipeline.
|
|
13
13
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Getting Started with Familia
|
|
2
|
+
|
|
3
|
+
This guide covers the core mental models you need to work effectively with Familia. If you're coming from ActiveRecord or another ORM, understanding these differences upfront will save debugging time.
|
|
4
|
+
|
|
5
|
+
## DataTypes: Live Proxies, Not Cached Relations
|
|
6
|
+
|
|
7
|
+
DataTypes (`list`, `set`, `zset`, `hashkey`) are *live proxies* to Redis keys, not cached relation objects. This is the most important conceptual difference from ActiveRecord.
|
|
8
|
+
|
|
9
|
+
| Aspect | ActiveRecord Relation | Familia DataType |
|
|
10
|
+
|--------|----------------------|------------------|
|
|
11
|
+
| Object identity | New object per query | Same object every call (memoized) |
|
|
12
|
+
| Data caching | Can memoize loaded records | No cache — every read hits Redis |
|
|
13
|
+
| Mutability | Mutable (chainable) | Frozen at creation |
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# ActiveRecord: new relation object each time, can cache results
|
|
17
|
+
User.where(active: true).object_id != User.where(active: true).object_id
|
|
18
|
+
|
|
19
|
+
# Familia: same frozen wrapper, always hits Redis
|
|
20
|
+
User.instances.object_id == User.instances.object_id # true
|
|
21
|
+
User.instances.frozen? # true
|
|
22
|
+
User.instances.to_a # hits Redis now
|
|
23
|
+
User.instances.to_a # hits Redis again
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Why This Matters
|
|
27
|
+
|
|
28
|
+
**Testing**: Class-level DataTypes (like `instances`) are frozen for thread safety. Attempting `define_singleton_method` on them raises `FrozenError`. To stub behavior in tests, stub the class method that returns the DataType, not the DataType instance itself:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# Won't work — raises FrozenError
|
|
32
|
+
User.instances.define_singleton_method(:member?) { |_| true }
|
|
33
|
+
|
|
34
|
+
# Works — stub the class method
|
|
35
|
+
allow(User).to receive(:instances).and_return(mock_sorted_set)
|
|
36
|
+
|
|
37
|
+
# Or stub a method on the class that uses instances
|
|
38
|
+
allow(ApiConfig).to receive(:delete_for_domain!).and_return(true)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Performance**: Since every read hits Redis, batch operations when possible:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# Inefficient: N Redis calls
|
|
45
|
+
ids.each { |id| User.instances.member?(id) }
|
|
46
|
+
|
|
47
|
+
# Better: single pipeline
|
|
48
|
+
User.dbclient.pipelined do
|
|
49
|
+
ids.each { |id| User.instances.member?(id) }
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Scalar Fields vs Collection Fields
|
|
54
|
+
|
|
55
|
+
Familia has a two-tier write model. Understanding when data hits Redis is critical.
|
|
56
|
+
|
|
57
|
+
**Scalar fields** (`field`) use deferred writes:
|
|
58
|
+
- Setters update in-memory only until `save` is called
|
|
59
|
+
- Fast writers (`field_name!`) write immediately
|
|
60
|
+
|
|
61
|
+
**Collection fields** (`list`, `set`, `zset`, `hashkey`) use immediate writes:
|
|
62
|
+
- Every mutating method executes the Redis command right away
|
|
63
|
+
- Cannot be rolled back if a subsequent operation fails
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
# Safe pattern: scalars first, then collections
|
|
67
|
+
plan.name = "Premium"
|
|
68
|
+
plan.save
|
|
69
|
+
|
|
70
|
+
plan.features.clear # immediate Redis DEL
|
|
71
|
+
plan.features.add("sso") # immediate Redis SADD
|
|
72
|
+
|
|
73
|
+
# Or use atomic_write for all-or-nothing
|
|
74
|
+
plan.atomic_write do
|
|
75
|
+
plan.name = "Premium"
|
|
76
|
+
plan.features.clear
|
|
77
|
+
plan.features.add("sso")
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
See [Transaction Safety](../transaction_safety.md) for details.
|
|
82
|
+
|
|
83
|
+
## Next Steps
|
|
84
|
+
|
|
85
|
+
- [Field System](field-system.md) — field definitions and types
|
|
86
|
+
- [Feature System](feature-system.md) — modular capabilities
|
|
87
|
+
- [DataType Collections](datatype-collections.md) — working with lists, sets, and hashes
|
data/docs/guides/index.md
CHANGED
|
@@ -9,6 +9,10 @@ Welcome to the comprehensive documentation for Familia v2.0. This guide collecti
|
|
|
9
9
|
|
|
10
10
|
## 📚 Guide Structure
|
|
11
11
|
|
|
12
|
+
### 🚀 Getting Started
|
|
13
|
+
|
|
14
|
+
0. **[Getting Started](getting-started.md)** - Mental models and key concepts for developers new to Familia
|
|
15
|
+
|
|
12
16
|
### 🏗️ Architecture & System Design
|
|
13
17
|
|
|
14
18
|
1. **[Feature System](feature-system.md)** - Modular architecture with dependencies and autoloader patterns
|