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
@@ -1,3 +1,7 @@
1
+ # try/integration/persistence_operations_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # try/core/persistence_operations_try.rb
2
6
  #
3
7
  # Comprehensive test coverage for core persistence methods: exists?, save, save_if_not_exists, create
@@ -13,6 +17,27 @@ class PersistenceTestModel < Familia::Horreum
13
17
  field :value
14
18
  end
15
19
 
20
+ # Create model with expiration feature for save_fields testing
21
+ class ExpirationPersistenceTest < Familia::Horreum
22
+ feature :expiration
23
+ identifier_field :id
24
+ field :id
25
+ field :name
26
+ field :email
27
+ field :status
28
+ field :metadata
29
+
30
+ default_expiration 3600 # 1 hour
31
+ end
32
+
33
+ # Simple model without expiration feature
34
+ class SimpleModel < Familia::Horreum
35
+ identifier_field :id
36
+ field :id
37
+ field :name
38
+ field :value
39
+ end
40
+
16
41
  # Clean up any existing test data
17
42
  cleanup_keys = []
18
43
  begin
@@ -111,9 +136,9 @@ result = @sine_new.save_if_not_exists
111
136
  [result, @sine_new.exists?]
112
137
  #=> [true, true]
113
138
 
114
- ## save_if_not_exists raises error for existing object
139
+ ## save_if_not_exists! raises error for existing object
115
140
  @sine_duplicate = PersistenceTestModel.new(id: @sine_new.identifier, name: 'Duplicate')
116
- @sine_duplicate.save_if_not_exists
141
+ @sine_duplicate.save_if_not_exists!
117
142
  #=!> Familia::RecordExistsError
118
143
 
119
144
  ## save_if_not_exists with update_expiration: false
@@ -128,14 +153,10 @@ original_name = 'Original Name'
128
153
  @sine_fail_test.save_if_not_exists
129
154
  # Now create duplicate and verify state doesn't change on failure
130
155
  @sine_fail_duplicate = PersistenceTestModel.new(id: @sine_fail_test.identifier, name: 'Changed Name')
131
- begin
132
- @sine_fail_duplicate.save_if_not_exists
133
- false # Should not reach here
134
- rescue Familia::RecordExistsError
135
- # State should be unchanged
136
- @sine_fail_duplicate.name == 'Changed Name'
137
- end
138
- #=> true
156
+ result = @sine_fail_duplicate.save_if_not_exists
157
+ # save_if_not_exists returns false on failure, state should be unchanged
158
+ [result == false, @sine_fail_duplicate.name == 'Changed Name']
159
+ #=> [true, true]
139
160
 
140
161
  # =============================================
141
162
  # 4. create Method Coverage (MISSING from current tests)
@@ -291,7 +312,142 @@ actual_key = @key_obj.dbkey
291
312
  # Cleanup
292
313
  # =============================================
293
314
 
