familia 2.0.0.pre14 → 2.0.0.pre16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smellage.yml +145 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +1 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +48 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +66 -6
- data/CLAUDE.md +1 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +4 -4
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre13.md +1 -1
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +623 -19
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +6 -6
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +49 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +13 -10
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +26 -15
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +75 -47
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/{autoloader.rb → features/autoloader.rb} +49 -23
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +68 -66
- data/lib/familia/features/expiration/extensions.rb +61 -0
- data/lib/familia/features/expiration.rb +35 -87
- data/lib/familia/features/external_identifier.rb +11 -12
- data/lib/familia/features/object_identifier.rb +58 -20
- data/lib/familia/features/quantization.rb +17 -22
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
- data/lib/familia/features/relationships/indexing.rb +176 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +69 -271
- data/lib/familia/features/safe_dump.rb +127 -132
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +5 -5
- data/lib/familia/features.rb +21 -21
- data/lib/familia/field_type.rb +24 -4
- data/lib/familia/horreum/core/connection.rb +229 -26
- data/lib/familia/horreum/core/database_commands.rb +27 -17
- data/lib/familia/horreum/core/serialization.rb +40 -20
- data/lib/familia/horreum/core/utils.rb +2 -1
- data/lib/familia/horreum/shared/settings.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +33 -45
- data/lib/familia/horreum/subclass/management.rb +72 -24
- data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
- data/lib/familia/horreum.rb +196 -114
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -15
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +1 -1
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +129 -11
- data/try/core/connection_try.rb +7 -7
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +10 -10
- data/try/core/errors_try.rb +8 -11
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +1 -1
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +433 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +3 -3
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +1 -1
- data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +1 -1
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +8 -10
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- metadata +77 -45
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -228
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/autoloadable.rb +0 -113
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/autoloadable/autoloadable_try.rb +0 -61
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -111
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -1,85 +1,126 @@
|
|
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.add_to_class_email_lookup
|
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
|
+
# Design Philosophy:
|
62
|
+
# Indexing is for finding objects by attribute, not ordering them.
|
63
|
+
# Use multi_index with UnsortedSet (no temporal scores), then sort in Ruby:
|
64
|
+
# employees = company.find_all_by_department('eng')
|
65
|
+
# sorted = employees.sort_by(&:hire_date)
|
66
|
+
|
8
67
|
module Indexing
|
68
|
+
using Familia::Refinements::StylizeWords
|
69
|
+
|
9
70
|
# Class-level indexing configurations
|
10
71
|
def self.included(base)
|
11
|
-
base.extend
|
12
|
-
base.include
|
72
|
+
base.extend ModelClassMethods
|
73
|
+
base.include ModelInstanceMethods
|
13
74
|
super
|
14
75
|
end
|
15
76
|
|
16
|
-
|
77
|
+
# Indexing::ModelClassMethods
|
78
|
+
#
|
79
|
+
module ModelClassMethods
|
17
80
|
# Define an indexed_by relationship for fast lookups
|
18
81
|
#
|
82
|
+
# Define a multi-value index (1:many mapping)
|
83
|
+
#
|
19
84
|
# @param field [Symbol] The field to index on
|
20
|
-
# @param index_name [Symbol] Name of the index
|
21
|
-
# @param
|
22
|
-
# @param
|
85
|
+
# @param index_name [Symbol] Name of the index
|
86
|
+
# @param within [Class, Symbol] The parent class that owns the index
|
87
|
+
# @param query [Boolean] Whether to generate query methods
|
23
88
|
#
|
24
|
-
# @example
|
25
|
-
#
|
89
|
+
# @example Instance-scoped multi-value indexing
|
90
|
+
# multi_index :department, :dept_index, within: Company
|
26
91
|
#
|
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 << {
|
92
|
+
def multi_index(field, index_name, within:, query: true)
|
93
|
+
MultiIndexGenerators.setup(
|
94
|
+
indexed_class: self,
|
43
95
|
field: field,
|
44
|
-
context_class: context_class,
|
45
|
-
context_class_name: context_class_name,
|
46
96
|
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)
|
97
|
+
within: within,
|
98
|
+
query: query,
|
99
|
+
)
|
57
100
|
end
|
58
101
|
|
59
|
-
# Define a
|
102
|
+
# Define a unique index lookup (1:1 mapping)
|
60
103
|
#
|
61
104
|
# @param field [Symbol] The field to index on
|
62
105
|
# @param index_name [Symbol] Name of the index hash
|
63
|
-
# @param
|
106
|
+
# @param within [Class, Symbol] Optional parent class for instance-scoped unique index
|
107
|
+
# @param query [Boolean] Whether to generate query methods
|
108
|
+
#
|
109
|
+
# @example Class-level unique index
|
110
|
+
# unique_index :email, :email_lookup
|
111
|
+
# unique_index :username, :username_lookup, query: false
|
112
|
+
#
|
113
|
+
# @example Instance-scoped unique index
|
114
|
+
# unique_index :badge_number, :badge_index, within: Company
|
64
115
|
#
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
def class_indexed_by(field, index_name, finder: true)
|
69
|
-
# Store metadata for this indexing relationship
|
70
|
-
indexing_relationships << {
|
116
|
+
def unique_index(field, index_name, within: nil, query: true)
|
117
|
+
UniqueIndexGenerators.setup(
|
118
|
+
indexed_class: self,
|
71
119
|
field: field,
|
72
|
-
context_class: self,
|
73
|
-
context_class_name: name,
|
74
120
|
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)
|
121
|
+
within: within,
|
122
|
+
query: query,
|
123
|
+
)
|
83
124
|
end
|
84
125
|
|
85
126
|
# Get all indexing relationships for this class
|
@@ -87,244 +128,114 @@ module Familia
|
|
87
128
|
@indexing_relationships ||= []
|
88
129
|
end
|
89
130
|
|
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
|
131
|
+
# Ensure proper DataType field is declared for index
|
132
|
+
# Similar to ensure_collection_field in participation system
|
133
|
+
def ensure_index_field(target_class, index_name, field_type)
|
134
|
+
return if target_class.method_defined?(index_name) || target_class.respond_to?(index_name)
|
230
135
|
|
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
|
136
|
+
target_class.send(field_type, index_name)
|
255
137
|
end
|
256
138
|
end
|
257
139
|
|
258
140
|
# Instance methods for indexed objects
|
259
|
-
module
|
260
|
-
# Update all indexes
|
261
|
-
|
141
|
+
module ModelInstanceMethods
|
142
|
+
# Update all indexes for a given parent context
|
143
|
+
# For class-level indexes (class_indexed_by), parent_context should be nil
|
144
|
+
# For relationship indexes (indexed_by), parent_context should be the parent instance
|
145
|
+
def update_all_indexes(old_values = {}, parent_context = nil)
|
262
146
|
return unless self.class.respond_to?(:indexing_relationships)
|
263
147
|
|
264
148
|
self.class.indexing_relationships.each do |config|
|
265
|
-
field = config
|
266
|
-
index_name = config
|
267
|
-
|
149
|
+
field = config.field
|
150
|
+
index_name = config.index_name
|
151
|
+
target_class = config.target_class
|
268
152
|
old_field_value = old_values[field]
|
269
153
|
|
270
154
|
# Determine which update method to call
|
271
|
-
if
|
272
|
-
# Class-level index (
|
155
|
+
if target_class == self.class
|
156
|
+
# Class-level index (unique_index without within:)
|
273
157
|
send("update_in_class_#{index_name}", old_field_value)
|
274
158
|
else
|
275
|
-
# Relationship index (
|
276
|
-
|
277
|
-
|
159
|
+
# Relationship index (unique_index or multi_index with within:) - requires parent context
|
160
|
+
next unless parent_context
|
161
|
+
|
162
|
+
# Use config_name for method naming
|
163
|
+
target_class_config = Familia.resolve_class(config.target_class).config_name
|
164
|
+
send("update_in_#{target_class_config}_#{index_name}", parent_context, old_field_value)
|
278
165
|
end
|
279
166
|
end
|
280
167
|
end
|
281
168
|
|
282
|
-
# Remove from all indexes
|
283
|
-
|
169
|
+
# Remove from all indexes for a given parent context
|
170
|
+
# For class-level indexes (class_indexed_by), parent_context should be nil
|
171
|
+
# For relationship indexes (indexed_by), parent_context should be the parent instance
|
172
|
+
def remove_from_all_indexes(parent_context = nil)
|
284
173
|
return unless self.class.respond_to?(:indexing_relationships)
|
285
174
|
|
286
175
|
self.class.indexing_relationships.each do |config|
|
287
|
-
index_name = config
|
288
|
-
|
176
|
+
index_name = config.index_name
|
177
|
+
target_class = config.target_class
|
289
178
|
|
290
179
|
# Determine which remove method to call
|
291
|
-
if
|
292
|
-
# Class-level index (
|
180
|
+
if target_class == self.class
|
181
|
+
# Class-level index (unique_index without within:)
|
293
182
|
send("remove_from_class_#{index_name}")
|
294
183
|
else
|
295
|
-
# Relationship index (
|
296
|
-
|
297
|
-
|
184
|
+
# Relationship index (unique_index or multi_index with within:) - requires parent context
|
185
|
+
next unless parent_context
|
186
|
+
|
187
|
+
# Use config_name for method naming
|
188
|
+
target_class_config = Familia.resolve_class(config.target_class).config_name
|
189
|
+
send("remove_from_#{target_class_config}_#{index_name}", parent_context)
|
298
190
|
end
|
299
191
|
end
|
300
192
|
end
|
301
193
|
|
302
194
|
# Get all indexes this object appears in
|
195
|
+
# Note: For target-scoped indexes, this only shows class-level indexes
|
196
|
+
# since target-scoped indexes require a specific target instance
|
303
197
|
#
|
304
198
|
# @return [Array<Hash>] Array of index information
|
305
|
-
def
|
199
|
+
def current_indexings
|
306
200
|
return [] unless self.class.respond_to?(:indexing_relationships)
|
307
201
|
|
308
202
|
memberships = []
|
309
203
|
|
310
204
|
self.class.indexing_relationships.each do |config|
|
311
|
-
field = config
|
312
|
-
index_name = config
|
313
|
-
|
205
|
+
field = config.field
|
206
|
+
index_name = config.index_name
|
207
|
+
target_class = config.target_class
|
208
|
+
cardinality = config.cardinality
|
314
209
|
field_value = send(field)
|
315
210
|
|
316
211
|
next unless field_value
|
317
212
|
|
318
|
-
|
319
|
-
|
320
|
-
|
213
|
+
if target_class == self.class
|
214
|
+
# Class-level index (unique_index without within:) - check hash key using DataType
|
215
|
+
index_hash = self.class.send(index_name)
|
216
|
+
next unless index_hash.key?(field_value.to_s)
|
217
|
+
|
321
218
|
memberships << {
|
322
|
-
|
219
|
+
target_class: 'class',
|
323
220
|
index_name: index_name,
|
324
221
|
field: field,
|
325
222
|
field_value: field_value,
|
326
|
-
index_key:
|
327
|
-
|
223
|
+
index_key: index_hash.dbkey,
|
224
|
+
cardinality: cardinality,
|
225
|
+
type: 'unique_index',
|
226
|
+
}
|
227
|
+
else
|
228
|
+
# Instance-scoped index (unique_index or multi_index with within:) - cannot check without target instance
|
229
|
+
# This would require scanning all possible target instances
|
230
|
+
memberships << {
|
231
|
+
target_class: config.target_class_config_name,
|
232
|
+
index_name: index_name,
|
233
|
+
field: field,
|
234
|
+
field_value: field_value,
|
235
|
+
index_key: 'target_dependent',
|
236
|
+
cardinality: cardinality,
|
237
|
+
type: cardinality == :unique ? 'unique_index' : 'multi_index',
|
238
|
+
note: 'Requires target instance for verification',
|
328
239
|
}
|
329
240
|
end
|
330
241
|
end
|
@@ -332,20 +243,29 @@ module Familia
|
|
332
243
|
memberships
|
333
244
|
end
|
334
245
|
|
335
|
-
# Check if this object is indexed in a specific
|
246
|
+
# Check if this object is indexed in a specific target
|
247
|
+
# For class-level indexes, checks the hash key
|
248
|
+
# For target-scoped indexes, returns false (requires target instance)
|
336
249
|
def indexed_in?(index_name)
|
337
250
|
return false unless self.class.respond_to?(:indexing_relationships)
|
338
251
|
|
339
|
-
config = self.class.indexing_relationships.find { |rel| rel
|
252
|
+
config = self.class.indexing_relationships.find { |rel| rel.index_name == index_name }
|
340
253
|
return false unless config
|
341
254
|
|
342
|
-
field = config
|
255
|
+
field = config.field
|
343
256
|
field_value = send(field)
|
344
257
|
return false unless field_value
|
345
258
|
|
346
|
-
|
347
|
-
|
348
|
-
|
259
|
+
target_class = config.target_class
|
260
|
+
|
261
|
+
if target_class == self.class
|
262
|
+
# Class-level index (class_indexed_by) - check hash key using DataType
|
263
|
+
index_hash = self.class.send(index_name)
|
264
|
+
index_hash.key?(field_value.to_s)
|
265
|
+
else
|
266
|
+
# Target-scoped index (indexed_by) - cannot verify without target instance
|
267
|
+
false
|
268
|
+
end
|
349
269
|
end
|
350
270
|
end
|
351
271
|
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
|