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.
Files changed (274) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/code-quality.yml +138 -0
  3. data/.github/workflows/code-smellage.yml +145 -0
  4. data/.github/workflows/docs.yml +31 -8
  5. data/.gitignore +1 -1
  6. data/.pre-commit-config.yaml +7 -1
  7. data/.reek.yml +98 -0
  8. data/.rubocop.yml +48 -10
  9. data/.talismanrc +9 -0
  10. data/.yardopts +18 -13
  11. data/CHANGELOG.rst +64 -4
  12. data/CLAUDE.md +1 -1
  13. data/Gemfile +6 -5
  14. data/Gemfile.lock +99 -23
  15. data/LICENSE.txt +1 -1
  16. data/README.md +285 -85
  17. data/changelog.d/README.md +2 -2
  18. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  19. data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
  20. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  21. data/docs/archive/README.md +3 -2
  22. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  23. data/docs/conf.py +29 -0
  24. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  25. data/docs/guides/feature-encrypted-fields.md +785 -0
  26. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  27. data/docs/guides/feature-external-identifiers.md +637 -0
  28. data/docs/guides/feature-object-identifiers.md +435 -0
  29. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  30. data/docs/guides/feature-relationships-methods.md +684 -0
  31. data/docs/guides/feature-relationships.md +200 -0
  32. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  33. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  34. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  35. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  36. data/docs/guides/index.md +176 -0
  37. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  38. data/docs/migrating/v2.0.0-pre.md +1 -1
  39. data/docs/migrating/v2.0.0-pre11.md +2 -2
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre5.md +33 -12
  42. data/docs/migrating/v2.0.0-pre6.md +2 -2
  43. data/docs/migrating/v2.0.0-pre7.md +8 -8
  44. data/docs/overview.md +623 -19
  45. data/docs/reference/api-technical.md +1365 -0
  46. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  47. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  48. data/examples/autoloader/mega_customer.rb +3 -1
  49. data/examples/encrypted_fields.rb +378 -0
  50. data/examples/json_usage_patterns.rb +144 -0
  51. data/examples/relationships.rb +13 -13
  52. data/examples/safe_dump.rb +6 -6
  53. data/examples/single_connection_transaction_confusions.rb +379 -0
  54. data/lib/familia/base.rb +49 -10
  55. data/lib/familia/connection/handlers.rb +223 -0
  56. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  57. data/lib/familia/connection/middleware.rb +75 -0
  58. data/lib/familia/connection/operation_core.rb +93 -0
  59. data/lib/familia/connection/operations.rb +277 -0
  60. data/lib/familia/connection/pipeline_core.rb +87 -0
  61. data/lib/familia/connection/transaction_core.rb +100 -0
  62. data/lib/familia/connection.rb +60 -186
  63. data/lib/familia/data_type/commands.rb +53 -51
  64. data/lib/familia/data_type/serialization.rb +108 -107
  65. data/lib/familia/data_type/types/counter.rb +1 -1
  66. data/lib/familia/data_type/types/hashkey.rb +13 -10
  67. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  68. data/lib/familia/data_type/types/lock.rb +3 -2
  69. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  70. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  71. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  72. data/lib/familia/data_type.rb +75 -47
  73. data/lib/familia/distinguisher.rb +85 -0
  74. data/lib/familia/encryption/encrypted_data.rb +15 -24
  75. data/lib/familia/encryption/manager.rb +6 -4
  76. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  77. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  78. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  79. data/lib/familia/encryption/request_cache.rb +7 -7
  80. data/lib/familia/encryption.rb +2 -3
  81. data/lib/familia/errors.rb +9 -3
  82. data/lib/familia/features/autoloader.rb +30 -12
  83. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  84. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  85. data/lib/familia/features/encrypted_fields.rb +66 -64
  86. data/lib/familia/features/expiration/extensions.rb +1 -1
  87. data/lib/familia/features/expiration.rb +31 -26
  88. data/lib/familia/features/external_identifier.rb +9 -12
  89. data/lib/familia/features/object_identifier.rb +56 -19
  90. data/lib/familia/features/quantization.rb +16 -21
  91. data/lib/familia/features/relationships/README.md +97 -0
  92. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  93. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  94. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  95. data/lib/familia/features/relationships/indexing.rb +176 -256
  96. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  97. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  98. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  99. data/lib/familia/features/relationships/participation.rb +656 -0
  100. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  101. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  102. data/lib/familia/features/relationships.rb +65 -266
  103. data/lib/familia/features/safe_dump.rb +127 -130
  104. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  105. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  106. data/lib/familia/features/transient_fields.rb +3 -5
  107. data/lib/familia/features.rb +4 -13
  108. data/lib/familia/field_type.rb +24 -4
  109. data/lib/familia/horreum/core/connection.rb +229 -26
  110. data/lib/familia/horreum/core/database_commands.rb +27 -17
  111. data/lib/familia/horreum/core/serialization.rb +40 -20
  112. data/lib/familia/horreum/core/utils.rb +2 -1
  113. data/lib/familia/horreum/shared/settings.rb +2 -1
  114. data/lib/familia/horreum/subclass/definition.rb +33 -45
  115. data/lib/familia/horreum/subclass/management.rb +72 -24
  116. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  117. data/lib/familia/horreum.rb +196 -114
  118. data/lib/familia/json_serializer.rb +0 -1
  119. data/lib/familia/logging.rb +11 -114
  120. data/lib/familia/refinements/dear_json.rb +122 -0
  121. data/lib/familia/refinements/logger_trace.rb +20 -17
  122. data/lib/familia/refinements/stylize_words.rb +65 -0
  123. data/lib/familia/refinements/time_literals.rb +60 -52
  124. data/lib/familia/refinements.rb +2 -1
  125. data/lib/familia/secure_identifier.rb +60 -28
  126. data/lib/familia/settings.rb +83 -7
  127. data/lib/familia/utils.rb +5 -87
  128. data/lib/familia/verifiable_identifier.rb +4 -4
  129. data/lib/familia/version.rb +1 -1
  130. data/lib/familia.rb +72 -14
  131. data/lib/middleware/database_middleware.rb +56 -14
  132. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  133. data/try/configuration/scenarios_try.rb +1 -1
  134. data/try/connection/fiber_context_preservation_try.rb +250 -0
  135. data/try/connection/handler_constraints_try.rb +59 -0
  136. data/try/connection/operation_mode_guards_try.rb +208 -0
  137. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  138. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  139. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  140. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  141. data/try/connection/transaction_mode_strict_try.rb +98 -0
  142. data/try/connection/transaction_mode_warn_try.rb +131 -0
  143. data/try/connection/transaction_modes_try.rb +249 -0
  144. data/try/core/autoloader_try.rb +120 -2
  145. data/try/core/connection_try.rb +7 -7
  146. data/try/core/conventional_inheritance_try.rb +130 -0
  147. data/try/core/create_method_try.rb +15 -23
  148. data/try/core/database_consistency_try.rb +10 -10
  149. data/try/core/errors_try.rb +8 -11
  150. data/try/core/familia_extended_try.rb +2 -2
  151. data/try/core/familia_members_methods_try.rb +76 -0
  152. data/try/core/isolated_dbclient_try.rb +165 -0
  153. data/try/core/middleware_try.rb +16 -16
  154. data/try/core/persistence_operations_try.rb +4 -4
  155. data/try/core/pools_try.rb +42 -26
  156. data/try/core/secure_identifier_try.rb +28 -24
  157. data/try/core/time_utils_try.rb +10 -10
  158. data/try/core/tools_try.rb +1 -1
  159. data/try/core/utils_try.rb +2 -2
  160. data/try/data_types/boolean_try.rb +4 -4
  161. data/try/data_types/datatype_base_try.rb +0 -2
  162. data/try/data_types/list_try.rb +10 -10
  163. data/try/data_types/sorted_set_try.rb +5 -5
  164. data/try/data_types/string_try.rb +12 -12
  165. data/try/data_types/unsortedset_try.rb +33 -0
  166. data/try/debugging/cache_behavior_tracer.rb +7 -7
  167. data/try/debugging/debug_aad_process.rb +1 -1
  168. data/try/debugging/debug_concealed_internal.rb +1 -1
  169. data/try/debugging/debug_cross_context.rb +1 -1
  170. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  171. data/try/debugging/encryption_method_tracer.rb +10 -10
  172. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  173. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  174. data/try/encryption/config_persistence_try.rb +2 -2
  175. data/try/encryption/encryption_core_try.rb +19 -19
  176. data/try/encryption/instance_variable_scope_try.rb +1 -1
  177. data/try/encryption/module_loading_try.rb +2 -2
  178. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  179. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  180. data/try/encryption/secure_memory_handling_try.rb +1 -1
  181. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  182. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  183. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  184. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  185. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  186. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  187. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  188. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  189. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  190. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  191. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  192. data/try/features/feature_dependencies_try.rb +3 -3
  193. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  194. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  195. data/try/features/quantization/quantization_try.rb +1 -1
  196. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  197. data/try/features/relationships/indexing_try.rb +433 -0
  198. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  199. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  200. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  201. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  202. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  203. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  204. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  205. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  206. data/try/features/relationships/relationships_performance_try.rb +20 -20
  207. data/try/features/relationships/relationships_try.rb +27 -38
  208. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  209. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  210. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  211. data/try/helpers/test_cleanup.rb +86 -0
  212. data/try/helpers/test_helpers.rb +3 -3
  213. data/try/horreum/base_try.rb +3 -2
  214. data/try/horreum/commands_try.rb +1 -1
  215. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  216. data/try/horreum/initialization_try.rb +11 -7
  217. data/try/horreum/relations_try.rb +21 -13
  218. data/try/horreum/serialization_try.rb +12 -11
  219. data/try/integration/cross_component_try.rb +3 -3
  220. data/try/memory/memory_basic_test.rb +1 -1
  221. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  222. data/try/models/customer_safe_dump_try.rb +1 -1
  223. data/try/models/customer_try.rb +8 -10
  224. data/try/models/datatype_base_try.rb +3 -3
  225. data/try/models/familia_object_try.rb +9 -8
  226. data/try/performance/benchmarks_try.rb +2 -2
  227. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  228. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  229. data/try/prototypes/atomic_saves_v4.rb +1 -1
  230. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  231. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  232. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  234. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  235. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  236. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  237. data/try/prototypes/pooling/pool_siege.rb +11 -11
  238. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  239. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  240. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  241. data/try/refinements/logger_trace_methods_try.rb +44 -0
  242. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  243. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  244. metadata +75 -43
  245. data/.rubocop_todo.yml +0 -208
  246. data/docs/connection_pooling.md +0 -192
  247. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  248. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  249. data/docs/guides/Feature-System-Autoloading.md +0 -198
  250. data/docs/guides/Home.md +0 -116
  251. data/docs/guides/Relationships-Guide.md +0 -737
  252. data/docs/guides/relationships-methods.md +0 -266
  253. data/docs/reference/auditing_database_commands.rb +0 -228
  254. data/examples/permissions.rb +0 -240
  255. data/lib/familia/features/relationships/cascading.rb +0 -437
  256. data/lib/familia/features/relationships/membership.rb +0 -497
  257. data/lib/familia/features/relationships/permission_management.rb +0 -264
  258. data/lib/familia/features/relationships/querying.rb +0 -615
  259. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  260. data/lib/familia/features/relationships/tracking.rb +0 -418
  261. data/lib/familia/refinements/snake_case.rb +0 -40
  262. data/lib/familia/validation/command_recorder.rb +0 -336
  263. data/lib/familia/validation/expectations.rb +0 -519
  264. data/lib/familia/validation/validation_helpers.rb +0 -443
  265. data/lib/familia/validation/validator.rb +0 -412
  266. data/lib/familia/validation.rb +0 -140
  267. data/try/data_types/set_try.rb +0 -33
  268. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  269. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  270. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  271. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  272. data/try/validation/command_validation_try.rb.disabled +0 -207
  273. data/try/validation/performance_validation_try.rb.disabled +0 -324
  274. 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