315
+ # =============================================
316
+ # 8. save_fields Method Coverage
317
+ # =============================================
318
+
319
+ ## save_fields basic functionality with specified fields
320
+ @save_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Original Name', email: 'test@example.com', status: 'active')
321
+ @save_fields_obj.save
322
+ # Modify fields locally
323
+ @save_fields_obj.name = 'Updated Name'
324
+ @save_fields_obj.status = 'inactive'
325
+ @save_fields_obj.metadata = { updated: true }
326
+ # Save only specific fields
327
+ result = @save_fields_obj.save_fields(:name, :metadata)
328
+ [result.class == ExpirationPersistenceTest, @save_fields_obj.exists?]
329
+ #=> [true, true]
330
+
331
+ ## Verify only specified fields were saved
332
+ @save_fields_obj.refresh!
333
+ [@save_fields_obj.name, @save_fields_obj.status, @save_fields_obj.metadata]
334
+ #=> ['Updated Name', 'active', { 'updated' => true }]
335
+
336
+ ## save_fields with update_expiration: true (default)
337
+ @exp_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Expiration Test')
338
+ @exp_obj.save
339
+ original_ttl = @exp_obj.ttl
340
+ # Wait a moment to ensure TTL decreases
341
+ sleep 0.1
342
+ @exp_obj.name = 'Updated with TTL'
343
+ @exp_obj.save_fields(:name) # Should update expiration by default
344
+ new_ttl = @exp_obj.ttl
345
+ # TTL should be refreshed (closer to default_expiration)
346
+ # Allow for small timing variations
347
+ new_ttl >= (ExpirationPersistenceTest.default_expiration - 10)
348
+ #=> true
349
+
350
+ ## save_fields with update_expiration: false
351
+ @no_exp_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'No Exp Update')
352
+ @no_exp_obj.save
353
+ # Wait briefly and get TTL
354
+ sleep 0.1
355
+ original_ttl = @no_exp_obj.ttl
356
+ @no_exp_obj.name = 'Updated without TTL'
357
+ @no_exp_obj.save_fields(:name, update_expiration: false)
358
+ new_ttl = @no_exp_obj.ttl
359
+ # TTL should be approximately the same (slightly less due to time passing)
360
+ (new_ttl - original_ttl).abs < 2
361
+ #=> true
362
+
363
+ ## save_fields with multiple fields
364
+ @multi_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Multi', email: 'multi@test.com')
365
+ @multi_fields_obj.save
366
+ @multi_fields_obj.name = 'Multi Updated'
367
+ @multi_fields_obj.email = 'updated@test.com'
368
+ @multi_fields_obj.status = 'new_status'
369
+ result = @multi_fields_obj.save_fields(:name, :email, :status)
370
+ @multi_fields_obj.refresh!
371
+ [@multi_fields_obj.name, @multi_fields_obj.email, @multi_fields_obj.status]
372
+ #=> ['Multi Updated', 'updated@test.com', 'new_status']
373
+
374
+ ## save_fields with string field names
375
+ @string_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'String Fields')
376
+ @string_fields_obj.save
377
+ @string_fields_obj.name = 'Updated via String'
378
+ result = @string_fields_obj.save_fields('name') # String instead of symbol
379
+ @string_fields_obj.refresh!
380
+ @string_fields_obj.name
381
+ #=> 'Updated via String'
382
+
383
+ ## save_fields error handling - empty fields
384
+ @empty_fields_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Empty Test')
385
+ @empty_fields_obj.save
386
+ @empty_fields_obj.save_fields()
387
+ #=!> ArgumentError
388
+
389
+ ## save_fields error handling - unknown field
390
+ @unknown_field_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Unknown Field')
391
+ @unknown_field_obj.save
392
+ @unknown_field_obj.save_fields(:nonexistent_field)
393
+ #=!> ArgumentError
394
+
395
+ ## save_fields with nil values
396
+ @nil_values_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Nil Values', status: 'initial')
397
+ @nil_values_obj.save
398
+ @nil_values_obj.status = nil
399
+ @nil_values_obj.save_fields(:status)
400
+ @nil_values_obj.refresh!
401
+ @nil_values_obj.status
402
+ #=> nil
403
+
404
+ ## save_fields with complex data types (Hash, Array)
405
+ @complex_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Complex')
406
+ @complex_obj.save
407
+ @complex_obj.metadata = {
408
+ tags: ['ruby', 'redis'],
409
+ config: { timeout: 30, retries: 3 },
410
+ enabled: true
411
+ }
412
+ @complex_obj.save_fields(:metadata)
413
+ @complex_obj.refresh!
414
+ expected_metadata = {
415
+ 'tags' => ['ruby', 'redis'],
416
+ 'config' => { 'timeout' => 30, 'retries' => 3 },
417
+ 'enabled' => true
418
+ }
419
+ @complex_obj.metadata == expected_metadata
420
+ #=> true
421
+
422
+ ## save_fields transactional behavior
423
+ @transaction_obj = ExpirationPersistenceTest.new(id: next_test_id, name: 'Transaction Test')
424
+ @transaction_obj.save
425
+ @transaction_obj.name = 'Updated in Transaction'
426
+ @transaction_obj.email = 'transaction@test.com'
427
+ # All fields should be saved atomically
428
+ @transaction_obj.save_fields(:name, :email)
429
+ @transaction_obj.refresh!
430
+ [@transaction_obj.name, @transaction_obj.email]
431
+ #=> ['Updated in Transaction', 'transaction@test.com']
432
+
433
+ ## save_fields performance with model without expiration feature
434
+
435
+ @simple_obj = SimpleModel.new(id: next_test_id, name: 'Simple', value: 'test')
436
+ @simple_obj.save
437
+ @simple_obj.name = 'Simple Updated'
438
+ # Should work without expiration feature (update_expiration param ignored)
439
+ result = @simple_obj.save_fields(:name, update_expiration: true)
440
+ @simple_obj.refresh!
441
+ @simple_obj.name
442
+ #=> 'Simple Updated'
443
+
444
+ # =============================================
445
+ # Cleanup
446
+ # =============================================
447
+
294
448
  # Clean up test data
