familia 2.0.0.pre18 → 2.0.0.pre21

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 (370) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +4 -9
  3. data/.github/workflows/code-smells.yml +64 -3
  4. data/.pre-commit-config.yaml +8 -6
  5. data/.reek.yml +10 -9
  6. data/.rubocop.yml +4 -0
  7. data/CHANGELOG.rst +205 -88
  8. data/CLAUDE.md +62 -10
  9. data/Gemfile +3 -3
  10. data/Gemfile.lock +27 -62
  11. data/README.md +39 -0
  12. data/bin/try +16 -0
  13. data/bin/tryouts +16 -0
  14. data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
  15. data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
  16. data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
  17. data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
  18. data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
  19. data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
  20. data/docs/1106-participates_in-bidirectional-solution.md +129 -0
  21. data/docs/guides/encryption.md +486 -0
  22. data/docs/guides/feature-encrypted-fields.md +123 -7
  23. data/docs/guides/feature-expiration.md +177 -133
  24. data/docs/guides/feature-external-identifiers.md +415 -443
  25. data/docs/guides/feature-object-identifiers.md +400 -269
  26. data/docs/guides/feature-quantization.md +120 -6
  27. data/docs/guides/feature-relationships-indexing.md +318 -0
  28. data/docs/guides/feature-relationships-methods.md +146 -604
  29. data/docs/guides/feature-relationships-participation.md +263 -0
  30. data/docs/guides/feature-relationships.md +118 -136
  31. data/docs/guides/feature-system-devs.md +176 -693
  32. data/docs/guides/feature-system.md +119 -6
  33. data/docs/guides/feature-transient-fields.md +81 -0
  34. data/docs/guides/field-system.md +778 -0
  35. data/docs/guides/index.md +32 -15
  36. data/docs/guides/logging.md +187 -0
  37. data/docs/guides/optimized-loading.md +674 -0
  38. data/docs/guides/thread-safety-monitoring.md +61 -0
  39. data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
  40. data/docs/migrating/v2.0.0-pre19.md +197 -0
  41. data/docs/migrating/v2.0.0-pre22.md +241 -0
  42. data/docs/overview.md +7 -9
  43. data/docs/reference/api-technical.md +267 -320
  44. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
  45. data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
  46. data/examples/autoloader/mega_customer.rb +2 -0
  47. data/examples/datatype_standalone.rb +282 -0
  48. data/examples/encrypted_fields.rb +2 -1
  49. data/examples/json_usage_patterns.rb +2 -0
  50. data/examples/relationships.rb +3 -0
  51. data/examples/safe_dump.rb +2 -1
  52. data/examples/sampling_demo.rb +53 -0
  53. data/examples/single_connection_transaction_confusions.rb +2 -1
  54. data/familia.gemspec +2 -1
  55. data/lib/familia/base.rb +2 -0
  56. data/lib/familia/connection/behavior.rb +254 -0
  57. data/lib/familia/connection/handlers.rb +97 -0
  58. data/lib/familia/connection/individual_command_proxy.rb +2 -0
  59. data/lib/familia/connection/middleware.rb +34 -24
  60. data/lib/familia/connection/operation_core.rb +3 -1
  61. data/lib/familia/connection/operations.rb +2 -0
  62. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +4 -2
  63. data/lib/familia/connection/transaction_core.rb +75 -9
  64. data/lib/familia/connection.rb +21 -5
  65. data/lib/familia/data_type/class_methods.rb +3 -1
  66. data/lib/familia/data_type/connection.rb +153 -7
  67. data/lib/familia/data_type/database_commands.rb +9 -4
  68. data/lib/familia/data_type/serialization.rb +10 -4
  69. data/lib/familia/data_type/settings.rb +2 -0
  70. data/lib/familia/data_type/types/counter.rb +2 -0
  71. data/lib/familia/data_type/types/hashkey.rb +8 -6
  72. data/lib/familia/data_type/types/listkey.rb +2 -0
  73. data/lib/familia/data_type/types/lock.rb +2 -0
  74. data/lib/familia/data_type/types/sorted_set.rb +2 -0
  75. data/lib/familia/data_type/types/stringkey.rb +2 -0
  76. data/lib/familia/data_type/types/unsorted_set.rb +2 -0
  77. data/lib/familia/data_type.rb +2 -0
  78. data/lib/familia/encryption/encrypted_data.rb +4 -2
  79. data/lib/familia/encryption/manager.rb +2 -0
  80. data/lib/familia/encryption/provider.rb +2 -0
  81. data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
  82. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
  83. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
  84. data/lib/familia/encryption/registry.rb +2 -0
  85. data/lib/familia/encryption/request_cache.rb +2 -0
  86. data/lib/familia/encryption.rb +9 -2
  87. data/lib/familia/errors.rb +53 -14
  88. data/lib/familia/features/autoloader.rb +2 -0
  89. data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
  90. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
  91. data/lib/familia/features/encrypted_fields.rb +2 -2
  92. data/lib/familia/features/expiration/extensions.rb +11 -11
  93. data/lib/familia/features/expiration.rb +29 -21
  94. data/lib/familia/features/external_identifier.rb +33 -7
  95. data/lib/familia/features/object_identifier.rb +2 -0
  96. data/lib/familia/features/quantization.rb +3 -1
  97. data/lib/familia/features/relationships/README.md +3 -1
  98. data/lib/familia/features/relationships/collection_operations.rb +2 -0
  99. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +177 -47
  100. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
  101. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +203 -63
  102. data/lib/familia/features/relationships/indexing.rb +40 -42
  103. data/lib/familia/features/relationships/indexing_relationship.rb +17 -5
  104. data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
  105. data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
  106. data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
  107. data/lib/familia/features/relationships/participation.rb +155 -69
  108. data/lib/familia/features/relationships/participation_membership.rb +69 -0
  109. data/lib/familia/features/relationships/participation_relationship.rb +34 -6
  110. data/lib/familia/features/relationships/score_encoding.rb +2 -0
  111. data/lib/familia/features/relationships.rb +5 -3
  112. data/lib/familia/features/safe_dump.rb +2 -0
  113. data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
  114. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
  115. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
  116. data/lib/familia/features/transient_fields.rb +2 -0
  117. data/lib/familia/features.rb +2 -0
  118. data/lib/familia/field_type.rb +5 -2
  119. data/lib/familia/horreum/connection.rb +28 -36
  120. data/lib/familia/horreum/database_commands.rb +131 -10
  121. data/lib/familia/horreum/definition.rb +18 -7
  122. data/lib/familia/horreum/management.rb +233 -57
  123. data/lib/familia/horreum/persistence.rb +314 -122
  124. data/lib/familia/horreum/related_fields.rb +2 -0
  125. data/lib/familia/horreum/serialization.rb +26 -4
  126. data/lib/familia/horreum/settings.rb +2 -0
  127. data/lib/familia/horreum/utils.rb +2 -8
  128. data/lib/familia/horreum.rb +46 -13
  129. data/lib/familia/identifier_extractor.rb +2 -0
  130. data/lib/familia/instrumentation.rb +156 -0
  131. data/lib/familia/json_serializer.rb +2 -0
  132. data/lib/familia/logging.rb +94 -37
  133. data/lib/familia/refinements/dear_json.rb +2 -0
  134. data/lib/familia/refinements/stylize_words.rb +2 -14
  135. data/lib/familia/refinements/time_literals.rb +2 -0
  136. data/lib/familia/refinements.rb +2 -0
  137. data/lib/familia/secure_identifier.rb +10 -2
  138. data/lib/familia/settings.rb +9 -7
  139. data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
  140. data/lib/familia/thread_safety/monitor.rb +328 -0
  141. data/lib/familia/utils.rb +13 -0
  142. data/lib/familia/verifiable_identifier.rb +3 -1
  143. data/lib/familia/version.rb +3 -1
  144. data/lib/familia.rb +31 -4
  145. data/lib/middleware/database_command_counter.rb +152 -0
  146. data/lib/middleware/database_logger.rb +325 -129
  147. data/lib/multi_result.rb +2 -0
  148. data/try/edge_cases/empty_identifiers_try.rb +2 -0
  149. data/try/edge_cases/hash_symbolization_try.rb +2 -0
  150. data/try/edge_cases/json_serialization_try.rb +2 -0
  151. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
  152. data/try/edge_cases/race_conditions_try.rb +4 -0
  153. data/try/edge_cases/reserved_keywords_try.rb +4 -0
  154. data/try/edge_cases/string_coercion_try.rb +6 -4
  155. data/try/edge_cases/ttl_side_effects_try.rb +4 -0
  156. data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
  157. data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
  158. data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
  159. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
  160. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
  161. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
  162. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
  163. data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
  164. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
  165. data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
  166. data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
  167. data/try/features/encrypted_fields/memory_security_try.rb +4 -0
  168. data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
  169. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
  170. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
  171. data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
  172. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
  173. data/try/features/encryption/config_persistence_try.rb +4 -0
  174. data/try/features/encryption/core_try.rb +4 -0
  175. data/try/features/encryption/instance_variable_scope_try.rb +4 -0
  176. data/try/features/encryption/module_loading_try.rb +4 -0
  177. data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
  178. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
  179. data/try/features/encryption/roundtrip_validation_try.rb +4 -0
  180. data/try/features/encryption/secure_memory_handling_try.rb +4 -0
  181. data/try/features/expiration/expiration_try.rb +5 -1
  182. data/try/features/external_identifier/external_identifier_try.rb +171 -8
  183. data/try/features/feature_dependencies_try.rb +2 -0
  184. data/try/features/feature_improvements_try.rb +2 -0
  185. data/try/features/field_groups_try.rb +2 -0
  186. data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
  187. data/try/features/object_identifier/object_identifier_try.rb +2 -0
  188. data/try/features/quantization/quantization_try.rb +4 -0
  189. data/try/features/real_feature_integration_try.rb +2 -0
  190. data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
  191. data/try/features/relationships/indexing_rebuild_try.rb +600 -0
  192. data/try/features/relationships/indexing_try.rb +30 -4
  193. data/try/features/relationships/participation_bidirectional_try.rb +242 -0
  194. data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
  195. data/try/features/relationships/participation_commands_verification_try.rb +2 -0
  196. data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
  197. data/try/features/relationships/participation_reverse_index_try.rb +15 -13
  198. data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
  199. data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
  200. data/try/features/relationships/relationships_api_changes_try.rb +6 -4
  201. data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
  202. data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
  203. data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
  204. data/try/features/relationships/relationships_performance_try.rb +4 -0
  205. data/try/features/relationships/relationships_performance_working_try.rb +4 -0
  206. data/try/features/relationships/relationships_try.rb +6 -4
  207. data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
  208. data/try/features/safe_dump/safe_dump_try.rb +4 -0
  209. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  210. data/try/features/transient_fields/refresh_reset_try.rb +3 -0
  211. data/try/features/transient_fields/simple_refresh_test.rb +3 -0
  212. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  213. data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
  214. data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
  215. data/try/integration/connection/fiber_context_preservation_try.rb +7 -3
  216. data/try/integration/connection/handler_constraints_try.rb +4 -0
  217. data/try/integration/connection/isolated_dbclient_try.rb +4 -0
  218. data/try/integration/connection/middleware_reconnect_try.rb +2 -0
  219. data/try/integration/connection/operation_mode_guards_try.rb +5 -1
  220. data/try/integration/connection/pipeline_fallback_integration_try.rb +15 -12
  221. data/try/integration/connection/pools_try.rb +4 -0
  222. data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
  223. data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
  224. data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
  225. data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
  226. data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
  227. data/try/integration/connection/transaction_modes_try.rb +4 -0
  228. data/try/integration/conventional_inheritance_try.rb +4 -0
  229. data/try/integration/create_method_try.rb +26 -22
  230. data/try/integration/cross_component_try.rb +4 -0
  231. data/try/integration/data_types/datatype_pipelines_try.rb +108 -0
  232. data/try/integration/data_types/datatype_transactions_try.rb +251 -0
  233. data/try/integration/database_consistency_try.rb +4 -0
  234. data/try/integration/familia_extended_try.rb +4 -0
  235. data/try/integration/familia_members_methods_try.rb +4 -0
  236. data/try/integration/models/customer_safe_dump_try.rb +9 -1
  237. data/try/integration/models/customer_try.rb +4 -0
  238. data/try/integration/models/datatype_base_try.rb +4 -0
  239. data/try/integration/models/familia_object_try.rb +5 -1
  240. data/try/integration/persistence_operations_try.rb +166 -10
  241. data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
  242. data/try/integration/save_methods_consistency_try.rb +241 -0
  243. data/try/integration/scenarios_try.rb +4 -0
  244. data/try/integration/secure_identifier_try.rb +4 -0
  245. data/try/integration/transaction_safety_core_try.rb +176 -0
  246. data/try/integration/transaction_safety_workflow_try.rb +291 -0
  247. data/try/integration/verifiable_identifier_try.rb +4 -0
  248. data/try/investigation/pipeline_routing/README.md +228 -0
  249. data/try/performance/benchmarks_try.rb +4 -0
  250. data/try/performance/transaction_safety_benchmark_try.rb +238 -0
  251. data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
  252. data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
  253. data/try/support/debugging/cache_behavior_tracer.rb +4 -0
  254. data/try/support/debugging/debug_aad_process.rb +3 -0
  255. data/try/support/debugging/debug_concealed_internal.rb +3 -0
  256. data/try/support/debugging/debug_concealed_reveal.rb +3 -0
  257. data/try/support/debugging/debug_context_aad.rb +3 -0
  258. data/try/support/debugging/debug_context_simple.rb +3 -0
  259. data/try/support/debugging/debug_cross_context.rb +3 -0
  260. data/try/support/debugging/debug_database_load.rb +3 -0
  261. data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
  262. data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
  263. data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
  264. data/try/support/debugging/debug_field_decrypt.rb +3 -0
  265. data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
  266. data/try/support/debugging/debug_load_path.rb +3 -0
  267. data/try/support/debugging/debug_method_definition.rb +3 -0
  268. data/try/support/debugging/debug_method_resolution.rb +3 -0
  269. data/try/support/debugging/debug_minimal.rb +3 -0
  270. data/try/support/debugging/debug_provider.rb +3 -0
  271. data/try/support/debugging/debug_secure_behavior.rb +3 -0
  272. data/try/support/debugging/debug_string_class.rb +3 -0
  273. data/try/support/debugging/debug_test.rb +3 -0
  274. data/try/support/debugging/debug_test_design.rb +3 -0
  275. data/try/support/debugging/encryption_method_tracer.rb +4 -0
  276. data/try/support/debugging/provider_diagnostics.rb +4 -0
  277. data/try/support/helpers/test_cleanup.rb +4 -0
  278. data/try/support/helpers/test_helpers.rb +5 -0
  279. data/try/support/memory/memory_basic_test.rb +4 -0
  280. data/try/support/memory/memory_detailed_test.rb +4 -0
  281. data/try/support/memory/memory_search_for_string.rb +4 -0
  282. data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
  283. data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
  284. data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
  285. data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
  286. data/try/support/prototypes/atomic_saves_v4.rb +4 -0
  287. data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
  288. data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  289. data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
  290. data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  291. data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
  292. data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
  293. data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
  294. data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
  295. data/try/support/prototypes/pooling/pool_siege.rb +4 -2
  296. data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
  297. data/try/thread_safety/README.md +496 -0
  298. data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
  299. data/try/thread_safety/connection_chain_race_try.rb +148 -0
  300. data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
  301. data/try/thread_safety/feature_registry_race_try.rb +226 -0
  302. data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
  303. data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
  304. data/try/thread_safety/field_registration_race_try.rb +222 -0
  305. data/try/thread_safety/logger_initialization_race_try.rb +170 -0
  306. data/try/thread_safety/middleware_registration_race_try.rb +154 -0
  307. data/try/thread_safety/module_config_race_try.rb +175 -0
  308. data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
  309. data/try/unit/core/autoloader_try.rb +4 -0
  310. data/try/unit/core/base_enhancements_try.rb +4 -0
  311. data/try/unit/core/connection_try.rb +4 -0
  312. data/try/unit/core/errors_try.rb +4 -0
  313. data/try/unit/core/extensions_try.rb +4 -0
  314. data/try/unit/core/familia_logger_try.rb +2 -0
  315. data/try/unit/core/familia_try.rb +4 -0
  316. data/try/unit/core/middleware_sampling_try.rb +335 -0
  317. data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
  318. data/try/unit/core/middleware_thread_safety_try.rb +245 -0
  319. data/try/unit/core/middleware_try.rb +4 -0
  320. data/try/unit/core/settings_try.rb +4 -0
  321. data/try/unit/core/time_utils_try.rb +4 -0
  322. data/try/unit/core/tools_try.rb +4 -0
  323. data/try/unit/core/utils_try.rb +37 -0
  324. data/try/unit/data_types/boolean_try.rb +5 -1
  325. data/try/unit/data_types/counter_try.rb +4 -0
  326. data/try/unit/data_types/datatype_base_try.rb +4 -0
  327. data/try/unit/data_types/hash_try.rb +4 -0
  328. data/try/unit/data_types/list_try.rb +4 -0
  329. data/try/unit/data_types/lock_try.rb +4 -0
  330. data/try/unit/data_types/sorted_set_try.rb +4 -0
  331. data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
  332. data/try/unit/data_types/string_try.rb +5 -1
  333. data/try/unit/data_types/unsortedset_try.rb +4 -0
  334. data/try/unit/familia_resolve_class_try.rb +116 -0
  335. data/try/unit/horreum/auto_indexing_on_save_try.rb +36 -16
  336. data/try/unit/horreum/automatic_index_validation_try.rb +255 -0
  337. data/try/unit/horreum/base_try.rb +5 -1
  338. data/try/unit/horreum/class_methods_try.rb +6 -2
  339. data/try/unit/horreum/commands_try.rb +4 -0
  340. data/try/unit/horreum/defensive_initialization_try.rb +4 -0
  341. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
  342. data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
  343. data/try/unit/horreum/field_categories_try.rb +4 -0
  344. data/try/unit/horreum/field_definition_try.rb +4 -0
  345. data/try/unit/horreum/initialization_try.rb +5 -1
  346. data/try/unit/horreum/json_type_preservation_try.rb +2 -0
  347. data/try/unit/horreum/optimized_loading_try.rb +156 -0
  348. data/try/unit/horreum/relations_try.rb +8 -4
  349. data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
  350. data/try/unit/horreum/serialization_try.rb +6 -2
  351. data/try/unit/horreum/settings_try.rb +4 -0
  352. data/try/unit/horreum/unique_index_edge_cases_try.rb +380 -0
  353. data/try/unit/horreum/unique_index_guard_validation_try.rb +283 -0
  354. data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
  355. data/try/unit/middleware/database_logger_methods_try.rb +251 -0
  356. data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
  357. data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
  358. data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
  359. data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
  360. data/try/unit/thread_safety_monitor_try.rb +149 -0
  361. metadata +81 -14
  362. data/.github/workflows/code-quality.yml +0 -138
  363. data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
  364. data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
  365. data/docs/archive/FAMILIA_UPDATE.md +0 -226
  366. data/docs/archive/README.md +0 -64
  367. data/docs/archive/api-reference.md +0 -333
  368. data/docs/guides/core-field-system.md +0 -806
  369. data/docs/guides/implementation.md +0 -276
  370. data/docs/guides/security-model.md +0 -183
