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.
Files changed (288) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/code-quality.yml +138 -0
  4. data/.github/workflows/code-smells.yml +85 -0
  5. data/.github/workflows/docs.yml +31 -8
  6. data/.gitignore +3 -1
  7. data/.pre-commit-config.yaml +7 -1
  8. data/.reek.yml +98 -0
  9. data/.rubocop.yml +54 -10
  10. data/.talismanrc +9 -0
  11. data/.yardopts +18 -13
  12. data/CHANGELOG.rst +86 -4
  13. data/CLAUDE.md +39 -1
  14. data/Gemfile +6 -5
  15. data/Gemfile.lock +99 -23
  16. data/LICENSE.txt +1 -1
  17. data/README.md +285 -85
  18. data/changelog.d/README.md +2 -2
  19. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  20. data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
  21. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  22. data/docs/archive/README.md +3 -2
  23. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  24. data/docs/conf.py +29 -0
  25. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  26. data/docs/guides/feature-encrypted-fields.md +785 -0
  27. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  28. data/docs/guides/feature-external-identifiers.md +637 -0
  29. data/docs/guides/feature-object-identifiers.md +435 -0
  30. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  31. data/docs/guides/feature-relationships-methods.md +684 -0
  32. data/docs/guides/feature-relationships.md +200 -0
  33. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  34. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  35. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  36. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  37. data/docs/guides/index.md +176 -0
  38. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  39. data/docs/migrating/v2.0.0-pre.md +1 -1
  40. data/docs/migrating/v2.0.0-pre11.md +2 -2
  41. data/docs/migrating/v2.0.0-pre12.md +2 -2
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +624 -20
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +7 -7
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +51 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/class_methods.rb +63 -0
  65. data/lib/familia/data_type/commands.rb +53 -51
  66. data/lib/familia/data_type/connection.rb +83 -0
  67. data/lib/familia/data_type/serialization.rb +108 -107
  68. data/lib/familia/data_type/settings.rb +96 -0
  69. data/lib/familia/data_type/types/counter.rb +1 -1
  70. data/lib/familia/data_type/types/hashkey.rb +15 -11
  71. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  72. data/lib/familia/data_type/types/lock.rb +3 -2
  73. data/lib/familia/data_type/types/sorted_set.rb +128 -14
  74. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
  75. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  76. data/lib/familia/data_type.rb +12 -171
  77. data/lib/familia/distinguisher.rb +85 -0
  78. data/lib/familia/encryption/encrypted_data.rb +15 -24
  79. data/lib/familia/encryption/manager.rb +6 -4
  80. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  81. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  82. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  83. data/lib/familia/encryption/request_cache.rb +7 -7
  84. data/lib/familia/encryption.rb +2 -3
  85. data/lib/familia/errors.rb +9 -3
  86. data/lib/familia/features/autoloader.rb +30 -12
  87. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  88. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  89. data/lib/familia/features/encrypted_fields.rb +71 -66
  90. data/lib/familia/features/expiration/extensions.rb +1 -1
  91. data/lib/familia/features/expiration.rb +31 -26
  92. data/lib/familia/features/external_identifier.rb +57 -19
  93. data/lib/familia/features/object_identifier.rb +134 -25
  94. data/lib/familia/features/quantization.rb +16 -21
  95. data/lib/familia/features/relationships/README.md +97 -0
  96. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  97. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  98. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
  99. data/lib/familia/features/relationships/indexing.rb +182 -256
  100. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  101. data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
  102. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  103. data/lib/familia/features/relationships/participation.rb +656 -0
  104. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  105. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  106. data/lib/familia/features/relationships.rb +65 -266
  107. data/lib/familia/features/safe_dump.rb +127 -130
  108. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  109. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  110. data/lib/familia/features/transient_fields.rb +10 -7
  111. data/lib/familia/features.rb +10 -14
  112. data/lib/familia/field_type.rb +6 -4
  113. data/lib/familia/horreum/connection.rb +297 -0
  114. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
  115. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
  116. data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
  117. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
  118. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
  119. data/lib/familia/horreum/serialization.rb +172 -0
  120. data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
  121. data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
  122. data/lib/familia/horreum.rb +222 -119
  123. data/lib/familia/json_serializer.rb +0 -1
  124. data/lib/familia/logging.rb +11 -114
  125. data/lib/familia/refinements/dear_json.rb +122 -0
  126. data/lib/familia/refinements/logger_trace.rb +20 -17
  127. data/lib/familia/refinements/stylize_words.rb +65 -0
  128. data/lib/familia/refinements/time_literals.rb +60 -52
  129. data/lib/familia/refinements.rb +2 -1
  130. data/lib/familia/secure_identifier.rb +60 -28
  131. data/lib/familia/settings.rb +83 -7
  132. data/lib/familia/utils.rb +5 -87
  133. data/lib/familia/verifiable_identifier.rb +4 -4
  134. data/lib/familia/version.rb +1 -1
  135. data/lib/familia.rb +72 -14
  136. data/lib/middleware/database_middleware.rb +56 -14
  137. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  138. data/try/configuration/scenarios_try.rb +2 -2
  139. data/try/connection/fiber_context_preservation_try.rb +250 -0
  140. data/try/connection/handler_constraints_try.rb +59 -0
  141. data/try/connection/operation_mode_guards_try.rb +208 -0
  142. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  143. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  144. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  145. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  146. data/try/connection/transaction_mode_strict_try.rb +98 -0
  147. data/try/connection/transaction_mode_warn_try.rb +131 -0
  148. data/try/connection/transaction_modes_try.rb +249 -0
  149. data/try/core/autoloader_try.rb +120 -2
  150. data/try/core/connection_try.rb +10 -10
  151. data/try/core/conventional_inheritance_try.rb +130 -0
  152. data/try/core/create_method_try.rb +15 -23
  153. data/try/core/database_consistency_try.rb +11 -10
  154. data/try/core/errors_try.rb +11 -14
  155. data/try/core/familia_extended_try.rb +2 -2
  156. data/try/core/familia_members_methods_try.rb +76 -0
  157. data/try/core/familia_try.rb +1 -1
  158. data/try/core/isolated_dbclient_try.rb +165 -0
  159. data/try/core/middleware_try.rb +16 -16
  160. data/try/core/persistence_operations_try.rb +4 -4
  161. data/try/core/pools_try.rb +42 -26
  162. data/try/core/secure_identifier_try.rb +28 -24
  163. data/try/core/time_utils_try.rb +10 -10
  164. data/try/core/tools_try.rb +3 -3
  165. data/try/core/utils_try.rb +2 -2
  166. data/try/data_types/boolean_try.rb +4 -4
  167. data/try/data_types/datatype_base_try.rb +0 -2
  168. data/try/data_types/list_try.rb +10 -10
  169. data/try/data_types/sorted_set_try.rb +5 -5
  170. data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
  171. data/try/data_types/string_try.rb +12 -12
  172. data/try/data_types/unsortedset_try.rb +33 -0
  173. data/try/debugging/cache_behavior_tracer.rb +7 -7
  174. data/try/debugging/debug_aad_process.rb +1 -1
  175. data/try/debugging/debug_concealed_internal.rb +1 -1
  176. data/try/debugging/debug_cross_context.rb +1 -1
  177. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  178. data/try/debugging/encryption_method_tracer.rb +10 -10
  179. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  180. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  181. data/try/encryption/config_persistence_try.rb +2 -2
  182. data/try/encryption/encryption_core_try.rb +19 -19
  183. data/try/encryption/instance_variable_scope_try.rb +1 -1
  184. data/try/encryption/module_loading_try.rb +2 -2
  185. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  186. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  187. data/try/encryption/secure_memory_handling_try.rb +1 -1
  188. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  189. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  190. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  191. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  192. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  193. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  194. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  195. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  196. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  197. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  198. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  199. data/try/features/feature_dependencies_try.rb +3 -3
  200. data/try/features/field_groups_try.rb +244 -0
  201. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  202. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  203. data/try/features/quantization/quantization_try.rb +1 -1
  204. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  205. data/try/features/relationships/indexing_try.rb +443 -0
  206. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  207. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  208. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  209. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  210. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  211. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  212. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  213. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  214. data/try/features/relationships/relationships_performance_try.rb +20 -20
  215. data/try/features/relationships/relationships_try.rb +27 -38
  216. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  217. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  218. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  219. data/try/helpers/test_cleanup.rb +86 -0
  220. data/try/helpers/test_helpers.rb +6 -7
  221. data/try/horreum/auto_indexing_on_save_try.rb +212 -0
  222. data/try/horreum/base_try.rb +3 -2
  223. data/try/horreum/commands_try.rb +3 -1
  224. data/try/horreum/defensive_initialization_try.rb +86 -0
  225. data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
  226. data/try/horreum/initialization_try.rb +11 -7
  227. data/try/horreum/relations_try.rb +21 -13
  228. data/try/horreum/serialization_try.rb +12 -11
  229. data/try/horreum/settings_try.rb +2 -0
  230. data/try/integration/cross_component_try.rb +3 -3
  231. data/try/memory/memory_basic_test.rb +1 -1
  232. data/try/memory/memory_docker_ruby_dump.sh +2 -2
  233. data/try/models/customer_safe_dump_try.rb +1 -1
  234. data/try/models/customer_try.rb +13 -15
  235. data/try/models/datatype_base_try.rb +3 -3
  236. data/try/models/familia_object_try.rb +9 -8
  237. data/try/performance/benchmarks_try.rb +2 -2
  238. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  239. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  240. data/try/prototypes/atomic_saves_v4.rb +1 -1
  241. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  242. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  243. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  244. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  245. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  246. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  247. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  248. data/try/prototypes/pooling/pool_siege.rb +11 -11
  249. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  250. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  251. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  252. data/try/refinements/logger_trace_methods_try.rb +44 -0
  253. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  254. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  255. data/try/valkey.conf +26 -0
  256. metadata +92 -52
  257. data/.rubocop_todo.yml +0 -208
  258. data/docs/connection_pooling.md +0 -192
  259. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  260. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  261. data/docs/guides/Feature-System-Autoloading.md +0 -198
  262. data/docs/guides/Home.md +0 -116
  263. data/docs/guides/Relationships-Guide.md +0 -737
  264. data/docs/guides/relationships-methods.md +0 -266
  265. data/docs/reference/auditing_database_commands.rb +0 -228
  266. data/examples/permissions.rb +0 -240
  267. data/lib/familia/features/relationships/cascading.rb +0 -437
  268. data/lib/familia/features/relationships/membership.rb +0 -497
  269. data/lib/familia/features/relationships/permission_management.rb +0 -264
  270. data/lib/familia/features/relationships/querying.rb +0 -615
  271. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  272. data/lib/familia/features/relationships/tracking.rb +0 -418
  273. data/lib/familia/horreum/core/connection.rb +0 -73
  274. data/lib/familia/horreum/core.rb +0 -21
  275. data/lib/familia/refinements/snake_case.rb +0 -40
  276. data/lib/familia/validation/command_recorder.rb +0 -336
  277. data/lib/familia/validation/expectations.rb +0 -519
  278. data/lib/familia/validation/validation_helpers.rb +0 -443
  279. data/lib/familia/validation/validator.rb +0 -412
  280. data/lib/familia/validation.rb +0 -140
  281. data/try/data_types/set_try.rb +0 -33
  282. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  283. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  284. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  285. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  286. data/try/validation/command_validation_try.rb.disabled +0 -207
  287. data/try/validation/performance_validation_try.rb.disabled +0 -324
  288. 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,306 @@
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 (called automatically on save)
37
+ # - employee.remove_from_class_email_index
38
+ # - employee.update_in_class_email_index(old_email)
39
+ #
40
+ # Note: Class-level indexes auto-populate on save(). Instance-scoped indexes
41
+ # (with within:) remain manual as they require parent context.
42
+ module UniqueIndexGenerators
43
+ module_function
44
+
45
+ using Familia::Refinements::StylizeWords
46
+
47
+ # Main setup method that orchestrates unique index creation
48
+ #
49
+ # @param indexed_class [Class] The class being indexed (e.g., Employee)
50
+ # @param field [Symbol] The field to index
51
+ # @param index_name [Symbol] Name of the index
52
+ # @param within [Class, Symbol, nil] Parent class for instance-scoped index
53
+ # @param query [Boolean] Whether to generate query methods
54
+ def setup(indexed_class:, field:, index_name:, within:, query:)
55
+ # Normalize parameters and determine scope type
56
+ target_class, scope_type = if within
57
+ k = Familia.resolve_class(within)
58
+ [k, :instance]
59
+ else
60
+ [indexed_class, :class]
61
+ end
62
+
63
+ # Store metadata for this indexing relationship
64
+ indexed_class.indexing_relationships << IndexingRelationship.new(
65
+ field: field,
66
+ target_class: target_class,
67
+ index_name: index_name,
68
+ query: query,
69
+ cardinality: :unique,
70
+ )
71
+
72
+ # Generate appropriate methods based on scope type
73
+ case scope_type
74
+ when :instance
75
+ # Instance-scoped index (within: Company)
76
+ if query && target_class.is_a?(Class)
77
+ generate_query_methods_destination(indexed_class, field, target_class, index_name)
78
+ end
79
+ generate_mutation_methods_self(indexed_class, field, target_class, index_name)
80
+ when :class
81
+ # Class-level index (no within:)
82
+ indexed_class.send(:ensure_index_field, indexed_class, index_name, :class_hashkey)
83
+ generate_query_methods_class(field, index_name, indexed_class) if query
84
+ generate_mutation_methods_class(field, index_name, indexed_class)
85
+ end
86
+ end
87
+
88
+ # Generates query methods ON THE PARENT CLASS (Company when within: Company):
89
+ # - company.find_by_badge_number(badge) - find by field value
90
+ # - company.find_all_by_badge_number([badges]) - batch lookup
91
+ # - company.badge_index - DataType accessor
92
+ # - company.rebuild_badge_index - rebuild index
93
+ #
94
+ # @param indexed_class [Class] The class being indexed (e.g., Employee)
95
+ # @param field [Symbol] The field to index (e.g., :badge_number)
96
+ # @param target_class [Class] The parent class (e.g., Company)
97
+ # @param index_name [Symbol] Name of the index (e.g., :badge_index)
98
+ def generate_query_methods_destination(indexed_class, field, target_class, index_name)
99
+ # Resolve target class using Familia pattern
100
+ actual_target_class = Familia.resolve_class(target_class)
101
+
102
+ # Ensure the index field is declared (creates accessor that returns DataType)
103
+ actual_target_class.send(:ensure_index_field, actual_target_class, index_name, :hashkey)
104
+
105
+ # Generate instance query method (e.g., company.find_by_badge_number)
106
+ actual_target_class.class_eval do
107
+ define_method("find_by_#{field}") do |field_value|
108
+ # Use declared field accessor instead of manual instantiation
109
+ index_hash = send(index_name)
110
+
111
+ # Get the identifier from the hash
112
+ object_id = index_hash[field_value.to_s]
113
+ return nil unless object_id
114
+
115
+ indexed_class.new(object_id)
116
+ end
117
+
118
+ # Generate bulk query method (e.g., company.find_all_by_badge_number)
119
+ define_method("find_all_by_#{field}") do |field_values|
120
+ field_values = Array(field_values)
121
+ return [] if field_values.empty?
122
+
123
+ # Use declared field accessor instead of manual instantiation
124
+ index_hash = send(index_name)
125
+
126
+ # Get all identifiers from the hash
127
+ object_ids = index_hash.values_at(*field_values.map(&:to_s))
128
+ # Filter out nil values and instantiate objects
129
+ object_ids.compact.map { |object_id| indexed_class.new(object_id) }
130
+ end
131
+
132
+ # Accessor method already created by ensure_index_field above
133
+ # No need to manually define it here
134
+
135
+ # Generate method to rebuild the unique index for this parent instance
136
+ define_method("rebuild_#{index_name}") do
137
+ # Use declared field accessor instead of manual instantiation
138
+ index_hash = send(index_name)
139
+
140
+ # Clear existing index using DataType method
141
+ index_hash.clear
142
+
143
+ # Rebuild from all existing objects
144
+ # This would need to scan through all objects belonging to this parent
145
+ # Implementation depends on how objects are stored/tracked
146
+ end
147
+ end
148
+ end
149
+
150
+ # Generates mutation methods ON THE INDEXED CLASS (Employee):
151
+ # Instance methods for parent-scoped unique index operations:
152
+ # - employee.add_to_company_badge_index(company)
153
+ # - employee.remove_from_company_badge_index(company)
154
+ # - employee.update_in_company_badge_index(company, old_badge)
155
+ #
156
+ # @param indexed_class [Class] The class being indexed (e.g., Employee)
157
+ # @param field [Symbol] The field to index (e.g., :badge_number)
158
+ # @param target_class [Class] The parent class (e.g., Company)
159
+ # @param index_name [Symbol] Name of the index (e.g., :badge_index)
160
+ def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
161
+ target_class_config = target_class.config_name
162
+ indexed_class.class_eval do
163
+ method_name = "add_to_#{target_class_config}_#{index_name}"
164
+ Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
165
+
166
+ define_method(method_name) do |target_instance|
167
+ return unless target_instance
168
+
169
+ field_value = send(field)
170
+ return unless field_value
171
+
172
+ # Use declared field accessor on target instance
173
+ index_hash = target_instance.send(index_name)
174
+
175
+ # Use HashKey DataType method
176
+ index_hash[field_value.to_s] = identifier
177
+ end
178
+
179
+ method_name = "remove_from_#{target_class_config}_#{index_name}"
180
+ Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
181
+
182
+ define_method(method_name) do |target_instance|
183
+ return unless target_instance
184
+
185
+ field_value = send(field)
186
+ return unless field_value
187
+
188
+ # Use declared field accessor on target instance
189
+ index_hash = target_instance.send(index_name)
190
+
191
+ # Remove using HashKey DataType method
192
+ index_hash.remove(field_value.to_s)
193
+ end
194
+
195
+ method_name = "update_in_#{target_class_config}_#{index_name}"
196
+ Familia.ld("[UniqueIndexGenerators] #{name} method #{method_name}")
197
+
198
+ define_method(method_name) do |target_instance, old_field_value = nil|
199
+ return unless target_instance
200
+
201
+ new_field_value = send(field)
202
+
203
+ # Use Familia's transaction method for atomicity with DataType abstraction
204
+ target_instance.transaction do |_tx|
205
+ # Use declared field accessor on target instance
206
+ index_hash = target_instance.send(index_name)
207
+
208
+ # Remove old value if provided
209
+ index_hash.remove(old_field_value.to_s) if old_field_value
210
+
211
+ # Add new value if present
212
+ index_hash[new_field_value.to_s] = identifier if new_field_value
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ # Generates query methods ON THE INDEXED CLASS (Employee):
219
+ # Class-level methods (singleton):
220
+ # - Employee.find_by_email(email)
221
+ # - Employee.find_all_by_email([emails])
222
+ # - Employee.email_index
223
+ # - Employee.rebuild_email_index
224
+ def generate_query_methods_class(field, index_name, indexed_class)
225
+ indexed_class.define_singleton_method("find_by_#{field}") do |field_value|
226
+ index_hash = send(index_name) # Access the class-level hashkey DataType
227
+ object_id = index_hash[field_value.to_s]
228
+
229
+ return nil unless object_id
230
+
231
+ new(object_id)
232
+ end
233
+
234
+ # Generate class-level bulk query method
235
+ indexed_class.define_singleton_method("find_all_by_#{field}") do |field_values|
236
+ field_values = Array(field_values)
237
+ return [] if field_values.empty?
238
+
239
+ index_hash = send(index_name) # Access the class-level hashkey DataType
240
+ object_ids = index_hash.values_at(*field_values.map(&:to_s))
241
+ # Filter out nil values and instantiate objects
242
+ object_ids.compact.map { |object_id| new(object_id) }
243
+ end
244
+
245
+ # The index accessor method is already created by the class_hashkey declaration
246
+ # No need to manually create it - Horreum handles this automatically
247
+
248
+ # Generate method to rebuild the class-level index
249
+ indexed_class.define_singleton_method("rebuild_#{index_name}") do
250
+ index_hash = send(index_name) # Access the class-level hashkey DataType
251
+
252
+ # Clear existing index using DataType method
253
+ index_hash.clear
254
+
255
+ # Rebuild from all existing objects
256
+ # This would need to scan through all objects of this class
257
+ # Implementation depends on how objects are stored/tracked
258
+ end
259
+ end
260
+
261
+ # Generates mutation methods ON THE INDEXED CLASS (Employee):
262
+ # Instance methods for class-level index operations:
263
+ # - employee.add_to_class_email_index
264
+ # - employee.remove_from_class_email_index
265
+ # - employee.update_in_class_email_index(old_email)
266
+ def generate_mutation_methods_class(field, index_name, indexed_class)
267
+ indexed_class.class_eval do
268
+ define_method("add_to_class_#{index_name}") do
269
+ index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
270
+ field_value = send(field)
271
+
272
+ return unless field_value
273
+
274
+ index_hash[field_value.to_s] = identifier
275
+ end
276
+
277
+ define_method("remove_from_class_#{index_name}") do
278
+ index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
279
+ field_value = send(field)
280
+
281
+ return unless field_value
282
+
283
+ index_hash.remove(field_value.to_s)
284
+ end
285
+
286
+ define_method("update_in_class_#{index_name}") do |old_field_value = nil|
287
+ new_field_value = send(field)
288
+
289
+ # Use class-level transaction for atomicity with DataType abstraction
290
+ self.class.transaction do |_tx|
291
+ index_hash = self.class.send(index_name) # Access the class-level hashkey DataType
292
+
293
+ # Remove old value if provided
294
+ index_hash.remove(old_field_value.to_s) if old_field_value
295
+
296
+ # Add new value if present
297
+ index_hash[new_field_value.to_s] = identifier if new_field_value
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end