familia 2.0.0.pre17 → 2.0.0.pre19
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/CHANGELOG.rst +118 -6
- data/CLAUDE.md +43 -11
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +52 -0
- data/bin/irb +1 -1
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/core-field-system.md +48 -26
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +2 -1
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
- data/lib/familia/data_type/serialization.rb +9 -5
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +130 -11
- data/lib/familia/horreum/definition.rb +8 -38
- data/lib/familia/horreum/management.rb +38 -27
- data/lib/familia/horreum/persistence.rb +191 -67
- data/lib/familia/horreum/serialization.rb +94 -73
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +41 -18
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +268 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
- data/pr_agent.toml +31 -0
- data/pr_compliance_checklist.yaml +45 -0
- data/try/edge_cases/empty_identifiers_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +31 -31
- data/try/edge_cases/json_serialization_try.rb +2 -2
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
- data/try/edge_cases/race_conditions_try.rb +1 -1
- data/try/edge_cases/reserved_keywords_try.rb +1 -1
- data/try/edge_cases/string_coercion_try.rb +5 -5
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
- data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
- data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
- data/try/features/encrypted_fields/memory_security_try.rb +1 -1
- data/try/features/encrypted_fields/missing_current_key_version_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 +1 -1
- data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
- data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
- data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
- data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
- data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
- data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
- data/try/features/expiration/expiration_try.rb +2 -2
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +1 -1
- data/try/features/feature_improvements_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_try.rb +1 -1
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/real_feature_integration_try.rb +17 -14
- data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
- data/try/features/relationships/indexing_try.rb +34 -5
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +4 -4
- data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +5 -5
- data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
- data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
- data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
- data/try/features/relationships/relationships_performance_try.rb +1 -1
- data/try/features/relationships/relationships_performance_working_try.rb +1 -1
- data/try/features/relationships/relationships_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_try.rb +1 -1
- data/try/features/transient_fields/redacted_string_try.rb +1 -1
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
- data/try/{core → integration/connection}/pools_try.rb +1 -1
- data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
- data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
- data/try/{core → integration}/create_method_try.rb +23 -23
- data/try/integration/cross_component_try.rb +1 -1
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/{core → integration}/database_consistency_try.rb +11 -8
- data/try/{core → integration}/familia_extended_try.rb +1 -1
- data/try/{core → integration}/familia_members_methods_try.rb +1 -1
- data/try/{models → integration/models}/customer_safe_dump_try.rb +6 -2
- data/try/{models → integration/models}/customer_try.rb +1 -1
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +2 -2
- data/try/{core → integration}/persistence_operations_try.rb +163 -11
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +1 -1
- data/try/{core → integration}/secure_identifier_try.rb +1 -1
- data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
- data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
- data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
- data/try/{core → unit/core}/autoloader_try.rb +1 -1
- data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
- data/try/{core → unit/core}/connection_try.rb +1 -1
- data/try/{core → unit/core}/errors_try.rb +1 -1
- data/try/{core → unit/core}/extensions_try.rb +1 -1
- data/try/unit/core/familia_logger_try.rb +110 -0
- data/try/{core → unit/core}/familia_try.rb +1 -1
- data/try/{core → unit/core}/middleware_try.rb +41 -1
- data/try/{core → unit/core}/settings_try.rb +1 -1
- data/try/{core → unit/core}/time_utils_try.rb +1 -1
- data/try/{core → unit/core}/tools_try.rb +1 -1
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
- data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
- data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
- data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
- data/try/{data_types → unit/data_types}/list_try.rb +1 -1
- data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
- data/try/{data_types → unit/data_types}/string_try.rb +2 -2
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/{horreum → unit/horreum}/base_try.rb +4 -4
- data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
- data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
- data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
- data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
- data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
- data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
- data/try/{horreum → unit/horreum}/initialization_try.rb +3 -3
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
- metadata +147 -126
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/try/{debugging → support/debugging}/README.md +0 -0
- /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
- /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
- /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
- /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
- /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
- /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -49,11 +49,11 @@ module Familia
|
|
49
49
|
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
50
50
|
# @param field [Symbol] The field to index
|
51
51
|
# @param index_name [Symbol] Name of the index
|
52
|
-
# @param within [Class, Symbol, nil]
|
52
|
+
# @param within [Class, Symbol, nil] Scope class for instance-scoped index
|
53
53
|
# @param query [Boolean] Whether to generate query methods
|
54
54
|
def setup(indexed_class:, field:, index_name:, within:, query:)
|
55
55
|
# Normalize parameters and determine scope type
|
56
|
-
|
56
|
+
scope_class, scope_type = if within
|
57
57
|
k = Familia.resolve_class(within)
|
58
58
|
[k, :instance]
|
59
59
|
else
|
@@ -63,7 +63,8 @@ module Familia
|
|
63
63
|
# Store metadata for this indexing relationship
|
64
64
|
indexed_class.indexing_relationships << IndexingRelationship.new(
|
65
65
|
field: field,
|
66
|
-
|
66
|
+
scope_class: scope_class,
|
67
|
+
within: within,
|
67
68
|
index_name: index_name,
|
68
69
|
query: query,
|
69
70
|
cardinality: :unique,
|
@@ -73,10 +74,10 @@ module Familia
|
|
73
74
|
case scope_type
|
74
75
|
when :instance
|
75
76
|
# Instance-scoped index (within: Company)
|
76
|
-
if query &&
|
77
|
-
generate_query_methods_destination(indexed_class, field,
|
77
|
+
if query && scope_class.is_a?(Class)
|
78
|
+
generate_query_methods_destination(indexed_class, field, scope_class, index_name)
|
78
79
|
end
|
79
|
-
generate_mutation_methods_self(indexed_class, field,
|
80
|
+
generate_mutation_methods_self(indexed_class, field, scope_class, index_name)
|
80
81
|
when :class
|
81
82
|
# Class-level index (no within:)
|
82
83
|
indexed_class.send(:ensure_index_field, indexed_class, index_name, :class_hashkey)
|
@@ -85,7 +86,8 @@ module Familia
|
|
85
86
|
end
|
86
87
|
end
|
87
88
|
|
88
|
-
# Generates query methods ON THE
|
89
|
+
# Generates query methods ON THE SCOPE CLASS (Company when within: Company)
|
90
|
+
#
|
89
91
|
# - company.find_by_badge_number(badge) - find by field value
|
90
92
|
# - company.find_all_by_badge_number([badges]) - batch lookup
|
91
93
|
# - company.badge_index - DataType accessor
|
@@ -93,47 +95,57 @@ module Familia
|
|
93
95
|
#
|
94
96
|
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
95
97
|
# @param field [Symbol] The field to index (e.g., :badge_number)
|
96
|
-
# @param
|
98
|
+
# @param scope_class [Class] The scope class providing uniqueness context (e.g., Company)
|
97
99
|
# @param index_name [Symbol] Name of the index (e.g., :badge_index)
|
98
|
-
def generate_query_methods_destination(indexed_class, field,
|
99
|
-
# Resolve
|
100
|
-
|
100
|
+
def generate_query_methods_destination(indexed_class, field, scope_class, index_name)
|
101
|
+
# Resolve scope class using Familia pattern
|
102
|
+
actual_scope_class = Familia.resolve_class(scope_class)
|
101
103
|
|
102
104
|
# Ensure the index field is declared (creates accessor that returns DataType)
|
103
|
-
|
105
|
+
actual_scope_class.send(:ensure_index_field, actual_scope_class, index_name, :hashkey)
|
104
106
|
|
105
107
|
# Generate instance query method (e.g., company.find_by_badge_number)
|
106
|
-
|
107
|
-
define_method("find_by_#{field}") do |
|
108
|
+
actual_scope_class.class_eval do
|
109
|
+
define_method(:"find_by_#{field}") do |provided_value|
|
108
110
|
# Use declared field accessor instead of manual instantiation
|
109
111
|
index_hash = send(index_name)
|
110
112
|
|
111
|
-
# Get the identifier from the hash
|
112
|
-
|
113
|
-
|
113
|
+
# Get the identifier from the hash using .get method.
|
114
|
+
# We use .get instead of [] because it's part of the standard interface
|
115
|
+
# common across all DataType classes (List, UnsortedSet, SortedSet, HashKey).
|
116
|
+
# While unique indexes always use HashKey, using .get maintains consistency
|
117
|
+
# with the broader DataType API patterns used throughout Familia.
|
118
|
+
record_id = index_hash.get(provided_value)
|
119
|
+
return nil unless record_id
|
114
120
|
|
115
|
-
indexed_class.
|
121
|
+
indexed_class.find_by_identifier(record_id)
|
116
122
|
end
|
117
123
|
|
118
124
|
# Generate bulk query method (e.g., company.find_all_by_badge_number)
|
119
|
-
define_method("find_all_by_#{field}") do |
|
120
|
-
|
121
|
-
|
125
|
+
define_method(:"find_all_by_#{field}") do |provided_ids|
|
126
|
+
# Convert to array and filter nil inputs before querying Redis.
|
127
|
+
# This prevents wasteful lookups for empty string keys (nil.to_s → "").
|
128
|
+
# Output may contain fewer elements than input (standard ORM behavior).
|
129
|
+
provided_ids = Array(provided_ids).compact
|
130
|
+
return [] if provided_ids.empty?
|
122
131
|
|
123
132
|
# Use declared field accessor instead of manual instantiation
|
124
133
|
index_hash = send(index_name)
|
125
134
|
|
126
135
|
# Get all identifiers from the hash
|
127
|
-
|
128
|
-
|
129
|
-
|
136
|
+
record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
|
137
|
+
|
138
|
+
# Filter out nil values (non-existent records) and instantiate objects
|
139
|
+
record_ids.compact.map { |record_id|
|
140
|
+
indexed_class.find_by_identifier(record_id)
|
141
|
+
}
|
130
142
|
end
|
131
143
|
|
132
144
|
# Accessor method already created by ensure_index_field above
|
133
145
|
# No need to manually define it here
|
134
146
|
|
135
147
|
# Generate method to rebuild the unique index for this parent instance
|
136
|
-
define_method("rebuild_#{index_name}") do
|
148
|
+
define_method(:"rebuild_#{index_name}") do
|
137
149
|
# Use declared field accessor instead of manual instantiation
|
138
150
|
index_hash = send(index_name)
|
139
151
|
|
@@ -147,63 +159,102 @@ module Familia
|
|
147
159
|
end
|
148
160
|
end
|
149
161
|
|
150
|
-
# Generates mutation methods ON THE INDEXED CLASS (Employee)
|
151
|
-
#
|
152
|
-
# -
|
162
|
+
# Generates mutation methods ON THE INDEXED CLASS (Employee)
|
163
|
+
#
|
164
|
+
# Instance methods for scope-scoped unique index operations:
|
165
|
+
# - employee.add_to_company_badge_index(company) - automatically validates uniqueness
|
153
166
|
# - employee.remove_from_company_badge_index(company)
|
154
167
|
# - employee.update_in_company_badge_index(company, old_badge)
|
168
|
+
# - employee.guard_unique_company_badge_index!(company) - manual validation
|
155
169
|
#
|
156
170
|
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
157
171
|
# @param field [Symbol] The field to index (e.g., :badge_number)
|
158
|
-
# @param
|
172
|
+
# @param scope_class [Class] The scope class providing uniqueness context (e.g., Company)
|
159
173
|
# @param index_name [Symbol] Name of the index (e.g., :badge_index)
|
160
|
-
def generate_mutation_methods_self(indexed_class, field,
|
161
|
-
|
174
|
+
def generate_mutation_methods_self(indexed_class, field, scope_class, index_name)
|
175
|
+
scope_class_config = scope_class.config_name
|
162
176
|
indexed_class.class_eval do
|
163
|
-
method_name = "add_to_#{
|
177
|
+
method_name = :"add_to_#{scope_class_config}_#{index_name}"
|
164
178
|
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
165
179
|
|
166
|
-
define_method(method_name) do |
|
167
|
-
return unless
|
180
|
+
define_method(method_name) do |scope_instance|
|
181
|
+
return unless scope_instance
|
168
182
|
|
169
183
|
field_value = send(field)
|
170
184
|
return unless field_value
|
171
185
|
|
172
|
-
#
|
173
|
-
|
186
|
+
# Automatically validate uniqueness before adding to index.
|
187
|
+
# Skip validation inside transactions since guard methods require read
|
188
|
+
# operations not available in MULTI/EXEC blocks.
|
189
|
+
unless Fiber[:familia_transaction]
|
190
|
+
guard_method = :"guard_unique_#{scope_class_config}_#{index_name}!"
|
191
|
+
send(guard_method, scope_instance) if respond_to?(guard_method)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Use declared field accessor on scope instance
|
195
|
+
index_hash = scope_instance.send(index_name)
|
174
196
|
|
175
|
-
#
|
197
|
+
# Set the value (guard already validated uniqueness)
|
176
198
|
index_hash[field_value.to_s] = identifier
|
177
199
|
end
|
178
200
|
|
179
|
-
|
201
|
+
# Add a guard method to enforce unique constraint on this instance-scoped index
|
202
|
+
#
|
203
|
+
# @param scope_instance [Object] The scope instance providing uniqueness context (e.g., a Company)
|
204
|
+
# @raise [Familia::RecordExistsError] if a record with the same field value
|
205
|
+
# exists in the scope's index. Values are compared as strings.
|
206
|
+
# @return [void]
|
207
|
+
#
|
208
|
+
# @example
|
209
|
+
# employee.guard_unique_company_badge_index!(company)
|
210
|
+
#
|
211
|
+
method_name = :"guard_unique_#{scope_class_config}_#{index_name}!"
|
180
212
|
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
181
213
|
|
182
|
-
define_method(method_name) do |
|
183
|
-
return unless
|
214
|
+
define_method(method_name) do |scope_instance|
|
215
|
+
return unless scope_instance
|
184
216
|
|
185
217
|
field_value = send(field)
|
186
218
|
return unless field_value
|
187
219
|
|
188
|
-
# Use declared field accessor on
|
189
|
-
index_hash =
|
220
|
+
# Use declared field accessor on scope instance
|
221
|
+
index_hash = scope_instance.send(index_name)
|
222
|
+
existing_id = index_hash.get(field_value.to_s)
|
223
|
+
|
224
|
+
if existing_id && existing_id != identifier
|
225
|
+
raise Familia::RecordExistsError,
|
226
|
+
"#{self.class} exists in #{scope_instance.class} with #{field}=#{field_value}"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
method_name = :"remove_from_#{scope_class_config}_#{index_name}"
|
231
|
+
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
232
|
+
|
233
|
+
define_method(method_name) do |scope_instance|
|
234
|
+
return unless scope_instance
|
235
|
+
|
236
|
+
field_value = send(field)
|
237
|
+
return unless field_value
|
238
|
+
|
239
|
+
# Use declared field accessor on scope instance
|
240
|
+
index_hash = scope_instance.send(index_name)
|
190
241
|
|
191
242
|
# Remove using HashKey DataType method
|
192
243
|
index_hash.remove(field_value.to_s)
|
193
244
|
end
|
194
245
|
|
195
|
-
method_name = "update_in_#{
|
246
|
+
method_name = :"update_in_#{scope_class_config}_#{index_name}"
|
196
247
|
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
197
248
|
|
198
|
-
define_method(method_name) do |
|
199
|
-
return unless
|
249
|
+
define_method(method_name) do |scope_instance, old_field_value = nil|
|
250
|
+
return unless scope_instance
|
200
251
|
|
201
252
|
new_field_value = send(field)
|
202
253
|
|
203
254
|
# Use Familia's transaction method for atomicity with DataType abstraction
|
204
|
-
|
205
|
-
# Use declared field accessor on
|
206
|
-
index_hash =
|
255
|
+
scope_instance.transaction do |_tx|
|
256
|
+
# Use declared field accessor on scope instance
|
257
|
+
index_hash = scope_instance.send(index_name)
|
207
258
|
|
208
259
|
# Remove old value if provided
|
209
260
|
index_hash.remove(old_field_value.to_s) if old_field_value
|
@@ -222,31 +273,47 @@ module Familia
|
|
222
273
|
# - Employee.email_index
|
223
274
|
# - Employee.rebuild_email_index
|
224
275
|
def generate_query_methods_class(field, index_name, indexed_class)
|
225
|
-
|
226
|
-
|
227
|
-
|
276
|
+
# Generate class-level single record method
|
277
|
+
indexed_class.define_singleton_method(:"find_by_#{field}") do |provided_id|
|
278
|
+
index_hash = send(index_name) # access the class-level hashkey DataType
|
279
|
+
|
280
|
+
# Get the identifier from the db hashkey using .get method.
|
281
|
+
#
|
282
|
+
# We use .get instead of [] because it's part of the standard interface
|
283
|
+
# common across all DataType classes (List, UnsortedSet, SortedSet, HashKey).
|
284
|
+
# While unique indexes always use HashKey, using .get maintains consistency
|
285
|
+
# with the broader DataType API patterns used throughout Familia.
|
286
|
+
record_id = index_hash.get(provided_id)
|
228
287
|
|
229
|
-
return nil unless
|
288
|
+
return nil unless record_id
|
230
289
|
|
231
|
-
|
290
|
+
indexed_class.find_by_identifier(record_id)
|
232
291
|
end
|
233
292
|
|
234
293
|
# Generate class-level bulk query method
|
235
|
-
indexed_class.define_singleton_method("find_all_by_#{field}") do |
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
294
|
+
indexed_class.define_singleton_method(:"find_all_by_#{field}") do |provided_ids|
|
295
|
+
# Convert to array and filter nil inputs before querying Redis.
|
296
|
+
# This prevents wasteful lookups for empty string keys (nil.to_s → "").
|
297
|
+
# Output may contain fewer elements than input (standard ORM behavior).
|
298
|
+
provided_ids = Array(provided_ids).compact
|
299
|
+
return [] if provided_ids.empty?
|
300
|
+
|
301
|
+
index_hash = send(index_name) # access the class-level hashkey DataType
|
302
|
+
|
303
|
+
# Get multiple identifiers from the db hashkey using .values_at
|
304
|
+
record_ids = index_hash.values_at(*provided_ids.map(&:to_s))
|
305
|
+
|
306
|
+
# Filter out nil values (non-existent records) and instantiate objects
|
307
|
+
record_ids.compact.map { |record_id|
|
308
|
+
indexed_class.find_by_identifier(record_id)
|
309
|
+
}
|
243
310
|
end
|
244
311
|
|
245
312
|
# The index accessor method is already created by the class_hashkey declaration
|
246
313
|
# No need to manually create it - Horreum handles this automatically
|
247
314
|
|
248
315
|
# Generate method to rebuild the class-level index
|
249
|
-
indexed_class.define_singleton_method("rebuild_#{index_name}") do
|
316
|
+
indexed_class.define_singleton_method(:"rebuild_#{index_name}") do
|
250
317
|
index_hash = send(index_name) # Access the class-level hashkey DataType
|
251
318
|
|
252
319
|
# Clear existing index using DataType method
|
@@ -265,16 +332,35 @@ module Familia
|
|
265
332
|
# - employee.update_in_class_email_index(old_email)
|
266
333
|
def generate_mutation_methods_class(field, index_name, indexed_class)
|
267
334
|
indexed_class.class_eval do
|
268
|
-
define_method("add_to_class_#{index_name}") do
|
335
|
+
define_method(:"add_to_class_#{index_name}") do
|
269
336
|
index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
|
270
337
|
field_value = send(field)
|
271
338
|
|
272
339
|
return unless field_value
|
273
340
|
|
341
|
+
# Just set the value - uniqueness should be validated before save
|
274
342
|
index_hash[field_value.to_s] = identifier
|
275
343
|
end
|
276
344
|
|
277
|
-
|
345
|
+
# Add a guard method to enforce unique constraint on this specific index
|
346
|
+
#
|
347
|
+
# @raise [Familia::RecordExistsError] if a record with the same
|
348
|
+
# field value exists. Values are compared as strings.
|
349
|
+
#
|
350
|
+
# @return [void]
|
351
|
+
define_method(:"guard_unique_#{index_name}!") do
|
352
|
+
field_value = send(field)
|
353
|
+
return unless field_value
|
354
|
+
|
355
|
+
index_hash = self.class.send(index_name)
|
356
|
+
existing_id = index_hash.get(field_value.to_s)
|
357
|
+
|
358
|
+
if existing_id && existing_id != identifier
|
359
|
+
raise Familia::RecordExistsError, "#{self.class} exists #{field}=#{field_value}"
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
define_method(:"remove_from_class_#{index_name}") do
|
278
364
|
index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
|
279
365
|
field_value = send(field)
|
280
366
|
|
@@ -283,7 +369,7 @@ module Familia
|
|
283
369
|
index_hash.remove(field_value.to_s)
|
284
370
|
end
|
285
371
|
|
286
|
-
define_method("update_in_class_#{index_name}") do |old_field_value = nil|
|
372
|
+
define_method(:"update_in_class_#{index_name}") do |old_field_value = nil|
|
287
373
|
new_field_value = send(field)
|
288
374
|
|
289
375
|
# Use class-level transaction for atomicity with DataType abstraction
|
@@ -50,7 +50,7 @@ module Familia
|
|
50
50
|
# Terminology:
|
51
51
|
# - unique_index: 1:1 field-to-object mapping (HashKey)
|
52
52
|
# - multi_index: 1:many field-to-objects mapping (UnsortedSet, no scores)
|
53
|
-
# - within:
|
53
|
+
# - within: scope class providing uniqueness boundary for instance-scoped indexes
|
54
54
|
# - query: whether to generate find_by_* methods (default: true)
|
55
55
|
#
|
56
56
|
# Key Patterns:
|
@@ -89,7 +89,7 @@ module Familia
|
|
89
89
|
#
|
90
90
|
# @param field [Symbol] The field to index on
|
91
91
|
# @param index_name [Symbol] Name of the index
|
92
|
-
# @param within [Class, Symbol] The
|
92
|
+
# @param within [Class, Symbol] The scope class providing uniqueness context
|
93
93
|
# @param query [Boolean] Whether to generate query methods
|
94
94
|
#
|
95
95
|
# @example Instance-scoped multi-value indexing
|
@@ -109,7 +109,7 @@ module Familia
|
|
109
109
|
#
|
110
110
|
# @param field [Symbol] The field to index on
|
111
111
|
# @param index_name [Symbol] Name of the index hash
|
112
|
-
# @param within [Class, Symbol] Optional
|
112
|
+
# @param within [Class, Symbol] Optional scope class for instance-scoped unique index
|
113
113
|
# @param query [Boolean] Whether to generate query methods
|
114
114
|
#
|
115
115
|
# @example Class-level unique index
|
@@ -136,70 +136,68 @@ module Familia
|
|
136
136
|
|
137
137
|
# Ensure proper DataType field is declared for index
|
138
138
|
# Similar to ensure_collection_field in participation system
|
139
|
-
def ensure_index_field(
|
140
|
-
return if
|
139
|
+
def ensure_index_field(scope_class, index_name, field_type)
|
140
|
+
return if scope_class.method_defined?(index_name) || scope_class.respond_to?(index_name)
|
141
141
|
|
142
|
-
|
142
|
+
scope_class.send(field_type, index_name)
|
143
143
|
end
|
144
144
|
end
|
145
145
|
|
146
146
|
# Instance methods for indexed objects
|
147
147
|
module ModelInstanceMethods
|
148
|
-
# Update all indexes for a given
|
149
|
-
# For class-level indexes (
|
150
|
-
# For
|
151
|
-
def update_all_indexes(old_values = {},
|
148
|
+
# Update all indexes for a given scope context
|
149
|
+
# For class-level indexes (unique_index without within:), scope_context should be nil
|
150
|
+
# For instance-scoped indexes (with within:), scope_context should be the scope instance
|
151
|
+
def update_all_indexes(old_values = {}, scope_context = nil)
|
152
152
|
return unless self.class.respond_to?(:indexing_relationships)
|
153
153
|
|
154
154
|
self.class.indexing_relationships.each do |config|
|
155
155
|
field = config.field
|
156
156
|
index_name = config.index_name
|
157
|
-
target_class = config.target_class
|
158
157
|
old_field_value = old_values[field]
|
159
158
|
|
160
159
|
# Determine which update method to call
|
161
|
-
if
|
160
|
+
if config.within.nil?
|
162
161
|
# Class-level index (unique_index without within:)
|
163
162
|
send("update_in_class_#{index_name}", old_field_value)
|
164
163
|
else
|
165
|
-
#
|
166
|
-
next unless
|
164
|
+
# Instance-scoped index (unique_index or multi_index with within:) - requires scope context
|
165
|
+
next unless scope_context
|
167
166
|
|
168
167
|
# Use config_name for method naming
|
169
|
-
|
170
|
-
send("update_in_#{
|
168
|
+
scope_class_config = Familia.resolve_class(config.scope_class).config_name
|
169
|
+
send("update_in_#{scope_class_config}_#{index_name}", scope_context, old_field_value)
|
171
170
|
end
|
172
171
|
end
|
173
172
|
end
|
174
173
|
|
175
|
-
# Remove from all indexes for a given
|
176
|
-
# For class-level indexes (
|
177
|
-
# For
|
178
|
-
def remove_from_all_indexes(
|
174
|
+
# Remove from all indexes for a given scope context
|
175
|
+
# For class-level indexes (unique_index without within:), scope_context should be nil
|
176
|
+
# For instance-scoped indexes (with within:), scope_context should be the scope instance
|
177
|
+
def remove_from_all_indexes(scope_context = nil)
|
179
178
|
return unless self.class.respond_to?(:indexing_relationships)
|
180
179
|
|
181
180
|
self.class.indexing_relationships.each do |config|
|
182
181
|
index_name = config.index_name
|
183
|
-
target_class = config.target_class
|
184
182
|
|
185
183
|
# Determine which remove method to call
|
186
|
-
if
|
184
|
+
if config.within.nil?
|
187
185
|
# Class-level index (unique_index without within:)
|
188
186
|
send("remove_from_class_#{index_name}")
|
189
187
|
else
|
190
|
-
#
|
191
|
-
next unless
|
188
|
+
# Instance-scoped index (unique_index or multi_index with within:) - requires scope context
|
189
|
+
next unless scope_context
|
192
190
|
|
193
191
|
# Use config_name for method naming
|
194
|
-
|
195
|
-
send("remove_from_#{
|
192
|
+
scope_class_config = Familia.resolve_class(config.scope_class).config_name
|
193
|
+
send("remove_from_#{scope_class_config}_#{index_name}", scope_context)
|
196
194
|
end
|
197
195
|
end
|
198
196
|
end
|
199
197
|
|
200
198
|
# Get all indexes this object appears in
|
201
|
-
# Note: For
|
202
|
-
# since
|
199
|
+
# Note: For instance-scoped indexes, this only shows class-level indexes
|
200
|
+
# since instance-scoped indexes require a specific scope instance
|
203
201
|
#
|
204
202
|
# @return [Array<Hash>] Array of index information
|
205
203
|
def current_indexings
|
@@ -210,19 +208,18 @@ module Familia
|
|
210
208
|
self.class.indexing_relationships.each do |config|
|
211
209
|
field = config.field
|
212
210
|
index_name = config.index_name
|
213
|
-
target_class = config.target_class
|
214
211
|
cardinality = config.cardinality
|
215
212
|
field_value = send(field)
|
216
213
|
|
217
214
|
next unless field_value
|
218
215
|
|
219
|
-
if
|
216
|
+
if config.within.nil?
|
220
217
|
# Class-level index (unique_index without within:) - check hash key using DataType
|
221
218
|
index_hash = self.class.send(index_name)
|
222
219
|
next unless index_hash.key?(field_value.to_s)
|
223
220
|
|
224
221
|
memberships << {
|
225
|
-
|
222
|
+
scope_class: 'class',
|
226
223
|
index_name: index_name,
|
227
224
|
field: field,
|
228
225
|
field_value: field_value,
|
@@ -231,17 +228,17 @@ module Familia
|
|
231
228
|
type: 'unique_index',
|
232
229
|
}
|
233
230
|
else
|
234
|
-
# Instance-scoped index (unique_index or multi_index with within:) - cannot check without
|
235
|
-
# This would require scanning all possible
|
231
|
+
# Instance-scoped index (unique_index or multi_index with within:) - cannot check without scope instance
|
232
|
+
# This would require scanning all possible scope instances
|
236
233
|
memberships << {
|
237
|
-
|
234
|
+
scope_class: config.scope_class_config_name,
|
238
235
|
index_name: index_name,
|
239
236
|
field: field,
|
240
237
|
field_value: field_value,
|
241
|
-
index_key: '
|
238
|
+
index_key: 'scope_dependent',
|
242
239
|
cardinality: cardinality,
|
243
240
|
type: cardinality == :unique ? 'unique_index' : 'multi_index',
|
244
|
-
note: 'Requires
|
241
|
+
note: 'Requires scope instance for verification',
|
245
242
|
}
|
246
243
|
end
|
247
244
|
end
|
@@ -249,9 +246,9 @@ module Familia
|
|
249
246
|
memberships
|
250
247
|
end
|
251
248
|
|
252
|
-
# Check if this object is indexed in a specific
|
249
|
+
# Check if this object is indexed in a specific scope
|
253
250
|
# For class-level indexes, checks the hash key
|
254
|
-
# For
|
251
|
+
# For instance-scoped indexes, returns false (requires scope instance)
|
255
252
|
def indexed_in?(index_name)
|
256
253
|
return false unless self.class.respond_to?(:indexing_relationships)
|
257
254
|
|
@@ -262,14 +259,12 @@ module Familia
|
|
262
259
|
field_value = send(field)
|
263
260
|
return false unless field_value
|
264
261
|
|
265
|
-
|
266
|
-
|
267
|
-
if target_class == self.class
|
262
|
+
if config.within.nil?
|
268
263
|
# Class-level index (class_indexed_by) - check hash key using DataType
|
269
264
|
index_hash = self.class.send(index_name)
|
270
265
|
index_hash.key?(field_value.to_s)
|
271
266
|
else
|
272
|
-
#
|
267
|
+
# Instance-scoped index (with within:) - cannot verify without scope instance
|
273
268
|
false
|
274
269
|
end
|
275
270
|
end
|
@@ -14,20 +14,30 @@ module Familia
|
|
14
14
|
# Similar to ParticipationRelationship but for attribute-based lookups
|
15
15
|
# rather than collection membership.
|
16
16
|
#
|
17
|
+
# Terminology:
|
18
|
+
# - `scope_class`: The class that provides the uniqueness boundary for
|
19
|
+
# instance-scoped indexes. For example, in `unique_index :badge_number,
|
20
|
+
# :badge_index, within: Company`, the Company is the scope class.
|
21
|
+
# - `within`: Preserves the original DSL parameter to explicitly distinguish
|
22
|
+
# class-level indexes (within: nil) from instance-scoped indexes (within:
|
23
|
+
# SomeClass). This avoids brittle class comparisons and prevents issues
|
24
|
+
# with inheritance scenarios.
|
25
|
+
#
|
17
26
|
IndexingRelationship = Data.define(
|
18
27
|
:field, # Symbol - field being indexed (e.g., :email, :department)
|
19
28
|
:index_name, # Symbol - name of the index (e.g., :email_index, :dept_index)
|
20
|
-
:
|
29
|
+
:scope_class, # Class/Symbol - scope class for instance-scoped indexes (within:)
|
30
|
+
:within, # Class/Symbol/nil - within: parameter (nil for class-level, Class for instance-scoped)
|
21
31
|
:cardinality, # Symbol - :unique (1:1) or :multi (1:many)
|
22
32
|
:query # Boolean - whether to generate query methods
|
23
33
|
) do
|
24
34
|
#
|
25
|
-
# Get the normalized config name for the
|
35
|
+
# Get the normalized config name for the scope class
|
26
36
|
#
|
27
37
|
# @return [String] The config name (e.g., "user", "company", "test_company")
|
28
38
|
#
|
29
|
-
def
|
30
|
-
|
39
|
+
def scope_class_config_name
|
40
|
+
scope_class.config_name
|
31
41
|
end
|
32
42
|
end
|
33
43
|
end
|
@@ -1,9 +1,8 @@
|
|
1
1
|
# lib/familia/features/safe_dump.rb
|
2
2
|
|
3
3
|
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# at runtime, so thread safety is not a concern for this feature.
|
4
|
+
# Class instance variables are used here for configuration. These are set
|
5
|
+
# once at loadtime and not mutated, so thread safety is not an issue here.
|
7
6
|
#
|
8
7
|
module Familia
|
9
8
|
module Features
|
data/lib/familia/field_type.rb
CHANGED
@@ -143,7 +143,8 @@ module Familia
|
|
143
143
|
val = args.first
|
144
144
|
|
145
145
|
# If no value provided, return current stored value
|
146
|
-
|
146
|
+
# Handle Redis::Future objects during transactions
|
147
|
+
return hget(field_name) if val.nil? || val.is_a?(Redis::Future)
|
147
148
|
|
148
149
|
begin
|
149
150
|
# Trace the operation if debugging is enabled
|