familia 2.0.0.pre15 → 2.0.0.pre17
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/ci.yml +2 -2
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smells.yml +85 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +3 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +54 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +86 -4
- data/CLAUDE.md +39 -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 +42 -42
- 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 +624 -20
- 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 +7 -7
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +51 -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/class_methods.rb +63 -0
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +15 -11
- 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 +128 -14
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +12 -171
- 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 +71 -66
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +57 -19
- data/lib/familia/features/object_identifier.rb +134 -25
- 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 +306 -0
- data/lib/familia/features/relationships/indexing.rb +182 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +164 -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 +10 -7
- data/lib/familia/features.rb +10 -14
- data/lib/familia/field_type.rb +6 -4
- data/lib/familia/horreum/connection.rb +297 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
- data/lib/familia/horreum/serialization.rb +172 -0
- data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
- data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
- data/lib/familia/horreum.rb +222 -119
- 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 +2 -2
- 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 +10 -10
- 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 +11 -10
- data/try/core/errors_try.rb +11 -14
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/familia_try.rb +1 -1
- 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 +3 -3
- 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/sorted_set_zadd_options_try.rb +625 -0
- 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/field_groups_try.rb +244 -0
- 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 +443 -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 +3 -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 +6 -7
- data/try/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +3 -1
- data/try/horreum/defensive_initialization_try.rb +86 -0
- data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -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/horreum/settings_try.rb +2 -0
- 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 +2 -2
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +13 -15
- 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
- data/try/valkey.conf +26 -0
- metadata +92 -52
- 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/horreum/core/connection.rb +0 -73
- data/lib/familia/horreum/core.rb +0 -21
- 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
@@ -1,85 +1,132 @@
|
|
1
1
|
# lib/familia/features/relationships/indexing.rb
|
2
2
|
|
3
|
+
require_relative 'indexing_relationship'
|
4
|
+
require_relative 'indexing/multi_index_generators'
|
5
|
+
require_relative 'indexing/unique_index_generators'
|
6
|
+
|
3
7
|
module Familia
|
4
8
|
module Features
|
5
9
|
module Relationships
|
6
|
-
# Indexing module for
|
7
|
-
# Provides O(1)
|
10
|
+
# Indexing module for attribute-based lookups using Valkey/Redis data structures.
|
11
|
+
# Provides O(1) field-to-object mappings without relationship semantics.
|
12
|
+
#
|
13
|
+
# @example Class-level unique index (1:1 mapping via HashKey)
|
14
|
+
# class User < Familia::Horreum
|
15
|
+
# feature :relationships
|
16
|
+
# field :email
|
17
|
+
# unique_index :email, :email_lookup
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# user = User.new(user_id: 'u1', email: 'alice@example.com')
|
21
|
+
# user.save # Automatically populates email_lookup index
|
22
|
+
# User.find_by_email('alice@example.com') # → user
|
23
|
+
#
|
24
|
+
# @example Instance-scoped unique index (within parent, 1:1 via HashKey)
|
25
|
+
# class Employee < Familia::Horreum
|
26
|
+
# feature :relationships
|
27
|
+
# field :badge_number
|
28
|
+
# unique_index :badge_number, :badge_index, within: Company
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# company = Company.new(company_id: 'c1')
|
32
|
+
# employee = Employee.new(emp_id: 'e1', badge_number: '12345')
|
33
|
+
# employee.add_to_company_badge_index(company)
|
34
|
+
# company.find_by_badge_number('12345') # → employee
|
35
|
+
#
|
36
|
+
# @example Instance-scoped multi-value index (within parent, 1:many via UnsortedSet)
|
37
|
+
# class Employee < Familia::Horreum
|
38
|
+
# feature :relationships
|
39
|
+
# field :department
|
40
|
+
# multi_index :department, :dept_index, within: Company
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# company = Company.new(company_id: 'c1')
|
44
|
+
# emp1 = Employee.new(emp_id: 'e1', department: 'engineering')
|
45
|
+
# emp2 = Employee.new(emp_id: 'e2', department: 'engineering')
|
46
|
+
# emp1.add_to_company_dept_index(company)
|
47
|
+
# emp2.add_to_company_dept_index(company)
|
48
|
+
# company.find_all_by_department('engineering') # → [emp1, emp2]
|
49
|
+
#
|
50
|
+
# Terminology:
|
51
|
+
# - unique_index: 1:1 field-to-object mapping (HashKey)
|
52
|
+
# - multi_index: 1:many field-to-objects mapping (UnsortedSet, no scores)
|
53
|
+
# - within: parent class for instance-scoped indexes
|
54
|
+
# - query: whether to generate find_by_* methods (default: true)
|
55
|
+
#
|
56
|
+
# Key Patterns:
|
57
|
+
# - Class unique: "user:email_index" → HashKey
|
58
|
+
# - Instance unique: "company:c1:badge_index" → HashKey
|
59
|
+
# - Instance multi: "company:c1:dept_index:engineering" → UnsortedSet
|
60
|
+
#
|
61
|
+
# Auto-Indexing:
|
62
|
+
# Class-level unique_index declarations automatically populate on save():
|
63
|
+
# user = User.new(email: 'test@example.com')
|
64
|
+
# user.save # Auto-indexes email → user_id
|
65
|
+
# Instance-scoped indexes (with within:) remain manual (require parent context).
|
66
|
+
#
|
67
|
+
# Design Philosophy:
|
68
|
+
# Indexing is for finding objects by attribute, not ordering them.
|
69
|
+
# Use multi_index with UnsortedSet (no temporal scores), then sort in Ruby:
|
70
|
+
# employees = company.find_all_by_department('eng')
|
71
|
+
# sorted = employees.sort_by(&:hire_date)
|
72
|
+
|
8
73
|
module Indexing
|
74
|
+
using Familia::Refinements::StylizeWords
|
75
|
+
|
9
76
|
# Class-level indexing configurations
|
10
77
|
def self.included(base)
|
11
|
-
base.extend
|
12
|
-
base.include
|
78
|
+
base.extend ModelClassMethods
|
79
|
+
base.include ModelInstanceMethods
|
13
80
|
super
|
14
81
|
end
|
15
82
|
|
16
|
-
|
83
|
+
# Indexing::ModelClassMethods
|
84
|
+
#
|
85
|
+
module ModelClassMethods
|
17
86
|
# Define an indexed_by relationship for fast lookups
|
18
87
|
#
|
88
|
+
# Define a multi-value index (1:many mapping)
|
89
|
+
#
|
19
90
|
# @param field [Symbol] The field to index on
|
20
|
-
# @param index_name [Symbol] Name of the index
|
21
|
-
# @param
|
22
|
-
# @param
|
91
|
+
# @param index_name [Symbol] Name of the index
|
92
|
+
# @param within [Class, Symbol] The parent class that owns the index
|
93
|
+
# @param query [Boolean] Whether to generate query methods
|
23
94
|
#
|
24
|
-
# @example
|
25
|
-
#
|
95
|
+
# @example Instance-scoped multi-value indexing
|
96
|
+
# multi_index :department, :dept_index, within: Company
|
26
97
|
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
context_class = parent
|
31
|
-
context_class_name = if context_class.is_a?(Class)
|
32
|
-
# Extract just the class name without module prefixes or object representations
|
33
|
-
class_name = context_class.name
|
34
|
-
class_name = class_name.split('::').last if class_name
|
35
|
-
class_name || context_class.to_s.split('::').last
|
36
|
-
else
|
37
|
-
# For symbol parent, convert to string
|
38
|
-
context_class.to_s
|
39
|
-
end
|
40
|
-
|
41
|
-
# Store metadata for this indexing relationship
|
42
|
-
indexing_relationships << {
|
98
|
+
def multi_index(field, index_name, within:, query: true)
|
99
|
+
MultiIndexGenerators.setup(
|
100
|
+
indexed_class: self,
|
43
101
|
field: field,
|
44
|
-
context_class: context_class,
|
45
|
-
context_class_name: context_class_name,
|
46
102
|
index_name: index_name,
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
# Generate finder methods on the context class
|
51
|
-
if finder && context_class.is_a?(Class)
|
52
|
-
generate_context_finder_methods(context_class, field, index_name)
|
53
|
-
end
|
54
|
-
|
55
|
-
# Generate instance methods for relationship indexing
|
56
|
-
generate_relationship_index_methods(context_class_name, field, index_name)
|
103
|
+
within: within,
|
104
|
+
query: query,
|
105
|
+
)
|
57
106
|
end
|
58
107
|
|
59
|
-
# Define a
|
108
|
+
# Define a unique index lookup (1:1 mapping)
|
60
109
|
#
|
61
110
|
# @param field [Symbol] The field to index on
|
62
111
|
# @param index_name [Symbol] Name of the index hash
|
63
|
-
# @param
|
112
|
+
# @param within [Class, Symbol] Optional parent class for instance-scoped unique index
|
113
|
+
# @param query [Boolean] Whether to generate query methods
|
114
|
+
#
|
115
|
+
# @example Class-level unique index
|
116
|
+
# unique_index :email, :email_lookup
|
117
|
+
# unique_index :username, :username_lookup, query: false
|
118
|
+
#
|
119
|
+
# @example Instance-scoped unique index
|
120
|
+
# unique_index :badge_number, :badge_index, within: Company
|
64
121
|
#
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
def class_indexed_by(field, index_name, finder: true)
|
69
|
-
# Store metadata for this indexing relationship
|
70
|
-
indexing_relationships << {
|
122
|
+
def unique_index(field, index_name, within: nil, query: true)
|
123
|
+
UniqueIndexGenerators.setup(
|
124
|
+
indexed_class: self,
|
71
125
|
field: field,
|
72
|
-
context_class: self,
|
73
|
-
context_class_name: name,
|
74
126
|
index_name: index_name,
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
# Generate class-level finder methods if requested
|
79
|
-
generate_class_finder_methods(field, index_name) if finder
|
80
|
-
|
81
|
-
# Generate instance methods for class-level indexing
|
82
|
-
generate_direct_index_methods(field, index_name)
|
127
|
+
within: within,
|
128
|
+
query: query,
|
129
|
+
)
|
83
130
|
end
|
84
131
|
|
85
132
|
# Get all indexing relationships for this class
|
@@ -87,244 +134,114 @@ module Familia
|
|
87
134
|
@indexing_relationships ||= []
|
88
135
|
end
|
89
136
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
word.to_s.split('_').map(&:capitalize).join
|
95
|
-
end
|
96
|
-
|
97
|
-
# Generate finder methods on the context class (e.g., Customer.find_by_display_name)
|
98
|
-
def generate_context_finder_methods(context_class, field, index_name)
|
99
|
-
# Resolve context class if it's a symbol/string
|
100
|
-
actual_context_class = context_class.is_a?(Class) ? context_class : Object.const_get(camelize_word(context_class))
|
101
|
-
|
102
|
-
# Store reference to the indexed class for the finder methods
|
103
|
-
indexed_class = self
|
104
|
-
|
105
|
-
# Generate finder method (e.g., Customer.find_by_display_name)
|
106
|
-
actual_context_class.define_singleton_method("find_by_#{field}") do |field_value|
|
107
|
-
index_key = "#{self.name.downcase}:#{index_name}"
|
108
|
-
object_id = dbclient.hget(index_key, field_value.to_s)
|
109
|
-
|
110
|
-
return nil unless object_id
|
111
|
-
|
112
|
-
indexed_class.new(object_id)
|
113
|
-
end
|
114
|
-
|
115
|
-
# Generate bulk finder method (e.g., Customer.find_all_by_display_name)
|
116
|
-
actual_context_class.define_singleton_method("find_all_by_#{field}") do |field_values|
|
117
|
-
return [] if field_values.empty?
|
118
|
-
|
119
|
-
index_key = "#{self.name.downcase}:#{index_name}"
|
120
|
-
object_ids = dbclient.hmget(index_key, *field_values.map(&:to_s))
|
121
|
-
|
122
|
-
# Filter out nil values and instantiate objects
|
123
|
-
object_ids.compact.map { |object_id| indexed_class.new(object_id) }
|
124
|
-
end
|
125
|
-
|
126
|
-
# Generate method to get the index hash directly
|
127
|
-
actual_context_class.define_singleton_method(index_name) do
|
128
|
-
index_key = "#{self.name.downcase}:#{index_name}"
|
129
|
-
Familia::HashKey.new(nil, dbkey: index_key, logical_database: logical_database)
|
130
|
-
end
|
131
|
-
|
132
|
-
# Generate method to rebuild the index
|
133
|
-
actual_context_class.define_singleton_method("rebuild_#{index_name}") do
|
134
|
-
index_key = "#{self.name.downcase}:#{index_name}"
|
135
|
-
|
136
|
-
# Clear existing index
|
137
|
-
dbclient.del(index_key)
|
138
|
-
|
139
|
-
# This is a simplified version - in practice, you'd need to iterate
|
140
|
-
# through all objects that should be in this index
|
141
|
-
# Implementation would depend on how you track which objects belong to this context
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
# Generate class-level finder methods
|
146
|
-
def generate_class_finder_methods(field, index_name)
|
147
|
-
# Generate class-level finder method (e.g., Domain.find_by_display_name)
|
148
|
-
define_singleton_method("find_by_#{field}") do |field_value|
|
149
|
-
index_key = "#{self.name.downcase}:#{index_name}"
|
150
|
-
object_id = dbclient.hget(index_key, field_value.to_s)
|
151
|
-
|
152
|
-
return nil unless object_id
|
153
|
-
|
154
|
-
new(object_id)
|
155
|
-
end
|
156
|
-
|
157
|
-
# Generate class-level bulk finder method
|
158
|
-
define_singleton_method("find_all_by_#{field}") do |field_values|
|
159
|
-
return [] if field_values.empty?
|
160
|
-
|
161
|
-
index_key = "#{self.name.downcase}:#{index_name}"
|
162
|
-
object_ids = dbclient.hmget(index_key, *field_values.map(&:to_s))
|
163
|
-
|
164
|
-
# Filter out nil values and instantiate objects
|
165
|
-
object_ids.compact.map { |object_id| self.new(object_id) }
|
166
|
-
end
|
167
|
-
|
168
|
-
# Generate method to get the class-level index hash directly
|
169
|
-
define_singleton_method("#{index_name}") do
|
170
|
-
index_key = "#{self.name.downcase}:#{index_name}"
|
171
|
-
Familia::HashKey.new(nil, dbkey: index_key, logical_database: logical_database)
|
172
|
-
end
|
173
|
-
|
174
|
-
# Generate method to rebuild the class-level index
|
175
|
-
define_singleton_method("rebuild_#{index_name}") do
|
176
|
-
index_key = "#{self.name.downcase}:#{index_name}"
|
177
|
-
|
178
|
-
# Clear existing index
|
179
|
-
dbclient.del(index_key)
|
180
|
-
|
181
|
-
# Rebuild from all existing objects
|
182
|
-
# This would need to scan through all objects of this class
|
183
|
-
# Implementation depends on how objects are stored/tracked
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
# Generate instance methods for class-level indexing (class_indexed_by)
|
188
|
-
def generate_direct_index_methods(field, index_name)
|
189
|
-
# Class-level index methods
|
190
|
-
define_method("add_to_class_#{index_name}") do
|
191
|
-
index_key = "#{self.class.name.downcase}:#{index_name}"
|
192
|
-
field_value = send(field)
|
193
|
-
|
194
|
-
return unless field_value
|
195
|
-
|
196
|
-
dbclient.hset(index_key, field_value.to_s, identifier)
|
197
|
-
end
|
198
|
-
|
199
|
-
define_method("remove_from_class_#{index_name}") do
|
200
|
-
index_key = "#{self.class.name.downcase}:#{index_name}"
|
201
|
-
field_value = send(field)
|
202
|
-
|
203
|
-
return unless field_value
|
204
|
-
|
205
|
-
dbclient.hdel(index_key, field_value.to_s)
|
206
|
-
end
|
207
|
-
|
208
|
-
define_method("update_in_class_#{index_name}") do |old_field_value = nil|
|
209
|
-
index_key = "#{self.class.name.downcase}:#{index_name}"
|
210
|
-
new_field_value = send(field)
|
211
|
-
|
212
|
-
dbclient.multi do |tx|
|
213
|
-
# Remove old value if provided
|
214
|
-
tx.hdel(index_key, old_field_value.to_s) if old_field_value
|
215
|
-
|
216
|
-
# Add new value if present
|
217
|
-
tx.hset(index_key, new_field_value.to_s, identifier) if new_field_value
|
218
|
-
end
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
# Generate instance methods for relationship indexing (indexed_by with parent:)
|
223
|
-
def generate_relationship_index_methods(context_class_name, field, index_name)
|
224
|
-
# All indexes are stored at class level - parent is only conceptual
|
225
|
-
define_method("add_to_#{context_class_name.downcase}_#{index_name}") do |context_instance = nil|
|
226
|
-
index_key = "#{self.class.name.downcase}:#{index_name}"
|
227
|
-
field_value = send(field)
|
228
|
-
|
229
|
-
return unless field_value
|
137
|
+
# Ensure proper DataType field is declared for index
|
138
|
+
# Similar to ensure_collection_field in participation system
|
139
|
+
def ensure_index_field(target_class, index_name, field_type)
|
140
|
+
return if target_class.method_defined?(index_name) || target_class.respond_to?(index_name)
|
230
141
|
|
231
|
-
|
232
|
-
end
|
233
|
-
|
234
|
-
define_method("remove_from_#{context_class_name.downcase}_#{index_name}") do |context_instance = nil|
|
235
|
-
index_key = "#{self.class.name.downcase}:#{index_name}"
|
236
|
-
field_value = send(field)
|
237
|
-
|
238
|
-
return unless field_value
|
239
|
-
|
240
|
-
dbclient.hdel(index_key, field_value.to_s)
|
241
|
-
end
|
242
|
-
|
243
|
-
define_method("update_in_#{context_class_name.downcase}_#{index_name}") do |context_instance = nil, old_field_value = nil|
|
244
|
-
index_key = "#{self.class.name.downcase}:#{index_name}"
|
245
|
-
new_field_value = send(field)
|
246
|
-
|
247
|
-
dbclient.multi do |tx|
|
248
|
-
# Remove old value if provided
|
249
|
-
tx.hdel(index_key, old_field_value.to_s) if old_field_value
|
250
|
-
|
251
|
-
# Add new value if present
|
252
|
-
tx.hset(index_key, new_field_value.to_s, identifier) if new_field_value
|
253
|
-
end
|
254
|
-
end
|
142
|
+
target_class.send(field_type, index_name)
|
255
143
|
end
|
256
144
|
end
|
257
145
|
|
258
146
|
# Instance methods for indexed objects
|
259
|
-
module
|
260
|
-
# Update all indexes
|
261
|
-
|
147
|
+
module ModelInstanceMethods
|
148
|
+
# Update all indexes for a given parent context
|
149
|
+
# For class-level indexes (class_indexed_by), parent_context should be nil
|
150
|
+
# For relationship indexes (indexed_by), parent_context should be the parent instance
|
151
|
+
def update_all_indexes(old_values = {}, parent_context = nil)
|
262
152
|
return unless self.class.respond_to?(:indexing_relationships)
|
263
153
|
|
264
154
|
self.class.indexing_relationships.each do |config|
|
265
|
-
field = config
|
266
|
-
index_name = config
|
267
|
-
|
155
|
+
field = config.field
|
156
|
+
index_name = config.index_name
|
157
|
+
target_class = config.target_class
|
268
158
|
old_field_value = old_values[field]
|
269
159
|
|
270
160
|
# Determine which update method to call
|
271
|
-
if
|
272
|
-
# Class-level index (
|
161
|
+
if target_class == self.class
|
162
|
+
# Class-level index (unique_index without within:)
|
273
163
|
send("update_in_class_#{index_name}", old_field_value)
|
274
164
|
else
|
275
|
-
# Relationship index (
|
276
|
-
|
277
|
-
|
165
|
+
# Relationship index (unique_index or multi_index with within:) - requires parent context
|
166
|
+
next unless parent_context
|
167
|
+
|
168
|
+
# Use config_name for method naming
|
169
|
+
target_class_config = Familia.resolve_class(config.target_class).config_name
|
170
|
+
send("update_in_#{target_class_config}_#{index_name}", parent_context, old_field_value)
|
278
171
|
end
|
279
172
|
end
|
280
173
|
end
|
281
174
|
|
282
|
-
# Remove from all indexes
|
283
|
-
|
175
|
+
# Remove from all indexes for a given parent context
|
176
|
+
# For class-level indexes (class_indexed_by), parent_context should be nil
|
177
|
+
# For relationship indexes (indexed_by), parent_context should be the parent instance
|
178
|
+
def remove_from_all_indexes(parent_context = nil)
|
284
179
|
return unless self.class.respond_to?(:indexing_relationships)
|
285
180
|
|
286
181
|
self.class.indexing_relationships.each do |config|
|
287
|
-
index_name = config
|
288
|
-
|
182
|
+
index_name = config.index_name
|
183
|
+
target_class = config.target_class
|
289
184
|
|
290
185
|
# Determine which remove method to call
|
291
|
-
if
|
292
|
-
# Class-level index (
|
186
|
+
if target_class == self.class
|
187
|
+
# Class-level index (unique_index without within:)
|
293
188
|
send("remove_from_class_#{index_name}")
|
294
189
|
else
|
295
|
-
# Relationship index (
|
296
|
-
|
297
|
-
|
190
|
+
# Relationship index (unique_index or multi_index with within:) - requires parent context
|
191
|
+
next unless parent_context
|
192
|
+
|
193
|
+
# Use config_name for method naming
|
194
|
+
target_class_config = Familia.resolve_class(config.target_class).config_name
|
195
|
+
send("remove_from_#{target_class_config}_#{index_name}", parent_context)
|
298
196
|
end
|
299
197
|
end
|
300
198
|
end
|
301
199
|
|
302
200
|
# Get all indexes this object appears in
|
201
|
+
# Note: For target-scoped indexes, this only shows class-level indexes
|
202
|
+
# since target-scoped indexes require a specific target instance
|
303
203
|
#
|
304
204
|
# @return [Array<Hash>] Array of index information
|
305
|
-
def
|
205
|
+
def current_indexings
|
306
206
|
return [] unless self.class.respond_to?(:indexing_relationships)
|
307
207
|
|
308
208
|
memberships = []
|
309
209
|
|
310
210
|
self.class.indexing_relationships.each do |config|
|
311
|
-
field = config
|
312
|
-
index_name = config
|
313
|
-
|
211
|
+
field = config.field
|
212
|
+
index_name = config.index_name
|
213
|
+
target_class = config.target_class
|
214
|
+
cardinality = config.cardinality
|
314
215
|
field_value = send(field)
|
315
216
|
|
316
217
|
next unless field_value
|
317
218
|
|
318
|
-
|
319
|
-
|
320
|
-
|
219
|
+
if target_class == self.class
|
220
|
+
# Class-level index (unique_index without within:) - check hash key using DataType
|
221
|
+
index_hash = self.class.send(index_name)
|
222
|
+
next unless index_hash.key?(field_value.to_s)
|
223
|
+
|
321
224
|
memberships << {
|
322
|
-
|
225
|
+
target_class: 'class',
|
323
226
|
index_name: index_name,
|
324
227
|
field: field,
|
325
228
|
field_value: field_value,
|
326
|
-
index_key:
|
327
|
-
|
229
|
+
index_key: index_hash.dbkey,
|
230
|
+
cardinality: cardinality,
|
231
|
+
type: 'unique_index',
|
232
|
+
}
|
233
|
+
else
|
234
|
+
# Instance-scoped index (unique_index or multi_index with within:) - cannot check without target instance
|
235
|
+
# This would require scanning all possible target instances
|
236
|
+
memberships << {
|
237
|
+
target_class: config.target_class_config_name,
|
238
|
+
index_name: index_name,
|
239
|
+
field: field,
|
240
|
+
field_value: field_value,
|
241
|
+
index_key: 'target_dependent',
|
242
|
+
cardinality: cardinality,
|
243
|
+
type: cardinality == :unique ? 'unique_index' : 'multi_index',
|
244
|
+
note: 'Requires target instance for verification',
|
328
245
|
}
|
329
246
|
end
|
330
247
|
end
|
@@ -332,20 +249,29 @@ module Familia
|
|
332
249
|
memberships
|
333
250
|
end
|
334
251
|
|
335
|
-
# Check if this object is indexed in a specific
|
252
|
+
# Check if this object is indexed in a specific target
|
253
|
+
# For class-level indexes, checks the hash key
|
254
|
+
# For target-scoped indexes, returns false (requires target instance)
|
336
255
|
def indexed_in?(index_name)
|
337
256
|
return false unless self.class.respond_to?(:indexing_relationships)
|
338
257
|
|
339
|
-
config = self.class.indexing_relationships.find { |rel| rel
|
258
|
+
config = self.class.indexing_relationships.find { |rel| rel.index_name == index_name }
|
340
259
|
return false unless config
|
341
260
|
|
342
|
-
field = config
|
261
|
+
field = config.field
|
343
262
|
field_value = send(field)
|
344
263
|
return false unless field_value
|
345
264
|
|
346
|
-
|
347
|
-
|
348
|
-
|
265
|
+
target_class = config.target_class
|
266
|
+
|
267
|
+
if target_class == self.class
|
268
|
+
# Class-level index (class_indexed_by) - check hash key using DataType
|
269
|
+
index_hash = self.class.send(index_name)
|
270
|
+
index_hash.key?(field_value.to_s)
|
271
|
+
else
|
272
|
+
# Target-scoped index (indexed_by) - cannot verify without target instance
|
273
|
+
false
|
274
|
+
end
|
349
275
|
end
|
350
276
|
end
|
351
277
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Features
|
5
|
+
module Relationships
|
6
|
+
using Familia::Refinements::StylizeWords
|
7
|
+
|
8
|
+
# IndexingRelationship
|
9
|
+
#
|
10
|
+
# Stores metadata about indexing relationships defined at class level.
|
11
|
+
# Used to configure code generation and runtime behavior for unique_index
|
12
|
+
# and multi_index declarations.
|
13
|
+
#
|
14
|
+
# Similar to ParticipationRelationship but for attribute-based lookups
|
15
|
+
# rather than collection membership.
|
16
|
+
#
|
17
|
+
IndexingRelationship = Data.define(
|
18
|
+
:field, # Symbol - field being indexed (e.g., :email, :department)
|
19
|
+
:index_name, # Symbol - name of the index (e.g., :email_index, :dept_index)
|
20
|
+
:target_class, # Class/Symbol - parent class for instance-scoped indexes (within:)
|
21
|
+
:cardinality, # Symbol - :unique (1:1) or :multi (1:many)
|
22
|
+
:query # Boolean - whether to generate query methods
|
23
|
+
) do
|
24
|
+
#
|
25
|
+
# Get the normalized config name for the target class
|
26
|
+
#
|
27
|
+
# @return [String] The config name (e.g., "user", "company", "test_company")
|
28
|
+
#
|
29
|
+
def target_class_config_name
|
30
|
+
target_class.config_name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|