familia 2.10.0 → 2.11.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/ci.yml +1 -4
- data/.github/workflows/release-gem.yml +161 -0
- data/.rubocop.yml +3 -1
- data/CHANGELOG.rst +160 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +44 -40
- data/README.md +26 -0
- data/Rakefile +19 -0
- data/docs/guides/datatype-collections.md +30 -2
- data/docs/guides/feature-encrypted-fields.md +4 -1
- 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/migrating/{v2.10.0.md → v2.10.md} +128 -1
- data/docs/reference/api-technical.md +2 -1
- data/lib/familia/connection/operations.rb +194 -0
- data/lib/familia/connection/transaction_core.rb +51 -0
- data/lib/familia/connection.rb +1 -1
- data/lib/familia/core_ext/securerandom.rb +57 -0
- data/lib/familia/data_type/collection_base.rb +22 -7
- data/lib/familia/data_type/serialization.rb +21 -4
- data/lib/familia/data_type/types/sorted_set.rb +4 -2
- data/lib/familia/data_type.rb +1 -1
- data/lib/familia/encryption/manager.rb +47 -8
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +62 -2
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +5 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +6 -0
- data/lib/familia/encryption/request_cache.rb +18 -1
- data/lib/familia/features/encrypted_fields.rb +4 -1
- data/lib/familia/features/external_identifier.rb +50 -19
- 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 +5 -3
- data/lib/familia/features/relationships/participation/target_methods.rb +33 -12
- data/lib/familia/features/relationships/participation.rb +6 -1
- data/lib/familia/features/relationships/participation_membership.rb +12 -5
- data/lib/familia/horreum/atomic_write.rb +24 -34
- data/lib/familia/horreum/management.rb +22 -7
- data/lib/familia/horreum/persistence.rb +110 -52
- data/lib/familia/index_descriptor.rb +258 -0
- 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 +14 -1
- data/lib/familia/migration.rb +2 -0
- data/lib/familia/secure_identifier.rb +32 -21
- data/lib/familia/settings.rb +66 -8
- data/lib/familia/utils.rb +15 -0
- data/lib/familia/verifiable_identifier.rb +31 -7
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -0
- data/try/bug_fixes/class_destroy_index_cleanup_try.rb +50 -0
- data/try/bug_fixes/permission_query_try.rb +64 -0
- data/try/bug_fixes/sorted_set_members_count_try.rb +40 -0
- data/try/bug_fixes/stale_unique_index_try.rb +54 -0
- data/try/features/atomic_write_watch_try.rb +108 -18
- data/try/features/cross_model_atomic_write_try.rb +402 -0
- data/try/features/encryption/aes_gcm_salt_rotation_try.rb +219 -0
- data/try/features/encryption/config_persistence_try.rb +35 -0
- data/try/features/encryption/request_cache_try.rb +15 -0
- data/try/features/external_identifier/external_identifier_try.rb +73 -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_membership_security_try.rb +64 -0
- data/try/features/relationships/participation_reverse_methods_try.rb +4 -2
- data/try/integration/verifiable_identifier_try.rb +24 -3
- data/try/investigation/cross_model_atomic_poc_try.rb +130 -0
- data/try/support/stress/atomic_write_ownership_stress.rb +152 -0
- data/try/thread_safety/atomic_write_ownership_race_try.rb +107 -104
- data/try/unit/core/securerandom_polyfill_try.rb +61 -0
- data/try/unit/data_types/each_record_try.rb +1 -1
- data/try/unit/horreum/auto_indexing_on_save_try.rb +9 -8
- metadata +20 -3
- 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: c322fb98599ca1596829b047f78d414a3216a4e94a61ded4c6d3a4355dd7780a
|
|
4
|
+
data.tar.gz: a0af405d479ba7547b2fe65fe17e7a9b0fc5fb04cfe8b5563ce10726187d0396
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac3aac1967a9730670b6f43f464c001d6c0acad7e599e528d94edb5b60b7939fb59dc5ef320962bfaacd1c204603e36d09647670f2511320a98c9b4fc9f0d715
|
|
7
|
+
data.tar.gz: f5f7e14e48be039962af4bcda0ac6c61248143353f773239c5b4ac0a9e9af498f64618d2f18dc48f903c7349d47e375f6e87c6ed686d7e0bbc6ebb444a4456b7
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Release Gem Workflow
|
|
2
|
+
#
|
|
3
|
+
# Automatically builds and publishes the familia gem to RubyGems.org when a
|
|
4
|
+
# GitHub release is published with a semantic version tag (vMAJOR.MINOR.PATCH,
|
|
5
|
+
# e.g. v2.10.1, v3.0.0).
|
|
6
|
+
#
|
|
7
|
+
# Follows the canonical RubyGems Trusted Publishing workflow:
|
|
8
|
+
# https://guides.rubygems.org/trusted-publishing/
|
|
9
|
+
#
|
|
10
|
+
# ============================================================================
|
|
11
|
+
# SETUP INSTRUCTIONS (one-time)
|
|
12
|
+
# ============================================================================
|
|
13
|
+
#
|
|
14
|
+
# This workflow uses RubyGems Trusted Publishing (OIDC). No long-lived API
|
|
15
|
+
# key needs to be stored in GitHub.
|
|
16
|
+
#
|
|
17
|
+
# ----------------------------------------------------------------------------
|
|
18
|
+
# 1. Configure the trusted publisher on RubyGems.org
|
|
19
|
+
# ----------------------------------------------------------------------------
|
|
20
|
+
#
|
|
21
|
+
# a. Sign in to https://rubygems.org and enable MFA on your account
|
|
22
|
+
# (required for trusted publishing).
|
|
23
|
+
#
|
|
24
|
+
# b. Open the gem's trusted publisher page (works for both existing gems
|
|
25
|
+
# and the first-ever publish):
|
|
26
|
+
# Existing gem: https://rubygems.org/gems/familia/trusted_publishers
|
|
27
|
+
# First publish: https://rubygems.org/profile/oidc/pending_trusted_publishers/new
|
|
28
|
+
#
|
|
29
|
+
# c. Click "Create" and fill in:
|
|
30
|
+
# Publisher type: GitHub Actions
|
|
31
|
+
# Repository owner: delano
|
|
32
|
+
# Repository name: familia
|
|
33
|
+
# Workflow filename: release-gem.yml
|
|
34
|
+
# Environment: rubygems.org (must match `environment.name` below)
|
|
35
|
+
#
|
|
36
|
+
# d. Save. RubyGems will now accept short-lived OIDC tokens issued by this
|
|
37
|
+
# workflow running in this repository.
|
|
38
|
+
#
|
|
39
|
+
# ----------------------------------------------------------------------------
|
|
40
|
+
# 2. Configure the GitHub environment
|
|
41
|
+
# ----------------------------------------------------------------------------
|
|
42
|
+
#
|
|
43
|
+
# a. In GitHub: Settings -> Environments -> New environment
|
|
44
|
+
# Name: `rubygems.org` (must match `environment.name` below)
|
|
45
|
+
#
|
|
46
|
+
# b. Under "Deployment branches and tags", restrict to tags matching:
|
|
47
|
+
# v*.*.*
|
|
48
|
+
# This prevents the environment (and its OIDC token) from being used
|
|
49
|
+
# from any branch or non-release tag.
|
|
50
|
+
#
|
|
51
|
+
# c. Optional but recommended: add required reviewers to gate publishes
|
|
52
|
+
# behind manual approval.
|
|
53
|
+
#
|
|
54
|
+
# No GitHub secrets are required - OIDC handles authentication.
|
|
55
|
+
#
|
|
56
|
+
# ----------------------------------------------------------------------------
|
|
57
|
+
# 3. Cutting a release
|
|
58
|
+
# ----------------------------------------------------------------------------
|
|
59
|
+
#
|
|
60
|
+
# a. Bump Familia::VERSION in lib/familia/version.rb (semver: MAJOR.MINOR.PATCH).
|
|
61
|
+
# b. Update CHANGELOG.rst and commit on main.
|
|
62
|
+
# c. On GitHub, draft a Release with tag `vX.Y.Z` (matching the constant)
|
|
63
|
+
# and publish it. This workflow runs automatically: it verifies the tag
|
|
64
|
+
# matches the gemspec version, builds the gem, and pushes it.
|
|
65
|
+
#
|
|
66
|
+
# ----------------------------------------------------------------------------
|
|
67
|
+
# Fallback: API key (only if Trusted Publishing isn't an option)
|
|
68
|
+
# ----------------------------------------------------------------------------
|
|
69
|
+
#
|
|
70
|
+
# 1. Create an API key at https://rubygems.org/profile/api_keys with scope
|
|
71
|
+
# "Push rubygem" restricted to the `familia` gem.
|
|
72
|
+
# 2. Store it as a repo secret named `RUBYGEMS_API_KEY`.
|
|
73
|
+
# 3. Drop the `id-token: write` permission and `environment:` block, and
|
|
74
|
+
# replace the `rubygems/release-gem` step with:
|
|
75
|
+
#
|
|
76
|
+
# - name: Publish to RubyGems
|
|
77
|
+
# env:
|
|
78
|
+
# GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
|
79
|
+
# run: gem push familia-*.gem
|
|
80
|
+
#
|
|
81
|
+
# ----------------------------------------------------------------------------
|
|
82
|
+
# Action provenance (SHA pins)
|
|
83
|
+
# ----------------------------------------------------------------------------
|
|
84
|
+
#
|
|
85
|
+
# All third-party actions below are pinned to a full commit SHA, per GitHub's
|
|
86
|
+
# secure-use guidance for release-critical workflows. The accompanying
|
|
87
|
+
# comment records the tag the SHA was resolved from so renovate/dependabot
|
|
88
|
+
# can keep them current.
|
|
89
|
+
#
|
|
90
|
+
# actions/checkout - official GitHub action
|
|
91
|
+
# ruby/setup-ruby - official Ruby org action (GitHub-verified creator)
|
|
92
|
+
# rubygems/release-gem - official RubyGems action for trusted publishing
|
|
93
|
+
#
|
|
94
|
+
# ============================================================================
|
|
95
|
+
|
|
96
|
+
name: Release Gem
|
|
97
|
+
|
|
98
|
+
on:
|
|
99
|
+
release:
|
|
100
|
+
types: [published]
|
|
101
|
+
|
|
102
|
+
# Coarse default. Each job re-declares the narrowest permissions it needs.
|
|
103
|
+
permissions:
|
|
104
|
+
contents: read
|
|
105
|
+
|
|
106
|
+
# Don't allow two release runs to race - one published tag, one publish.
|
|
107
|
+
concurrency:
|
|
108
|
+
group: release-gem-${{ github.event.release.tag_name }}
|
|
109
|
+
cancel-in-progress: false
|
|
110
|
+
|
|
111
|
+
jobs:
|
|
112
|
+
release:
|
|
113
|
+
name: Build and push gem
|
|
114
|
+
runs-on: ubuntu-latest
|
|
115
|
+
timeout-minutes: 10
|
|
116
|
+
|
|
117
|
+
environment:
|
|
118
|
+
name: rubygems.org
|
|
119
|
+
url: https://rubygems.org/gems/familia
|
|
120
|
+
|
|
121
|
+
permissions:
|
|
122
|
+
contents: write # rubygems/release-gem attaches built .gem to the GitHub release
|
|
123
|
+
id-token: write # OIDC token for RubyGems Trusted Publishing
|
|
124
|
+
|
|
125
|
+
steps:
|
|
126
|
+
- name: Checkout
|
|
127
|
+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
128
|
+
with:
|
|
129
|
+
persist-credentials: false
|
|
130
|
+
|
|
131
|
+
- name: Set up Ruby
|
|
132
|
+
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
|
|
133
|
+
with:
|
|
134
|
+
bundler-cache: true
|
|
135
|
+
# Pinned to 3.2, the oldest non-experimental Ruby in ci.yml's matrix
|
|
136
|
+
# and the gemspec's required_ruby_version floor, so the release build
|
|
137
|
+
# matches the minimum supported configuration that CI exercises.
|
|
138
|
+
ruby-version: "3.2"
|
|
139
|
+
|
|
140
|
+
- name: Verify release tag matches Familia::VERSION
|
|
141
|
+
env:
|
|
142
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
143
|
+
run: |
|
|
144
|
+
set -euo pipefail
|
|
145
|
+
case "${RELEASE_TAG}" in
|
|
146
|
+
v[0-9]*.[0-9]*.[0-9]*) ;;
|
|
147
|
+
*)
|
|
148
|
+
echo "Release tag '${RELEASE_TAG}' is not a vMAJOR.MINOR.PATCH semver tag." >&2
|
|
149
|
+
exit 1
|
|
150
|
+
;;
|
|
151
|
+
esac
|
|
152
|
+
tag_version="${RELEASE_TAG#v}"
|
|
153
|
+
gem_version="$(ruby -r ./lib/familia/version.rb -e 'print Familia::VERSION')"
|
|
154
|
+
if [ "${tag_version}" != "${gem_version}" ]; then
|
|
155
|
+
echo "Release tag ${RELEASE_TAG} (${tag_version}) != Familia::VERSION (${gem_version})." >&2
|
|
156
|
+
exit 1
|
|
157
|
+
fi
|
|
158
|
+
echo "Releasing familia ${gem_version} from tag ${RELEASE_TAG}"
|
|
159
|
+
|
|
160
|
+
- name: Build and push gem to RubyGems
|
|
161
|
+
uses: rubygems/release-gem@6317d8d1f7e28c24d28f6eff169ea854948bd9f7 # v1.2.0
|
data/.rubocop.yml
CHANGED
|
@@ -135,9 +135,11 @@ Style/StringLiterals:
|
|
|
135
135
|
Enabled: true
|
|
136
136
|
EnforcedStyle: single_quotes
|
|
137
137
|
|
|
138
|
+
# Every lib/ file carries `# frozen_string_literal: true`; require it rather
|
|
139
|
+
# than flag it (the previous `never` contradicted the entire codebase).
|
|
138
140
|
Style/FrozenStringLiteralComment:
|
|
139
141
|
Enabled: true
|
|
140
|
-
EnforcedStyle:
|
|
142
|
+
EnforcedStyle: always_true
|
|
141
143
|
|
|
142
144
|
Naming/MemoizedInstanceVariableName:
|
|
143
145
|
Enabled: false
|
data/CHANGELOG.rst
CHANGED
|
@@ -7,6 +7,166 @@ 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.11.0:
|
|
11
|
+
|
|
12
|
+
2.11.0 — 2026-06-22
|
|
13
|
+
===================
|
|
14
|
+
|
|
15
|
+
Added
|
|
16
|
+
-----
|
|
17
|
+
|
|
18
|
+
- Project-wide relationship introspection: ``Familia.index_descriptors``,
|
|
19
|
+
``Familia.unique_indexes``, ``Familia.multi_indexes``, and
|
|
20
|
+
``Familia.participation_descriptors`` aggregate index/participation metadata
|
|
21
|
+
across all loaded ``Horreum`` subclasses, returning ``Familia::IndexDescriptor``
|
|
22
|
+
objects (``coordinate``, ``each_record``, ``rebuild!``, ``stale_format?``).
|
|
23
|
+
|
|
24
|
+
- ``Familia.stale_indexes`` and ``Familia.assert_indexes_current!`` detect
|
|
25
|
+
class-level unique indexes still holding pre-2.10.0 JSON-encoded identifiers and
|
|
26
|
+
fail fast (or warn) at boot. Rebuild with ``Familia.stale_indexes.each(&:rebuild!)``.
|
|
27
|
+
|
|
28
|
+
- ``Familia.legacy_json_encoded?`` predicate for detecting legacy-format
|
|
29
|
+
identifiers.
|
|
30
|
+
|
|
31
|
+
Changed
|
|
32
|
+
-------
|
|
33
|
+
|
|
34
|
+
- New ``encryption_hkdf_salt`` and ``encryption_hkdf_salt_history`` settings
|
|
35
|
+
configure the AES-GCM HKDF salt, decoupled from ``encryption_personalization``
|
|
36
|
+
(now used only by the XChaCha20/BLAKE2b providers, still capped at 16 bytes). The
|
|
37
|
+
salt has no length limit and supports rotation; no data migration is required
|
|
38
|
+
(issue #311).
|
|
39
|
+
|
|
40
|
+
- ``feature :external_identifier`` accepts a callable ``secret:``, resolved lazily
|
|
41
|
+
at first use (issue #311).
|
|
42
|
+
|
|
43
|
+
- The opt-in request-scoped key cache keys on the resolved salt, so an encrypt and a
|
|
44
|
+
later decrypt of the same value within one request share a single derived key
|
|
45
|
+
(issue #311).
|
|
46
|
+
|
|
47
|
+
Fixed
|
|
48
|
+
-----
|
|
49
|
+
|
|
50
|
+
- ``SortedSet#members(n)`` / ``#revmembers(n)`` returned one fewer element than
|
|
51
|
+
requested.
|
|
52
|
+
|
|
53
|
+
- Participation permission queries (``<collection>_with_permission``) now return
|
|
54
|
+
matching members instead of raising on a non-existent ``zrangebyscore`` call.
|
|
55
|
+
|
|
56
|
+
- Class-level ``Model.destroy!(id)`` now removes the record's unique-index entries
|
|
57
|
+
instead of leaving them orphaned, so a stale ``find_by_<field>`` can no longer
|
|
58
|
+
resolve a deleted record.
|
|
59
|
+
|
|
60
|
+
- Changing a ``unique_index`` field value and saving now removes the old value's
|
|
61
|
+
index entry in the same transaction (``multi_index`` keeps add-only semantics).
|
|
62
|
+
|
|
63
|
+
Security
|
|
64
|
+
--------
|
|
65
|
+
|
|
66
|
+
- ``Familia::VerifiableIdentifier`` now requires ``VERIFIABLE_ID_HMAC_SECRET``
|
|
67
|
+
(issue #310, S1). The committed fallback secret, which allowed identifier
|
|
68
|
+
forgery, is removed; the secret is read lazily, so requiring the file without it
|
|
69
|
+
set does not raise.
|
|
70
|
+
|
|
71
|
+
- AES-GCM keys derive from a per-deployment ``encryption_hkdf_salt`` instead of a
|
|
72
|
+
static library salt (issue #310, S2). A blank salt or personalization now raises
|
|
73
|
+
rather than silently using a weak/global value; existing ciphertext still
|
|
74
|
+
decrypts (issue #311).
|
|
75
|
+
|
|
76
|
+
- External identifiers derive via SHA-256, or keyed HMAC-SHA256 with a ``secret:``,
|
|
77
|
+
instead of a Mersenne-Twister PRNG seeded from a truncated digest (issue #310, S3).
|
|
78
|
+
|
|
79
|
+
- ``ParticipationMembership#target_instance`` resolves class names through the
|
|
80
|
+
``Familia.resolve_class`` allowlist instead of ``Object.const_get`` (issue #310, S4).
|
|
81
|
+
|
|
82
|
+
- The request-scoped key cache is wiped on entry to ``with_request_cache`` as well
|
|
83
|
+
as on exit, so a reused fiber cannot inherit another request's keys (issue #310, S6).
|
|
84
|
+
|
|
85
|
+
Documentation
|
|
86
|
+
-------------
|
|
87
|
+
|
|
88
|
+
- Documented the relationship introspection API and stale-index boot guard across
|
|
89
|
+
the relationships guide and methods reference. Renamed
|
|
90
|
+
``docs/migrating/v2.10.0.md`` to ``docs/migrating/v2.10.md``.
|
|
91
|
+
|
|
92
|
+
- Clarified that the migration ``Script`` SHA-1 is the Redis ``EVALSHA`` identity,
|
|
93
|
+
not a security checksum (issue #310, S5).
|
|
94
|
+
|
|
95
|
+
AI Assistance
|
|
96
|
+
-------------
|
|
97
|
+
|
|
98
|
+
- The 2.11.0 changes, their tryouts, and this changelog were drafted with AI
|
|
99
|
+
assistance.
|
|
100
|
+
|
|
101
|
+
.. _changelog-2.10.1:
|
|
102
|
+
|
|
103
|
+
2.10.1 — 2026-06-06
|
|
104
|
+
===================
|
|
105
|
+
|
|
106
|
+
Added
|
|
107
|
+
-----
|
|
108
|
+
|
|
109
|
+
- ``record_class:`` option for collection DataTypes (``list``/``set``/
|
|
110
|
+
``sorted_set``/``hashkey``). This loading-only hint tells ``each_record`` which
|
|
111
|
+
class to hydrate via ``load_multi`` without changing how the collection
|
|
112
|
+
serializes or deserializes reads. Use this when you want ``each_record`` lookup
|
|
113
|
+
behavior but no changes to read behavior. Issue #297
|
|
114
|
+
|
|
115
|
+
- ``Familia.atomic_write(*instances)`` persists multiple Horreum instances in a
|
|
116
|
+
single ``MULTI/EXEC``. Includes an optional ``watch_keys:``/``pre_check:``
|
|
117
|
+
variant for race-safe, write-once semantics. All participating instances must
|
|
118
|
+
resolve to the same logical database (raising ``Familia::CrossDatabaseError``
|
|
119
|
+
otherwise) and must share a hash slot on Redis Cluster. #296
|
|
120
|
+
|
|
121
|
+
Changed
|
|
122
|
+
-------
|
|
123
|
+
|
|
124
|
+
- ``participates_in`` / ``class_participates_in`` collections now default to
|
|
125
|
+
using ``record_class:``. This change requires **no data migration and causes no
|
|
126
|
+
behavior changes**: existing collections already stored raw identifiers, and
|
|
127
|
+
read operations (``members``, ``to_a``, ``member?``, ``score``) behave exactly
|
|
128
|
+
as before. The only difference is that ``each_record`` is now supported. Pre-
|
|
129
|
+
declared collections are left untouched. Issue #297
|
|
130
|
+
|
|
131
|
+
Fixed
|
|
132
|
+
-----
|
|
133
|
+
|
|
134
|
+
- Enabled ``each_record`` on ``participates_in`` and ``class_participates_in``
|
|
135
|
+
collections by automatically declaring them with ``record_class: <participant
|
|
136
|
+
class>``. This resolves ``Familia::Problem`` exceptions and loads participant
|
|
137
|
+
records via ``load_multi`` across all collection types. Issue #297
|
|
138
|
+
|
|
139
|
+
- Suppressed per-member ``[deserialize] Raw fallback`` warning storm when
|
|
140
|
+
iterating ``record_class`` collections with non-JSON identifiers (such as UUIDs
|
|
141
|
+
or prefixed IDs). These expected raw values are now logged at the debug level
|
|
142
|
+
instead of warnings. Issue #297
|
|
143
|
+
|
|
144
|
+
- Resolved a connection-pooling bug where the ``WATCH``-based optimistic lock
|
|
145
|
+
in ``atomic_write(watch_keys:)``, ``save_if_not_exists!``, and ``create!`` was
|
|
146
|
+
silent/inert. The ``WATCH`` and ``MULTI/EXEC`` commands are now driven through
|
|
147
|
+
the same connection, ensuring concurrent modifications correctly abort and raise
|
|
148
|
+
as
|
|
149
|
+
documented. #296
|
|
150
|
+
|
|
151
|
+
AI Assistance
|
|
152
|
+
-------------
|
|
153
|
+
|
|
154
|
+
- AI diagnosed the participation iteration bug and identified that ``reference: true``
|
|
155
|
+
introduced unintended read-behavior changes. Designed and implemented the
|
|
156
|
+
``record_class:`` option to decouple ``each_record`` lookup from read deserialization,
|
|
157
|
+
suppressed a resulting per-member deserialize warning storm, kept intentional
|
|
158
|
+
raw-string semantics on ``instances`` and ``unique_index``, updated stale
|
|
159
|
+
flowcharts in ``datatype-collections.md``, and added regression coverage. Issue #297
|
|
160
|
+
|
|
161
|
+
- Root-caused and fixed a split-connection defect with Claude Code: implemented a
|
|
162
|
+
single-connection ``execute_watched_transaction`` primitive (avoiding fiber-pinning
|
|
163
|
+
that degrades atomic commands) and added real concurrent-modification tests to
|
|
164
|
+
replace simulated aborts. #296
|
|
165
|
+
|
|
166
|
+
- Designed and built multi-model atomic writes on top of the new ``WATCH`` primitive:
|
|
167
|
+
implemented the same-database guard, orchestration logic, and a test suite covering
|
|
168
|
+
two-model commits, rollback on error, cross-database rejection, and race conditions. #296
|
|
169
|
+
|
|
10
170
|
.. _changelog-2.10.0:
|
|
11
171
|
|
|
12
172
|
2.10.0 — 2026-06-04
|
data/Gemfile
CHANGED
|
@@ -15,6 +15,12 @@ end
|
|
|
15
15
|
group :development, :test do
|
|
16
16
|
gem 'benchmark', '~> 0.4', require: false
|
|
17
17
|
gem 'debug', require: false
|
|
18
|
+
# reek pulls in dry-configurable transitively (via dry-schema). Its 1.4.0
|
|
19
|
+
# release bumped the minimum Ruby to 3.3, which breaks `bundle install` on
|
|
20
|
+
# Ruby 3.2 (our gemspec's required_ruby_version floor). 1.4.0 only adds
|
|
21
|
+
# Config#to_data, which we don't use (we don't use Dry::Configurable at all),
|
|
22
|
+
# so cap below 1.4 to keep the dev bundle installable on Ruby 3.2.
|
|
23
|
+
gem 'dry-configurable', '>= 1.3', '< 1.5', require: false
|
|
18
24
|
gem 'irb', '~> 1.15.2', require: false
|
|
19
25
|
gem 'json_schemer', '~> 2.0', require: false
|
|
20
26
|
gem 'rake', '~> 13.0', require: false
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
familia (2.
|
|
4
|
+
familia (2.11.0)
|
|
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
32
|
dry-configurable (1.3.0)
|
|
33
33
|
dry-core (~> 1.1)
|
|
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
|
|
@@ -201,6 +204,7 @@ DEPENDENCIES
|
|
|
201
204
|
benchmark (~> 0.4)
|
|
202
205
|
concurrent-ruby (~> 1.3.6)
|
|
203
206
|
debug
|
|
207
|
+
dry-configurable (>= 1.3, < 1.5)
|
|
204
208
|
familia!
|
|
205
209
|
irb (~> 1.15.2)
|
|
206
210
|
json_schemer (~> 2.0)
|
data/README.md
CHANGED
|
@@ -475,6 +475,32 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
475
475
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
476
476
|
5. Open a Pull Request
|
|
477
477
|
|
|
478
|
+
### Running the Tests
|
|
479
|
+
|
|
480
|
+
Familia's test suite uses the [Tryouts](https://github.com/delano/tryouts)
|
|
481
|
+
framework. The simplest way to run it:
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
bundle install
|
|
485
|
+
bundle exec rake test # full suite, with a guaranteed UTF-8 locale
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
`rake test` runs the suite in a child process with a UTF-8 locale, which the
|
|
489
|
+
suite needs: some tryouts contain UTF-8 source (e.g. Unicode cases) and assert on
|
|
490
|
+
string encodings, so when no locale is set Ruby falls back to
|
|
491
|
+
`Encoding.default_external = US-ASCII`, the runner aborts with `invalid byte
|
|
492
|
+
sequence in US-ASCII`, and encoding specs fail spuriously. (A locale you already
|
|
493
|
+
have is kept; most shells and CI provide a UTF-8 one.)
|
|
494
|
+
|
|
495
|
+
To invoke the runner directly — e.g. for a single file — ensure your shell has a
|
|
496
|
+
UTF-8 locale first:
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
export LANG=C.UTF-8 # or en_US.UTF-8; only needed if unset
|
|
500
|
+
bundle exec try -vf # full suite
|
|
501
|
+
bundle exec try try/path/to/foo_try.rb # a single file
|
|
502
|
+
```
|
|
503
|
+
|
|
478
504
|
### PR Compliance Checks
|
|
479
505
|
|
|
480
506
|
Pull requests are automatically reviewed by [Qodo Merge](https://qodo.ai) with compliance checks for:
|
data/Rakefile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Run the Tryouts test suite with a guaranteed UTF-8 locale.
|
|
4
|
+
#
|
|
5
|
+
# The suite's source and specs are UTF-8. The tryouts runner reads each test file
|
|
6
|
+
# with File.read, which honours Encoding.default_external; when the caller's
|
|
7
|
+
# locale is unset that defaults to US-ASCII and the run aborts on non-ASCII source
|
|
8
|
+
# ("invalid byte sequence in US-ASCII"), with encoding-sensitive specs failing
|
|
9
|
+
# spuriously. Running the suite in a child process with a UTF-8 locale makes it
|
|
10
|
+
# work regardless of the caller's environment. A caller that already has a UTF-8
|
|
11
|
+
# locale (CI, most shells) keeps it -- the defaults below only fill in when unset.
|
|
12
|
+
desc 'Run the Tryouts test suite (ensures a UTF-8 locale)'
|
|
13
|
+
task :test do
|
|
14
|
+
ENV['LANG'] ||= 'C.UTF-8'
|
|
15
|
+
ENV['LC_ALL'] ||= 'C.UTF-8'
|
|
16
|
+
sh 'bundle exec try -vf'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
task default: :test
|