295
449
  test_keys = Familia.dbclient.keys('persistencetestmodel:*')
296
450
  test_keys.concat(Familia.dbclient.keys('encryptedpersistencetest:*')) if defined?(EncryptedPersistenceTest)
451
+ test_keys.concat(Familia.dbclient.keys('expirationpersistencetest:*'))
452
+ test_keys.concat(Familia.dbclient.keys('simplemodel:*'))
297
453
  Familia.dbclient.del(*test_keys) if test_keys.any?
@@ -1,5 +1,7 @@
1
1
  # try/integration/relationships_persistence_round_trip_try.rb
2
2
  #
3
+ # frozen_string_literal: true
4
+
3
5
  # CRITICAL PRIORITY: Full Persistence Round-Trip Testing for Relationships
4
6
  #
5
7
  # PURPOSE:
@@ -113,22 +115,23 @@ class ::RTPDomain < Familia::Horreum
113
115
  end
114
116
 
115
117
  # Setup - create test data with known values
116
- @test_user_id = "rtp_user_#{Familia.now.to_i}"
117
- @test_email = "roundtrip@example.com"
118
- @test_name = "Alice Roundtrip"
118
+ @test_run_id = Familia.now.to_i
119
+ @test_user_id = "rtp_user_#{@test_run_id}"
120
+ @test_email = "roundtrip-#{@test_run_id}@example.com"
121
+ @test_name = "Alice Roundtrip #{@test_run_id}"
119
122
  @test_age = 30
120
123
 
121
- @test_company_id = "rtp_comp_#{Familia.now.to_i}"
124
+ @test_company_id = "rtp_comp_#{@test_run_id}"
122
125
  @test_company_name = "Acme Corp"
123
126
  @test_industry = "Technology"
124
127
 
125
- @test_emp_id = "rtp_emp_#{Familia.now.to_i}"
126
- @test_emp_email = "employee@acme.com"
128
+ @test_emp_id = "rtp_emp_#{@test_run_id}"
129
+ @test_emp_email = "employee-#{@test_run_id}@acme.com"
127
130
  @test_department = "engineering"
128
- @test_badge = "BADGE_RTP_001"
131
+ @test_badge = "BADGE_RTP_#{@test_run_id}"
129
132
  @test_hire_date = Time.now.to_i
130
133
 
131
- @test_domain_id = "rtp_dom_#{Familia.now.to_i}"
134
+ @test_domain_id = "rtp_dom_#{@test_run_id}"
132
135
  @test_domain_name = "example.com"
133
136
  @test_domain_created = Familia.now.to_i
134
137
 
@@ -291,10 +294,10 @@ RTPEmployee.exists?(@test_emp_id)
291
294
 
292
295
  ## Multiple employees in same department
293
296
  @emp2_id = "rtp_emp2_#{Familia.now.to_i}"