@@ -0,0 +1,600 @@
1
+ # try/features/relationships/indexing_rebuild_try.rb
2
+ #
3
+ # Comprehensive tests for index rebuild functionality
4
+ # Tests both unique_index (class-level and instance-scoped) and multi_index rebuild
5
+
6
+ require_relative '../../support/helpers/test_helpers'
7
+
8
+ Familia.enable_database_logging = true
9
+
10
+ # Test classes for class-level unique index rebuild
11
+ class ::RebuildTestUser < Familia::Horreum
12
+ feature :relationships
13
+
14
+ identifier_field :user_id
15
+ field :user_id
16
+ field :email
17
+ field :username
18
+ field :department
19
+
20
+ unique_index :email, :email_lookup
21
+ unique_index :username, :username_lookup
22
+
23
+ class_sorted_set :instances, reference: true
24
+ end
25
+
26
+ # Test classes for instance-scoped unique index rebuild
27
+ # Define Company first so Employee can reference it
28
+ class ::RebuildTestCompany < Familia::Horreum
29
+ feature :relationships
30
+
31
+ identifier_field :company_id
32
+ field :company_id
33
+ field :name
34
+
35
+ sorted_set :employees
36
+
37
+ class_sorted_set :instances, reference: true
38
+ end
39
+
40
+ class ::RebuildTestEmployee < Familia::Horreum
41
+ feature :relationships
42
+
43
+ identifier_field :emp_id
44
+ field :emp_id
45
+ field :email
46
+ field :badge_number
47
+ field :department
48
+
49
+ unique_index :badge_number, :badge_index, within: RebuildTestCompany
50
+ multi_index :department, :dept_index, within: RebuildTestCompany
51
+
52
+ participates_in RebuildTestCompany, :employees, score: :emp_id
53
+
54
+ class_sorted_set :instances, reference: true
55
+ end
56
+
57
+ # Setup: Create test data for class-level unique index
58
+ @user1 = RebuildTestUser.new(user_id: "user_1", email: "user1@test.com", username: "user1")
59
+ @user1.save
60
+ @user2 = RebuildTestUser.new(user_id: "user_2", email: "user2@test.com", username: "user2")
61
+ @user2.save
62
+ @user3 = RebuildTestUser.new(user_id: "user_3", email: "user3@test.com", username: "user3")
63
+ @user3.save
64
+
65
+ # Setup: Create test data for instance-scoped indexes
66
+ @company = RebuildTestCompany.new(company_id: "company_1", name: "Acme Corp")
67
+ @company.save
68
+
69
+ @emp1 = RebuildTestEmployee.new(emp_id: "emp_1", email: "emp1@acme.com", badge_number: "BADGE001", department: "engineering")
70
+ @emp1.save
71
+ @emp1.add_to_rebuild_test_company_employees(@company)
72
+
73
+ @emp2 = RebuildTestEmployee.new(emp_id: "emp_2", email: "emp2@acme.com", badge_number: "BADGE002", department: "sales")
74
+ @emp2.save
75
+ @emp2.add_to_rebuild_test_company_employees(@company)
76
+
77
+ @emp3 = RebuildTestEmployee.new(emp_id: "emp_3", email: "emp3@acme.com", badge_number: "BADGE003", department: "engineering")
78
+ @emp3.save
79
+ @emp3.add_to_rebuild_test_company_employees(@company)
80
+
81
+ # =============================================
82
+ # 1. Class-Level Unique Index Rebuild Tests
83
+ # =============================================
84
+
85
+ ## Class-level rebuild method exists
86
+ RebuildTestUser.respond_to?(:rebuild_email_lookup)
87
+ #=> true
88
+
89
+ ## Index starts empty before rebuild
90
+ RebuildTestUser.email_lookup.clear
91
+ RebuildTestUser.email_lookup.size
92
+ #=> 0
93
+
94
+ ## Rebuild returns count of indexed objects
95
+ count = RebuildTestUser.rebuild_email_lookup
96
+ count
97
+ #=> 3
98
+
99
+ ## Index size matches object count after rebuild
100
+ RebuildTestUser.email_lookup.size
101
+ #=> 3
102
+
103
+ ## Find by email works after rebuild
104
+ found = RebuildTestUser.find_by_email("user1@test.com")
105
+ found&.user_id
106
+ #=> "user_1"
107
+
108
+ ## All users are findable after rebuild
109
+ found = RebuildTestUser.find_by_email("user2@test.com")
110
+ found&.user_id
111
+ #=> "user_2"
112
+
113
+ ## Third user is findable after rebuild
114
+ found = RebuildTestUser.find_by_email("user3@test.com")
115
+ found&.user_id
116
+ #=> "user_3"
117
+
118
+ ## Bulk query works after rebuild
119
+ emails = ["user1@test.com", "user3@test.com"]
120
+ found_users = RebuildTestUser.find_all_by_email(emails)
121
+ found_users.map(&:user_id).sort
122
+ #=> ["user_1", "user_3"]
123
+
124
+ ## Rebuild can be called multiple times
125
+ count = RebuildTestUser.rebuild_email_lookup
126
+ count
127
+ #=> 3
128
+
129
+ ## Index remains consistent after multiple rebuilds
130
+ RebuildTestUser.email_lookup.size
131
+ #=> 3
132
+
133
+ ## Rebuild for second unique index works independently
134
+ RebuildTestUser.username_lookup.clear
135
+ count = RebuildTestUser.rebuild_username_lookup
136
+ count
137
+ #=> 3
138
+
139
+ ## Second index is populated correctly
140
+ found = RebuildTestUser.find_by_username("user2")
141
+ found&.user_id
142
+ #=> "user_2"
143
+
144
+ # =============================================
145
+ # 2. Class-Level Index Rebuild Edge Cases
146
+ # =============================================
147
+
148
+ ## Rebuild with nil field values skips those objects
149
+ @user_nil = RebuildTestUser.new(user_id: "user_nil", email: nil, username: "user_nil")
150
+ @user_nil.save
151
+ RebuildTestUser.email_lookup.clear
152
+ count = RebuildTestUser.rebuild_email_lookup
153
+ count
154
+ #=> 3
155
+
156
+ ## Nil field values are not in index
157
+ RebuildTestUser.find_by_email("")
158
+ #=> nil
159
+
160
+ ## Non-nil fields are still indexed for object with nil field
161
+ found = RebuildTestUser.find_by_username("user_nil")
162
+ found&.user_id
163
+ #=> "user_nil"
164
+
165
+ ## Rebuild with empty string field values skips those objects
166
+ @user_empty = RebuildTestUser.new(user_id: "user_empty", email: "", username: "user_empty")
167
+ @user_empty.save
168
+ RebuildTestUser.email_lookup.clear
169
+ count = RebuildTestUser.rebuild_email_lookup
170
+ count
171
+ #=> 3
172
+
173
+ ## Empty string field values are not in index
174
+ RebuildTestUser.find_by_email("")
175
+ #=> nil
176
+
177
+ ## Rebuild with whitespace-only field values skips those objects
178
+ @user_whitespace = RebuildTestUser.new(user_id: "user_ws", email: " ", username: "user_ws")
179
+ @user_whitespace.save
180
+ RebuildTestUser.email_lookup.clear
181
+ count = RebuildTestUser.rebuild_email_lookup
182
+ count
183
+ #=> 3
184
+
185
+ ## Whitespace-only field values are not in index
186
+ RebuildTestUser.find_by_email(" ")
187
+ #=> nil
188
+
189
+ ## Rebuild handles stale object IDs gracefully
190
+ RebuildTestUser.instances.add("stale_user_id")
191
+ RebuildTestUser.email_lookup.clear
192
+ count = RebuildTestUser.rebuild_email_lookup
193
+ count
194
+ #=> 3
195
+
196
+ ## Index size is correct despite stale ID
197
+ RebuildTestUser.email_lookup.size
198
+ #=> 3
199
+
200
+ ## Rebuild with no instances returns zero
201
+ RebuildTestUser.instances.clear
202
+ RebuildTestUser.email_lookup.clear
203
+ count = RebuildTestUser.rebuild_email_lookup
204
+ count
205
+ #=> 0
206
+
207
+ ## Index is empty after rebuilding with no instances
208
+ RebuildTestUser.email_lookup.size
209
+ #=> 0
210
+
211
+ # Restore instances for remaining tests
212
+ ## Check instances key
213
+ RebuildTestUser.instances.dbkey
214
+ #=> "rebuild_test_user:instances"
215
+
216
+ ## Manually populate instances for testing
217
+ result1 = RebuildTestUser.instances.add("user_1")
218
+ result2 = RebuildTestUser.instances.add("user_2")
219
+ result3 = RebuildTestUser.instances.add("user_3")
220
+ [result1, result2, result3]
221
+ #=> [true, true, true]
222
+
223
+ ## Instances restored successfully
224
+ RebuildTestUser.instances.size
225
+ #=> 3
226
+
227
+ # =============================================
228
+ # 3. Instance-Scoped Unique Index Rebuild
229
+ # =============================================
230
+
231
+ ## Instance-scoped rebuild method exists
232
+ @company.respond_to?(:rebuild_badge_index)
233
+ #=> true
234
+
235
+ ## Instance-scoped index starts empty before rebuild
236
+ @company.badge_index.clear
237
+ @company.badge_index.size
238
+ #=> 0
239
+
240
+ ## Instance-scoped rebuild returns count
241
+ count = @company.rebuild_badge_index
242
+ count
243
+ #=> 3
244
+
245
+ ## Instance-scoped index size matches after rebuild
246
+ @company.badge_index.size
247
+ #=> 3
248
+
249
+ ## Find by badge works after instance-scoped rebuild
250
+ found = @company.find_by_badge_number("BADGE001")
251
+ found&.emp_id
252
+ #=> "emp_1"
253
+
254
+ ## All employees findable after instance-scoped rebuild
255
+ found = @company.find_by_badge_number("BADGE002")
256
+ found&.emp_id
257
+ #=> "emp_2"
258
+
259
+ ## Third employee findable after instance-scoped rebuild
260
+ found = @company.find_by_badge_number("BADGE003")
261
+ found&.emp_id
262
+ #=> "emp_3"
263
+
264
+ ## Bulk query works after instance-scoped rebuild
265
+ badges = ["BADGE001", "BADGE003"]
266
+ found_emps = @company.find_all_by_badge_number(badges)
267
+ found_emps.map(&:emp_id).sort
268
+ #=> ["emp_1", "emp_3"]
269
+
270
+ ## Instance-scoped rebuild only indexes employees in that company
271
+ @company2 = RebuildTestCompany.new(company_id: "company_2", name: "TechCo")
272
+ @company2.save
273
+ @company2.badge_index.clear
274
+ count = @company2.rebuild_badge_index
275
+ count
276
+ #=> 0
277
+
278
+ ## Second company has empty index after rebuild
279
+ @company2.badge_index.size
280
+ #=> 0
281
+
282
+ ## First company still has correct index
283
+ @company.badge_index.size
284
+ #=> 3
285
+
286
+ # =============================================
287
+ # 4. Multi-Value Index Rebuild
288
+ # =============================================
289
+
290
+ ## Multi-index rebuild method exists
291
+ @company.respond_to?(:rebuild_dept_index)
292
+ #=> true
293
+
294
+ ## Multi-index starts empty before rebuild
295
+ engineering_set = @company.dept_index_for("engineering")
296
+ engineering_set.clear
297
+ sales_set = @company.dept_index_for("sales")
298
+ sales_set.clear
299
+ engineering_set.size
300
+ #=> 0
301
+
302
+ ## Multi-index rebuild returns processed count
303
+ count = @company.rebuild_dept_index
304
+ count
305
+ #=> 3
306
+
307
+ ## Manually populate departments to test structure
308
+ @emp1.add_to_rebuild_test_company_dept_index(@company)
309
+ @emp2.add_to_rebuild_test_company_dept_index(@company)
310
+ @emp3.add_to_rebuild_test_company_dept_index(@company)
311
+ engineering_count = @company.dept_index_for("engineering").size
312
+ engineering_count
313
+ #=> 2
314
+
315
+ ## Sales department has correct count
316
+ sales_count = @company.dept_index_for("sales").size
317
+ sales_count
318
+ #=> 1
319
+
320
+ ## Find all by department works for engineering
321
+ eng_emps = @company.find_all_by_department("engineering")
322
+ eng_emps.map(&:emp_id).sort
323
+ #=> ["emp_1", "emp_3"]
324
+
325
+ ## Find all by department works for sales
326
+ sales_emps = @company.find_all_by_department("sales")
327
+ sales_emps.map(&:emp_id)
328
+ #=> ["emp_2"]
329
+
330
+ ## Sample from department returns employees
331
+ sample = @company.sample_from_department("engineering", 1)
332
+ ["emp_1", "emp_3"].include?(sample.first&.emp_id)
333
+ #=> true
334
+
335
+ # =============================================
336
+ # 5. Rebuild Progress Callbacks
337
+ # =============================================
338
+
339
+ ## Instances collection has users before rebuild
340
+ RebuildTestUser.instances.size
341
+ #=> 3
342
+
343
+ ## Rebuild accepts batch_size parameter
344
+ RebuildTestUser.email_lookup.clear
345
+ count = RebuildTestUser.rebuild_email_lookup(batch_size: 1)
346
+ count
347
+ #=> 3
348
+
349
+ ## Index works correctly with small batch size
350
+ found = RebuildTestUser.find_by_email("user1@test.com")
351
+ found&.user_id
352
+ #=> "user_1"
353
+
354
+ ## Rebuild accepts large batch_size parameter
355
+ RebuildTestUser.email_lookup.clear
356
+ count = RebuildTestUser.rebuild_email_lookup(batch_size: 1000)
357
+ count
358
+ #=> 3
359
+
360
+ ## Index works correctly with large batch size
361
+ found = RebuildTestUser.find_by_email("user2@test.com")
362
+ found&.user_id
363
+ #=> "user_2"
364
+
365
+ ## Rebuild accepts progress callback block
366
+ @progress_updates = []
367
+ RebuildTestUser.email_lookup.clear
368
+ count = RebuildTestUser.rebuild_email_lookup { |progress| @progress_updates << progress }
369
+ count
370
+ #=> 3
371
+
372
+ ## Progress callback receives updates
373
+ @progress_updates.size > 0
374
+ #=> true
375
+
376
+ ## Progress callback includes completed count
377
+ @progress_updates.last[:completed]
378
+ #=> 3
379
+
380
+ ## Progress callback includes total count
381
+ @progress_updates.last[:total]
382
+ #=> 3
383
+
384
+ ## Progress callback includes rate information
385
+ @progress_updates.last.key?(:rate)
386
+ #=> true
387
+
388
+ ## Progress updates are incremental
389
+ completed_values = @progress_updates.map { |p| p[:completed] }
390
+ completed_values.sort == completed_values
391
+ #=> true
392
+
393
+ # =============================================
394
+ # 6. Rebuild with Modified Data
395
+ # =============================================
396
+
397
+ ## Rebuild reflects updated field values
398
+ @user1.email = "updated1@test.com"
399
+ @user1.save
400
+ RebuildTestUser.email_lookup.clear
401
+ RebuildTestUser.rebuild_email_lookup
402
+ found = RebuildTestUser.find_by_email("updated1@test.com")
403
+ found&.user_id
404
+ #=> "user_1"
405
+
406
+ ## Old email is not in index after rebuild
407
+ RebuildTestUser.find_by_email("user1@test.com")
408
+ #=> nil
409
+
410
+ ## Rebuild after deleting object removes from index
411
+ @user3.destroy if @user3.respond_to?(:destroy)
412
+ RebuildTestUser.instances.remove(@user3.identifier)
413
+ RebuildTestUser.email_lookup.clear
414
+ count = RebuildTestUser.rebuild_email_lookup
415
+ count
416
+ #=> 2
417
+
418
+ ## Deleted object not findable after rebuild
419
+ RebuildTestUser.find_by_email("user3@test.com")
420
+ #=> nil
421
+
422
+ ## Remaining objects still findable
423
+ found = RebuildTestUser.find_by_email("user2@test.com")
424
+ found&.user_id
425
+ #=> "user_2"
426
+
427
+ # =============================================
428
+ # 7. Instance-Scoped Rebuild with Batch Sizes
429
+ # =============================================
430
+
431
+ ## Instance-scoped rebuild accepts batch_size
432
+ @company.badge_index.clear
433
+ count = @company.rebuild_badge_index(batch_size: 1)
434
+ count
435
+ #=> 3
436
+
437
+ ## Instance-scoped index works with small batch
438
+ found = @company.find_by_badge_number("BADGE001")
439
+ found&.emp_id
440
+ #=> "emp_1"
441
+
442
+ ## Instance-scoped rebuild with large batch_size
443
+ @company.badge_index.clear
444
+ count = @company.rebuild_badge_index(batch_size: 100)
445
+ count
446
+ #=> 3
447
+
448
+ ## Instance-scoped index works with large batch
449
+ found = @company.find_by_badge_number("BADGE002")
450
+ found&.emp_id
451
+ #=> "emp_2"
452
+
453
+ # =============================================
454
+ # 8. Concurrent Rebuilds (Thread Safety)
455
+ # =============================================
456
+
457
+ ## Multiple rebuilds don't corrupt index
458
+ RebuildTestUser.email_lookup.clear
459
+ counts = 3.times.map { RebuildTestUser.rebuild_email_lookup }
460
+ counts
461
+ #=> [2, 2, 2]
462
+
463
+ ## Index remains consistent after concurrent rebuilds
464
+ RebuildTestUser.email_lookup.size
465
+ #=> 2
466
+
467
+ ## All expected objects findable after concurrent rebuilds
468
+ found1 = RebuildTestUser.find_by_email("updated1@test.com")
469
+ found2 = RebuildTestUser.find_by_email("user2@test.com")
470
+ [found1&.user_id, found2&.user_id].sort
471
+ #=> ["user_1", "user_2"]
472
+
473
+ # =============================================
474
+ # 9. Orphaned Data Cleanup (SCAN-based)
475
+ # =============================================
476
+
477
+ ## Clear all dept indexes from earlier tests
478
+ ["engineering", "sales", "marketing", "finance"].each do |dept|
479
+ @company.dept_index_for(dept).clear rescue nil
480
+ end
481
+
482
+ ## Manually create orphaned stale data in finance dept
483
+ @company.dept_index_for("finance").add("emp_1")
484
+ @company.dept_index_for("finance").add("emp_2")
485
+ @company.dept_index_for("finance").size
486
+ #=> 2
487
+
488
+ ## Also add some marketing entries (will be orphaned)
489
+ @company.dept_index_for("marketing").add("emp_1")
490
+ @company.dept_index_for("marketing").add("emp_3")
491
+ @company.dept_index_for("marketing").size
492
+ #=> 2
493
+
494
+ ## Rebuild via participation collection processes 3 employees
495
+ @company.rebuild_dept_index
496
+ #=> 3
497
+
498
+ ## After rebuild: Current engineering dept correctly has both emp1 and emp3
499
+ @company.dept_index_for("engineering").size
500
+ #=> 2
501
+
502
+ ## After rebuild: Current sales dept correctly has emp2
503
+ @company.dept_index_for("sales").size
504
+ #=> 1
505
+
506
+ ## SCAN cleanup removes orphaned finance keys
507
+ @company.dept_index_for("finance").size
508
+ #=> 0
509
+
510
+ ## SCAN cleanup removes orphaned marketing keys
511
+ @company.dept_index_for("marketing").size
512
+ #=> 0
513
+
514
+ # =============================================
515
+ # 10. Scope Filtering (SCAN Strategy)
516
+ # =============================================
517
+
518
+ ## Company 1 has 3 employees
519
+ @company.employees.size
520
+ #=> 3
521
+
522
+ ## Clear company 1 badge index to force SCAN strategy
523
+ @company.badge_index.clear
524
+ @company.badge_index.size
525
+ #=> 0
526
+
527
+ ## Company has 3 employees participating
528
+ @company.employees.size
529
+ #=> 3
530
+
531
+ ## Rebuild company 1 index via SCAN - should filter to only company 1's employees
532
+ count = @company.rebuild_badge_index
533
+ count
534
+ #=> 3
535
+
536
+ ## Company 1 index only has its own employees (scope filtering verified)
537
+ @company.badge_index.size
538
+ #=> 3
539
+
540
+ ## All expected employees found via index
541
+ found1 = @company.find_by_badge_number("BADGE001")
542
+ found2 = @company.find_by_badge_number("BADGE002")
543
+ found3 = @company.find_by_badge_number("BADGE003")
544
+ [found1&.emp_id, found2&.emp_id, found3&.emp_id]
545
+ #=> ["emp_1", "emp_2", "emp_3"]
546
+
547
+ # =============================================
548
+ # 11. Cardinality Guard Protection
549
+ # =============================================
550
+
551
+ ## Cardinality guard prevents multi-index corruption
552
+ # Note: This would require manually calling the private method with wrong cardinality
553
+ # The architecture prevents this via factory pattern, but guard provides explicit protection
554
+ begin
555
+ # Simulate calling rebuild_via_participation with multi-index cardinality
556
+ Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_participation(
557
+ @company,
558
+ RebuildTestEmployee,
559
+ :department,
560
+ :add_to_rebuild_test_company_dept_index,
561
+ @company.employees,
562
+ :multi, # Wrong cardinality!
563
+ batch_size: 100
564
+ )
565
+ "should have raised"
566
+ rescue ArgumentError => e
567
+ e.message.include?("only supports unique indexes")
568
+ end
569
+ #=> true
570
+
571
+ ## Guard accepts correct cardinality (:unique)
572
+ begin
573
+ index_config = RebuildTestEmployee.indexing_relationships.find { |r| r.index_name == :badge_index }
574
+ Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_participation(
575
+ @company,
576
+ RebuildTestEmployee,
577
+ :badge_number,
578
+ :add_to_rebuild_test_company_badge_index,
579
+ @company.employees,
580
+ :unique, # Correct cardinality
581
+ batch_size: 100
582
+ )
583
+ "no error"
584
+ rescue ArgumentError
585
+ "should not raise"
586
+ end
587
+ #=> "no error"
588
+
589
+ # Teardown
590
+ RebuildTestUser.email_lookup.clear
591
+ RebuildTestUser.username_lookup.clear
592
+ RebuildTestUser.instances.clear
593
+ @company.badge_index.clear
594
+ @company.employees.clear
595
+ # Clear all department index keys
596
+ ["engineering", "sales", "marketing", "finance"].each do |dept|
597
+ @company.dept_index_for(dept).clear rescue nil
598
+ end
599
+ RebuildTestCompany.instances.clear
600
+ RebuildTestEmployee.instances.clear
@@ -1,5 +1,7 @@
1
1
  # try/features/relationships/indexing_try.rb
