familia 2.13.0 → 2.14.0.pre1
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/dependabot.yml +5 -0
- data/.github/workflows/ci.yml +86 -0
- data/.github/workflows/claude-code-review.yml +1 -1
- data/.github/workflows/claude.yml +1 -1
- data/.rubocop.yml +3 -0
- data/AGENTS.md +2 -1
- data/CHANGELOG.rst +216 -0
- data/Gemfile +8 -3
- data/Gemfile.lock +23 -23
- data/README.md +3 -0
- data/changelog.d/README.md +6 -4
- data/changelog.d/fragments/.gitkeep +0 -0
- data/changelog.d/scriv.ini +1 -0
- data/docs/guides/datatype-collections.md +3 -1
- data/docs/guides/encryption.md +3 -2
- data/docs/guides/feature-encrypted-fields.md +8 -6
- data/docs/guides/feature-relationships-indexing.md +189 -3
- data/docs/guides/feature-relationships-participation.md +6 -0
- data/docs/guides/feature-relationships.md +6 -0
- data/docs/guides/getting-started.md +8 -3
- data/docs/guides/index.md +18 -1
- data/docs/guides/optimized-loading.md +1 -1
- data/docs/guides/schema-validation.md +14 -6
- data/docs/migrating/dbkey-encoding.md +233 -0
- data/docs/migrating/key-patterns-and-index-rebuilds.md +303 -0
- data/docs/migrating/refresh.md +187 -0
- data/docs/migrating/runtime-dependencies.md +175 -0
- data/docs/migrating/transaction-replies.md +350 -0
- data/docs/overview.md +39 -25
- data/docs/reference/api-technical.md +235 -21
- data/docs/reference/migrations-specification.md +1 -0
- data/docs/reference/transaction_safety.md +175 -19
- data/examples/encrypted_fields.rb +9 -8
- data/examples/json_usage_patterns.rb +3 -2
- data/familia.gemspec +3 -4
- data/lib/familia/atomic_operations.rb +16 -5
- data/lib/familia/connection/behavior.rb +22 -0
- data/lib/familia/connection/handlers.rb +39 -0
- data/lib/familia/connection/operations.rb +13 -3
- data/lib/familia/connection/scope.rb +299 -0
- data/lib/familia/connection/transaction_core.rb +12 -3
- data/lib/familia/connection.rb +113 -0
- data/lib/familia/data_type/collection_base.rb +1 -1
- data/lib/familia/data_type/connection.rb +5 -0
- data/lib/familia/data_type/serialization.rb +35 -12
- data/lib/familia/data_type/types/counter.rb +21 -2
- data/lib/familia/data_type/types/hashkey.rb +153 -60
- data/lib/familia/data_type/types/json_stringkey.rb +75 -26
- data/lib/familia/data_type/types/listkey.rb +75 -16
- data/lib/familia/data_type/types/lock.rb +54 -2
- data/lib/familia/data_type/types/sorted_set.rb +193 -97
- data/lib/familia/data_type/types/stringkey.rb +30 -5
- data/lib/familia/data_type/types/unsorted_set.rb +54 -18
- data/lib/familia/data_type.rb +118 -6
- data/lib/familia/encryption/encrypted_data.rb +6 -6
- data/lib/familia/encryption/manager.rb +6 -6
- data/lib/familia/encryption/strict_base64.rb +41 -0
- data/lib/familia/encryption.rb +10 -8
- data/lib/familia/errors.rb +108 -9
- data/lib/familia/features/expiration/extensions.rb +7 -1
- data/lib/familia/features/expiration.rb +66 -24
- data/lib/familia/features/external_identifier.rb +7 -0
- data/lib/familia/features/housekeeping/enforce_collection_caps.rb +4 -0
- data/lib/familia/features/housekeeping.rb +4 -0
- data/lib/familia/features/object_identifier.rb +7 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +442 -65
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +7 -5
- data/lib/familia/features/relationships/indexing/record_key_ownership.rb +501 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +76 -6
- data/lib/familia/features/relationships/indexing.rb +71 -13
- data/lib/familia/features/relationships/participation/participant_methods.rb +22 -6
- data/lib/familia/features/relationships/participation/staged_operations.rb +10 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +33 -8
- data/lib/familia/features/relationships/participation/through_model_operations.rb +23 -0
- data/lib/familia/features/relationships/participation.rb +41 -10
- data/lib/familia/features/relationships.rb +14 -0
- data/lib/familia/features/schema_validation.rb +8 -0
- data/lib/familia/field_type.rb +2 -2
- data/lib/familia/horreum/atomic_write.rb +6 -3
- data/lib/familia/horreum/connection.rb +4 -1
- data/lib/familia/horreum/database_commands.rb +28 -7
- data/lib/familia/horreum/definition.rb +11 -7
- data/lib/familia/horreum/management/audit.rb +443 -73
- data/lib/familia/horreum/management/audit_report.rb +22 -4
- data/lib/familia/horreum/management/repair.rb +66 -13
- data/lib/familia/horreum/management.rb +232 -67
- data/lib/familia/horreum/persistence.rb +332 -54
- data/lib/familia/horreum/related_fields.rb +51 -32
- data/lib/familia/horreum/serialization.rb +78 -18
- data/lib/familia/horreum.rb +98 -14
- data/lib/familia/index_descriptor.rb +28 -2
- data/lib/familia/logging.rb +8 -4
- data/lib/familia/migration/base.rb +22 -4
- data/lib/familia/migration/model.rb +77 -5
- data/lib/familia/migration/pipeline.rb +18 -1
- data/lib/familia/migration/rake_tasks.rb +4 -1
- data/lib/familia/migration/registry.rb +135 -43
- data/lib/familia/migration/runner.rb +26 -1
- data/lib/familia/schema_registry.rb +22 -1
- data/lib/familia/utils.rb +193 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +0 -1
- data/lib/middleware/database_logger.rb +29 -0
- data/try/bug_fixes/binary_dbkey_log_messages_try.rb +194 -0
- data/try/bug_fixes/binary_dbkey_update_expiration_try.rb +135 -0
- data/try/bug_fixes/binary_dbkey_watch_abort_message_try.rb +66 -0
- data/try/bug_fixes/binary_identifier_non_ascii_prefix_try.rb +152 -0
- data/try/bug_fixes/binary_identifier_serialize_value_debug_try.rb +98 -0
- data/try/bug_fixes/binary_scope_id_instance_scoped_audit_try.rb +88 -0
- data/try/bug_fixes/glob_prefix_key_patterns_try.rb +262 -0
- data/try/bug_fixes/hashkey_refresh_read_only_try.rb +104 -0
- data/try/bug_fixes/horreum_refresh_absent_fields_try.rb +124 -0
- data/try/bug_fixes/horreum_refresh_aliased_identifier_try.rb +120 -0
- data/try/bug_fixes/horreum_refresh_exception_safety_try.rb +140 -0
- data/try/bug_fixes/horreum_refresh_identifier_forms_try.rb +226 -0
- data/try/bug_fixes/horreum_refresh_identity_try.rb +254 -0
- data/try/bug_fixes/index_bucket_key_audits_try.rb +268 -0
- data/try/bug_fixes/invalid_scope_id_index_tracker_try.rb +108 -0
- data/try/bug_fixes/invalid_scope_id_nested_scope_audit_try.rb +85 -0
- data/try/bug_fixes/invalid_scope_id_non_ascii_value_audit_try.rb +92 -0
- data/try/bug_fixes/invalid_utf8_field_value_index_try.rb +115 -0
- data/try/bug_fixes/invalid_utf8_long_field_value_warning_try.rb +71 -0
- data/try/bug_fixes/multi_byte_delimiter_ownership_try.rb +90 -0
- data/try/bug_fixes/multi_index_blank_value_buckets_try.rb +341 -0
- data/try/bug_fixes/multi_index_blank_value_other_index_try.rb +109 -0
- data/try/bug_fixes/multi_index_rebuild_glob_scope_try.rb +370 -0
- data/try/bug_fixes/multi_index_record_ownership_try.rb +540 -0
- data/try/bug_fixes/multi_index_tail_index_name_ownership_try.rb +87 -0
- data/try/bug_fixes/object_key_type_scans_try.rb +223 -0
- data/try/bug_fixes/partial_write_index_maintenance_try.rb +19 -4
- data/try/bug_fixes/participation_entry_invalid_utf8_try.rb +118 -0
- data/try/bug_fixes/refresh_missing_key_try.rb +160 -0
- data/try/bug_fixes/refresh_operation_mode_try.rb +60 -0
- data/try/bug_fixes/unique_index_guard_identifier_bytes_try.rb +136 -0
- data/try/bug_fixes/unique_index_integer_identifier_try.rb +95 -0
- data/try/edge_cases/legacy_data_detection/deserialization_log_identifier_try.rb +197 -0
- data/try/edge_cases/legacy_data_detection/deserialize_value_override_try.rb +211 -0
- data/try/edge_cases/legacy_data_detection/find_by_id_legacy_string_try.rb +2 -2
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -0
- data/try/features/encryption/bundled_gem_independence_try.rb +99 -0
- data/try/features/relationships/multi_index_ownership_concurrent_declaration_try.rb +115 -0
- data/try/features/relationships/rebuild_via_scan_try.rb +15 -2
- data/try/features/relationships/relationships_performance_try.rb +13 -8
- data/try/features/schema_registry_try.rb +63 -0
- data/try/integration/connection/connection_scope_try.rb +594 -0
- data/try/integration/futures/admin_futures_try.rb +82 -0
- data/try/integration/futures/conditional_write_scripting_try.rb +100 -0
- data/try/integration/futures/expiration_futures_try.rb +113 -0
- data/try/integration/futures/features_futures_try.rb +70 -0
- data/try/integration/futures/hashkey_futures_try.rb +353 -0
- data/try/integration/futures/horreum_management_futures_try.rb +149 -0
- data/try/integration/futures/horreum_persistence_futures_try.rb +177 -0
- data/try/integration/futures/listkey_futures_try.rb +264 -0
- data/try/integration/futures/migration_futures_try.rb +166 -0
- data/try/integration/futures/related_field_predicates_futures_try.rb +101 -0
- data/try/integration/futures/relationships_futures_try.rb +469 -0
- data/try/integration/futures/scalar_futures_try.rb +416 -0
- data/try/integration/futures/sorted_set_futures_try.rb +320 -0
- data/try/integration/futures/subsecond_expiration_futures_try.rb +145 -0
- data/try/integration/futures/unsorted_set_futures_try.rb +187 -0
- data/try/integration/persistence_operations_try.rb +1 -0
- data/try/integration/transaction_safety_core_try.rb +6 -5
- data/try/integration/transaction_safety_workflow_try.rb +8 -7
- data/try/migration/model_try.rb +44 -0
- data/try/migration/schema_validation_try.rb +217 -0
- data/try/support/debugging/debug_aad_process.rb +1 -0
- data/try/support/debugging/debug_concealed_internal.rb +1 -0
- data/try/support/debugging/debug_concealed_reveal.rb +1 -0
- data/try/support/debugging/debug_context_aad.rb +1 -0
- data/try/support/debugging/debug_context_simple.rb +1 -0
- data/try/support/debugging/debug_cross_context.rb +1 -0
- data/try/support/debugging/debug_database_load.rb +1 -0
- data/try/support/debugging/debug_encrypted_json_check.rb +1 -0
- data/try/support/debugging/debug_encrypted_json_step_by_step.rb +1 -0
- data/try/support/debugging/debug_exists_lifecycle.rb +1 -0
- data/try/support/debugging/debug_field_decrypt.rb +1 -0
- data/try/support/debugging/debug_fresh_cross_context.rb +1 -0
- data/try/support/debugging/debug_load_path.rb +1 -0
- data/try/support/debugging/debug_method_resolution.rb +1 -0
- data/try/support/debugging/debug_provider.rb +1 -0
- data/try/support/debugging/debug_secure_behavior.rb +1 -0
- data/try/support/debugging/debug_test_design.rb +1 -0
- data/try/support/helpers/fresh_ruby.rb +28 -0
- data/try/support/helpers/test_helpers.rb +5 -4
- data/try/unit/atomic_operations_try.rb +36 -0
- data/try/unit/core/escape_glob_try.rb +107 -0
- data/try/unit/core/runtime_dependencies_try.rb +56 -0
- data/try/unit/horreum/base_try.rb +8 -5
- data/try/unit/horreum/related_field_lifecycle_try.rb +254 -1
- data/try/unit/horreum/serialization_try.rb +7 -0
- data/try/unit/utils/future_aware_helpers_try.rb +95 -2
- metadata +74 -38
- data/changelog.d/fragments/20260901_221800_delano_find_by_id_legacy_string.rst +0 -17
- data/changelog.d/fragments/20260902_043148_delano_406_valid_debug_log.md +0 -7
- data/changelog.d/fragments/20260902_043513_delano_407_deserialization_log_redaction.md +0 -7
- data/changelog.d/fragments/20260902_120000_delano_405_envelope_provenance.rst +0 -32
- data/changelog.d/fragments/20260902_130000_delano_408_envelope_encoding_validation.rst +0 -26
- data/changelog.d/fragments/20260904_120000_delano_security_audit_anchors.rst +0 -4
- data/changelog.d/fragments/20260904_150000_delano_encryption_identifier_precondition.rst +0 -6
- data/changelog.d/fragments/20260904_160000_delano_rebuild_via_scan_failure.rst +0 -4
- data/changelog.d/fragments/20260905_000000_delano_external_identifier_format_validation.rst +0 -4
- data/changelog.d/fragments/20260905_000500_delano_concurrent_rebuild_temp_key.rst +0 -78
- data/changelog.d/fragments/20260905_111500_delano_identifier_secret_env.rst +0 -16
- data/changelog.d/fragments/20260905_213000_delano_related_field_lifecycle.rst +0 -104
- data/changelog.d/fragments/20260906_203000_delano_unique_index_rebuild_fallback.rst +0 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78abdfef8c91211092845604967187b510a3b01f8b00772340cfb1c6ff081819
|
|
4
|
+
data.tar.gz: 8e982398843fb0c227be798af2c3b6de60b2daf40a6d9e8791ac4c122e586f55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d612da7f16d268eb3b214a43cfa8f53c26e7766e70294ce3dabd134e2ca1c065726811f87c0e4dccb2aaacb463aba05a3e13148c7d8b6578933361c1c62c9e8
|
|
7
|
+
data.tar.gz: 39a032c23215875127fb97cdcbb7501ee28a9e624df60e81ab5feab700feeb1fdeae98df28bfbf660271d622aa77a74a3004ea9a4b8061345e0972e40ecb4094
|
data/.github/dependabot.yml
CHANGED
|
@@ -13,3 +13,8 @@ updates:
|
|
|
13
13
|
directory: "/" # Location of package manifests
|
|
14
14
|
schedule:
|
|
15
15
|
interval: "weekly"
|
|
16
|
+
ignore:
|
|
17
|
+
# dry-configurable 1.4.0 requires Ruby >= 3.3. Remove with Ruby 3.2
|
|
18
|
+
# support (see the Gemfile).
|
|
19
|
+
- dependency-name: "dry-configurable"
|
|
20
|
+
versions: [">= 1.4"]
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -13,6 +13,92 @@ permissions:
|
|
|
13
13
|
contents: read
|
|
14
14
|
|
|
15
15
|
jobs:
|
|
16
|
+
# The repo's own bundle includes development and test gems, which can supply
|
|
17
|
+
# a library that familia requires but does not declare (base64 arrived this
|
|
18
|
+
# way, through ruby-prof). This job bundles familia alone, as an application
|
|
19
|
+
# does, and fails if it cannot be required or if Ruby warns that a required
|
|
20
|
+
# library is leaving the default gems.
|
|
21
|
+
consumer-load:
|
|
22
|
+
timeout-minutes: 10
|
|
23
|
+
|
|
24
|
+
runs-on: ubuntu-24.04
|
|
25
|
+
|
|
26
|
+
strategy:
|
|
27
|
+
fail-fast: false
|
|
28
|
+
matrix:
|
|
29
|
+
ruby: ["3.2", "3.3", "3.4", "4.0"]
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v7
|
|
33
|
+
- name: Set up Ruby
|
|
34
|
+
uses: ruby/setup-ruby@v1
|
|
35
|
+
with:
|
|
36
|
+
ruby-version: ${{ matrix.ruby }}
|
|
37
|
+
|
|
38
|
+
- name: Require familia from a bundle that contains only familia
|
|
39
|
+
run: |
|
|
40
|
+
consumer="$RUNNER_TEMP/consumer"
|
|
41
|
+
mkdir -p "$consumer"
|
|
42
|
+
cd "$consumer"
|
|
43
|
+
printf "source 'https://rubygems.org'\ngem 'familia', path: '%s'\n" "$GITHUB_WORKSPACE" > Gemfile
|
|
44
|
+
bundle install
|
|
45
|
+
if ! out=$(bundle exec ruby -e 'require "familia"; puts "familia #{Familia::VERSION} loaded"' 2>&1); then
|
|
46
|
+
echo "$out"
|
|
47
|
+
echo "::error::require 'familia' failed in a bundle that contains only familia"
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
echo "$out"
|
|
51
|
+
if grep -q "loaded from the standard library" <<<"$out"; then
|
|
52
|
+
echo "::error::familia requires a library that is leaving Ruby's default gems without declaring it in familia.gemspec"
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# An application can resolve any release familia.gemspec allows, not only
|
|
57
|
+
# the newest one consumer-load gets. This job pins each runtime dependency
|
|
58
|
+
# to the lowest release the gemspec allows and fails if familia, or
|
|
59
|
+
# json_schemer (which familia requires on first schema validation), cannot
|
|
60
|
+
# be required. Ruby 3.4 is the first release that raises LoadError under
|
|
61
|
+
# Bundler for libraries that left the default gems (base64, bigdecimal).
|
|
62
|
+
# Warnings are not checked here: oj 3.16.2 to 3.16.4 warn about ostruct
|
|
63
|
+
# but load, and consumer-load checks warnings with the newest releases.
|
|
64
|
+
consumer-load-lowest:
|
|
65
|
+
timeout-minutes: 10
|
|
66
|
+
|
|
67
|
+
runs-on: ubuntu-24.04
|
|
68
|
+
|
|
69
|
+
steps:
|
|
70
|
+
- uses: actions/checkout@v7
|
|
71
|
+
- name: Set up Ruby
|
|
72
|
+
uses: ruby/setup-ruby@v1
|
|
73
|
+
with:
|
|
74
|
+
ruby-version: "3.4"
|
|
75
|
+
|
|
76
|
+
- name: Require familia with the lowest dependency releases it allows
|
|
77
|
+
run: |
|
|
78
|
+
consumer="$RUNNER_TEMP/consumer-lowest"
|
|
79
|
+
mkdir -p "$consumer"
|
|
80
|
+
cd "$consumer"
|
|
81
|
+
ruby - "$GITHUB_WORKSPACE" > Gemfile <<'RUBY'
|
|
82
|
+
workspace = ARGV.fetch(0)
|
|
83
|
+
spec = Dir.chdir(workspace) { Gem::Specification.load('familia.gemspec') }
|
|
84
|
+
puts "source 'https://rubygems.org'"
|
|
85
|
+
puts "gem 'familia', path: #{workspace.dump}"
|
|
86
|
+
spec.runtime_dependencies.each do |dep|
|
|
87
|
+
found, = Gem::SpecFetcher.fetcher.spec_for_dependency(dep)
|
|
88
|
+
lowest = found.map { |s, _source| s.version }.min or abort "no release satisfies #{dep}"
|
|
89
|
+
puts "gem #{dep.name.dump}, #{lowest.to_s.dump}"
|
|
90
|
+
end
|
|
91
|
+
RUBY
|
|
92
|
+
cat Gemfile
|
|
93
|
+
bundle install
|
|
94
|
+
probe='require "familia"; require "json_schemer"; puts "familia #{Familia::VERSION} and json_schemer #{JSONSchemer::VERSION} loaded"'
|
|
95
|
+
if ! out=$(bundle exec ruby -e "$probe" 2>&1); then
|
|
96
|
+
echo "$out"
|
|
97
|
+
echo "::error::familia or json_schemer failed to load with the lowest dependency releases familia.gemspec allows"
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
echo "$out"
|
|
101
|
+
|
|
16
102
|
build:
|
|
17
103
|
timeout-minutes: 10
|
|
18
104
|
|
|
@@ -48,7 +48,7 @@ jobs:
|
|
|
48
48
|
|
|
49
49
|
- name: Run Claude Code Review
|
|
50
50
|
id: claude-review
|
|
51
|
-
uses: anthropics/claude-code-action@
|
|
51
|
+
uses: anthropics/claude-code-action@d70510b174a1244958df0b03166a54912811f3e6 # v1.0.264
|
|
52
52
|
with:
|
|
53
53
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
54
54
|
|
|
@@ -32,7 +32,7 @@ jobs:
|
|
|
32
32
|
|
|
33
33
|
- name: Run Claude Code
|
|
34
34
|
id: claude
|
|
35
|
-
uses: anthropics/claude-code-action@
|
|
35
|
+
uses: anthropics/claude-code-action@d70510b174a1244958df0b03166a54912811f3e6 # v1.0.264
|
|
36
36
|
with:
|
|
37
37
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
38
38
|
|
data/.rubocop.yml
CHANGED
|
@@ -30,6 +30,9 @@ AllCops:
|
|
|
30
30
|
- "rspec/**/*.rb"
|
|
31
31
|
- "tmp/**/*.rb"
|
|
32
32
|
- "try/**/*.rb"
|
|
33
|
+
# Setting Exclude here replaces RuboCop's default list, which excludes
|
|
34
|
+
# vendor/. CI installs gems into vendor/bundle inside the checkout.
|
|
35
|
+
- "vendor/**/*"
|
|
33
36
|
|
|
34
37
|
#
|
|
35
38
|
# Trailing commas
|
data/AGENTS.md
CHANGED
|
@@ -173,8 +173,9 @@ in one MULTI/EXEC; collection mutations auto-route into the open transaction via
|
|
|
173
173
|
`Fiber[:familia_transaction]`. Constraints:
|
|
174
174
|
|
|
175
175
|
- 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.
|
|
176
|
-
- Cannot nest inside another `transaction`/`atomic_write` (`Familia::OperationModeError`).
|
|
176
|
+
- Cannot nest inside another `transaction`/`atomic_write` or run inside a `pipelined` block (`Familia::OperationModeError`).
|
|
177
177
|
- Collection return values inside the block are `Redis::Future` — do not inspect before EXEC.
|
|
178
|
+
- Methods that need a reply to continue (iterators, finders and loaders, `fetch`, `refresh!`, the partial writers) raise `Familia::OperationModeError` inside the block. The policy and method lists are in docs/reference/transaction_safety.md, rule 4.
|
|
178
179
|
|
|
179
180
|
**Factory — `build` for create-and-populate:**
|
|
180
181
|
|
data/CHANGELOG.rst
CHANGED
|
@@ -7,6 +7,222 @@ 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.14.0.pre1:
|
|
11
|
+
|
|
12
|
+
2.14.0.pre1 — 2026-11-03
|
|
13
|
+
========================
|
|
14
|
+
|
|
15
|
+
Added
|
|
16
|
+
-----
|
|
17
|
+
|
|
18
|
+
- Added ``Familia::IdentifierMismatchError`` (a ``Familia::HorreumError``), with ``#key`` and ``#resolved_key``. Its message shows a key whose bytes are not valid UTF-8 with every invalid byte written as ``\xNN``. #445 #453
|
|
19
|
+
- ``Horreum#deserialize_value`` accepts ``dbkey:``, the key the value was read from. The error log entry for a value that is not JSON names it. Familia passes ``dbkey:`` only when every ``deserialize_value`` in the object's super chain accepts it, by name or through ``**``; an override with the 2.13.0 signature, ``deserialize_value(val, symbolize: false, field_name: nil)``, is called without it. #445
|
|
20
|
+
- Added ``Familia.escape_glob(str)``, which escapes ``*``, ``?``, ``[``, ``]`` and ``\`` for use in KEYS and SCAN MATCH patterns. It escapes bytes and keeps the input's encoding, so it accepts strings with invalid UTF-8 and binary data. #446
|
|
21
|
+
- Added ``Familia::Horreum.dbkey_pattern(identifier_glob = '*', key_suffix = suffix)``, which returns a key pattern with the class prefix, delimiter and suffix escaped, and ``Familia::Horreum::OBJECT_KEY_TYPE`` (``"hash"``), the Redis type of an object key. #446
|
|
22
|
+
- Added ``Familia::IndexBucketConflictError`` (a ``Familia::PersistenceError``), whose ``conflicts`` maps each key to the type it holds, or to ``:too_many_candidate_owners`` for a key whose owner was not checked, and whose ``owners`` maps each key of another record to that record's hash key. #446
|
|
23
|
+
- Added ``Familia::Features::Relationships::Indexing::RecordKeyOwnership::MAX_CANDIDATE_SPLITS`` (8), the most candidate owner records the ``multi_index`` rebuild and audits check for one key, and ``Familia::Features::Relationships::Indexing::RecordKeyOwnership::MAX_CANDIDATE_BATCH_BYTES`` (1 MiB), the most bytes of candidate owner identifiers they check in one pipeline. An identifier longer than that is checked in a pipeline of its own. #446
|
|
24
|
+
- ``audit_multi_indexes`` results include ``foreign_owned_keys``: the sets under the bucket prefix that the rebuild leaves alone, each with ``key``, ``field_value``, ``owner_key`` and ``reason`` (``:other_record``, or ``:too_many_candidate_owners`` with a nil ``owner_key``), plus ``scope_id`` for an instance-scoped index. An entry without an ``owner_key`` sets the status to ``:issues_found`` and makes ``AuditReport#healthy?`` false. Each ``multi_indexes`` entry of ``AuditReport#to_h`` has ``:foreign_owned_keys``, the number of these keys, and each ``multi_index`` line of ``AuditReport#to_s`` ends with ``foreign_owned_keys=<n>``. They had neither. #446
|
|
25
|
+
- Added ``Familia::Migration::Model#scan_type``, the SCAN ``TYPE`` option of a model migration. It defaults to ``"hash"`` when ``@scan_pattern`` is left unset and to ``nil`` (every type) otherwise. #446
|
|
26
|
+
- Added ``Familia.transform_reply(reply) { |r| ... }``, which converts a concrete command reply and returns a ``Redis::Future`` untouched. #447
|
|
27
|
+
- Added ``Familia.transaction_or_pipeline?``, which reports whether the current fiber is inside a Familia transaction or pipeline block, and ``Familia.assert_replies_available!(operation, conn: nil)``, which raises ``Familia::OperationModeError`` there. When ``conn`` is given it raises only when ``conn`` is a transaction or pipeline connection. #447
|
|
28
|
+
- Added ``Familia::SchemaValidatorLoadError``, a ``Familia::Problem`` whose ``cause`` is the ``LoadError`` that stopped ``json_schemer`` from loading. #448
|
|
29
|
+
- Added ``Familia.normalize_key_encoding(str)``, which returns ``str`` as UTF-8 when its bytes are valid UTF-8 and as BINARY otherwise, the encoding redis-client gives a key or member it reads. The bytes are unchanged. #453
|
|
30
|
+
- Added ``Familia.with_connection_scope(uri = nil) { |client| ... }``. While the block runs, every ``dbclient`` lookup in the calling fiber that would otherwise reach ``CreateConnectionHandler`` (no open transaction or pipeline, no fiber-local connection, no ``connection_provider``, no class-level ``@dbclient``) resolves to one ``Redis`` client per distinct connection configuration (keyed by ``URI#conf``), created on first use with ``Familia.create_dbclient``. The scope closes every client it created when the block exits, on return and on exception. The block receives what ``Familia.dbclient(uri)`` returns inside the scope: the scope's client for ``uri`` (``Familia.uri`` when nil), or, when an open transaction or pipeline, a fiber-local connection or a ``connection_provider`` answers first, that connection, and the scope creates no client for it. A nested call in the same fiber joins the open scope. ``transaction``, ``pipelined`` and ``atomic_write`` inside the block run on the scope's client for their URI when their connection lookup reaches the scope. #456
|
|
31
|
+
- Inside a ``with_connection_scope`` block, between the WATCH and the EXEC of a WATCH-guarded transaction, other lookups get a second scope client for that URI, so a transaction they run cannot clear the WATCH. Inside ``Horreum#watch``, issue the MULTI/EXEC, UNWATCH and DISCARD on the yielded connection; the receiver's own ``transaction``, ``unwatch`` and ``discard`` run on the second client and are not guarded by the WATCH. #456
|
|
32
|
+
- Threads and fibers spawned inside a ``with_connection_scope`` block are not the owner and resolve connections as if no scope were open. An open transaction or pipeline, a fiber-local connection, a ``connection_provider`` and a class-level ``@dbclient`` take precedence over the scope. Connection resolution outside a scope is unchanged: without a provider, lookups still create fresh clients. #456
|
|
33
|
+
- A ``Familia.middleware_version`` bump (``Familia.reconnect!``) makes an open connection scope retire its clients on the next lookup: new lookups get fresh clients and the retired ones stay open until the block exits. #456
|
|
34
|
+
- Added ``Familia.current_connection_scope``, returning the ``Familia::Connection::Scope`` the current fiber has open, or nil. #456
|
|
35
|
+
- Added ``Familia::Connection::Scope`` with ``owner``, ``clients``, ``size``, ``active?``, ``released?`` and ``release!``. ``release!`` closes every held client and is safe to call more than once; a later ``Scope#client`` call raises ``Familia::Problem``, as does a ``client`` or ``release!`` call from a fiber other than the owner. #456
|
|
36
|
+
- Added ``Familia::Connection::ScopedConnectionHandler`` (``supports transaction: true, pipelined: true``), placed after ``ProviderConnectionHandler`` in the ``Familia.dbclient`` chain and after ``CachedConnectionHandler`` in the Horreum class chain, before ``CreateConnectionHandler`` in both. The DataType chain is unchanged; it reaches the scope through the parent's chain or ``Familia.dbclient``. #456
|
|
37
|
+
- Added ``with_connection_scope(uri = nil)`` to Horreum classes and DataType instances through ``Familia::Connection::Behavior``, and to Horreum instances, which forward to their class. It opens the same scope and yields the client resolved through the receiver's own chain, so a class-level ``@dbclient`` or ``logical_database`` setting is honoured. #456
|
|
38
|
+
|
|
39
|
+
Changed
|
|
40
|
+
-------
|
|
41
|
+
|
|
42
|
+
- ``familia`` now requires ``redis`` ``5.4`` or later (``>= 5.4, < 6.0``, was ``>= 5.0``). ``Lock#release``, and inside a block ``HashKey#hsetnx``, ``JsonStringKey#setnx`` and ``JsonStringKey#value`` with a default, now pass a reply-coercion block to ``Redis#call``. Earlier ``redis`` versions ignore the block, so with them these methods would return, or their Futures resolve to, ``1`` or ``0`` instead of ``true`` or ``false``. See ``docs/migrating/runtime-dependencies.md``. #447
|
|
43
|
+
- ``familia.gemspec`` requires ``oj`` ``~> 3.16, >= 3.16.2`` (was ``~> 3.16``) and ``json_schemer`` ``~> 2.2`` (was ``~> 2.0``). Bundles that pin an older ``oj`` or ``json_schemer`` no longer resolve. The excluded ``json_schemer`` ``2.0.0`` to ``2.1.1`` require ``base64`` without declaring it; now that ``require 'familia'`` no longer loads ``base64``, they would fail to load on Ruby 3.4+ in a bundle without it. See ``docs/migrating/runtime-dependencies.md``. #448
|
|
44
|
+
- ``require 'familia'`` no longer loads ``base64``. Applications that call ``Base64`` must ``require 'base64'`` themselves and, on Ruby 3.4+, list ``base64`` in their Gemfile. See ``docs/migrating/runtime-dependencies.md``. #448
|
|
45
|
+
- ``Horreum#refresh!`` and ``Horreum#refresh`` raise ``Familia::KeyNotFoundError`` when the record's key does not exist, as documented, and leave the object unchanged. They returned normally (``refresh!`` returned ``[]``), kept the in-memory persistent field values, including an ``objid`` generated to build the key, reset transient fields to ``nil`` and cleared dirty tracking. Code that refreshes a record that may not be saved yet must rescue the error or use ``find_by_id``, which returns ``nil``. See ``docs/migrating/refresh.md``. #443 #445
|
|
46
|
+
- ``HashKey#refresh!`` and ``HashKey#refresh`` raise ``Familia::KeyNotFoundError`` for a missing key instead of ``Redis::CommandError`` (``ERR wrong number of arguments for 'hmset' command``). Update ``rescue`` clauses. See ``docs/migrating/refresh.md``. #443
|
|
47
|
+
- ``Horreum#refresh!``, ``Horreum#refresh``, ``HashKey#refresh!`` and ``HashKey#refresh`` raise ``Familia::OperationModeError`` inside a transaction, pipeline or ``atomic_write`` block, before sending any command. They raised ``NoMethodError`` on the ``Redis::Future`` returned by ``HGETALL``. See ``docs/migrating/refresh.md``. #445 #447
|
|
48
|
+
- ``Horreum#refresh!`` and ``Horreum#refresh`` set every persistent field that the stored hash does not contain to ``nil``, as ``load`` and ``find_by_id`` do. They kept the in-memory value and then cleared dirty tracking. This covers a value set in memory and never saved, including an ``objid`` or ``extid`` generated on first read, a field another writer removed, and a default that an ``init`` hook set on an object built with ``new``. ``objid_generator_used`` is reset to ``nil`` with ``objid``. A Symbol or String identifier field that is not stored keeps its value, including a field named by its ``as:`` method. See ``docs/migrating/refresh.md``. #445
|
|
49
|
+
- ``Horreum#refresh!`` and ``Horreum#refresh`` no longer call ``Horreum#naive_refresh`` or ``Horreum#initialize_with_keyword_args_deserialize_value``, and ``find_by_id``, ``find_by_dbkey``, ``load_multi`` and ``load_multi_by_keys`` no longer call ``initialize_with_keyword_args_deserialize_value``, so an application override of either method does not run when they load a record. A direct ``naive_refresh`` call still calls ``initialize_with_keyword_args_deserialize_value``. #445
|
|
50
|
+
- ``Horreum#refresh!`` and ``Horreum#refresh`` raise ``Familia::IdentifierMismatchError`` and leave the object unchanged when the stored values give no identifier or one that names a different key, for example a stored identifier field that differs from the key, or a missing field that a Proc or method identifier reads. They assigned a stored identifier that differed from the key, so a later ``save`` wrote to another record or created a new one. See ``docs/migrating/refresh.md``. #445
|
|
51
|
+
- ``HashKey#refresh!`` returns the fields it read as a ``Hash``, as ``HashKey#hgetall`` returns them, instead of the ``HMSET`` reply ``"OK"``. #445
|
|
52
|
+
- ``HashKey#refresh!`` and ``HashKey#refresh`` no longer reset the key's expiration. Call ``update_expiration`` to extend it. See ``docs/migrating/refresh.md``. #445
|
|
53
|
+
- ``HashKey#refresh!`` and ``HashKey#refresh`` no longer run the dirty-write check against the parent, so they no longer warn or raise ``Familia::Problem`` when the parent has unsaved fields. #445
|
|
54
|
+
- ``all``, ``scan_pattern``, ``keys_count``, ``scan_count``, ``keys_any?``, ``scan_any?`` and ``scan_keys`` escape the class prefix, delimiter and suffix. Identifier filters are still glob patterns. The ``all`` and ``scan_pattern`` arguments are now literal suffixes. #446
|
|
55
|
+
- ``scan_pattern(nil)`` leaves the suffix out, as ``dbkey`` does, and returns ``"customer:*"`` instead of ``"customer:*:"``. #446
|
|
56
|
+
- ``find_keys`` escapes the class prefix and delimiter. Its suffix argument is still a glob pattern. #446
|
|
57
|
+
- ``all``, ``keys_count``, ``scan_count``, ``keys_any?``, ``scan_any?``, ``scan_keys``, ``audit_instances``, ``rebuild_instances``, the unique-index SCAN rebuild fallback and model migrations with the default ``@scan_pattern`` use only matching keys that hold a hash. ``all``, ``keys_count`` and ``keys_any?`` send ``KEYS`` and then, in one pipeline, one ``TYPE`` per returned key; they sent no ``TYPE``. #446
|
|
58
|
+
- ``Familia::Migration::Model``, and so ``Familia::Migration::Pipeline``, default ``@scan_pattern`` to the model class's ``scan_pattern``, which follows ``Familia.delim`` and the class suffix instead of a hardcoded ``:`` and ``object``. For a class with another suffix or a ``Familia.delim`` other than ``:``, a migration that leaves ``@scan_pattern`` unset now processes the class's records, so an actual run applies its changes to them. The 2.13.0 pattern ``<prefix>:*:object`` matched none of them, so such a migration processed no records and ``Migration::Runner`` still recorded it as applied. ``Runner#run`` skips a migration recorded as applied; ``Runner#run_one`` runs it again. #446
|
|
59
|
+
- The generated ``rebuild_<index>`` of a ``multi_index`` checks every bucket key it will write before it deletes anything. If one holds a type other than set, or is a set field or ``multi_index`` bucket of an existing record of the same class with a longer identifier, it raises ``Familia::IndexBucketConflictError`` and leaves the index and that key unchanged. It raises the same way, before deleting anything, when a key it would write or delete has more than ``MAX_CANDIDATE_SPLITS`` candidate owner records. #446
|
|
60
|
+
- The ``rebuild_<index>`` of a ``multi_index`` reports a bucket key to its progress block in the ``:clearing`` phase, and counts it, only when its ``DEL`` removed the key. #446
|
|
61
|
+
- ``repair_multi_indexes!`` records a rebuild that raises ``Familia::IndexBucketConflictError`` in ``:skipped`` with ``reason: :bucket_conflict``, ``conflicts``, ``owners`` and, for an instance-scoped index, ``scope_id``, and goes on with the next scope or index. ``repair_all!`` returns status ``:partial_failure`` for such an entry. #446
|
|
62
|
+
- The ``multi_index`` rebuild and ``audit_multi_indexes`` pass the ``TYPE`` option to SCAN when they look for bucket sets. ``audit_related_fields`` and ``audit_participations`` pass it for the type the field or collection stores, so keys of another type are not reported or repaired. #446
|
|
63
|
+
- ``scan_count`` (``count!``), ``scan_any?`` (``any!``), ``scan_keys``, ``rebuild_instances``, ``audit_instances``, ``audit_unique_indexes``, ``audit_multi_indexes``, ``audit_participations``, ``audit_related_fields``, ``health_check``, ``repair_all!`` and ``repair_multi_indexes!``; ``repair_instances!``, ``repair_indexes!``, ``repair_participations!`` and ``repair_related_fields!`` called without audit results; the generated ``rebuild_<index>`` of a ``multi_index``; the unique-index SCAN rebuild fallback; and ``Familia::Migration::Model`` and ``Familia::Migration::Pipeline`` migrations with the default ``@scan_pattern`` or a ``@scan_type`` send SCAN with the ``TYPE`` option. They sent SCAN without ``TYPE``. Redis added the ``TYPE`` option to SCAN in 6.0.0, so these calls need Redis 6.0 or later, or another server whose SCAN accepts ``TYPE``. README.md lists Valkey/Redis 6.0+ as a prerequisite in both versions. #446
|
|
64
|
+
- ``audit_related_fields`` sends ``TYPE`` for the hash key of the record a related-field key names and counts the record as existing only when that key holds a hash. When it holds another type, such as a string, set, list or sorted set, ``audit_related_fields`` and ``health_check(audit_collections: true)`` report the related-field key in ``orphaned_keys``, unless they skip it as a ``multi_index`` bucket (see Security), and ``repair_related_fields!`` and ``repair_all!(audit_collections: true)`` delete it with ``DEL``; the record's hash key stays. It sent ``EXISTS``, which counts a key of any type, so none of them reported or deleted such a related-field key. #446
|
|
65
|
+
- The instance-scoped ``add_to_*`` and ``update_in_*`` of a ``multi_index`` no longer add an object whose field value is empty or whitespace only to ``<scope key>:<index>``, the key ``<index>_for`` names for a blank value, as the class-level methods already did. They still record the membership, so ``save`` indexes the object once its value is no longer blank. Inside a transaction, ``add_to_*`` with such a value adds one reply fewer to ``MultiResult#results``, since it queues no ``SADD``. With a ``nil`` value they add and record nothing, as before. The instance-scoped ``find_all_by_*`` and ``sample_from_*`` return ``[]`` for a blank value (``nil``, empty or whitespace only), as the class-level ``sample_from_*`` already did. They returned objects from ``<scope key>:<index>``. #446
|
|
66
|
+
- Keys and related-field members that the instance-scoped ``add_to_*`` and ``update_in_*`` of a ``multi_index`` wrote for blank values before stay in place after upgrading: ``update_in_*`` with a blank old value and ``remove_from_*`` with a blank value no longer send ``SREM`` for the object's identifier to ``<scope key>:<index>``, and ``Horreum#destroy!``, or a ``save`` after the object's hash expired or was deleted, no longer sends it for a blank value the object recorded. The methods of that scope do not read ``<scope key>:<index>`` as data, because it can also be a related field of the scope class with the index's name. When the scope identifier contains the delimiter, the key can also be a bucket of an index of a shorter scope. When the scope identifier is the name of a class-level index of the scope class, or starts with that name and the delimiter, the key can also be a class-level bucket of that index. The audit of that other index reads such a key and its rebuild deletes or rewrites it, unless the scope class declares a set field named ``<index>`` and the scope record with that identifier exists. Familia does not tell such a key apart from a live bucket or a related field; see the indexing guide's "Blank field values" section. #446
|
|
67
|
+
- Inside ``transaction``, ``atomic_write`` and ``pipelined`` blocks, DataType and Horreum methods that only convert a reply now return the command's ``Redis::Future`` instead of raising ``NoMethodError``. Its value is the reply as redis-rb returns it, without Familia's conversion (for example ``empty?`` resolves to the count). Affected: ``empty?`` on every collection, the generated related-field predicates (``user.tags?``, ``User.instances?``), ``HashKey#values``, ``#hgetall``, ``#values_at``, ``#scan``, ``#randfield`` with ``withvalues: true``; ``ListKey#range``, ``#members``, ``#[]`` (and ``#slice``) with a range or a start and length, ``#member?``, ``#pop`` and ``#shift`` with a count; ``SortedSet#score``, ``#member?``, ``#rank``, ``#revrank``, ``#members``, ``#revmembers``, the range readers, ``#at``, ``#first``, ``#last``, ``#popmin``, ``#popmax``, ``#mscore``, ``#union``, ``#inter``, ``#diff``, ``#randmember``, ``#scan``; ``UnsortedSet#members``, ``#intersection``, ``#union``, ``#difference``, ``#scan``, ``#sample``; ``StringKey#size``, ``#empty?``; ``JsonStringKey#char_count``, ``#empty?``; ``Counter#value``, ``#reset``; ``expired?``; ``Horreum.any?``, ``.in_instances?``, ``.multiget``, ``.storage_inspect``; ``Migration::Registry#applied_at``, ``#all_applied``, ``#metadata``; the generated participation methods ``score_in_<target>_<collection>`` and ``in_<target>_<collection>?`` on a sorted-set or list participation. Return values outside a block are unchanged, except ``HashKey#randfield(count, withvalues: true)`` and ``SortedSet#diff(withscores: true)`` (see Fixed). #447
|
|
68
|
+
- Methods that need a reply to continue now raise ``Familia::OperationModeError`` inside ``transaction``, ``atomic_write`` and ``pipelined`` blocks instead of ``NoMethodError`` or a wrong result: ``each`` and the raw iterators on every collection, Ruby's conversion methods (``StringKey#to_s`` and ``#to_i``, which ``Counter`` and ``Lock`` inherit, ``JsonStringKey#to_s``, ``#to_i`` and ``#to_f``, ``to_a`` on ``ListKey``, ``SortedSet`` and ``UnsortedSet``, and ``as_json`` and ``to_json`` on every DataType; a scalar's ``as_json`` returned the GET Future and its ``to_json`` raised ``Familia::SerializerError``), ``HashKey#fetch``, ``Counter#increment_if_less_than``, ``Lock#locked?``, ``Lock#empty?`` (and so the generated predicate of a ``lock`` field), ``Lock#held_by?`` (returned ``false``, even for the holder), ``extend_expiration`` (returned ``false``) and ``ttl_report``. #447
|
|
69
|
+
- The finders and loaders (``find_by_dbkey``, ``find_by_identifier``, ``load_multi``, ``load_multi_by_keys``, ``all``, ``find_by_objid``, ``find_by_extid``), ``keys_count``, ``keys_any?``, ``scan_count``, ``scan_any?``, class-level ``destroy!``, the index finders, rebuilds and ``guard_unique_*!`` methods, the participation readers, ``current_indexings`` (reported every class-level index whose field was set), ``relationship_status``, ``validate_relationships!``, the ``audit_*``, ``health_check``, ``repair_*`` and ``scan_keys`` methods, the index format checks ``IndexDescriptor#stale_format?``, ``#format_current?``, ``Familia.stale_indexes`` and ``Familia.assert_indexes_current!`` (raised ``TypeError`` after queueing HRANDFIELD), ``AtomicOperations.atomic_swap``, ``.sweep_orphaned_temp_keys`` and ``.with_rebuild`` given a transaction or pipeline connection (``with_rebuild`` raised ``NoMethodError`` after queueing its lock), ``run_chores!``, ``EnforceCollectionCaps``, ``Migration::Base.run`` and ``.check_only``, and ``Migration::Runner#run``, ``#run_one``, ``#rollback``, ``#status`` and ``#pending`` also raise ``Familia::OperationModeError`` inside ``transaction``, ``atomic_write`` and ``pipelined`` blocks instead of ``NoMethodError`` or a wrong result. #447
|
|
70
|
+
- ``Migration::Registry#pending``, ``#status``, ``#record_rollback``, ``#schema_changed?``, ``#schema_drift`` and ``#restore_backup`` raise ``Familia::OperationModeError`` when the registry's client is a transaction or pipeline connection. #447
|
|
71
|
+
- ``Migration::Registry#client`` without ``redis:`` returns the current transaction or pipeline connection inside a Familia block without keeping it, so it no longer keeps a connection from a block that has completed. Outside a block it still keeps the first ``Familia.dbclient`` it resolves, and ``#redis`` returns that kept connection (or the ``redis:`` client), never a transaction or pipeline connection. #447
|
|
72
|
+
- A ``Migration::Registry`` built without ``redis:``, as ``Migration::Runner`` builds its default, and first used outside a block kept a plain connection for life: inside a later block its commands ran at once, outside the MULTI, and every registry method returned its usual value. Inside a block such a registry now returns the ``Redis::Future`` from ``applied?``, ``applied_at``, ``all_applied`` and ``metadata`` (``applied?`` resolves to the ZSCORE score or ``nil``, and the Future is truthy), queues ``record_applied`` and returns its Future, and raises ``Familia::OperationModeError`` from ``pending``, ``status``, ``record_rollback``, ``schema_changed?``, ``schema_drift`` and ``restore_backup``. A registry first used inside a block returned ``true`` from ``applied?`` for every migration and kept the block's connection, so a later ``record_applied`` returned a Future and wrote nothing. The block's connection is on the database the block was opened on, so inside a block opened by a model with another ``logical_database`` such a registry's commands go to that database. To keep a registry on its own connection, pass one: ``Familia::Migration::Registry.new(redis: Familia.create_dbclient)``. #447
|
|
73
|
+
- ``save``, ``save_if_not_exists!``, ``create!``, ``build``, ``atomic_write`` and ``Familia.atomic_write`` raise ``Familia::OperationModeError`` inside a pipeline as well as a transaction. Inside a pipeline they previously raised ``Familia::ConflictingContextError`` or a spurious ``Familia::RecordExistsError``. #447
|
|
74
|
+
- Inside a pipeline, the ``add_to_*`` and ``update_in_*`` methods of a ``unique_index`` raise ``Familia::OperationModeError`` before queueing anything, and the error names the method called. ``claim_unique_*!`` names itself the same way inside a transaction or pipeline. Inside a pipeline they previously raised a spurious ``Familia::RecordExistsError`` (``add_to_*`` on an instance-scoped index), ``Familia::ConflictingContextError`` (``update_in_*`` with no field value) or ``Familia::OperationModeError`` naming ``claim_field``, and ``add_to_*`` with no field value returned ``nil``. Inside a transaction they behave as before. #447
|
|
75
|
+
- ``commit_fields``, ``save_fields``, ``multi_field_update`` and ``multi_field_fast_write`` raise ``Familia::OperationModeError`` inside any transaction or pipeline, before changing state. Inside a caller's transaction, ``multi_field_update`` and ``multi_field_fast_write`` previously queued their write, which committed with the outer EXEC; call them before the block or use ``atomic_write``. ``commit_fields`` and ``save_fields`` raised ``NoMethodError`` there. #447
|
|
76
|
+
- The generated staged participation methods (``stage_<name>_instance``, ``stage_<name>``, ``activate_<name>_instance``, ``unstage_<name>_instance``, ``unstage_<name>``) and the add and remove methods of a ``through:`` participation raise ``Familia::OperationModeError`` inside any transaction or pipeline before queueing anything, and the error names the generated method. Inside a caller's transaction, ``unstage_<name>_instance`` previously queued its deletes, which committed with the outer EXEC, and returned ``true``; call it before or after the block. The others raised ``NoMethodError``, ``Familia::ConflictingContextError`` or an error naming an internal call, some after queueing part of their writes. #447
|
|
77
|
+
- ``Horreum#destroy!`` raises ``Familia::OperationModeError`` inside a transaction or pipeline, before queueing anything, when the class has instance-scoped indexes. It previously raised ``NoMethodError`` after queueing any ``object_identifier`` and ``external_identifier`` lookup deletes. Other classes still queue their deletes into the caller's transaction. #447
|
|
78
|
+
- ``ListKey#pushx`` and ``#unshiftx`` queue their TTL refresh inside a transaction or pipeline instead of skipping it. ``SortedSet#popmin`` and ``#popmax``, which raised ``NoMethodError`` there, queue theirs too, except with a count that pops nothing (``0`` or negative), as outside a block. #447
|
|
79
|
+
- Inside a transaction or pipeline, ``HashKey#hsetnx``, ``ListKey#insert``, ``JsonStringKey#setnx`` and ``JsonStringKey#value`` with a default queue their write and TTL refresh as one ``EVAL`` that refreshes only when the write takes effect, as outside a block. ``hsetnx`` and ``insert`` previously skipped the refresh there; ``JsonStringKey#setnx`` and ``#value`` reset an existing key's TTL even when the key was already set. The returned Futures resolve to the same values as before. #447
|
|
80
|
+
- Inside a transaction or pipeline, when the ``json_string`` has a default expiration (a positive ``default_expiration``, its own or inherited, and no ``no_expiration: true``), ``JsonStringKey#setnx`` now adds one reply to ``MultiResult#results`` instead of two and ``#value`` with a default adds two instead of three (the separate ``EXPIRE`` reply is gone), so later replies move up one index; without a default expiration ``#setnx`` adds one reply and ``#value`` with a default adds two, as before. ``ListKey#pushx`` and ``#unshiftx`` on a list with a default expiration added one reply, the count, and now add two, the count then the ``EXPIRE`` reply, so the index of every later reply increases by one; without a default expiration they add one as before. ``SortedSet#popmin`` and ``#popmax``, which raised ``NoMethodError`` there, add the ZPOPMIN or ZPOPMAX reply and, with a default expiration and a count above ``0``, the ``EXPIRE`` reply after it. #447
|
|
81
|
+
- Inside a pipeline, the instance-scoped ``add_to_*`` and the class-level ``add_to_class_*`` of a ``multi_index`` no longer queue ``EXISTS`` on the object's key, so for a value that is not blank they add one reply fewer to ``MultiResult#results`` and later replies move up one index; for an empty or whitespace-only value, ``add_to_*`` adds only the reply of the ``HSET`` that records the membership instead of the ``EXISTS``, ``SADD`` and ``HSET`` replies. An unsaved object is indexed there without ``Familia::PersistenceError``, as before. Inside a transaction they did not queue ``EXISTS``. #447
|
|
82
|
+
- Inside a transaction or pipeline, ``HashKey#hsetnx``, ``ListKey#insert``, ``JsonStringKey#setnx`` and ``JsonStringKey#value`` with a default send ``EVAL`` and need scripting permission (ACL ``@scripting`` or ``+eval``). Without it they raise ``Redis::PermissionError`` (``NOPERM``) and a transaction applies none of the block's commands. Outside a block they send the plain command as before. #447
|
|
83
|
+
- Schema validation (``Familia::SchemaRegistry.validate`` and ``.validate!``, the ``schema_validation`` feature's instance methods, and ``Familia::Migration::Base#validate_schema`` and ``#validate_schema!``) raises ``Familia::SchemaValidatorLoadError`` when ``json_schemer`` is installed but one of its own requires fails. It previously warned ``json_schemer gem not installed`` and disabled validation, so every record passed. When ``json_schemer`` itself cannot be loaded, validation still warns and is disabled. See ``docs/migrating/runtime-dependencies.md``. #448
|
|
84
|
+
- When ``json_schemer`` is installed but one of its own requires fails, ``Familia::Migration::Model#migrate`` raises ``Familia::SchemaValidatorLoadError`` at the first record it validates, through ``validate_before_transform?``, ``validate_after_transform?`` or a ``#validate_schema`` call in ``#process_record``, in dry runs and actual runs. ``Familia::Migration::Pipeline`` does not run the validation hooks; it raises only when ``#should_process?``, ``#build_update_fields`` or ``#execute_update`` calls ``#validate_schema``, and only in an actual run. Where ``#migrate`` raises it, ``Familia::Migration::Base.run`` and ``.cli_run`` let it propagate, and ``Familia::Migration::Runner`` records the migration as ``:failed`` and not applied. #448
|
|
85
|
+
- ``Familia::Migration::Model#migrate`` in interactive mode (``@interactive = true``) raises ``Familia::Migration::Errors::PreconditionFailed`` instead of ``LoadError`` when ``pry-byebug`` is missing from the application bundle or fails to load, so ``Familia::Migration::Runner`` records the migration as ``:failed``. ``rake familia:migrate:run`` and ``familia:migrate:dry_run`` then print the failure and exit 0, as for any other failed migration; the uncaught ``LoadError`` used to end them with exit status 1. ``familia`` does not depend on ``pry-byebug``. See ``docs/migrating/runtime-dependencies.md``. #448
|
|
86
|
+
- ``Familia.join``, and so ``Familia.dbkey``, ``Horreum.dbkey``, ``Horreum#dbkey`` and the ``dbkey`` of a DataType with a parent, returns its result in the encoding ``Familia.normalize_key_encoding`` gives it. Key bytes are unchanged; the encoding changes for an identifier that is UTF-8 with invalid bytes (now BINARY), BINARY with valid UTF-8 bytes (now UTF-8), or in another encoding with non-ASCII bytes, such as ISO-8859-1 (now BINARY or UTF-8 by its bytes), and for an ASCII-only result whose first part is a Symbol, an Integer or a String not labelled UTF-8, such as a key under a class's default Symbol prefix (now UTF-8). A DataType's ``dbkey:`` option, a standalone DataType's key and the patterns of ``dbkey_pattern``, ``scan_pattern`` and ``find_keys`` keep the encoding they are given, and ``dbkey_pattern`` raises ``Encoding::CompatibilityError`` for a BINARY or ISO-8859-1 identifier glob with non-ASCII bytes under a non-ASCII prefix. See ``docs/migrating/dbkey-encoding.md``. #453
|
|
87
|
+
- ``DatabaseLogger.command_string``, the text of captured commands and of unstructured log lines, always returns UTF-8 or ASCII-only text. When the joined command is not valid UTF-8, or its arguments' encodings are incompatible, each argument is read as UTF-8 with every invalid byte written as ``\xNN``, so a BINARY member ``"bad\xFF"`` reads ``bad\xFF`` with a literal backslash and a BINARY argument with valid UTF-8 bytes reads as that text. Structured log lines are unchanged. #453
|
|
88
|
+
- The deserialization, ``find_by_id``, ``commit_fields``, ``refresh!``, ``update_expiration``, ``serialize_value`` and ``HashKey`` write log lines, the warning for an indexed field value longer than 256 characters, the structured context of ``Familia.debug``, ``Familia.info`` and ``Familia.error``, ``Familia.trace`` output, and the messages of the ``TypeError`` a ``HashKey`` store raises, ``WatchAbortError`` and the dirty-write warning and error show a key or value whose bytes are not valid UTF-8 with every invalid byte written as ``\xNN``. Other log lines that name a key, such as those of the atomic index rebuild and of ``repair_participations!``, write its bytes as they are. The legacy JSON-encoded identifier warning writes ``->`` instead of ``→``. #453
|
|
89
|
+
- The ``participations`` reverse-index entry for a target whose identifier is in an encoding other than UTF-8 or BINARY and has non-ASCII bytes, such as ISO-8859-1 ``"caf\xE9"``, holds the collection key's bytes. 2.13.0 stored the key transcoded to UTF-8, which is not the collection's key. ``remove_from_<target>_<collection>`` does not remove an entry written by 2.13.0, and adding the membership again leaves both. ``participating_ids_for_target`` returns the identifier's bytes, as UTF-8 when they are valid UTF-8 and as BINARY otherwise (``"caf\xE9".b``), which finds the target; 2.13.0 returned one that found no record. ``current_participations`` and ``relationship_status`` leave the membership out and return the participant's other memberships, as 2.13.0 did. #453
|
|
90
|
+
- Familia passes a new argument to these methods, so an application override that keeps the 2.13.0 parameters raises ``ArgumentError`` when Familia calls it: the Horreum class method ``instantiate_from_hash(obj_hash, dbkey: nil)`` (public, ``@api private``), which ``find_by_dbkey`` (and so ``find_by_id`` and class-level ``destroy!``), ``load_multi`` and ``load_multi_by_keys`` call with ``dbkey:``; ``read_instance_index_scopes`` (public, from the ``relationships`` feature), which takes an optional ``operation:`` that ``Horreum#destroy!`` passes and ``save`` and ``save_if_not_exists!`` do not; the private class method ``scan_matching_keys(pattern, client, batch_size: 100, type: nil)``, which ``audit_participations``, and so ``health_check``, ``repair_all!`` and ``repair_participations!`` without arguments, call with ``type:`` for each ``participates_in`` collection; the private ``Horreum#log_deserialization_issue(val, field_name, dbkey = nil)``, which ``deserialize_value`` calls with three arguments for every value that is not JSON; and ``Familia::IndexedFieldFastWriteError#initialize(field, index_name, offenders: nil, inside_block: false)``, which a ``field!`` fast writer calls with ``inside_block: true`` inside a transaction or pipeline. They took ``(obj_hash)``, no arguments, ``(pattern, client, batch_size: 100)``, ``(val, field_name)`` and ``(field, index_name, offenders: nil)``. ``Familia::AtomicOperations.assert_rebuild_context!(redis)`` (public) requires ``redis``, which ``with_rebuild`` passes. It took no arguments. A call without ``redis``, or an override without the parameter, raises ``ArgumentError``. #445 #446 #447
|
|
91
|
+
|
|
92
|
+
Removed
|
|
93
|
+
-------
|
|
94
|
+
|
|
95
|
+
- ``familia.gemspec`` no longer declares ``connection_pool``, and ``require 'familia'`` no longer requires it itself. ``familia`` calls no ``connection_pool`` API, and it no longer limits the ``connection_pool`` version an application resolves (previously ``>= 2.4, < 4.0``). Applications that pass a ``ConnectionPool`` to ``Familia.connection_provider`` should list ``connection_pool`` in their Gemfile and require it. See ``docs/migrating/runtime-dependencies.md``. #448
|
|
96
|
+
|
|
97
|
+
Fixed
|
|
98
|
+
-----
|
|
99
|
+
|
|
100
|
+
- ``require 'familia'`` no longer fails with ``cannot load such file -- base64 (LoadError)`` on Ruby 3.4+ in an application bundle without ``base64``. #448
|
|
101
|
+
- On Ruby 3.4+, ``require 'familia'`` no longer fails with ``cannot load such file -- bigdecimal (LoadError)`` when the bundle resolves ``oj`` ``3.16.0`` or ``3.16.1`` and nothing in it declares ``bigdecimal`` (for example with ``json_schemer`` ``2.0.0`` to ``2.1.1``). The ``json_schemer`` floor fixes it: ``json_schemer`` ``2.2.0`` to ``2.5.0`` declare ``bigdecimal``, so the bundle contains it. The ``oj`` floor also excludes ``3.16.0`` and ``3.16.1``, which require ``bigdecimal`` without declaring it. #448
|
|
102
|
+
- ``Familia::Encryption.benchmark`` no longer requires ``benchmark``, a bundled gem from Ruby 4.0, so it no longer raises ``cannot load such file -- benchmark (LoadError)`` under Bundler on Ruby 4.0 when the application bundle lacks ``benchmark``. #448
|
|
103
|
+
- A related field declared after an instance of the class already existed (``participates_in`` finishing at load while an early request holds an instance, or any late ``Klass.list :x``) is now built by that instance's accessor on first access instead of raising ``RuntimeError: <Class>#<field> is nil. Did you override initialize without calling super?``, so ``update_expiration``, ``persist!``, ``ttl_report`` and instance ``destroy!`` work on instances created before the declaration. #430
|
|
104
|
+
- Such a late related field is built once per instance, from the current definition, under the class's build lock (concurrent first access constructs it once), and that first build freezes the definition like any other materialization. Already-built fields are never rebuilt. An accessor inherited from an ancestor that declared the field after the subclass was defined has no definition in the subclass registry and raises ``Familia::HorreumError`` naming that gap. #430
|
|
105
|
+
- A related-field declaration (instance-level or ``class_*``) now defines the accessors before it inserts the definition into the registry. ``update_expiration``, ``persist!``, ``ttl_report`` and instance ``destroy!`` snapshot the registry and call ``send(name)``; a snapshot taken between the insert and the accessor definition could call a method that did not exist yet and raise ``NoMethodError``. #430
|
|
106
|
+
- A record whose identifier field holds an Integer can be saved more than once when its class declares a ``unique_index``. Unless the application declared the index hashkey itself, before ``unique_index``, the generated ``guard_unique_<index>!`` raised ``Familia::RecordExistsError`` naming the record itself as the owner on every save after the first, and the instance-scoped guard behind ``add_to_<scope>_<index>`` refused a second add of the same record. Both guards now compare the identifier's string form. #444
|
|
107
|
+
- ``Horreum#refresh!`` and ``Horreum#refresh`` leave the object as it was when a field setter raises on a stored value, for example ``Familia::EncryptionError`` for an encrypted field whose stored envelope names an algorithm that is not available. Field values, transient fields and dirty tracking are restored before the error propagates. They left the fields before the failing one assigned, transient fields reset, and the assigned fields marked dirty. #445
|
|
108
|
+
- ``Horreum#refresh!`` and ``Horreum#refresh`` no longer send ``HDEL`` to the ``objid_lookup`` or ``extid_lookup`` hash when the in-memory ``objid`` or ``extid`` differs from the stored one. This removed another record's entry for an object built from that record's attributes, and the object's own entry when ``objid`` or ``extid`` is the identifier field. #445
|
|
109
|
+
- ``HashKey#refresh!`` and ``HashKey#refresh`` no longer write to the hash; they send only ``HGETALL``. They sent ``EXISTS``, ``HGETALL`` and then ``HMSET`` with every field ``HGETALL`` returned, each value deserialized and serialized again, which rewrote every value that round trip changed: in a ``hashkey`` with no ``reference:`` or ``class:`` option, ``plainstring`` became ``"plainstring"``, ``1.50`` became ``1.5`` and ``{"b": 1}`` became ``{"b":1}``; with ``class:``, a value for which the class's ``from_json`` raised or returned ``nil`` became ``null``. A field changed or deleted between the ``HGETALL`` and the ``HMSET`` was set back to the value read. #445
|
|
110
|
+
- ``Horreum#naive_refresh`` no longer raises ``Familia::NoIdentifier`` on an object whose identifier is not set yet, and no longer sends ``HDEL`` to ``objid_lookup`` on an object with no ``objid`` yet whose Proc identifier reads ``objid``. #445
|
|
111
|
+
- ``find_by_id``, ``find_by_dbkey``, ``load_multi``, ``load_multi_by_keys`` and ``Horreum#naive_refresh`` no longer generate an ``objid`` or send ``HDEL`` to ``objid_lookup`` or ``extid_lookup`` when a stored value is not JSON and the identifier reads ``objid`` or ``extid``. #445
|
|
112
|
+
- The error log entry for a stored value that is not JSON no longer computes the identifier. It names the key the value was read from (``find_by_id``, ``find_by_dbkey``, ``load_multi``, ``load_multi_by_keys`` and ``storage_inspect`` pass it), otherwise the key built from a Symbol or String identifier field's current value, or ``no dbkey``. It named a key built from the generated ``objid``. #445
|
|
113
|
+
- With a glob character in the class prefix, delimiter or suffix, ``all``, ``scan_pattern``, ``keys_count``, ``scan_count``, ``keys_any?``, ``scan_any?``, ``scan_keys``, ``find_keys``, ``audit_instances``, ``rebuild_instances``, the unique-index SCAN rebuild fallback and model migrations no longer include another class's keys or miss the class's own keys. #446
|
|
114
|
+
- A ``multi_index`` rebuild no longer deletes these keys of other records of the same class: the hash and fields of a record whose identifier equals a class-level index name, the set fields of a scope record whose identifier is ``<scope>:<index>``, and the buckets of a scope whose identifier starts with ``<scope>:<index>:``. Such a key belongs to the existing record with the longest identifier. ``audit_multi_indexes`` reads it for that record. A key that ends in an instance-scoped index name, such as ``company:c-1:dept_index:x:dept_index``, is a key of record ``c-1:dept_index:x`` only when the scope class declares a set field of that name; otherwise it is the bucket of ``x:dept_index`` in ``c-1``. #446
|
|
115
|
+
- ``audit_multi_indexes``, ``repair_multi_indexes!``, ``repair_all!`` and ``health_check`` no longer raise ``Redis::WrongTypeError`` when a key under a multi-index bucket prefix is not a set, such as the hash of a record whose identifier equals a class-level index name. #446
|
|
116
|
+
- A ``multi_index`` bucket whose field value equals the class suffix, such as ``customer:role_index:object``, is no longer taken for an object: ``audit_instances`` no longer reports it as missing, ``scan_count`` and ``keys_count`` no longer count it, and ``all``, ``health_check``, ``repair_instances!`` and ``rebuild_instances`` no longer raise ``Redis::WrongTypeError``. A key of another type at an object key, such as a string at ``customer:x:object``, is no longer reported as missing by ``audit_instances`` and ``health_check``, and ``repair_instances!`` and ``repair_all!`` no longer raise ``Redis::WrongTypeError`` on it. #446
|
|
117
|
+
- ``audit_participations`` no longer raises ``Redis::WrongTypeError`` on a set bucket named like a sorted set or list collection. #446
|
|
118
|
+
- ``extract_identifier_from_key`` compares and slices the key as bytes and returns the identifier in the key's encoding. It returns ``nil`` for a key with no identifier segment between the prefix and the suffix, such as ``customer:object``; it returned ``""``. ``customer::object`` still gives ``""``. ``audit_instances``, ``audit_related_fields``, ``audit_participations``, ``audit_unique_indexes``, ``audit_multi_indexes``, ``health_check`` and ``repair_all!`` of a class whose prefix holds non-ASCII characters no longer raise ``Encoding::CompatibilityError`` for a record whose identifier is not valid UTF-8, including one that is a member of a participation collection. #446 #453
|
|
119
|
+
- ``audit_multi_indexes``, ``repair_multi_indexes!``, ``health_check`` and ``repair_all!`` of a class with an instance-scoped ``multi_index`` no longer raise ``Encoding::CompatibilityError`` when a scope identifier labelled UTF-8 or BINARY is not valid UTF-8 and the scope class prefix holds non-ASCII characters, or the field value does and the class declares ``participates_in`` of the scope class. Without that declaration, for a scope identifier given as UTF-8 with invalid bytes and a non-ASCII field value, the audit no longer reports the bucket's records as stale (``:value_mismatch``) and ``repair_multi_indexes!`` no longer rebuilds the scope on every run. The audit matches such a bucket to its scope and records, including the bucket of a scope whose identifier contains the delimiter and the index name. With the declaration and a scope identifier in another encoding with non-ASCII bytes, such as ISO-8859-1 ``"caf\xE9"``, the audit still reports the scope's buckets as ``:field_value_unheld`` orphans and ``repair_multi_indexes!`` rebuilds them on every run. #446 #453
|
|
120
|
+
- With a blank field value, a ``multi_index`` no longer reads or changes a related field whose name equals the index name, such as a ``class_set :roles`` beside ``multi_index :role, :roles`` or a ``set :tags`` of the scope class beside ``multi_index :tag, :tags, within: Company``. The instance-scoped ``remove_from_*`` and ``update_in_*`` and the class-level ``update_in_class_*`` removed the object from it, the class-level ``find_all_by_*`` returned its members, and ``indexed_in?`` and ``current_indexings`` read it. ``IndexDescriptor#each_record`` raises ``ArgumentError`` for a blank ``value:`` of a ``multi_index``, as it does for ``nil``; it iterated the related field's members. #446
|
|
121
|
+
- ``audit_multi_indexes`` no longer reports a class-level bucket of the scope class whose field value contains ``:<index>:`` as an orphaned bucket of an instance-scoped index, unless the hash key of the scope record the key names holds a hash. #446
|
|
122
|
+
- ``SortedSet#diff(withscores: true)`` returns ``[member, score]`` pairs. It raised ``ArgumentError: unknown keyword: :withscores``. #447
|
|
123
|
+
- ``HashKey#increment``, ``#decrement`` and ``#incrbyfloat`` return the command's ``Redis::Future`` inside ``transaction``, ``atomic_write`` and ``pipelined`` blocks. They raised ``NoMethodError`` there, which aborted the whole block. #447
|
|
124
|
+
- ``HashKey#randfield(count, withvalues: true)`` returns ``[field, value]`` pairs. It returned ``[[[field, raw_value], nil]]`` when one pair came back and raised ``ArgumentError`` for more. #447
|
|
125
|
+
- ``HashKey#hsetnx`` outside a block sends ``EXPIRE`` with the key's ``default_expiration`` after it sets a new field, replacing any TTL the key had, and sends none when the field exists or the ``default_expiration`` is 0. A ``class_hashkey`` declared without ``default_expiration:`` or ``no_expiration: true`` takes its class's positive ``default_expiration``, also without ``feature :expiration``, so one written only through ``hsetnx`` now expires, with all its fields, once ``hsetnx`` sets no new field within that time; saving a record does not refresh class-level keys. ``hsetnx`` never refreshed the TTL, so such a hash had none. #447
|
|
126
|
+
- ``Lock#release`` inside a transaction or pipeline returns the EVAL ``Redis::Future``, which resolves to ``true`` or ``false`` after the block, and ``MultiResult#results`` carries the same Boolean. It returned ``false`` even when the queued script released the lock. The Future is truthy whatever the outcome, so read its value after the block. A queued release does not stop the block's other commands when the lock was lost before EXEC. #447
|
|
127
|
+
- ``JsonStringKey#empty?`` on a key declared with ``default:`` reads with GET only, inside and outside a block; its return value outside a block is unchanged. It wrote the default to a missing key (SETNX) and refreshed the TTL. #447
|
|
128
|
+
- ``Familia::IndexedFieldFastWriteError`` raised by a ``field!`` fast writer inside a transaction or pipeline says to write before or after the block, and the new ``#inside_block`` reader is ``true`` for that case. Its message recommended ``multi_field_update`` or ``save``, which raise ``Familia::OperationModeError`` there. #447
|
|
129
|
+
- ``Familia.join`` no longer raises ``Encoding::CompatibilityError`` when its parts carry non-ASCII bytes in incompatible encodings, such as a non-ASCII class ``prefix`` with a BINARY identifier. The parts are joined as bytes, so ``save``, ``exists?``, ``load`` and ``rebuild_instances`` work for such a record. #453
|
|
130
|
+
- ``save`` and ``add_to_<scope>_<index>`` no longer raise ``Familia::RecordExistsError`` from ``guard_unique_<index>!`` or ``guard_unique_<scope>_<index>!`` against the record's own index entry for a loaded record whose identifier is not valid UTF-8, or a record whose identifier was given as UTF-8 with invalid bytes, such as ``"bad\xFF"``. The guards compare identifiers by their bytes. As 2.13.0 did for ``'q'`` against ``'"q"'``, they read a stored owner wrapped in one pair of double quotes as the identifier inside, so a record given ``"\xFF"`` now passes them and takes the entry of a record whose identifier is ``"\"\xFF\""``. Do not use identifiers that start and end with a double quote with unique indexes. #453
|
|
131
|
+
- An instance-scoped ``multi_index`` with a non-ASCII field value no longer raises ``Encoding::CompatibilityError`` from ``add_to_<scope>_<index>``, ``update_in_<scope>_<index>`` or ``remove_from_<scope>_<index>`` given a scope whose identifier is not labelled UTF-8 and holds non-ASCII bytes (BINARY, such as ``'zoé'.b`` or ``"bin\xFE".b``, or ISO-8859-1), or from ``save`` or ``destroy!`` of a record indexed in a scope whose identifier is not valid UTF-8. ``destroy!`` removes the record's index entries. #453
|
|
132
|
+
- A record whose indexed field value is not valid UTF-8, such as a value given as BINARY ``"r\xFEle".b``, which loads back as UTF-8 with the same bytes, no longer makes ``save`` and ``destroy!`` of the loaded record with a ``multi_index`` on that field, or ``audit_multi_indexes``, ``audit_unique_indexes``, ``health_check`` and ``repair_all!`` of its class, raise ``Encoding::CompatibilityError``. ``save`` and ``find_all_by_<field>`` no longer raise it for a ``multi_index`` value longer than 256 characters given as BINARY with non-ASCII bytes under a class whose name holds non-ASCII text. #453
|
|
133
|
+
- ``current_participations``, ``relationship_status``, ``participating_ids_for_target``, ``participating_in_target?`` given collection names, and the generated ``<target>_ids``, ``<target>_instances`` and ``<target>_count`` no longer raise ``ArgumentError`` (invalid byte sequence in UTF-8) for a participant whose ``participations`` set holds an entry for a target identifier that is not valid UTF-8, given as BINARY (``"bin\xFE".b``) or as UTF-8. They read each entry by its bytes. ``current_participations`` returns the participant's other memberships and that membership, with ``target_id`` the identifier's bytes as BINARY. #453
|
|
134
|
+
- Reading a collection with non-ASCII members (``members``, ``each``, ``revrange``), ``rebuild_<index>``, ``commit_fields``, ``HashKey#refresh!`` and reading a legacy JSON-encoded entry of a scope's ``unique_index`` or ``multi_index`` (``[]``, ``get``, ``members``, ``find_by_<field>``, ``find_all_by_<field>``) no longer raise ``Encoding::CompatibilityError`` for a record or scope given a BINARY identifier that is not valid UTF-8. Nor do ``save``, ``find_by_id``, ``refresh!``, ``commit_fields``, ``save_fields``, the fast writers and adding the record to a collection, for such a record of a class whose name holds non-ASCII text. With debug on, neither does ``find_by_id`` given such an identifier for a record with a non-ASCII field value, nor a collection write of a BINARY value with non-ASCII bytes. #453
|
|
135
|
+
- ``atomic_write`` given ``watch_keys:`` that include a BINARY key with non-ASCII bytes, such as the key of a record given ``"bin\xFE".b``, and a non-ASCII UTF-8 key retries a WATCH conflict and raises ``WatchAbortError`` when the retries run out, instead of raising ``Encoding::CompatibilityError`` on the first conflict. #453
|
|
136
|
+
- ``DatabaseLogger`` no longer raises ``Encoding::CompatibilityError`` after a command whose arguments are in incompatible encodings, or, with unstructured logging, after a command with a non-ASCII BINARY argument. The command had run, but the caller received the error. #453
|
|
137
|
+
|
|
138
|
+
Security
|
|
139
|
+
--------
|
|
140
|
+
|
|
141
|
+
- The generated ``rebuild_<index>`` of a ``multi_index`` put the scope instance's identifier (the class prefix, for a class-level index) into its SCAN pattern unescaped and deleted every match. Rebuilding the index of a scope whose identifier holds a glob character deleted other scopes' index sets: rebuilding ``c-*`` deleted those of ``c-1`` and ``c-2``. The literal part of the pattern is now escaped, and a matched key is deleted only if it starts with that literal prefix, holds a set and is not a set field or ``multi_index`` bucket of an existing record of the same class with a longer identifier. A record exists when its hash key holds a hash. A matched key with more than ``MAX_CANDIDATE_SPLITS`` candidate owner records stops the rebuild before it deletes anything. #446
|
|
142
|
+
- ``repair_participations!`` removed members from another class's collection of the same name when the target class prefix held a glob character. ``audit_participations`` now escapes the pattern and keeps only keys with the target's literal prefix and suffix. #446
|
|
143
|
+
- ``repair_related_fields!`` deleted a ``multi_index`` bucket whose field value equaled the name of a related field of the class, such as ``customer:role_index:notes`` for role ``notes`` and a ``notes`` field, or ``company:c-1:dept_index:tags`` for dept ``tags`` and a ``tags`` field of the scope class. ``repair_participations!`` removed every member of a bucket whose field value equaled a collection name. ``audit_related_fields`` and ``audit_participations`` now skip a key that is a bucket of a class-level ``multi_index`` of the class, or of an instance-scoped ``multi_index`` whose scope record exists, unless the hash key of the record the key names holds a hash. The bucket of field value ``object`` is a set at such a hash key and does not count as that record. #446
|
|
144
|
+
|
|
145
|
+
Documentation
|
|
146
|
+
-------------
|
|
147
|
+
|
|
148
|
+
- Added ``docs/migrating/refresh.md`` covering the ``Horreum#refresh!``, ``Horreum#refresh``, ``HashKey#refresh!`` and ``HashKey#refresh`` changes. #445
|
|
149
|
+
- Corrected the ``docs/overview.md`` transient-field examples. They called ``reload``, which does not exist, instead of ``refresh!``. The ``LoginAttempt`` example also used ``redacted_field`` and ``RedactedString#reveal``, which do not exist, declared no identifier, and read a transient value after ``refresh!``, which resets it to nil. The ``SecureModel`` example passed two names to ``field``, ``encrypted_field`` and ``transient_field``, which take one, declared no identifier or encryption keys, called ``create`` instead of ``create!``, and called ``ConcealedString#reveal`` without a block. #445
|
|
150
|
+
- Added ``docs/migrating/key-patterns-and-index-rebuilds.md``, the migration guide for the changes in #446, and documented ``multi_index`` rebuilds and ``Familia::IndexBucketConflictError`` in the indexing guide and the index rebuilding reference. #446
|
|
151
|
+
- ``docs/reference/transaction_safety.md`` rule 4 describes how each method handles command replies inside transactions and pipelines. #447
|
|
152
|
+
- ``docs/migrating/transaction-replies.md`` lists the calls inside a block that behaved differently before and need a code change: partial writes and unstaging that committed with an outer transaction, ``extend_expiration``, the ``Lock`` ownership checks and ``release``, ``current_indexings``, ``Migration::Registry`` without ``redis:``, ``as_json`` on a scalar DataType, the instance-scoped ``add_to_*`` with no field value inside a pipeline, the TTL refresh of ``HashKey#hsetnx``, ``ListKey#insert``, ``JsonStringKey#setnx`` and ``JsonStringKey#value``, and the TTL ``HashKey#hsetnx`` now sets outside a block. #447
|
|
153
|
+
- Added ``docs/migrating/runtime-dependencies.md``, which lists the application changes needed for ``familia``'s dependency changes since ``2.13.0``: ``base64``, ``connection_pool``, the ``oj``, ``json_schemer`` and ``redis`` floors, interactive migrations and ``Familia::SchemaValidatorLoadError``. #447 #448
|
|
154
|
+
- The schema validation guide no longer tells applications to add ``json_schemer`` to their Gemfile; ``familia`` declares it. #448
|
|
155
|
+
- Added ``docs/migrating/dbkey-encoding.md``, which lists the encoding of a key in 2.13.0 and in this release, the log text changes, the participations reverse-index entries and audits for ISO-8859-1 identifiers, what to change, and what 2.13.0 cannot read or delete after a rollback. #453
|
|
156
|
+
- Documented connection scopes in ``docs/reference/api-technical.md`` (``Connection Scopes``, with Rack middleware and job examples) and ``docs/guides/index.md``, and corrected the resolution order listed under ``Connection Chain Debugging``. #456
|
|
157
|
+
|
|
158
|
+
AI Assistance
|
|
159
|
+
-------------
|
|
160
|
+
|
|
161
|
+
- The changes were developed with AI assistance. #443 #444 #445 #446 #447 #448 #453 #458
|
|
162
|
+
|
|
163
|
+
.. _changelog-2.13.0:
|
|
164
|
+
|
|
165
|
+
2.13.0 — 2026-09-17
|
|
166
|
+
===================
|
|
167
|
+
|
|
168
|
+
Added
|
|
169
|
+
-----
|
|
170
|
+
|
|
171
|
+
- Added ``Familia::AtomicOperations.with_rebuild(final_key, redis, ttl: DEFAULT_REBUILD_TTL, preserve_for: DEFAULT_PRESERVE_TTL)``, which yields ``(temp_key, touch)`` and owns the rebuild lock, the bounded-TTL temporary key, the atomic swap, and cleanup. Raises ``Familia::RebuildInProgressError`` when the lock is already held.
|
|
172
|
+
- Added ``Familia::AtomicOperations.sweep_orphaned_temp_keys(redis, pattern: '*:rebuild:*', older_than: DEFAULT_PRESERVE_TTL, dry_run: false)``, which deletes temporary rebuild keys that have no TTL and are older than the window, skipping any index with a live ``:rebuild-lock``. Run it only once every process is on this version.
|
|
173
|
+
- Added ``Familia::RebuildInProgressError`` (a ``Familia::PersistenceError`` with ``#key``) and its subclass ``Familia::RebuildLockLostError``.
|
|
174
|
+
- Added ``Familia::AtomicOperations::DEFAULT_REBUILD_TTL`` (300) and ``DEFAULT_PRESERVE_TTL`` (3600).
|
|
175
|
+
- ``Familia::VerifiableIdentifier.secret_key`` now reads ``IDENTIFIER_SECRET`` when ``VERIFIABLE_ID_HMAC_SECRET`` is unset or blank. ``VERIFIABLE_ID_HMAC_SECRET`` takes precedence when nonblank, and ``KeyError`` is still raised on first use when neither is set. #423
|
|
176
|
+
- Added ``Horreum.configure_related_field(name, **opts)`` to merge options into a declared related-field definition at boot, with an optional ``scope:`` (``:instance`` or ``:class``). Options are validated with the DataType's own rules; an unknown field name raises ``ArgumentError``. #428
|
|
177
|
+
- Related-field definitions now freeze at first use (the first instance for instance-level fields, the first accessor call for class-level fields). After that, ``configure_related_field`` raises ``Familia::RelatedFieldFrozenError`` and mutating ``Klass.related_fields[:x].opts`` raises ``FrozenError``. #428
|
|
178
|
+
|
|
179
|
+
Changed
|
|
180
|
+
-------
|
|
181
|
+
|
|
182
|
+
- ``Familia::FieldType#deserialize(value, record)`` is now called on the storage-to-object path (``load``, ``find_by_id``, ``refresh!``, ``naive_refresh``) for every persistent field. Custom field types that override it now see stored values pass through it. #405
|
|
183
|
+
- ``Model.new(secret: envelope_json)`` now encrypts raw envelope JSON as plaintext. Rehydrate from a raw storage hash with ``naive_refresh`` instead. #405
|
|
184
|
+
- ``rebuild_instances`` and the ``rebuild_via_instances``, ``rebuild_via_participation``, and ``rebuild_via_scan`` strategies run under ``with_rebuild``. A rebuild of an index that is already being rebuilt raises ``Familia::RebuildInProgressError`` immediately.
|
|
185
|
+
- ``AtomicOperations.atomic_swap`` now issues ``RENAME`` and ``PERSIST`` from one Lua script and accepts ``preserve_for:``, ``lock: { key:, token: }`` (a blank token raises ``ArgumentError``), and ``populated:``.
|
|
186
|
+
- ``AtomicOperations.with_rebuild`` raises ``Familia::OperationModeError`` inside a ``Familia.transaction`` or pipeline.
|
|
187
|
+
- A blank ``VERIFIABLE_ID_HMAC_SECRET`` no longer raises on its own; it falls through to ``IDENTIFIER_SECRET``. See ``docs/migrating/identifier-secret.md``. #423
|
|
188
|
+
- Re-declaring a related field whose definition has already materialized raises ``Familia::RelatedFieldFrozenError`` instead of silently replacing the definition. Re-declaring before first use still replaces it. #428
|
|
189
|
+
- Class-level related fields validate their options at the declaration line, as instance-level fields now do. #428
|
|
190
|
+
- Declaration, re-declaration, ``configure_related_field`` and the freeze serialize on the class's ``related_fields_mutex``, which is created when the class is defined. DataType construction runs outside that mutex, so a custom ``init`` that reads another collection of the same class no longer raises ``ThreadError``. #428
|
|
191
|
+
- Class-level collections are built lazily on the first accessor call, single-flight under a per-class build lock, and cached in ``Klass.class_related_field_cache`` instead of ``@<name>`` on the class. #428
|
|
192
|
+
- The first ``Klass.new`` and the related-field cascades (``update_expiration``, ``persist!``, ``ttl_report``, ``delete_related_fields!``, class-level ``destroy!``, ``guard_atomic_write_database!``, the unique-index rebuild fallback scan, and ``Horreum.inherited``) iterate registry snapshots (``related_fields_snapshot``, ``class_related_fields_snapshot``), so a field declared concurrently no longer raises ``RuntimeError: can't add a new key into hash during iteration``. #428
|
|
193
|
+
- ``initialize_relatives`` no longer writes ``parent`` into the shared definition Hash. #428
|
|
194
|
+
- Subclasses receive their own copies of related-field definitions, and a subclass that inherits a class-level field without re-declaring it gets a working accessor keyed under the subclass. #428
|
|
195
|
+
|
|
196
|
+
Fixed
|
|
197
|
+
-----
|
|
198
|
+
|
|
199
|
+
- ``Model.find_by_id``, ``find_by_dbkey``, and ``load_multi`` no longer raise ``Familia::NoIdentifier`` when a stored field value is not valid JSON. The record loads with the raw string and logs ``Legacy plain string in ...``.
|
|
200
|
+
- ``EncryptedData.valid?`` no longer echoes the candidate value into the debug log, and ``EncryptedData.validate!`` and ``Encryption::Manager#decrypt`` name only the JSON parser's error class in their ``EncryptionError``. #406
|
|
201
|
+
- Deserialization failure logs and the ``Familia::Instrumentation.notify_error`` payload no longer carry the stored value. ``value_length`` is always reported; a bounded ``value_preview`` is logged only when ``Familia.debug?`` is on and is never sent to ``notify_error``. #407
|
|
202
|
+
- ``ExternalIdentifier.extid?`` rejects uppercase IDs, and external identifier format declarations require one ``%{id}`` placeholder. #400 #401
|
|
203
|
+
- ``Familia::ThreadSafety::InstrumentedMutex#synchronize`` now always acquires the underlying mutex. It was a no-op unless the thread-safety monitor was enabled, so every class-level registry guard built on it did not lock. #428
|
|
204
|
+
- Instance-scoped unique-index rebuilds no longer mistake the backing index for a membership collection when no participation collection is declared.
|
|
205
|
+
|
|
206
|
+
Security
|
|
207
|
+
--------
|
|
208
|
+
|
|
209
|
+
- Encrypted field setters no longer duck-type the assigned value. Only a ``Familia::Encryption::StoredEnvelope``, applied by ``EncryptedFieldType#deserialize`` on hydration, is stored verbatim; every other assignment is encrypted. #405
|
|
210
|
+
- The envelope ``encoding`` field is validated against ``Familia::Encryption::ENVELOPE_ENCODINGS`` before use. Anything else raises ``EncryptionError: Unsupported encoding`` from ``validate_decryptable!``, ``Manager#decrypt``, and ``ConcealedString`` wrapping, and ``decryptable?`` returns false. Envelopes without the field still decrypt as UTF-8. Binding the field into the AAD so that a valid-but-wrong name is also rejected remains open; it needs a new envelope version. #408
|
|
211
|
+
- Assigning plaintext to an encrypted field raises ``Familia::NoIdentifier`` when the record has no identifier, so ciphertext cannot become undecryptable once an identifier is assigned.
|
|
212
|
+
- ``rebuild_via_scan`` propagates batch failures and preserves the live index instead of replacing it with incomplete contents.
|
|
213
|
+
- Index rebuilds hold a per-index ``<final_key>:rebuild-lock`` refreshed by compare-and-extend and raise ``Familia::RebuildLockLostError`` when another rebuild takes it over. ``AtomicOperations.build_temp_key`` appends a 64-bit random nonce (``<key>:rebuild:<timestamp>:<16 hex>``), temporary keys carry a refreshed TTL, and a failed swap retains the key for ``preserve_for`` (default 3600 seconds).
|
|
214
|
+
- ``AtomicOperations.atomic_swap`` fails closed: one Lua script verifies the lock token and performs the ``RENAME`` (or the empty-rebuild ``DEL``), a lost lock raises ``Familia::RebuildLockLostError``, and a populated temporary key that is gone at swap time raises ``Familia::PersistenceError``.
|
|
215
|
+
|
|
216
|
+
Documentation
|
|
217
|
+
-------------
|
|
218
|
+
|
|
219
|
+
- Linked security audit status entries to their July 26 audit findings.
|
|
220
|
+
|
|
221
|
+
AI Assistance
|
|
222
|
+
-------------
|
|
223
|
+
|
|
224
|
+
- Several of the fixes, the rebuild lock design, and their regression tests were developed with AI assistance.
|
|
225
|
+
|
|
10
226
|
.. _changelog-2.12.0:
|
|
11
227
|
|
|
12
228
|
2.12.0 — 2026-08-03
|
data/Gemfile
CHANGED
|
@@ -5,7 +5,13 @@ source 'https://rubygems.org'
|
|
|
5
5
|
gemspec
|
|
6
6
|
|
|
7
7
|
group :test do
|
|
8
|
+
# The tryouts build fixture keys and inspect envelopes with Base64. Declare
|
|
9
|
+
# it here rather than relying on another gem in the bundle to bring it in.
|
|
10
|
+
gem 'base64', require: false
|
|
8
11
|
gem 'concurrent-ruby', '~> 1.3.8', require: false
|
|
12
|
+
# The pooling tryouts build ConnectionPool::Wrapper providers, as an
|
|
13
|
+
# application would. familia itself does not use connection_pool.
|
|
14
|
+
gem 'connection_pool', require: false
|
|
9
15
|
gem 'ruby-prof'
|
|
10
16
|
gem 'stackprof'
|
|
11
17
|
gem 'timecop', require: false
|
|
@@ -20,13 +26,12 @@ group :development, :test do
|
|
|
20
26
|
# Ruby 3.2 (our gemspec's required_ruby_version floor). 1.4.0 only adds
|
|
21
27
|
# Config#to_data, which we don't use (we don't use Dry::Configurable at all),
|
|
22
28
|
# so cap below 1.4 to keep the dev bundle installable on Ruby 3.2.
|
|
23
|
-
gem 'dry-configurable', '>= 1.3', '< 1.
|
|
29
|
+
gem 'dry-configurable', '>= 1.3', '< 1.4', require: false
|
|
24
30
|
gem 'irb', '~> 1.18.0', require: false
|
|
25
|
-
gem 'json_schemer', '~> 2.0', require: false
|
|
26
31
|
gem 'rake', '~> 13.0', require: false
|
|
27
32
|
gem 'redcarpet', require: false
|
|
28
33
|
gem 'reek', require: false
|
|
29
|
-
gem 'rubocop', '~> 1.
|
|
34
|
+
gem 'rubocop', '~> 1.92.1', require: false
|
|
30
35
|
gem 'rubocop-performance', require: false
|
|
31
36
|
gem 'rubocop-thread_safety', require: false
|
|
32
37
|
gem 'ruby-lsp', require: false
|