294
- @emp2_badge = "BADGE_RTP_002"
297
+ @emp2_badge = "BADGE_RTP_002_#{@test_run_id}"
295
298
  @emp2 = RTPEmployee.new(
296
299
  emp_id: @emp2_id,
297
- email: "emp2@acme.com",
300
+ email: "emp2-#{@test_run_id}@acme.com",
298
301
  department: @test_department,
299
302
  badge_number: @emp2_badge
300
303
  )
@@ -323,7 +326,7 @@ RTPDomain.exists?(@test_domain_id)
323
326
  #=> true
324
327
 
325
328
  ## Add domain to company participation collection
326
- @company.add_domain(@domain)
329
+ @company.add_domains_instance(@domain)
327
330
  @company.domains.size
328
331
  #=> 1
329
332
 
@@ -377,7 +380,7 @@ RTPDomain.all_domains.size
377
380
  #=> @new_age
378
381
 
379
382
  ## Update email and verify index updates
380
- @new_email = "newemail@example.com"
383
+ @new_email = "newemail-#{@test_run_id}@example.com"
381
384
  @old_email = @user.email
382
385
  @user.email = @new_email
383
386
  @user.save
@@ -396,8 +399,8 @@ RTPUser.find_by_email(@old_email)
396
399
  ## User with nil field saves correctly
397
400
  @user_nil_age = RTPUser.new(
398
401
  user_id: "rtp_nil_#{Familia.now.to_i}",
399
- email: "nil@example.com",
400
- name: "Nil Tester",
402
+ email: "nil-#{@test_run_id}@example.com",
403
+ name: "Nil Tester #{@test_run_id}",
401
404
  age: nil
402
405
  )
403
406
  @user_nil_age.save