2
2
  #
3
+ # frozen_string_literal: true
4
+
3
5
  # Comprehensive tests for Familia indexing relationships functionality
4
6
  # Tests both multi_index (parent-context) and unique_index (class-level) indexing
5
7
  #
@@ -62,7 +64,7 @@ end
62
64
  @user3.save
63
65
 
64
66
  @company_id = "comp_#{rand(10000000)}"
65
- @company = TestCompany.create(company_id: @company_id, name: 'Acme Corp')
67
+ @company = TestCompany.create!(company_id: @company_id, name: 'Acme Corp')
66
68
  @emp1 = TestEmployee.new(emp_id: 'emp_001', email: 'alice@acme.com', department: 'engineering', manager_id: 'mgr_001', badge_number: 'BADGE001')
67
69
  @emp1.save
68
70
  @emp2 = TestEmployee.new(emp_id: 'emp_002', email: 'bob@acme.com', department: 'sales', manager_id: 'mgr_002', badge_number: 'BADGE002')
@@ -86,7 +88,7 @@ sample = @company.sample_from_department(@emp2.department)
86
88
 
87
89
  ## First indexing relationship has correct configuration
88
90
  config = @user1.class.indexing_relationships.first
89
- [config.field, config.index_name, config.target_class == TestUser, config.query]
91
+ [config.field, config.index_name, config.scope_class == TestUser, config.query]
90
92
  #=> [:email, :email_lookup, true, true]
