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,380 @@
1
+ # try/unit/horreum/unique_index_edge_cases_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # Testing edge cases for unique index validation
6
+ # lib/familia/features/relationships/indexing/unique_index_generators.rb
7
+ # lib/familia/horreum/persistence.rb
8
+
9
+ require_relative '../../../lib/familia'
10
+
11
+ Familia.debug = false
12
+
13
+ # ========================================
14
+ # Setup: Define test models
15
+ # ========================================
16
+
17
+ class EdgeCaseCompany < Familia::Horreum
18
+ feature :relationships
19
+
20
+ identifier_field :company_id
21
+ field :company_id
22
+ field :company_name
23
+
24
+ # init receives no arguments - fields already set from new()
25
+ # Use ||= to apply defaults if needed
26
+ def init
27
+ # No defaults needed for this class
28
+ # Could add: @company_name ||= 'Unknown Company'
29
+ end
30
+ end
31
+
32
+ class EdgeCaseEmployee < Familia::Horreum
33
+ feature :relationships
34
+
35
+ identifier_field :emp_id
36
+ field :emp_id
37
+ field :email
38
+ field :badge_number
39
+ field :department
40
+ field :status
41
+
42
+ # Class-level unique index for email (auto-populates on save)
43
+ unique_index :email, :email_index
44
+
45
+ # Instance-scoped unique index for badge_number within Company
46
+ unique_index :badge_number, :badge_index, within: EdgeCaseCompany
47
+
48
+ # Multi-index for department (1:many) within Company
49
+ multi_index :department, :dept_index, within: EdgeCaseCompany
50
+
51
+ # init receives no arguments - fields already set from new()
52
+ # Use ||= to apply defaults if needed
53
+ def init
54
+ @status ||= 'active' # Apply default status if not provided
55
+ end
56
+ end
57
+
58
+ class EdgeCaseProduct < Familia::Horreum
59
+ feature :relationships
60
+
61
+ identifier_field :product_id
62
+ field :product_id
63
+ field :sku
64
+
65
+ # Allow empty strings in unique index
66
+ unique_index :sku, :sku_index
67
+
68
+ # init receives no arguments - fields already set from new()
69
+ # Use ||= to apply defaults if needed
70
+ def init
71
+ # No defaults needed for this class
72
+ end
73
+ end
74
+
75
+ # Clear all indexes before starting
76
+ EdgeCaseEmployee.email_index.clear
77
+ EdgeCaseProduct.sku_index.clear
78
+
79
+ # ========================================
80
+ # Test 1: Duplicate instance-scoped index values
81
+ # ========================================
82
+
83
+ ## Setup companies and employees
84
+ @company1 = EdgeCaseCompany.new(company_id: 'c1', company_name: 'Acme Corp')
85
+ @company2 = EdgeCaseCompany.new(company_id: 'c2', company_name: 'Tech Inc')
86
+ @company1.save
87
+ @company2.save
88
+ #=> true
89
+
90
+ ## Create employee with badge_number in company1
91
+ @emp1 = EdgeCaseEmployee.new(emp_id: 'e1', email: 'john@test.com', badge_number: 'B12345')
92
+ @emp1.save # This auto-populates email index
93
+ @emp1.add_to_edge_case_company_badge_index(@company1) # Manual for instance-scoped
94
+ @company1.find_by_badge_number('B12345')&.emp_id
95
+ #=> 'e1'
96
+
97
+ ## Create another employee with same badge_number - guard should detect duplicate
98
+ @emp2 = EdgeCaseEmployee.new(emp_id: 'e2', email: 'jane@test.com', badge_number: 'B12345')
99
+ @emp2.save # Different email is OK
100
+ begin
101
+ @emp2.guard_unique_edge_case_company_badge_index!(@company1)
102
+ false
103
+ rescue Familia::RecordExistsError
104
+ true
105
+ end
106
+ #=> true
107
+
108
+ ## Same badge_number should work in different company (different scope)
109
+ @emp2.add_to_edge_case_company_badge_index(@company2)
110
+ @company2.find_by_badge_number('B12345')&.emp_id
111
+ #=> 'e2'
112
+
113
+ ## Verify badge exists in both companies with different employees
114
+ [@company1.find_by_badge_number('B12345')&.emp_id, @company2.find_by_badge_number('B12345')&.emp_id]
115
+ #=> ['e1', 'e2']
116
+
117
+ ## Cleanup for next test
118
+ EdgeCaseEmployee.email_index.clear
119
+ @company1.badge_index.clear
120
+ @company2.badge_index.clear
121
+ #=> 1
122
+
123
+ # ========================================
124
+ # Test 2: Field updates with auto-index cleanup
125
+ # ========================================
126
+
127
+ ## Create employee with email (save auto-populates index)
128
+ @emp3 = EdgeCaseEmployee.new(emp_id: 'e3', email: 'original@test.com')
129
+ @emp3.save
130
+ EdgeCaseEmployee.find_by_email('original@test.com')&.emp_id
131
+ #=> 'e3'
132
+
133
+ ## Update email - must manually update index (no automatic cleanup on field change)
134
+ old_email = @emp3.email
135
+ @emp3.email = 'updated@test.com'
136
+ @emp3.update_in_class_email_index(old_email) # Manual update required
137
+ EdgeCaseEmployee.find_by_email('original@test.com')
138
+ #=> nil
139
+
140
+ ## New email should resolve to employee
141
+ EdgeCaseEmployee.find_by_email('updated@test.com')&.emp_id
142
+ #=> 'e3'
143
+
144
+ ## Update instance-scoped index
145
+ @emp3.badge_number = 'B99999'
146
+ @emp3.add_to_edge_case_company_badge_index(@company1)
147
+ @company1.find_by_badge_number('B99999')&.emp_id
148
+ #=> 'e3'
149
+
150
+ ## Change badge and update index
151
+ old_badge = @emp3.badge_number
152
+ @emp3.badge_number = 'B11111'
153
+ @emp3.update_in_edge_case_company_badge_index(@company1, old_badge)
154
+ @company1.find_by_badge_number('B99999')
155
+ #=> nil
156
+
157
+ ## New badge should work
158
+ @company1.find_by_badge_number('B11111')&.emp_id
159
+ #=> 'e3'
160
+
161
+ ## Cleanup
162
+ EdgeCaseEmployee.email_index.clear
163
+ @company1.badge_index.clear
164
+ #=> 1
165
+
166
+ # ========================================
167
+ # Test 3: Save within explicit transactions (validation bypass)
168
+ # ========================================
169
+
170
+ ## Create first employee successfully
171
+ @emp4 = EdgeCaseEmployee.new(emp_id: 'e4', email: 'txn@test.com')
172
+ @emp4.save
173
+ EdgeCaseEmployee.find_by_email('txn@test.com')&.emp_id
174
+ #=> 'e4'
175
+
176
+ ## Save cannot be called inside transaction - it raises OperationModeError
177
+ @emp5 = EdgeCaseEmployee.new(emp_id: 'e5', email: 'txn@test.com')
178
+ error_raised = false
179
+ begin
180
+ EdgeCaseEmployee.transaction do |tx|
181
+ @emp5.save # This will raise
182
+ end
183
+ rescue Familia::OperationModeError => e
184
+ error_raised = e.message.include?("Cannot call save within a transaction")
185
+ end
186
+ error_raised
187
+ #=> true
188
+
189
+ ## However, we can bypass validation by manually adding to index inside transaction
190
+ result = EdgeCaseEmployee.transaction do |tx|
191
+ # Manually add without validation (dangerous!)
192
+ EdgeCaseEmployee.email_index['txn_bypass@test.com'] = 'e5'
193
+ 'manual_bypass'
194
+ end
195
+ result.successful?
196
+ #=> true
197
+
198
+ ## After transaction, the manual entry exists (no validation occurred)
199
+ EdgeCaseEmployee.email_index['txn_bypass@test.com']
200
+ #=> 'e5'
201
+
202
+ ## After transaction, the manual entry exists (no validation occurred)
203
+ EdgeCaseEmployee.email_index['txn_bypass@test.com']
204
+ #=> 'e5'
205
+
206
+ ## Cleanup
207
+ EdgeCaseEmployee.email_index.clear
208
+ #=> 1
209
+
210
+ # ========================================
211
+ # Test 4: Multiple empty string values in same index
212
+ # ========================================
213
+
214
+ ## Create product with empty SKU
215
+ @prod1 = EdgeCaseProduct.new(product_id: 'p1', sku: '')
216
+ @prod1.save
217
+ EdgeCaseProduct.find_by_sku('')&.product_id
218
+ #=> 'p1'
219
+
220
+ ## Try to create another product with empty SKU - should fail
221
+ @prod2 = EdgeCaseProduct.new(product_id: 'p2', sku: '')
222
+ begin
223
+ @prod2.save
224
+ false
225
+ rescue Familia::RecordExistsError => e
226
+ e.message.include?('sku=')
227
+ end
228
+ #=> true
229
+
230
+ ## nil values should be skipped (not indexed)
231
+ @prod3 = EdgeCaseProduct.new(product_id: 'p3', sku: nil)
232
+ @prod3.save # Should succeed - nil values aren't indexed
233
+ @prod3.identifier
234
+ #=> 'p3'
235
+
236
+ ## Verify nil doesn't exist in index (empty string != nil)
237
+ EdgeCaseProduct.sku_index[''] # Empty string key
238
+ #=> 'p1'
239
+
240
+ ## nil is not indexed
241
+ EdgeCaseProduct.sku_index.keys.include?(nil)
242
+ #=> false
243
+
244
+ ## Cleanup
245
+ EdgeCaseProduct.sku_index.clear
246
+ #=> 1
247
+
248
+ # ========================================
249
+ # Test 5: Concurrent saves with same unique value
250
+ # ========================================
251
+
252
+ ## Setup fresh index
253
+ EdgeCaseEmployee.email_index.clear
254
+ #=> 0
255
+
256
+ ## Create two employees with same email (simulating race condition)
257
+ @emp6 = EdgeCaseEmployee.new(emp_id: 'e6', email: 'race@test.com')
258
+ @emp7 = EdgeCaseEmployee.new(emp_id: 'e7', email: 'race@test.com')
259
+ @emp7.emp_id
260
+ #=> 'e7'
261
+
262
+ ## First save succeeds
263
+ @emp6.save
264
+ EdgeCaseEmployee.find_by_email('race@test.com')&.emp_id
265
+ #=> 'e6'
266
+
267
+ ## Second save fails due to validation
268
+ begin
269
+ @emp7.save
270
+ false
271
+ rescue Familia::RecordExistsError
272
+ true
273
+ end
274
+ #=> true
275
+
276
+ ## Simulate race condition: both check validation, then both write
277
+ EdgeCaseEmployee.email_index.clear
278
+ @emp8 = EdgeCaseEmployee.new(emp_id: 'e8', email: 'race2@test.com')
279
+ @emp9 = EdgeCaseEmployee.new(emp_id: 'e9', email: 'race2@test.com')
280
+ [@emp8.emp_id, @emp9.emp_id]
281
+ #=> ['e8', 'e9']
282
+
283
+ ## Both pass validation check (index is empty)
284
+ begin
285
+ @emp8.guard_unique_email_index!
286
+ @emp9.guard_unique_email_index!
287
+ true
288
+ rescue
289
+ false
290
+ end
291
+ #=> true
292
+
293
+ ## Both write to index (last write wins in Redis)
294
+ @emp8.add_to_class_email_index
295
+ @emp9.add_to_class_email_index
296
+ # Verify the index contains the identifier (orphaned entry - wastes space but harmless)
297
+ EdgeCaseEmployee.email_index['race2@test.com']
298
+ #=> 'e9'
299
+
300
+ ## find_by returns nil for orphaned index entries (object never saved)
301
+ # This is correct behavior - orphaned entries degrade gracefully to nil
302
+ EdgeCaseEmployee.find_by_email('race2@test.com')
303
+ #=> nil
304
+
305
+ ## To properly handle concurrent saves, check existence inside transaction
306
+ # Note: Can't read inside MULTI block, so need WATCH/MULTI pattern
307
+ result = nil
308
+ EdgeCaseEmployee.dbclient.watch('edge_case_employee:email_index') do
309
+ if EdgeCaseEmployee.email_index['race3@test.com'].nil?
310
+ EdgeCaseEmployee.transaction do |tx|
311
+ EdgeCaseEmployee.email_index['race3@test.com'] = 'e10'
312
+ result = 'success'
313
+ end
314
+ else
315
+ result = 'duplicate'
316
+ end
317
+ end
318
+ result
319
+ #=> 'success'
320
+
321
+ ## Cleanup
322
+ EdgeCaseEmployee.email_index.clear
323
+ #=> 1
324
+
325
+ # ========================================
326
+ # Edge Case: Update with validation in compound operation
327
+ # ========================================
328
+
329
+ ## Test compound index updates in transaction
330
+ @company3 = EdgeCaseCompany.new(company_id: 'c3', company_name: 'Test Corp')
331
+ @company3.save
332
+ #=> true
333
+
334
+ ## Create employee
335
+ @emp11 = EdgeCaseEmployee.new(emp_id: 'e11', email: 'compound@test.com', badge_number: 'B555')
336
+ @emp11.save
337
+ @emp11.add_to_edge_case_company_badge_index(@company3)
338
+ @emp11.emp_id
339
+ #=> 'e11'
340
+
341
+ ## Update multiple indexed fields atomically
342
+ @emp11 = EdgeCaseEmployee.new(emp_id: 'e11', email: 'compound@test.com', badge_number: 'B555')
343
+ @emp11.save
344
+ @emp11.add_to_edge_case_company_badge_index(@company3)
345
+
346
+ old_email = @emp11.email
347
+ old_badge = @emp11.badge_number
348
+ @emp11.email = 'compound_new@test.com'
349
+ @emp11.badge_number = 'B666'
350
+
351
+ # Update both indexes in single transaction
352
+ result = EdgeCaseEmployee.transaction do |tx|
353
+ @emp11.update_in_class_email_index(old_email)
354
+ @emp11.update_in_edge_case_company_badge_index(@company3, old_badge)
355
+ 'updated'
356
+ end
357
+ result.successful?
358
+ #=> true
359
+
360
+ ## Verify updates succeeded
361
+ [EdgeCaseEmployee.find_by_email('compound_new@test.com')&.emp_id, @company3.find_by_badge_number('B666')&.emp_id]
362
+ #=> ['e11', 'e11']
363
+
364
+ ## Old values should be gone
365
+ [EdgeCaseEmployee.find_by_email('compound@test.com'), @company3.find_by_badge_number('B555')]
366
+ #=> [nil, nil]
367
+
368
+
369
+ # Final cleanup
370
+ EdgeCaseEmployee.email_index.clear
371
+ if @company3&.respond_to?(:badge_index) && @company3.badge_index.respond_to?(:clear)
372
+ @company3.badge_index.clear
373
+ end
374
+
375
+ # Clean up test objects - check if they still exist before destroying
376
+ [@company1, @company2, @company3].compact.each do |obj|
377
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
378
+ end
379
+
380
+ puts "All edge case tests completed"
@@ -0,0 +1,283 @@
1
+ # try/unit/horreum/unique_index_guard_validation_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ #
6
+ # Unique index guard validation tests
7
+ # Tests the guard_unique_*! methods for both class-level and instance-scoped indexes
8
+ #
9
+
10
+ require_relative '../../support/helpers/test_helpers'
11
+
12
+ # Test classes for unique index guard validation
13
+ class ::GuardUser < Familia::Horreum
14
+ feature :relationships
15
+
16
+ identifier_field :user_id
17
+ field :user_id
18
+ field :email
19
+ field :username
20
+
21
+ # Class-level unique indexes (auto-validated on save)
22
+ unique_index :email, :email_index
23
+ unique_index :username, :username_index
24
+ end
25
+
26
+ class ::GuardCompany < Familia::Horreum
27
+ feature :relationships
28
+
29
+ identifier_field :company_id
30
+ field :company_id
31
+ field :name
32
+ end
33
+
34
+ class ::GuardEmployee < Familia::Horreum
35
+ feature :relationships
36
+
37
+ identifier_field :emp_id
38
+ field :emp_id
39
+ field :badge_number
40
+ field :email
41
+
42
+ # Instance-scoped unique index (manually validated)
43
+ unique_index :badge_number, :badge_index, within: GuardCompany
44
+
45
+ # Class-level unique index (auto-validated)
46
+ unique_index :email, :email_index
47
+ end
48
+
49
+ # Setup
50
+ @user_id1 = "user_#{rand(1000000)}"
51
+ @user_id2 = "user_#{rand(1000000)}"
52
+ @company_id = "comp_#{rand(1000000)}"
53
+ @emp_id1 = "emp_#{rand(1000000)}"
54
+ @emp_id2 = "emp_#{rand(1000000)}"
55
+
56
+ @company = GuardCompany.new(company_id: @company_id, name: 'Test Corp')
57
+ @company.save
58
+
59
+ # =============================================
60
+ # 1. Class-Level Unique Index Guard Methods
61
+ # =============================================
62
+
63
+ ## Guard method exists for class-level unique index
64
+ @user1 = GuardUser.new(user_id: @user_id1, email: 'test@example.com', username: 'testuser')
65
+ @user1.respond_to?(:guard_unique_email_index!)
66
+ #=> true
67
+
68
+ ## Guard passes when no conflict exists
69
+ @user1.guard_unique_email_index!
70
+ #=> nil
71
+
72
+ ## Save succeeds after guard passes
73
+ @user1.save
74
+ #=> true
75
+
76
+ ## Guard fails when duplicate email exists
77
+ @user2 = GuardUser.new(user_id: @user_id2, email: 'test@example.com', username: 'different')
78
+ begin
79
+ @user2.guard_unique_email_index!
80
+ false
81
+ rescue Familia::RecordExistsError => e
82
+ e.message.include?('GuardUser exists email=test@example.com')
83
+ end
84
+ #=> true
85
+
86
+ ## Save automatically calls guard and raises error
87
+ begin
88
+ @user2.save
89
+ false
90
+ rescue Familia::RecordExistsError
91
+ true
92
+ end
93
+ #=> true
94
+
95
+ ## Guard allows same identifier (updating existing record)
96
+ @user1_copy = GuardUser.new(user_id: @user_id1, email: 'test@example.com', username: 'testuser')
97
+ @user1_copy.guard_unique_email_index!
98
+ #=> nil
99
+
100
+ ## Guard handles nil field values gracefully
101
+ @user_nil = GuardUser.new(user_id: "user_nil_#{rand(1000000)}", email: nil, username: 'niluser')
102
+ @user_nil.guard_unique_email_index!
103
+ #=> nil
104
+
105
+ ## Guard handles empty string field values
106
+ @user_empty1 = GuardUser.new(user_id: "user_empty1_#{rand(1000000)}", email: '', username: 'empty1')
107
+ @user_empty1.save
108
+ @user_empty2 = GuardUser.new(user_id: "user_empty2_#{rand(1000000)}", email: '', username: 'empty2')
109
+ begin
110
+ @user_empty2.save
111
+ false
112
+ rescue Familia::RecordExistsError => e
113
+ e.message.include?('GuardUser exists email=')
114
+ end
115
+ #=> true
116
+
117
+ # =============================================
118
+ # 2. Instance-Scoped Unique Index Guard Methods
119
+ # =============================================
120
+
121
+ ## Guard method exists for instance-scoped unique index
122
+ @emp1 = GuardEmployee.new(emp_id: @emp_id1, badge_number: 'BADGE123', email: 'emp1@example.com')
123
+ @emp1.respond_to?(:guard_unique_guard_company_badge_index!)
124
+ #=> true
125
+
126
+ ## Guard method requires parent instance parameter
127
+ @emp1.method(:guard_unique_guard_company_badge_index!).arity
128
+ #=> 1
129
+
130
+ ## Guard passes when no conflict exists in parent's index
131
+ @emp1.guard_unique_guard_company_badge_index!(@company)
132
+ #=> nil
133
+
134
+ ## Can add to index after guard passes
135
+ @emp1.add_to_guard_company_badge_index(@company)
136
+ @company.badge_index.has_key?('BADGE123')
137
+ #=> true
138
+
139
+ ## Guard fails when duplicate badge exists in same company
140
+ @emp2 = GuardEmployee.new(emp_id: @emp_id2, badge_number: 'BADGE123', email: 'emp2@example.com')
141
+ begin
142
+ @emp2.guard_unique_guard_company_badge_index!(@company)
143
+ false
144
+ rescue Familia::RecordExistsError => e
145
+ e.message.include?('GuardEmployee exists in GuardCompany with badge_number=BADGE123')
146
+ end
147
+ #=> true
148
+
149
+ ## Guard allows same employee to re-add (idempotent)
150
+ @emp1.guard_unique_guard_company_badge_index!(@company)
151
+ #=> nil
152
+
153
+ ## Guard passes for different company (different scope)
154
+ @company2_id = "comp_#{rand(1000000)}"
155
+ @company2 = GuardCompany.new(company_id: @company2_id, name: 'Other Corp')
156
+ @company2.save
157
+ @emp2.guard_unique_guard_company_badge_index!(@company2)
158
+ #=> nil
159
+
160
+ ## Can add same badge to different company
161
+ @emp2.add_to_guard_company_badge_index(@company2)
162
+ @company2.badge_index.has_key?('BADGE123')
163
+ #=> true
164
+
165
+ ## Guard handles nil parent instance gracefully
166
+ @emp3 = GuardEmployee.new(emp_id: "emp_#{rand(1000000)}", badge_number: 'BADGE456', email: 'emp3@example.com')
167
+ @emp3.guard_unique_guard_company_badge_index!(nil)
168
+ #=> nil
169
+
170
+ ## Guard handles nil badge_number gracefully
171
+ @emp_nil = GuardEmployee.new(emp_id: "emp_nil_#{rand(1000000)}", badge_number: nil, email: 'empnil@example.com')
172
+ @emp_nil.guard_unique_guard_company_badge_index!(@company)
173
+ #=> nil
174
+
175
+ # =============================================
176
+ # 3. Mixed Class and Instance-Scoped Validation
177
+ # =============================================
178
+
179
+ ## Employee has both class-level and instance-scoped indexes
180
+ @emp4_id = "emp_#{rand(1000000)}"
181
+ @emp4 = GuardEmployee.new(emp_id: @emp4_id, badge_number: 'BADGE789', email: 'unique@example.com')
182
+ @emp4.class
183
+ #=> GuardEmployee
184
+
185
+ ## Class-level email index auto-validates on save
186
+ @emp4.save
187
+ GuardEmployee.find_by_email('unique@example.com')&.emp_id
188
+ #=> @emp4_id
189
+
190
+ ## Instance-scoped badge index must be manually validated and added
191
+ @emp4.guard_unique_guard_company_badge_index!(@company)
192
+ @emp4.add_to_guard_company_badge_index(@company)
193
+ @company.badge_index.has_key?('BADGE789')
194
+ #=> true
195
+
196
+ ## Duplicate class-level index caught by save
197
+ @emp5_id = "emp_#{rand(1000000)}"
198
+ @emp5 = GuardEmployee.new(emp_id: @emp5_id, badge_number: 'BADGE999', email: 'unique@example.com')
199
+ begin
200
+ @emp5.save
201
+ false
202
+ rescue Familia::RecordExistsError => e
203
+ e.message.include?('GuardEmployee exists email=unique@example.com')
204
+ end
205
+ #=> true
206
+
207
+ ## Duplicate instance-scoped index requires manual guard
208
+ @emp6_id = "emp_#{rand(1000000)}"
209
+ @emp6 = GuardEmployee.new(emp_id: @emp6_id, badge_number: 'BADGE789', email: 'emp6@example.com')
210
+ @emp6.save # Succeeds - no auto-validation of instance-scoped indexes
211
+ begin
212
+ @emp6.guard_unique_guard_company_badge_index!(@company)
213
+ false
214
+ rescue Familia::RecordExistsError => e
215
+ e.message.include?('GuardEmployee exists in GuardCompany with badge_number=BADGE789')
216
+ end
217
+ #=> true
218
+
219
+ # =============================================
220
+ # 4. Guard Method Error Messages
221
+ # =============================================
222
+
223
+ ## Class-level guard error includes class and field
224
+ @user_dup = GuardUser.new(user_id: "user_dup_#{rand(1000000)}", email: 'test@example.com', username: 'dupuser')
225
+ begin
226
+ @user_dup.guard_unique_email_index!
227
+ rescue Familia::RecordExistsError => e
228
+ [e.message.include?('GuardUser'), e.message.include?('email=test@example.com')]
229
+ end
230
+ #=> [true, true]
231
+
232
+ ## Instance-scoped guard error includes both classes and field
233
+ begin
234
+ @emp2.guard_unique_guard_company_badge_index!(@company)
235
+ rescue Familia::RecordExistsError => e
236
+ [e.message.include?('GuardEmployee'), e.message.include?('GuardCompany'), e.message.include?('badge_number=BADGE123')]
237
+ end
238
+ #=> [true, true, true]
239
+
240
+ ## RecordExistsError is correct type
241
+ begin
242
+ @emp2.guard_unique_guard_company_badge_index!(@company)
243
+ rescue => e
244
+ e.class
245
+ end
246
+ #=> Familia::RecordExistsError
247
+
248
+ # =============================================
249
+ # 5. Transaction Context Behavior
250
+ # =============================================
251
+
252
+ ## Guard works outside transaction
253
+ @user_tx = GuardUser.new(user_id: "user_tx_#{rand(1000000)}", email: 'tx@example.com', username: 'txuser')
254
+ @user_tx.guard_unique_email_index!
255
+ #=> nil
256
+
257
+ ## Guard must be called outside transaction (new rule)
258
+ unique_timestamp = Time.now.to_i
259
+ unique_rand = rand(1000000)
260
+ email = "tx_unique_#{unique_timestamp}_#{unique_rand}@example.com"
261
+ @user_tx_unique = GuardUser.new(user_id: "user_tx_unique_#{unique_rand}", email: email, username: "txuser_#{unique_rand}")
262
+
263
+ # Guards should be called outside transactions
264
+ @user_tx_unique.send(:guard_unique_indexes!)
265
+ #=> nil
266
+
267
+ # Teardown - clean up test objects
268
+ [@user1, @user2, @user_nil, @user_empty1, @user_empty2, @user_dup, @user_tx, @user_tx_unique].each do |obj|
269
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
270
+ end
271
+
272
+ [@emp1, @emp2, @emp3, @emp_nil, @emp4, @emp5, @emp6].each do |obj|
273
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
274
+ end
275
+
276
+ [@company, @company2].each do |obj|
277
+ obj.destroy! if obj.respond_to?(:destroy!) && obj.respond_to?(:exists?) && obj.exists?
278
+ end
279
+
280
+ # Clean up class-level indexes
281
+ [GuardUser.email_index, GuardUser.username_index, GuardEmployee.email_index].each do |index|
282
+ index.delete! if index.respond_to?(:delete!) && index.respond_to?(:exists?) && index.exists?
283
+ end