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