91
93
 
92
94
  ## Second indexing relationship has query disabled
@@ -189,7 +191,7 @@ TestUser.respond_to?(:find_by_username)
189
191
 
190
192
  ## Instance-scoped unique index has correct configuration
191
193
  config = @emp1.class.indexing_relationships.find { |r| r.field == :badge_number }
192
- [config.index_name, config.target_class, config.cardinality]
194
+ [config.index_name, config.scope_class, config.cardinality]
193
195
  #=> [:badge_index, TestCompany, :unique]
194
196
 
195
197
  ## Target class gets finder method for unique index
@@ -251,6 +253,16 @@ found_emps = @company.find_all_by_badge_number('BADGE002')
251
253
  found_emps.map(&:emp_id)
252
254
  #=> ["emp_002"]
253
255
 
256
+ ## Instance-scoped bulk query filters nil inputs
257
+ badges_with_nil = [nil, 'BADGE001', nil]
258
+ found_emps = @company.find_all_by_badge_number(badges_with_nil)
259
+ found_emps.map(&:emp_id)
260
+ #=> ["emp_001"]
261
+
262
+ ## Instance-scoped bulk query with only nil returns empty
263
+ @company.find_all_by_badge_number([nil, nil]).length
264
+ #=> 0
265
+
254
266
  ## Update badge index entry
