familia 2.0.0.pre14 → 2.0.0.pre16

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