familia 2.0.0.pre14 → 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 +66 -6
- 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 +4 -4
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre13.md +1 -1
- 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/{autoloader.rb → features/autoloader.rb} +49 -23
- 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 +68 -66
- data/lib/familia/features/expiration/extensions.rb +61 -0
- data/lib/familia/features/expiration.rb +35 -87
- data/lib/familia/features/external_identifier.rb +11 -12
- data/lib/familia/features/object_identifier.rb +58 -20
- data/lib/familia/features/quantization.rb +17 -22
- 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 +69 -271
- data/lib/familia/features/safe_dump.rb +127 -132
- 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 +5 -5
- data/lib/familia/features.rb +21 -21
- 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 -15
- 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 +129 -11
- 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 +77 -45
- 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 -228
- 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/autoloadable.rb +0 -113
- 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/autoloadable/autoloadable_try.rb +0 -61
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
- 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
|
#
|
@@ -81,15 +81,26 @@ module Familia
|
|
81
81
|
# - Custom generators allow domain-specific security requirements
|
82
82
|
#
|
83
83
|
module ObjectIdentifier
|
84
|
+
Familia::Base.add_feature self, :object_identifier, depends_on: []
|
85
|
+
|
84
86
|
DEFAULT_GENERATOR = :uuid_v7
|
85
87
|
|
86
88
|
def self.included(base)
|
87
|
-
Familia.trace :LOADED, self, base
|
88
|
-
base.extend
|
89
|
+
Familia.trace :LOADED, self, base if Familia.debug?
|
90
|
+
base.extend ModelClassMethods
|
89
91
|
|
90
92
|
# Ensure default generator is set in feature options
|
91
93
|
base.add_feature_options(:object_identifier, generator: DEFAULT_GENERATOR)
|
92
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
|
+
|
93
104
|
# Register the objid field using a simple custom field type
|
94
105
|
base.register_field_type(ObjectIdentifierFieldType.new(:objid, as: :objid, fast_method: false))
|
95
106
|
end
|
@@ -99,7 +110,7 @@ module Familia
|
|
99
110
|
# Object identifier fields automatically generate unique identifiers when first
|
100
111
|
# accessed if not already set. The generation strategy is configurable via
|
101
112
|
# feature options. These fields preserve any values set during initialization
|
102
|
-
# to ensure data integrity when loading existing objects from
|
113
|
+
# to ensure data integrity when loading existing objects from the database.
|
103
114
|
#
|
104
115
|
# The field type tracks the generator used for each objid to provide provenance
|
105
116
|
# information for security-sensitive operations like external identifier generation.
|
@@ -149,6 +160,9 @@ module Familia
|
|
149
160
|
generator = options[:generator] || DEFAULT_GENERATOR
|
150
161
|
instance_variable_set(:"@#{field_name}_generator_used", generator)
|
151
162
|
|
163
|
+
# Update mapping from objid to model primary key
|
164
|
+
self.class.objid_lookup[generated_id] = identifier if respond_to?(:identifier) && identifier
|
165
|
+
|
152
166
|
generated_id
|
153
167
|
end
|
154
168
|
end
|
@@ -164,7 +178,7 @@ module Familia
|
|
164
178
|
# Override setter to preserve values during initialization
|
165
179
|
#
|
166
180
|
# This ensures that values passed during object initialization
|
167
|
-
# (e.g., when loading from Redis) are preserved and not overwritten
|
181
|
+
# (e.g., when loading from Valkey/Redis) are preserved and not overwritten
|
168
182
|
# by the lazy generation logic.
|
169
183
|
#
|
170
184
|
# @param klass [Class] The class to define the method on
|
@@ -175,9 +189,19 @@ module Familia
|
|
175
189
|
|
176
190
|
handle_method_conflict(klass, :"#{method_name}=") do
|
177
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
|
+
|
178
199
|
instance_variable_set(:"@#{field_name}", value)
|
179
200
|
|
180
|
-
#
|
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),
|
181
205
|
# we cannot determine the original generator, so we clear the provenance
|
182
206
|
# tracking to indicate unknown origin. This prevents false assumptions
|
183
207
|
# about the security properties of externally-provided identifiers.
|
@@ -203,7 +227,7 @@ module Familia
|
|
203
227
|
end
|
204
228
|
end
|
205
229
|
|
206
|
-
module
|
230
|
+
module ModelClassMethods
|
207
231
|
# Generate a new object identifier using the configured strategy
|
208
232
|
#
|
209
233
|
# @return [String] A new unique identifier
|
@@ -218,7 +242,7 @@ module Familia
|
|
218
242
|
when :uuid_v4
|
219
243
|
SecureRandom.uuid_v4
|
220
244
|
when :hex
|
221
|
-
Familia.
|
245
|
+
Familia.generate_id(16)
|
222
246
|
when Proc
|
223
247
|
generator.call
|
224
248
|
else
|
@@ -241,14 +265,21 @@ module Familia
|
|
241
265
|
|
242
266
|
if Familia.debug?
|
243
267
|
reference = caller(1..1).first
|
244
|
-
Familia.trace :FIND_BY_OBJID,
|
268
|
+
Familia.trace :FIND_BY_OBJID, nil, objid, reference
|
245
269
|
end
|
246
270
|
|
247
|
-
#
|
248
|
-
|
249
|
-
|
250
|
-
|
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)
|
251
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)
|
252
283
|
nil
|
253
284
|
end
|
254
285
|
end
|
@@ -281,6 +312,15 @@ module Familia
|
|
281
312
|
self.objid = value
|
282
313
|
end
|
283
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
|
+
|
284
324
|
# Initialize object identifier configuration
|
285
325
|
#
|
286
326
|
# Called during object initialization to set up the ID generation strategy.
|
@@ -298,10 +338,8 @@ module Familia
|
|
298
338
|
|
299
339
|
options = self.class.feature_options(:object_identifier)
|
300
340
|
generator = options[:generator] || DEFAULT_GENERATOR
|
301
|
-
Familia.trace :OBJID_INIT,
|
341
|
+
Familia.trace :OBJID_INIT, nil, "Generator strategy: #{generator}"
|
302
342
|
end
|
303
|
-
|
304
|
-
Familia::Base.add_feature self, :object_identifier, depends_on: []
|
305
343
|
end
|
306
344
|
end
|
307
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,17 +245,18 @@ module Familia
|
|
245
245
|
# NoDefault.qstamp() # Uses 10.minutes as fallback quantum
|
246
246
|
#
|
247
247
|
module Quantization
|
248
|
+
Familia::Base.add_feature self, :quantization
|
248
249
|
|
249
250
|
using Familia::Refinements::TimeLiterals
|
250
251
|
|
251
252
|
def self.included(base)
|
252
|
-
Familia.trace :LOADED, self, base
|
253
|
-
base.extend
|
253
|
+
Familia.trace :LOADED, self, base if Familia.debug?
|
254
|
+
base.extend ModelClassMethods
|
254
255
|
end
|
255
256
|
|
256
|
-
# Familia::Quantization::
|
257
|
+
# Familia::Quantization::ModelClassMethods
|
257
258
|
#
|
258
|
-
module
|
259
|
+
module ModelClassMethods
|
259
260
|
# Generates a quantized timestamp based on the given parameters
|
260
261
|
#
|
261
262
|
# This method rounds the current time to the nearest quantum and optionally
|
@@ -284,9 +285,7 @@ module Familia
|
|
284
285
|
#
|
285
286
|
def qstamp(quantum = nil, pattern: nil, time: nil)
|
286
287
|
# Handle array input format: [quantum, pattern]
|
287
|
-
if quantum.is_a?(Array)
|
288
|
-
quantum, pattern = quantum
|
289
|
-
end
|
288
|
+
quantum, pattern = quantum if quantum.is_a?(Array)
|
290
289
|
|
291
290
|
# Use default quantum if none specified
|
292
291
|
# Priority: provided quantum > class default_expiration > 10.minutes fallback
|
@@ -321,11 +320,11 @@ module Familia
|
|
321
320
|
end_bucket = qstamp(quantum, time: end_time)
|
322
321
|
|
323
322
|
while current <= end_bucket
|
324
|
-
if pattern
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
323
|
+
timestamps << if pattern
|
324
|
+
Time.at(current).strftime(pattern)
|
325
|
+
else
|
326
|
+
current
|
327
|
+
end
|
329
328
|
current += quantum
|
330
329
|
end
|
331
330
|
|
@@ -350,7 +349,7 @@ module Familia
|
|
350
349
|
bucket_start = qstamp(quantum, time: Time.at(bucket_time))
|
351
350
|
bucket_end = bucket_start + quantum - 1
|
352
351
|
|
353
|
-
timestamp
|
352
|
+
timestamp.between?(bucket_start, bucket_end)
|
354
353
|
end
|
355
354
|
end
|
356
355
|
|
@@ -395,10 +394,6 @@ module Familia
|
|
395
394
|
base_id = respond_to?(:identifier) ? identifier : object_id
|
396
395
|
"#{base_id}#{separator}#{timestamp}"
|
397
396
|
end
|
398
|
-
|
399
|
-
extend ClassMethods
|
400
|
-
|
401
|
-
Familia::Base.add_feature self, :quantization
|
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
|