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
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Features
|
5
|
+
module Relationships
|
6
|
+
module Indexing
|
7
|
+
# Generators for multi-value index (1:many) methods
|
8
|
+
#
|
9
|
+
# Multi-value indexes use UnsortedSet DataType for grouping objects by field value.
|
10
|
+
# Each field value gets its own set of object identifiers.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
# multi_index :department, :dept_index, within: Company
|
14
|
+
#
|
15
|
+
# Generates on Company (destination):
|
16
|
+
# - company.sample_from_department(dept, count=1)
|
17
|
+
# - company.find_all_by_department(dept)
|
18
|
+
# - company.dept_index_for(dept_value)
|
19
|
+
# - company.rebuild_dept_index
|
20
|
+
#
|
21
|
+
# Generates on Employee (self):
|
22
|
+
# - employee.add_to_company_dept_index(company)
|
23
|
+
# - employee.remove_from_company_dept_index(company)
|
24
|
+
# - employee.update_in_company_dept_index(company, old_dept)
|
25
|
+
module MultiIndexGenerators
|
26
|
+
module_function
|
27
|
+
|
28
|
+
using Familia::Refinements::StylizeWords
|
29
|
+
|
30
|
+
# Main setup method that orchestrates multi-value index creation
|
31
|
+
#
|
32
|
+
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
33
|
+
# @param field [Symbol] The field to index
|
34
|
+
# @param index_name [Symbol] Name of the index
|
35
|
+
# @param within [Class, Symbol] Parent class for instance-scoped index (required)
|
36
|
+
# @param query [Boolean] Whether to generate query methods
|
37
|
+
def setup(indexed_class:, field:, index_name:, within:, query:)
|
38
|
+
# Multi-index always requires a parent context
|
39
|
+
target_class = within
|
40
|
+
resolved_class = Familia.resolve_class(target_class)
|
41
|
+
|
42
|
+
# Store metadata for this indexing relationship
|
43
|
+
indexed_class.indexing_relationships << IndexingRelationship.new(
|
44
|
+
field: field,
|
45
|
+
target_class: target_class,
|
46
|
+
index_name: index_name,
|
47
|
+
query: query,
|
48
|
+
cardinality: :multi,
|
49
|
+
)
|
50
|
+
|
51
|
+
# Always generate the factory method - required by mutation methods
|
52
|
+
if target_class.is_a?(Class)
|
53
|
+
generate_factory_method(resolved_class, index_name)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Generate query methods on the parent class (optional)
|
57
|
+
if query && target_class.is_a?(Class)
|
58
|
+
generate_query_methods_destination(indexed_class, field, resolved_class, index_name)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Generate mutation methods on the indexed class
|
62
|
+
generate_mutation_methods_self(indexed_class, field, resolved_class, index_name)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Generates the factory method ON THE PARENT CLASS (Company when within: Company):
|
66
|
+
# - company.index_name_for(field_value) - DataType factory (always needed)
|
67
|
+
#
|
68
|
+
# This method is required by mutation methods even when query: false
|
69
|
+
#
|
70
|
+
# @param target_class [Class] The parent class (e.g., Company)
|
71
|
+
# @param index_name [Symbol] Name of the index (e.g., :dept_index)
|
72
|
+
def generate_factory_method(target_class, index_name)
|
73
|
+
actual_target_class = Familia.resolve_class(target_class)
|
74
|
+
|
75
|
+
actual_target_class.class_eval do
|
76
|
+
# Helper method to get index set for a specific field value
|
77
|
+
# This acts as a factory for field-value-specific DataTypes
|
78
|
+
define_method("#{index_name}_for") do |field_value|
|
79
|
+
# Return properly managed DataType instance with parameterized key
|
80
|
+
index_key = "#{index_name}:#{field_value}"
|
81
|
+
Familia::UnsortedSet.new(index_key, parent: self)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Generates query methods ON THE PARENT CLASS (Company when within: Company):
|
87
|
+
# - company.sample_from_department(dept, count=1) - random sampling
|
88
|
+
# - company.find_all_by_department(dept) - all objects
|
89
|
+
# - company.rebuild_dept_index - rebuild index
|
90
|
+
#
|
91
|
+
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
92
|
+
# @param field [Symbol] The field to index (e.g., :department)
|
93
|
+
# @param target_class [Class] The parent class (e.g., Company)
|
94
|
+
# @param index_name [Symbol] Name of the index (e.g., :dept_index)
|
95
|
+
def generate_query_methods_destination(indexed_class, field, target_class, index_name)
|
96
|
+
# Resolve target class using Familia pattern
|
97
|
+
actual_target_class = Familia.resolve_class(target_class)
|
98
|
+
|
99
|
+
# Generate instance sampling method (e.g., company.sample_from_department)
|
100
|
+
actual_target_class.class_eval do
|
101
|
+
|
102
|
+
define_method("sample_from_#{field}") do |field_value, count = 1|
|
103
|
+
index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet
|
104
|
+
|
105
|
+
# Get random members efficiently (O(1) via SRANDMEMBER with count)
|
106
|
+
# Returns array even for count=1 for consistent API
|
107
|
+
index_set.sample(count).map do |id|
|
108
|
+
indexed_class.new(index_set.deserialize_value(id))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Generate bulk query method (e.g., company.find_all_by_department)
|
113
|
+
define_method("find_all_by_#{field}") do |field_value|
|
114
|
+
index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet
|
115
|
+
|
116
|
+
# Get all members from set
|
117
|
+
index_set.members.map { |id| indexed_class.new(id) }
|
118
|
+
end
|
119
|
+
|
120
|
+
# Generate method to rebuild the index for this parent instance
|
121
|
+
define_method("rebuild_#{index_name}") do
|
122
|
+
# This would need to be implemented based on how you track which
|
123
|
+
# objects belong to this parent instance
|
124
|
+
# For now, just a placeholder
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Generates mutation methods ON THE INDEXED CLASS (Employee):
|
130
|
+
# - employee.add_to_company_dept_index(company)
|
131
|
+
# - employee.remove_from_company_dept_index(company)
|
132
|
+
# - employee.update_in_company_dept_index(company, old_dept)
|
133
|
+
#
|
134
|
+
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
135
|
+
# @param field [Symbol] The field to index (e.g., :department)
|
136
|
+
# @param target_class [Class] The parent class (e.g., Company)
|
137
|
+
# @param index_name [Symbol] Name of the index (e.g., :dept_index)
|
138
|
+
def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
|
139
|
+
target_class_config = target_class.config_name
|
140
|
+
indexed_class.class_eval do
|
141
|
+
method_name = "add_to_#{target_class_config}_#{index_name}"
|
142
|
+
Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
|
143
|
+
|
144
|
+
define_method(method_name) do |target_instance|
|
145
|
+
return unless target_instance
|
146
|
+
|
147
|
+
field_value = send(field)
|
148
|
+
return unless field_value
|
149
|
+
|
150
|
+
# Use helper method on target instance instead of manual instantiation
|
151
|
+
index_set = target_instance.send("#{index_name}_for", field_value)
|
152
|
+
|
153
|
+
# Use UnsortedSet DataType method (no scoring)
|
154
|
+
index_set.add(identifier)
|
155
|
+
end
|
156
|
+
|
157
|
+
method_name = "remove_from_#{target_class_config}_#{index_name}"
|
158
|
+
Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
|
159
|
+
|
160
|
+
define_method(method_name) do |target_instance|
|
161
|
+
return unless target_instance
|
162
|
+
|
163
|
+
field_value = send(field)
|
164
|
+
return unless field_value
|
165
|
+
|
166
|
+
# Use helper method on target instance instead of manual instantiation
|
167
|
+
index_set = target_instance.send("#{index_name}_for", field_value)
|
168
|
+
|
169
|
+
# Remove using UnsortedSet DataType method
|
170
|
+
index_set.remove(identifier)
|
171
|
+
end
|
172
|
+
|
173
|
+
method_name = "update_in_#{target_class_config}_#{index_name}"
|
174
|
+
Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
|
175
|
+
|
176
|
+
define_method(method_name) do |target_instance, old_field_value = nil|
|
177
|
+
return unless target_instance
|
178
|
+
|
179
|
+
new_field_value = send(field)
|
180
|
+
|
181
|
+
# Use Familia's transaction method for atomicity with DataType abstraction
|
182
|
+
target_instance.transaction do |_tx|
|
183
|
+
# Remove from old index if provided - use helper method
|
184
|
+
if old_field_value
|
185
|
+
old_index_set = target_instance.send("#{index_name}_for", old_field_value)
|
186
|
+
old_index_set.remove(identifier)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Add to new index if present - use helper method
|
190
|
+
if new_field_value
|
191
|
+
new_index_set = target_instance.send("#{index_name}_for", new_field_value)
|
192
|
+
new_index_set.add(identifier)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,301 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Features
|
5
|
+
module Relationships
|
6
|
+
module Indexing
|
7
|
+
# Generators for unique index (1:1) methods
|
8
|
+
#
|
9
|
+
# Unique indexes use HashKey DataType for field-to-object identifier mapping.
|
10
|
+
# Each field value maps to exactly one object identifier.
|
11
|
+
#
|
12
|
+
# Example (instance-scoped):
|
13
|
+
# unique_index :badge_number, :badge_index, within: Company
|
14
|
+
#
|
15
|
+
# Generates on Company (destination):
|
16
|
+
# - company.find_by_badge_number(badge)
|
17
|
+
# - company.find_all_by_badge_number([badges])
|
18
|
+
# - company.badge_index
|
19
|
+
# - company.rebuild_badge_index
|
20
|
+
#
|
21
|
+
# Generates on Employee (self):
|
22
|
+
# - employee.add_to_company_badge_index(company)
|
23
|
+
# - employee.remove_from_company_badge_index(company)
|
24
|
+
# - employee.update_in_company_badge_index(company, old_badge)
|
25
|
+
#
|
26
|
+
# Example (class-level):
|
27
|
+
# unique_index :email, :email_index
|
28
|
+
#
|
29
|
+
# Generates on Employee (class):
|
30
|
+
# - Employee.find_by_email(email)
|
31
|
+
# - Employee.find_all_by_email([emails])
|
32
|
+
# - Employee.email_index
|
33
|
+
# - Employee.rebuild_email_index
|
34
|
+
#
|
35
|
+
# Generates on Employee (self):
|
36
|
+
# - employee.add_to_class_email_index
|
37
|
+
# - employee.remove_from_class_email_index
|
38
|
+
# - employee.update_in_class_email_index(old_email)
|
39
|
+
module UniqueIndexGenerators
|
40
|
+
module_function
|
41
|
+
|
42
|
+
using Familia::Refinements::StylizeWords
|
43
|
+
|
44
|
+
# Main setup method that orchestrates unique index creation
|
45
|
+
#
|
46
|
+
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
47
|
+
# @param field [Symbol] The field to index
|
48
|
+
# @param index_name [Symbol] Name of the index
|
49
|
+
# @param within [Class, Symbol, nil] Parent class for instance-scoped index
|
50
|
+
# @param query [Boolean] Whether to generate query methods
|
51
|
+
def setup(indexed_class:, field:, index_name:, within:, query:)
|
52
|
+
# Normalize parameters and determine scope type
|
53
|
+
target_class, scope_type = if within
|
54
|
+
k = Familia.resolve_class(within)
|
55
|
+
[k, :instance]
|
56
|
+
else
|
57
|
+
[indexed_class, :class]
|
58
|
+
end
|
59
|
+
|
60
|
+
# Store metadata for this indexing relationship
|
61
|
+
indexed_class.indexing_relationships << IndexingRelationship.new(
|
62
|
+
field: field,
|
63
|
+
target_class: target_class,
|
64
|
+
index_name: index_name,
|
65
|
+
query: query,
|
66
|
+
cardinality: :unique,
|
67
|
+
)
|
68
|
+
|
69
|
+
# Generate appropriate methods based on scope type
|
70
|
+
case scope_type
|
71
|
+
when :instance
|
72
|
+
# Instance-scoped index (within: Company)
|
73
|
+
if query && target_class.is_a?(Class)
|
74
|
+
generate_query_methods_destination(indexed_class, field, target_class, index_name)
|
75
|
+
end
|
76
|
+
generate_mutation_methods_self(indexed_class, field, target_class, index_name)
|
77
|
+
when :class
|
78
|
+
# Class-level index (no within:)
|
79
|
+
indexed_class.send(:ensure_index_field, indexed_class, index_name, :class_hashkey)
|
80
|
+
generate_query_methods_class(field, index_name, indexed_class) if query
|
81
|
+
generate_mutation_methods_class(field, index_name, indexed_class)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Generates query methods ON THE PARENT CLASS (Company when within: Company):
|
86
|
+
# - company.find_by_badge_number(badge) - find by field value
|
87
|
+
# - company.find_all_by_badge_number([badges]) - batch lookup
|
88
|
+
# - company.badge_index - DataType accessor
|
89
|
+
# - company.rebuild_badge_index - rebuild index
|
90
|
+
#
|
91
|
+
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
92
|
+
# @param field [Symbol] The field to index (e.g., :badge_number)
|
93
|
+
# @param target_class [Class] The parent class (e.g., Company)
|
94
|
+
# @param index_name [Symbol] Name of the index (e.g., :badge_index)
|
95
|
+
def generate_query_methods_destination(indexed_class, field, target_class, index_name)
|
96
|
+
# Resolve target class using Familia pattern
|
97
|
+
actual_target_class = Familia.resolve_class(target_class)
|
98
|
+
|
99
|
+
# Ensure the index field is declared (creates accessor that returns DataType)
|
100
|
+
actual_target_class.send(:ensure_index_field, actual_target_class, index_name, :hashkey)
|
101
|
+
|
102
|
+
# Generate instance query method (e.g., company.find_by_badge_number)
|
103
|
+
actual_target_class.class_eval do
|
104
|
+
define_method("find_by_#{field}") do |field_value|
|
105
|
+
# Use declared field accessor instead of manual instantiation
|
106
|
+
index_hash = send(index_name)
|
107
|
+
|
108
|
+
# Get the identifier from the hash
|
109
|
+
object_id = index_hash[field_value.to_s]
|
110
|
+
return nil unless object_id
|
111
|
+
|
112
|
+
indexed_class.new(object_id)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Generate bulk query method (e.g., company.find_all_by_badge_number)
|
116
|
+
define_method("find_all_by_#{field}") do |field_values|
|
117
|
+
return [] if field_values.empty?
|
118
|
+
|
119
|
+
# Use declared field accessor instead of manual instantiation
|
120
|
+
index_hash = send(index_name)
|
121
|
+
|
122
|
+
# Get all identifiers from the hash
|
123
|
+
object_ids = index_hash.values_at(*field_values.map(&:to_s))
|
124
|
+
# Filter out nil values and instantiate objects
|
125
|
+
object_ids.compact.map { |object_id| indexed_class.new(object_id) }
|
126
|
+
end
|
127
|
+
|
128
|
+
# Accessor method already created by ensure_index_field above
|
129
|
+
# No need to manually define it here
|
130
|
+
|
131
|
+
# Generate method to rebuild the unique index for this parent instance
|
132
|
+
define_method("rebuild_#{index_name}") do
|
133
|
+
# Use declared field accessor instead of manual instantiation
|
134
|
+
index_hash = send(index_name)
|
135
|
+
|
136
|
+
# Clear existing index using DataType method
|
137
|
+
index_hash.clear
|
138
|
+
|
139
|
+
# Rebuild from all existing objects
|
140
|
+
# This would need to scan through all objects belonging to this parent
|
141
|
+
# Implementation depends on how objects are stored/tracked
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Generates mutation methods ON THE INDEXED CLASS (Employee):
|
147
|
+
# Instance methods for parent-scoped unique index operations:
|
148
|
+
# - employee.add_to_company_badge_index(company)
|
149
|
+
# - employee.remove_from_company_badge_index(company)
|
150
|
+
# - employee.update_in_company_badge_index(company, old_badge)
|
151
|
+
#
|
152
|
+
# @param indexed_class [Class] The class being indexed (e.g., Employee)
|
153
|
+
# @param field [Symbol] The field to index (e.g., :badge_number)
|
154
|
+
# @param target_class [Class] The parent class (e.g., Company)
|
155
|
+
# @param index_name [Symbol] Name of the index (e.g., :badge_index)
|
156
|
+
def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
|
157
|
+
target_class_config = target_class.config_name
|
158
|
+
indexed_class.class_eval do
|
159
|
+
method_name = "add_to_#{target_class_config}_#{index_name}"
|
160
|
+
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
161
|
+
|
162
|
+
define_method(method_name) do |target_instance|
|
163
|
+
return unless target_instance
|
164
|
+
|
165
|
+
field_value = send(field)
|
166
|
+
return unless field_value
|
167
|
+
|
168
|
+
# Use declared field accessor on target instance
|
169
|
+
index_hash = target_instance.send(index_name)
|
170
|
+
|
171
|
+
# Use HashKey DataType method
|
172
|
+
index_hash[field_value.to_s] = identifier
|
173
|
+
end
|
174
|
+
|
175
|
+
method_name = "remove_from_#{target_class_config}_#{index_name}"
|
176
|
+
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
177
|
+
|
178
|
+
define_method(method_name) do |target_instance|
|
179
|
+
return unless target_instance
|
180
|
+
|
181
|
+
field_value = send(field)
|
182
|
+
return unless field_value
|
183
|
+
|
184
|
+
# Use declared field accessor on target instance
|
185
|
+
index_hash = target_instance.send(index_name)
|
186
|
+
|
187
|
+
# Remove using HashKey DataType method
|
188
|
+
index_hash.remove(field_value.to_s)
|
189
|
+
end
|
190
|
+
|
191
|
+
method_name = "update_in_#{target_class_config}_#{index_name}"
|
192
|
+
Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
|
193
|
+
|
194
|
+
define_method(method_name) do |target_instance, old_field_value = nil|
|
195
|
+
return unless target_instance
|
196
|
+
|
197
|
+
new_field_value = send(field)
|
198
|
+
|
199
|
+
# Use Familia's transaction method for atomicity with DataType abstraction
|
200
|
+
target_instance.transaction do |_tx|
|
201
|
+
# Use declared field accessor on target instance
|
202
|
+
index_hash = target_instance.send(index_name)
|
203
|
+
|
204
|
+
# Remove old value if provided
|
205
|
+
index_hash.remove(old_field_value.to_s) if old_field_value
|
206
|
+
|
207
|
+
# Add new value if present
|
208
|
+
index_hash[new_field_value.to_s] = identifier if new_field_value
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Generates query methods ON THE INDEXED CLASS (Employee):
|
215
|
+
# Class-level methods (singleton):
|
216
|
+
# - Employee.find_by_email(email)
|
217
|
+
# - Employee.find_all_by_email([emails])
|
218
|
+
# - Employee.email_index
|
219
|
+
# - Employee.rebuild_email_index
|
220
|
+
def generate_query_methods_class(field, index_name, indexed_class)
|
221
|
+
indexed_class.define_singleton_method("find_by_#{field}") do |field_value|
|
222
|
+
index_hash = send(index_name) # Access the class-level hashkey DataType
|
223
|
+
object_id = index_hash[field_value.to_s]
|
224
|
+
|
225
|
+
return nil unless object_id
|
226
|
+
|
227
|
+
new(object_id)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Generate class-level bulk query method
|
231
|
+
indexed_class.define_singleton_method("find_all_by_#{field}") do |field_values|
|
232
|
+
return [] if field_values.empty?
|
233
|
+
|
234
|
+
index_hash = send(index_name) # Access the class-level hashkey DataType
|
235
|
+
object_ids = index_hash.values_at(*field_values.map(&:to_s))
|
236
|
+
# Filter out nil values and instantiate objects
|
237
|
+
object_ids.compact.map { |object_id| new(object_id) }
|
238
|
+
end
|
239
|
+
|
240
|
+
# The index accessor method is already created by the class_hashkey declaration
|
241
|
+
# No need to manually create it - Horreum handles this automatically
|
242
|
+
|
243
|
+
# Generate method to rebuild the class-level index
|
244
|
+
indexed_class.define_singleton_method("rebuild_#{index_name}") do
|
245
|
+
index_hash = send(index_name) # Access the class-level hashkey DataType
|
246
|
+
|
247
|
+
# Clear existing index using DataType method
|
248
|
+
index_hash.clear
|
249
|
+
|
250
|
+
# Rebuild from all existing objects
|
251
|
+
# This would need to scan through all objects of this class
|
252
|
+
# Implementation depends on how objects are stored/tracked
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# Generates mutation methods ON THE INDEXED CLASS (Employee):
|
257
|
+
# Instance methods for class-level index operations:
|
258
|
+
# - employee.add_to_class_email_index
|
259
|
+
# - employee.remove_from_class_email_index
|
260
|
+
# - employee.update_in_class_email_index(old_email)
|
261
|
+
def generate_mutation_methods_class(field, index_name, indexed_class)
|
262
|
+
indexed_class.class_eval do
|
263
|
+
define_method("add_to_class_#{index_name}") do
|
264
|
+
index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
|
265
|
+
field_value = send(field)
|
266
|
+
|
267
|
+
return unless field_value
|
268
|
+
|
269
|
+
index_hash[field_value.to_s] = identifier
|
270
|
+
end
|
271
|
+
|
272
|
+
define_method("remove_from_class_#{index_name}") do
|
273
|
+
index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
|
274
|
+
field_value = send(field)
|
275
|
+
|
276
|
+
return unless field_value
|
277
|
+
|
278
|
+
index_hash.remove(field_value.to_s)
|
279
|
+
end
|
280
|
+
|
281
|
+
define_method("update_in_class_#{index_name}") do |old_field_value = nil|
|
282
|
+
new_field_value = send(field)
|
283
|
+
|
284
|
+
# Use class-level transaction for atomicity with DataType abstraction
|
285
|
+
self.class.transaction do |_tx|
|
286
|
+
index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
|
287
|
+
|
288
|
+
# Remove old value if provided
|
289
|
+
index_hash.remove(old_field_value.to_s) if old_field_value
|
290
|
+
|
291
|
+
# Add new value if present
|
292
|
+
index_hash[new_field_value.to_s] = identifier if new_field_value
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|