255
267
  old_badge = @emp1.badge_number
256
268
  @emp1.badge_number = 'BADGE001_NEW'
@@ -277,7 +289,7 @@ old_badge = @emp1.badge_number
277
289
 
278
290
  ## Context-scoped multi_index relationship has correct configuration
279
291
  config = @emp1.class.indexing_relationships.find { |r| r.field == :department }
280
- [config.field, config.index_name, config.target_class]
292
+ [config.field, config.index_name, config.scope_class]
281
293
  #=> [:department, :dept_index, TestCompany]
282
294
 
283
295
  ## Context-scoped methods are generated with collision-free naming
@@ -420,6 +432,20 @@ found = TestUser.find_all_by_email(emails)
420
432
  found.map(&:user_id)
421
433
  #=> ["user_002"]
422
434
 
435
+ ## Bulk query filters nil inputs before querying
436
+ emails_with_nil = [nil, 'bob@example.com', nil]
437
+ found = TestUser.find_all_by_email(emails_with_nil)
438
+ found.map(&:user_id)
439
+ #=> ["user_002"]
440
+
441
+ ## Bulk query with only nil inputs returns empty array
442
+ TestUser.find_all_by_email([nil, nil]).length
443
+ #=> 0
444
+
445
+ ## Bulk query with nil as single value returns empty array
446
+ TestUser.find_all_by_email(nil).length
447
+ #=> 0
448
+
423
449
  ## Adding to index with nil field value does nothing
424
450
  @user_nil = TestUser.new(user_id: 'user_nil', email: nil)
425
451
  @user_nil.add_to_class_email_lookup