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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +1 -1
  3. data/.gitignore +1 -1
  4. data/AGENTS.md +198 -0
  5. data/CHANGELOG.rst +149 -0
  6. data/Gemfile.lock +2 -2
  7. data/README.md +2 -2
  8. data/docs/guides/datatype-collections.md +1 -1
  9. data/docs/guides/getting-started.md +87 -0
  10. data/docs/guides/index.md +4 -0
  11. data/docs/migrating/v2.10.0.md +167 -0
  12. data/examples/encrypted_fields.rb +43 -29
  13. data/examples/relationships.rb +66 -36
  14. data/examples/safe_dump.rb +7 -5
  15. data/familia.gemspec +0 -1
  16. data/lib/familia/data_type/collection_base.rb +4 -2
  17. data/lib/familia/data_type/serialization.rb +15 -2
  18. data/lib/familia/data_type.rb +163 -7
  19. data/lib/familia/encryption/encrypted_data.rb +27 -2
  20. data/lib/familia/encryption/manager.rb +17 -2
  21. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -1
  22. data/lib/familia/encryption/request_cache.rb +4 -28
  23. data/lib/familia/encryption.rb +1 -0
  24. data/lib/familia/features/encrypted_fields/concealed_string.rb +12 -0
  25. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +50 -67
  26. data/lib/familia/features/encrypted_fields.rb +14 -0
  27. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +18 -3
  28. data/lib/familia/features/relationships/indexing.rb +6 -2
  29. data/lib/familia/horreum/atomic_write.rb +107 -22
  30. data/lib/familia/horreum/definition.rb +51 -0
  31. data/lib/familia/horreum/dirty_tracking.rb +28 -0
  32. data/lib/familia/horreum/management.rb +115 -3
  33. data/lib/familia/horreum/persistence.rb +17 -4
  34. data/lib/familia/horreum/related_fields.rb +2 -0
  35. data/lib/familia/horreum.rb +1 -0
  36. data/lib/familia/instrumentation.rb +22 -0
  37. data/lib/familia/logging.rb +24 -3
  38. data/lib/familia/settings.rb +79 -1
  39. data/lib/familia/version.rb +1 -1
  40. data/lib/middleware/database_logger.rb +208 -128
  41. data/try/audit/audit_cross_references_try.rb +6 -6
  42. data/try/audit/audit_unique_indexes_try.rb +3 -2
  43. data/try/audit/repair_all_integration_try.rb +2 -1
  44. data/try/audit/repair_indexes_try.rb +2 -1
  45. data/try/features/atomic_write_watch_try.rb +164 -0
  46. data/try/features/build_block_try.rb +191 -0
  47. data/try/features/create_block_try.rb +58 -0
  48. data/try/features/dirty_write_new_object_try.rb +181 -0
  49. data/try/features/dirty_write_warnings_try.rb +456 -0
  50. data/try/features/encrypted_fields/aad_transient_fix_try.rb +164 -0
  51. data/try/features/encrypted_fields/aad_transient_proof_try.rb +253 -0
  52. data/try/features/encrypted_fields/concealed_string_core_try.rb +6 -4
  53. data/try/features/encrypted_fields/encrypted_data_try.rb +151 -0
  54. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -0
  55. data/try/features/encrypted_fields/envelope_version_branching_try.rb +106 -0
  56. data/try/features/encrypted_fields/envelope_version_try.rb +171 -0
  57. data/try/features/encrypted_fields/key_material_try.rb +205 -0
  58. data/try/features/encryption/request_cache_try.rb +88 -0
  59. data/try/features/relationships/participation_reverse_methods_try.rb +3 -2
  60. data/try/features/relationships/unique_index_each_record_try.rb +143 -0
  61. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  62. data/try/integration/examples/encrypted_fields_example_try.rb +67 -0
  63. data/try/integration/examples/relationships_example_try.rb +69 -0
  64. data/try/integration/examples/safe_dump_example_try.rb +60 -0
  65. data/try/unit/core/trace_caching_try.rb +58 -0
  66. data/try/unit/data_types/stringkey_extended_try.rb +1 -1
  67. data/try/unit/horreum/relations_try.rb +5 -0
  68. data/try/unit/middleware/database_logger_capture_toggle_try.rb +278 -0
  69. metadata +22 -2
  70. data/CLAUDE.md +0 -322
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d24c3b38092f1192f8e250553e8ef4d51b05c1bb00c48dfd7f6520d3a48a9a0e
4
- data.tar.gz: 1dd0a2aa47682736209f116adf378eb4a0eccafffd7c151b02a8ef4573e52551
3
+ metadata.gz: ab706939f766966f0471ff1285db5def4d14bfc9b0d428c5b0caf481595d57c2
4
+ data.tar.gz: 6d08805e52f5acd4a90fc117bb8a6b2e352c036daa9916c0a19079af827ddfec
5
5
  SHA512:
6
- metadata.gz: 10d69edb30370c7dba83b387f53429878fded6bf736e04fbefec4c228baf375806eeefa6c1d44c45c296e5d79b82345d1bc9cfbb0992ee18fc4c204dac250e10
7
- data.tar.gz: 9b52e601ae93d44a6db8b0f995828ae45d55872377a471658a8d63210607c56f70958e372654ac20a13b07344e9b9da0afcdaaa50203f0796326d5d08e93bf6b
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 CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
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
@@ -30,5 +30,5 @@ public/
30
30
  # Exclusions
31
31
  !README.md
32
32
  !CHANGELOG.md
33
- !CLAUDE.md
33
+ !AGENTS.md
34
34
  !LICENSE.txt
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.9.1)
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 - 2.5
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.5'
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 `CLAUDE.md` for the deferred-vs-immediate split.)
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