familia 2.0.0.pre15 → 2.0.0.pre16
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/code-quality.yml +138 -0
- data/.github/workflows/code-smellage.yml +145 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +1 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +48 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +64 -4
- data/CLAUDE.md +1 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +623 -19
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +6 -6
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +49 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +13 -10
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +26 -15
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +75 -47
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +66 -64
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +9 -12
- data/lib/familia/features/object_identifier.rb +56 -19
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
- data/lib/familia/features/relationships/indexing.rb +176 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +3 -5
- data/lib/familia/features.rb +4 -13
- data/lib/familia/field_type.rb +24 -4
- data/lib/familia/horreum/core/connection.rb +229 -26
- data/lib/familia/horreum/core/database_commands.rb +27 -17
- data/lib/familia/horreum/core/serialization.rb +40 -20
- data/lib/familia/horreum/core/utils.rb +2 -1
- data/lib/familia/horreum/shared/settings.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +33 -45
- data/lib/familia/horreum/subclass/management.rb +72 -24
- data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
- data/lib/familia/horreum.rb +196 -114
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +1 -1
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +7 -7
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +10 -10
- data/try/core/errors_try.rb +8 -11
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +1 -1
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +433 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +3 -3
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +1 -1
- data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +1 -1
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +8 -10
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- metadata +75 -43
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -8,7 +8,7 @@ module Familia
|
|
8
8
|
#
|
9
9
|
# Object identifiers are:
|
10
10
|
# - Unique across the system
|
11
|
-
# - Persistent (stored in Redis
|
11
|
+
# - Persistent (stored in Valkey/Redis)
|
12
12
|
# - Lazily generated (only when first accessed)
|
13
13
|
# - Configurable (multiple generation strategies available)
|
14
14
|
# - Preserved during initialization (existing IDs never regenerated)
|
@@ -50,7 +50,7 @@ module Familia
|
|
50
50
|
#
|
51
51
|
# # Custom generation strategy
|
52
52
|
# class TimestampedItem < Familia::Horreum
|
53
|
-
# feature :object_identifier, generator: -> { "item_#{
|
53
|
+
# feature :object_identifier, generator: -> { "item_#{Familia.now.to_i}_#{SecureRandom.hex(4)}" }
|
54
54
|
# field :data
|
55
55
|
# end
|
56
56
|
#
|
@@ -60,9 +60,9 @@ module Familia
|
|
60
60
|
# Data Integrity Guarantees:
|
61
61
|
#
|
62
62
|
# The feature preserves the object identifier passed during initialization,
|
63
|
-
# ensuring that existing objects loaded from Redis maintain their IDs:
|
63
|
+
# ensuring that existing objects loaded from Valkey/Redis maintain their IDs:
|
64
64
|
#
|
65
|
-
# # Loading existing object from Redis preserves ID
|
65
|
+
# # Loading existing object from Valkey/Redis preserves ID
|
66
66
|
# existing = User.new(objid: 'existing-uuid-value', email: 'existing@example.com')
|
67
67
|
# existing.objid # => "existing-uuid-value" (preserved, not regenerated)
|
68
68
|
#
|
@@ -71,7 +71,7 @@ module Familia
|
|
71
71
|
# - Lazy Generation: IDs generated only when first accessed
|
72
72
|
# - Thread-Safe: Generator strategy configured once during initialization
|
73
73
|
# - Memory Efficient: No unnecessary ID generation for unused objects
|
74
|
-
# - Redis Efficient: Only persists non-nil values to conserve memory
|
74
|
+
# - Valkey/Redis Efficient: Only persists non-nil values to conserve memory
|
75
75
|
#
|
76
76
|
# Security Considerations:
|
77
77
|
#
|
@@ -86,12 +86,21 @@ module Familia
|
|
86
86
|
DEFAULT_GENERATOR = :uuid_v7
|
87
87
|
|
88
88
|
def self.included(base)
|
89
|
-
Familia.trace :LOADED, self, base
|
90
|
-
base.extend
|
89
|
+
Familia.trace :LOADED, self, base if Familia.debug?
|
90
|
+
base.extend ModelClassMethods
|
91
91
|
|
92
92
|
# Ensure default generator is set in feature options
|
93
93
|
base.add_feature_options(:object_identifier, generator: DEFAULT_GENERATOR)
|
94
94
|
|
95
|
+
# Add class-level mapping for objid -> id lookups.
|
96
|
+
#
|
97
|
+
# If the model uses objid as it's primary key, this mapping will be
|
98
|
+
# redundant to the builtin functionality of horreum clases, that
|
99
|
+
# automatically populate ModelClass.instances sorted set. However,
|
100
|
+
# if the model uses any other field as primary key, this mapping
|
101
|
+
# is necessary to lookup objects by their objid.
|
102
|
+
base.class_hashkey :objid_lookup
|
103
|
+
|
95
104
|
# Register the objid field using a simple custom field type
|
96
105
|
base.register_field_type(ObjectIdentifierFieldType.new(:objid, as: :objid, fast_method: false))
|
97
106
|
end
|
@@ -101,7 +110,7 @@ module Familia
|
|
101
110
|
# Object identifier fields automatically generate unique identifiers when first
|
102
111
|
# accessed if not already set. The generation strategy is configurable via
|
103
112
|
# feature options. These fields preserve any values set during initialization
|
104
|
-
# to ensure data integrity when loading existing objects from
|
113
|
+
# to ensure data integrity when loading existing objects from the database.
|
105
114
|
#
|
106
115
|
# The field type tracks the generator used for each objid to provide provenance
|
107
116
|
# information for security-sensitive operations like external identifier generation.
|
@@ -151,6 +160,9 @@ module Familia
|
|
151
160
|
generator = options[:generator] || DEFAULT_GENERATOR
|
152
161
|
instance_variable_set(:"@#{field_name}_generator_used", generator)
|
153
162
|
|
163
|
+
# Update mapping from objid to model primary key
|
164
|
+
self.class.objid_lookup[generated_id] = identifier if respond_to?(:identifier) && identifier
|
165
|
+
|
154
166
|
generated_id
|
155
167
|
end
|
156
168
|
end
|
@@ -166,7 +178,7 @@ module Familia
|
|
166
178
|
# Override setter to preserve values during initialization
|
167
179
|
#
|
168
180
|
# This ensures that values passed during object initialization
|
169
|
-
# (e.g., when loading from Redis) are preserved and not overwritten
|
181
|
+
# (e.g., when loading from Valkey/Redis) are preserved and not overwritten
|
170
182
|
# by the lazy generation logic.
|
171
183
|
#
|
172
184
|
# @param klass [Class] The class to define the method on
|
@@ -177,9 +189,19 @@ module Familia
|
|
177
189
|
|
178
190
|
handle_method_conflict(klass, :"#{method_name}=") do
|
179
191
|
klass.define_method :"#{method_name}=" do |value|
|
192
|
+
# Remove old mapping if objid is changing
|
193
|
+
old_value = instance_variable_get(:"@#{field_name}")
|
194
|
+
if old_value && old_value != value
|
195
|
+
Familia.logger.info("Removing objid mapping for #{old_value}")
|
196
|
+
self.class.objid_lookup.remove_field(old_value)
|
197
|
+
end
|
198
|
+
|
180
199
|
instance_variable_set(:"@#{field_name}", value)
|
181
200
|
|
182
|
-
#
|
201
|
+
# Update mapping from objid to this new identifier
|
202
|
+
self.class.objid_lookup[value] = identifier unless value.nil? || identifier.nil?
|
203
|
+
|
204
|
+
# When setting objid from external source (e.g., loading from Valkey/Redis),
|
183
205
|
# we cannot determine the original generator, so we clear the provenance
|
184
206
|
# tracking to indicate unknown origin. This prevents false assumptions
|
185
207
|
# about the security properties of externally-provided identifiers.
|
@@ -205,7 +227,7 @@ module Familia
|
|
205
227
|
end
|
206
228
|
end
|
207
229
|
|
208
|
-
module
|
230
|
+
module ModelClassMethods
|
209
231
|
# Generate a new object identifier using the configured strategy
|
210
232
|
#
|
211
233
|
# @return [String] A new unique identifier
|
@@ -220,7 +242,7 @@ module Familia
|
|
220
242
|
when :uuid_v4
|
221
243
|
SecureRandom.uuid_v4
|
222
244
|
when :hex
|
223
|
-
Familia.
|
245
|
+
Familia.generate_id(16)
|
224
246
|
when Proc
|
225
247
|
generator.call
|
226
248
|
else
|
@@ -243,14 +265,21 @@ module Familia
|
|
243
265
|
|
244
266
|
if Familia.debug?
|
245
267
|
reference = caller(1..1).first
|
246
|
-
Familia.trace :FIND_BY_OBJID,
|
268
|
+
Familia.trace :FIND_BY_OBJID, nil, objid, reference
|
247
269
|
end
|
248
270
|
|
249
|
-
#
|
250
|
-
|
251
|
-
|
252
|
-
|
271
|
+
# Look up the primary ID from the external ID mapping
|
272
|
+
primary_id = objid_lookup[objid]
|
273
|
+
|
274
|
+
# If there is no mapping for this instance's objid, perhaps
|
275
|
+
# the object dbkey is already using the objid.
|
276
|
+
primary_id = objid if primary_id.nil?
|
277
|
+
|
278
|
+
find_by_id(primary_id)
|
253
279
|
rescue Familia::NotFound
|
280
|
+
# If the object was deleted but mapping wasn't cleaned up
|
281
|
+
# we could autoclean here, as long as we log it.
|
282
|
+
# objid_lookup.remove_field(objid)
|
254
283
|
nil
|
255
284
|
end
|
256
285
|
end
|
@@ -283,6 +312,15 @@ module Familia
|
|
283
312
|
self.objid = value
|
284
313
|
end
|
285
314
|
|
315
|
+
def destroy!
|
316
|
+
# Clean up objid mapping when object is destroyed
|
317
|
+
current_objid = instance_variable_get(:@objid)
|
318
|
+
|
319
|
+
self.class.objid_lookup.remove_field(current_objid) if current_objid
|
320
|
+
|
321
|
+
super if defined?(super)
|
322
|
+
end
|
323
|
+
|
286
324
|
# Initialize object identifier configuration
|
287
325
|
#
|
288
326
|
# Called during object initialization to set up the ID generation strategy.
|
@@ -300,9 +338,8 @@ module Familia
|
|
300
338
|
|
301
339
|
options = self.class.feature_options(:object_identifier)
|
302
340
|
generator = options[:generator] || DEFAULT_GENERATOR
|
303
|
-
Familia.trace :OBJID_INIT,
|
341
|
+
Familia.trace :OBJID_INIT, nil, "Generator strategy: #{generator}"
|
304
342
|
end
|
305
|
-
|
306
343
|
end
|
307
344
|
end
|
308
345
|
end
|
@@ -106,7 +106,7 @@ module Familia
|
|
106
106
|
# activity.save
|
107
107
|
# end
|
108
108
|
#
|
109
|
-
# def self.activity_for_hour(time =
|
109
|
+
# def self.activity_for_hour(time = Familia.now)
|
110
110
|
# bucket_id = "activity:#{qstamp(1.hour, time: time, pattern: '%Y%m%d%H')}"
|
111
111
|
# find(bucket_id)
|
112
112
|
# end
|
@@ -129,7 +129,7 @@ module Familia
|
|
129
129
|
# interval: interval.to_i)
|
130
130
|
# end
|
131
131
|
#
|
132
|
-
# metric.data_points.add(
|
132
|
+
# metric.data_points.add(value, timestamp)
|
133
133
|
# metric.timestamp = timestamp
|
134
134
|
# metric.value = value
|
135
135
|
# metric.save
|
@@ -175,13 +175,13 @@ module Familia
|
|
175
175
|
#
|
176
176
|
# def self.utc_hourly_key(metric_name)
|
177
177
|
# # Always use UTC for consistent global buckets
|
178
|
-
# timestamp = qstamp(1.hour, time:
|
178
|
+
# timestamp = qstamp(1.hour, time: Familia.now, pattern: '%Y%m%d%H')
|
179
179
|
# "global:#{metric_name}:#{timestamp}"
|
180
180
|
# end
|
181
181
|
#
|
182
182
|
# def self.local_daily_key(metric_name, timezone = 'America/New_York')
|
183
183
|
# # Use local timezone for region-specific buckets
|
184
|
-
# local_time =
|
184
|
+
# local_time = Familia.now.in_time_zone(timezone)
|
185
185
|
# timestamp = qstamp(1.day, time: local_time, pattern: '%Y%m%d')
|
186
186
|
# "#{timezone.gsub('/', '_')}:#{metric_name}:#{timestamp}"
|
187
187
|
# end
|
@@ -194,7 +194,7 @@ module Familia
|
|
194
194
|
#
|
195
195
|
# # Cache quantized timestamps to avoid repeated calculations
|
196
196
|
# def self.cached_qstamp(quantum, pattern: nil, time: nil)
|
197
|
-
# cache_key = "qstamp:#{quantum}:#{pattern}:#{(time ||
|
197
|
+
# cache_key = "qstamp:#{quantum}:#{pattern}:#{(time || Familia.now).to_i / quantum}"
|
198
198
|
# Rails.cache.fetch(cache_key, expires_in: quantum) do
|
199
199
|
# qstamp(quantum, pattern: pattern, time: time)
|
200
200
|
# end
|
@@ -245,19 +245,18 @@ module Familia
|
|
245
245
|
# NoDefault.qstamp() # Uses 10.minutes as fallback quantum
|
246
246
|
#
|
247
247
|
module Quantization
|
248
|
-
|
249
248
|
Familia::Base.add_feature self, :quantization
|
250
249
|
|
251
250
|
using Familia::Refinements::TimeLiterals
|
252
251
|
|
253
252
|
def self.included(base)
|
254
|
-
Familia.trace :LOADED, self, base
|
255
|
-
base.extend
|
253
|
+
Familia.trace :LOADED, self, base if Familia.debug?
|
254
|
+
base.extend ModelClassMethods
|
256
255
|
end
|
257
256
|
|
258
|
-
# Familia::Quantization::
|
257
|
+
# Familia::Quantization::ModelClassMethods
|
259
258
|
#
|
260
|
-
module
|
259
|
+
module ModelClassMethods
|
261
260
|
# Generates a quantized timestamp based on the given parameters
|
262
261
|
#
|
263
262
|
# This method rounds the current time to the nearest quantum and optionally
|
@@ -286,9 +285,7 @@ module Familia
|
|
286
285
|
#
|
287
286
|
def qstamp(quantum = nil, pattern: nil, time: nil)
|
288
287
|
# Handle array input format: [quantum, pattern]
|
289
|
-
if quantum.is_a?(Array)
|
290
|
-
quantum, pattern = quantum
|
291
|
-
end
|
288
|
+
quantum, pattern = quantum if quantum.is_a?(Array)
|
292
289
|
|
293
290
|
# Use default quantum if none specified
|
294
291
|
# Priority: provided quantum > class default_expiration > 10.minutes fallback
|
@@ -323,11 +320,11 @@ module Familia
|
|
323
320
|
end_bucket = qstamp(quantum, time: end_time)
|
324
321
|
|
325
322
|
while current <= end_bucket
|
326
|
-
if pattern
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
323
|
+
timestamps << if pattern
|
324
|
+
Time.at(current).strftime(pattern)
|
325
|
+
else
|
326
|
+
current
|
327
|
+
end
|
331
328
|
current += quantum
|
332
329
|
end
|
333
330
|
|
@@ -352,7 +349,7 @@ module Familia
|
|
352
349
|
bucket_start = qstamp(quantum, time: Time.at(bucket_time))
|
353
350
|
bucket_end = bucket_start + quantum - 1
|
354
351
|
|
355
|
-
timestamp
|
352
|
+
timestamp.between?(bucket_start, bucket_end)
|
356
353
|
end
|
357
354
|
end
|
358
355
|
|
@@ -397,8 +394,6 @@ module Familia
|
|
397
394
|
base_id = respond_to?(:identifier) ? identifier : object_id
|
398
395
|
"#{base_id}#{separator}#{timestamp}"
|
399
396
|
end
|
400
|
-
|
401
|
-
extend ClassMethods
|
402
397
|
end
|
403
398
|
end
|
404
399
|
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
<!--lib/familia/features/relationships/README.md-->
|
2
|
+
|
3
|
+
## Core Modules
|
4
|
+
|
5
|
+
**relationships.rb** - Main orchestrator that unifies all relationship functionality into a single feature, providing the public API and coordinating between all submodules.
|
6
|
+
|
7
|
+
**indexing.rb** - O(1) lookup capability via Valkey/Redis hashes and sets. Enables fast field-based searches when parent-scoped (within: ParentClass). Creates instance methods on parent class for scoped lookups.
|
8
|
+
|
9
|
+
**participation.rb** - Multi-presence management where objects can exist in multiple collections simultaneously with score-encoded metadata (timestamps, permissions, etc.). All add/remove operations use transactions for atomicity.
|
10
|
+
|
11
|
+
## Quick API Guide
|
12
|
+
|
13
|
+
**participates_in** - Collection membership ("this object belongs in that collection")
|
14
|
+
```ruby
|
15
|
+
participates_in Organization, :members, score: :joined_at, bidirectional: true
|
16
|
+
# Creates: org.members, org.add_member(), customer.add_to_organization_members()
|
17
|
+
```
|
18
|
+
|
19
|
+
**unique_index** - Fast unique lookups ("find object by unique field value")
|
20
|
+
```ruby
|
21
|
+
unique_index :email, :email_index, within: Organization # Scoped: org.find_by_email()
|
22
|
+
```
|
23
|
+
|
24
|
+
**multi_index** - Fast multi-value lookups ("find all objects by field value")
|
25
|
+
```ruby
|
26
|
+
multi_index :department, :dept_index, within: Organization
|
27
|
+
# Creates: org.sample_from_department(), org.find_all_by_department()
|
28
|
+
```
|
29
|
+
|
30
|
+
## Key Philosophy
|
31
|
+
|
32
|
+
The entire system embraces "where does this appear?" rather than "who owns this?" - enabling objects to exist in multiple contexts simultaneously while maintaining fast lookups and atomic operations.
|
33
|
+
|
34
|
+
## When to Use Which
|
35
|
+
|
36
|
+
<details>
|
37
|
+
<summary>📋 participates_in vs indexing - Decision Guide</summary>
|
38
|
+
|
39
|
+
### participates_in - Collection Membership
|
40
|
+
- **Purpose**: "This object belongs in that collection"
|
41
|
+
- **Storage**: SortedSet/Set/List of object IDs with optional scores
|
42
|
+
- **Use for**: Membership relationships, ordered lists, scored collections
|
43
|
+
- **Example**: Customers in an Organization, Tasks in a Project
|
44
|
+
- **Atomicity**: Transactions for all operations (collection + reverse index)
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
participates_in Organization, :members, score: :joined_at
|
48
|
+
# Creates: org.members (SortedSet), org.add_member(), customer.add_to_organization_members()
|
49
|
+
```
|
50
|
+
|
51
|
+
### unique_index - Fast Unique Lookups
|
52
|
+
- **Purpose**: "Find THE object by unique field value"
|
53
|
+
- **Storage**: HashKey for O(1) field-to-object mapping
|
54
|
+
- **Use for**: Email lookups, username searches, unique IDs
|
55
|
+
- **Example**: Find customer by email, find employee by badge number
|
56
|
+
- **Atomicity**: Transactions for updates (remove old + add new)
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
unique_index :email, :email_index, within: Organization
|
60
|
+
# Creates: org.find_by_email(), org.find_all_by_email()
|
61
|
+
```
|
62
|
+
|
63
|
+
### multi_index - Fast Multi-Value Lookups
|
64
|
+
- **Purpose**: "Find ALL objects by shared field value"
|
65
|
+
- **Storage**: UnsortedSet for O(1) field-to-objects mapping
|
66
|
+
- **Use for**: Grouping by department, status, category, tags
|
67
|
+
- **Example**: All employees in a department, all tasks with status
|
68
|
+
- **Atomicity**: Transactions for updates (remove from old set + add to new set)
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
multi_index :department, :dept_index, within: Organization
|
72
|
+
# Creates: org.sample_from_department(dept, count), org.find_all_by_department(dept)
|
73
|
+
```
|
74
|
+
|
75
|
+
</details>
|
76
|
+
|
77
|
+
> [!NOTE]
|
78
|
+
> **Scoping Patterns**: `unique_index` and `multi_index` use the `within:` parameter for instance-scoping, while participation uses distinct method names (`participates_in` vs `class_participates_in`) to reflect fundamentally different semantics (instance collections vs auto-tracking all instances).
|
79
|
+
|
80
|
+
> [!TIP]
|
81
|
+
> **Quick Decision Guide**
|
82
|
+
> - Need to store a collection of objects? → `participates_in`
|
83
|
+
> - Need to find ONE object by unique field? → `unique_index`
|
84
|
+
> - Need to find MANY objects by shared field? → `multi_index`
|
85
|
+
> - Combination? → Use all three together (very common)
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class Customer < Familia::Horreum
|
89
|
+
feature :relationships
|
90
|
+
|
91
|
+
participates_in Organization, :members # Customer belongs to org
|
92
|
+
unique_index :email, :email_index, within: Organization # Find by unique email
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
> [!NOTE]
|
97
|
+
> **Key**: `participates_in` = collections, `unique_index` = unique lookups, `multi_index` = group lookups.
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# lib/familia/features/relationships/collection_operations.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Features
|
5
|
+
module Relationships
|
6
|
+
# Shared collection operations for Participation module
|
7
|
+
# Provides common methods for working with Horreum-managed DataType collections
|
8
|
+
# Used by both ParticipantMethods and TargetMethods to reduce duplication
|
9
|
+
module CollectionOperations
|
10
|
+
using Familia::Refinements::StylizeWords
|
11
|
+
|
12
|
+
# Ensure a target class has the specified DataType field defined
|
13
|
+
# @param target_class [Class] The class that should have the collection
|
14
|
+
# @param collection_name [Symbol] Name of the collection field
|
15
|
+
# @param type [Symbol] Collection type (:sorted_set, :set, :list)
|
16
|
+
def ensure_collection_field(target_class, collection_name, type)
|
17
|
+
return if target_class.method_defined?(collection_name)
|
18
|
+
|
19
|
+
target_class.send(type, collection_name)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Add an item to a collection, handling type-specific operations
|
23
|
+
# @param collection [Familia::DataType] The collection to add to
|
24
|
+
# @param item [Object] The item to add (must respond to identifier)
|
25
|
+
# @param score [Float, nil] Score for sorted sets
|
26
|
+
# @param type [Symbol] Collection type
|
27
|
+
def add_to_collection(collection, item, type:, score: nil, target_class: nil, collection_name: nil)
|
28
|
+
case type
|
29
|
+
when :sorted_set
|
30
|
+
# Ensure score is never nil for sorted sets
|
31
|
+
score ||= calculate_item_score(item, target_class, collection_name)
|
32
|
+
collection.add(item.identifier, score)
|
33
|
+
when :list
|
34
|
+
# Lists use push/unshift operations
|
35
|
+
collection.add(item.identifier)
|
36
|
+
when :set
|
37
|
+
# Sets use simple add
|
38
|
+
collection.add(item.identifier)
|
39
|
+
else
|
40
|
+
raise ArgumentError, "Unknown collection type: #{type}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Remove an item from a collection
|
45
|
+
# @param collection [Familia::DataType] The collection to remove from
|
46
|
+
# @param item [Object] The item to remove (must respond to identifier)
|
47
|
+
# @param type [Symbol] Collection type
|
48
|
+
def remove_from_collection(collection, item, type: nil)
|
49
|
+
# All collection types support remove/delete
|
50
|
+
collection.remove(item.identifier)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Check if an item is a member of a collection
|
54
|
+
# @param collection [Familia::DataType] The collection to check
|
55
|
+
# @param item [Object] The item to check (must respond to identifier)
|
56
|
+
# @return [Boolean] True if item is in collection
|
57
|
+
def member_of_collection?(collection, item)
|
58
|
+
collection.member?(item.identifier)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Bulk add items to a collection using DataType methods
|
62
|
+
# @param collection [Familia::DataType] The collection to add to
|
63
|
+
# @param items [Array] Array of items to add
|
64
|
+
# @param type [Symbol] Collection type
|
65
|
+
def bulk_add_to_collection(collection, items, type:, target_class: nil, collection_name: nil)
|
66
|
+
return if items.empty?
|
67
|
+
|
68
|
+
case type
|
69
|
+
when :sorted_set
|
70
|
+
# Add items one by one for sorted sets to ensure proper scoring
|
71
|
+
items.each do |item|
|
72
|
+
score = calculate_item_score(item, target_class, collection_name)
|
73
|
+
collection.add(item.identifier, score)
|
74
|
+
end
|
75
|
+
when :set, :list
|
76
|
+
# For sets and lists, add items one by one using DataType methods
|
77
|
+
items.each do |item|
|
78
|
+
collection.add(item.identifier)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
raise ArgumentError, "Unknown collection type: #{type}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Calculate score for an item
|
88
|
+
# @param item [Object] The item to score
|
89
|
+
# @param target_class [Class, nil] The target class for participation scoring
|
90
|
+
# @param collection_name [Symbol, nil] The collection name for participation scoring
|
91
|
+
# @return [Float] The calculated score
|
92
|
+
def calculate_item_score(item, target_class = nil, collection_name = nil)
|
93
|
+
if item.respond_to?(:calculate_participation_score) && target_class && collection_name
|
94
|
+
item.calculate_participation_score(target_class, collection_name)
|
95
|
+
elsif item.respond_to?(:current_score)
|
96
|
+
item.current_score
|
97
|
+
else
|
98
|
+
Familia.now.to_f
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|