@@ -0,0 +1,241 @@
1
+ # try/integration/save_methods_consistency_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # Test coverage for save and save_if_not_exists consistency improvements
6
+ #
7
+ # This test verifies that both save and save_if_not_exists! produce identical
8
+ # results when creating new objects, including:
9
+ # - Timestamp updates (created/updated)
10
+ # - Unique index validation
11
+ # - Class-level index updates
12
+ # - Instance collection tracking
13
+
14
+ require_relative '../support/helpers/test_helpers'
15
+
16
+ # Model with timestamps to verify timestamp handling
17
+ class TimestampedModel < Familia::Horreum
18
+ identifier_field :id
19
+ field :id
20
+ field :name
21
+ field :created
22
+ field :updated
23
+
24
+ zset :instances
25
+ end
26
+
27
+ # Model with unique indexes to verify validation
28
+ class UniqueIndexModel < Familia::Horreum
29
+ feature :relationships
30
+ include Familia::Features::Relationships::Indexing
31
+
32
+ identifier_field :id
33
+ field :id
34
+ field :email
35
+
36
+ unique_index :email, :email_lookup
37
+ zset :instances
38
+ end
39
+
40
+ # Clean up any existing test data
41
+ cleanup_keys = Familia.dbclient.keys('timestampedmodel:*') +
42
+ Familia.dbclient.keys('uniqueindexmodel:*') +
43
+ Familia.dbclient.keys('*:email_lookup:*')
44
+ Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
45
+
46
+ @test_counter = 0
47
+ def next_id
48
+ @test_counter += 1
49
+ "test-#{Familia.now.to_i}-#{@test_counter}"
50
+ end
51
+
52
+ # =============================================
53
+ # 1. Timestamp Consistency Tests
54
+ # =============================================
55
+
56
+ ## save sets created and updated timestamps
57
+ @save_model = TimestampedModel.new(id: next_id, name: 'Save Test')
58
+ @save_model.save
59
+ [@save_model.created.nil?, @save_model.updated.nil?]
60
+ #=> [false, false]
61
+
62
+ ## save_if_not_exists! sets created and updated timestamps
63
+ @sine_model = TimestampedModel.new(id: next_id, name: 'SINE Test')
64
+ @sine_model.save_if_not_exists!
65
+ [@sine_model.created.nil?, @sine_model.updated.nil?]
66
+ #=> [false, false]
67
+
68
+ ## Both methods set timestamps to the same approximate time
69
+ time_diff = (@save_model.created - @save_model.updated).abs
70
+ time_diff < 1 # Should be within 1 second
71
+ #=> true
72
+
73
+ ## save_if_not_exists! timestamps match save behavior
74
+ sine_time_diff = (@sine_model.created - @sine_model.updated).abs
75
+ sine_time_diff < 1
76
+ #=> true
77
+
78
+ # =============================================
79
+ # 2. Instance Collection Consistency
80
+ # =============================================
81
+
82
+ ## save adds object to instances collection
83
+ @inst_save = TimestampedModel.new(id: next_id, name: 'Instance Save')
84
+ @inst_save.save
85
+ TimestampedModel.instances.members.include?(@inst_save.identifier)
86
+ #=> true
87
+
88
+ ## save_if_not_exists! adds object to instances collection
89
+ @inst_sine = TimestampedModel.new(id: next_id, name: 'Instance SINE')
90
+ @inst_sine.save_if_not_exists!
91
+ TimestampedModel.instances.members.include?(@inst_sine.identifier)
92
+ #=> true
93
+
94
+ ## Both methods produce identical instance collection state
95
+ [@inst_save, @inst_sine].all? { |obj| TimestampedModel.instances.members.include?(obj.identifier) }
96
+ #=> true
97
+
98
+ # =============================================
99
+ # 3. Unique Index Validation Consistency
100
+ # =============================================
101
+
102
+ ## save validates unique indexes
103
+ @unique_email_1 = "save-#{next_id}@test.com"
104
+ @unique_save = UniqueIndexModel.new(id: next_id, email: @unique_email_1)
105
+ @unique_save.save
106
+ @unique_save.exists?
107
+ #=> true
108
+
109
+ ## save raises RecordExistsError for duplicate unique index
110
+ @unique_dup_save = UniqueIndexModel.new(id: next_id, email: @unique_email_1)
111
+ @unique_dup_save.save
112
+ #=!> Familia::RecordExistsError
113
+
114
+ ## save_if_not_exists! validates unique indexes
115
+ @unique_email_2 = "sine-#{next_id}@test.com"
116
+ @unique_sine = UniqueIndexModel.new(id: next_id, email: @unique_email_2)
117
+ @unique_sine.save_if_not_exists!
118
+ @unique_sine.exists?
119
+ #=> true
120
+
121
+ ## save_if_not_exists! raises RecordExistsError for duplicate unique index
122
+ @unique_dup_sine = UniqueIndexModel.new(id: next_id, email: @unique_email_2)
123
+ @unique_dup_sine.save_if_not_exists!
124
+ #=!> Familia::RecordExistsError
125
+
126
+ # =============================================
127
+ # 4. Return Value Consistency
128
+ # =============================================
129
+
130
+ ## save returns true for successful save
131
+ @ret_save = TimestampedModel.new(id: next_id, name: 'Return Test Save')
132
+ result_save = @ret_save.save
133
+ result_save
134
+ #=> true
135
+
136
+ ## save_if_not_exists! returns true for successful save
137
+ @ret_sine = TimestampedModel.new(id: next_id, name: 'Return Test SINE')
138
+ result_sine = @ret_sine.save_if_not_exists!
139
+ result_sine
140
+ #=> true
141
+
142
+ ## save_if_not_exists returns true for new object
143
+ @ret_sine_safe = TimestampedModel.new(id: next_id, name: 'Return Safe SINE')
144
+ result = @ret_sine_safe.save_if_not_exists
145
+ result
146
+ #=> true
147
+
148
+ ## save_if_not_exists returns false for existing object
149
+ @ret_existing = TimestampedModel.new(id: next_id, name: 'Existing')
150
+ @ret_existing.save
151
+ @ret_dup = TimestampedModel.new(id: @ret_existing.identifier, name: 'Duplicate')
152
+ result = @ret_dup.save_if_not_exists
153
+ result
154
+ #=> false
155
+
156
+ # =============================================
157
+ # 5. Data Persistence Consistency
158
+ # =============================================
159
+
160
+ ## save persists all fields
161
+ @data_save = TimestampedModel.new(id: next_id, name: 'Data Save Test')
162
+ @data_save.save
163
+ @data_save.refresh
164
+ @data_save.name
165
+ #=> 'Data Save Test'
166
+
167
+ ## save_if_not_exists! persists all fields
168
+ @data_sine = TimestampedModel.new(id: next_id, name: 'Data SINE Test')
169
+ @data_sine.save_if_not_exists!
170
+ @data_sine.refresh
171
+ @data_sine.name
172
+ #=> 'Data SINE Test'
173
+
174
+ ## Both methods produce identical persistence
175
+ [@data_save.name, @data_sine.name]
176
+ #=> ['Data Save Test', 'Data SINE Test']
177
+
178
+ # =============================================
179
+ # 6. Expiration Handling Consistency
180
+ # =============================================
181
+
182
+ ## save with update_expiration: true handles TTL
183
+ @exp_save = TimestampedModel.new(id: next_id, name: 'Exp Save')
184
+ @exp_save.save(update_expiration: true)
185
+ # No default expiration set, so TTL is -1 (no expiration)
186
+ @exp_save.ttl == -1
187
+ #=> true
188
+
189
+ ## save_if_not_exists! with update_expiration: true handles TTL
190
+ @exp_sine = TimestampedModel.new(id: next_id, name: 'Exp SINE')
191
+ @exp_sine.save_if_not_exists!(update_expiration: true)
192
+ # No default expiration set, so TTL is -1 (no expiration)
193
+ @exp_sine.ttl == -1
194
+ #=> true
195
+
196
+ # =============================================
197
+ # 7. OptimisticLockError Behavior
198
+ # =============================================
199
+
200
+ ## save_if_not_exists allows OptimisticLockError to propagate
201
+ # Note: This is difficult to test reliably without mocking, but we can
202
+ # verify the method signature and rescue clause structure through the API
203
+
204
+ ## save_if_not_exists rescues only RecordExistsError
205
+ @opt_test = TimestampedModel.new(id: next_id, name: 'Opt Test')
206
+ @opt_test.save
207
+ @opt_dup = TimestampedModel.new(id: @opt_test.identifier, name: 'Opt Dup')
208
+ # This should return false, not raise
209
+ result = @opt_dup.save_if_not_exists
210
+ result
211
+ #=> false
212
+
213
+ # =============================================
214
+ # 8. Edge Cases
215
+ # =============================================
216
+
217
+ ## save works with nil field values
218
+ @nil_save = TimestampedModel.new(id: next_id, name: nil)
219
+ @nil_save.save
220
+ @nil_save.exists?
221
+ #=> true
222
+
223
+ ## save_if_not_exists! works with nil field values
224
+ @nil_sine = TimestampedModel.new(id: next_id, name: nil)
225
+ @nil_sine.save_if_not_exists!
226
+ @nil_sine.exists?
227
+ #=> true
228
+
229
+ ## Both methods handle empty strings
230
+ @empty_save = TimestampedModel.new(id: next_id, name: '')
231
+ @empty_save.save
232
+ @empty_sine = TimestampedModel.new(id: next_id, name: '')
233
+ @empty_sine.save_if_not_exists!
234
+ [@empty_save.exists?, @empty_sine.exists?]
235
+ #=> [true, true]
236
+
237
+ # Cleanup
238
+ cleanup_keys = Familia.dbclient.keys('timestampedmodel:*') +
239
+ Familia.dbclient.keys('uniqueindexmodel:*') +
240
+ Familia.dbclient.keys('*:email_lookup:*')
241
+ Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
@@ -1,3 +1,7 @@
1
+ # try/integration/scenarios_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # Comprehensive configuration scenarios
2
6
 
3
7
  require_relative '../support/helpers/test_helpers'
@@ -1,3 +1,7 @@
1
+ # try/integration/secure_identifier_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # try/core/secure_identifier_try.rb
2
6
 
3
7
  # Test Familia::SecureIdentifier methods