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,4 +1,6 @@
1
1
  # lib/familia/data_type/database_commands.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class DataType
@@ -22,11 +24,14 @@ module Familia
22
24
  end
23
25
 
24
26
  # Deletes the entire dbkey
25
- # @return [Boolean] true if the key was deleted, false otherwise
27
+ #
28
+ # We return the dbclient.del command's return value instead of a friendly
29
+ # boolean b/c that logic doesn't work inside of a transaction. The return
30
+ # value in that case is a Redis::Future which based on the name indicates
31
+ # that the commend hasn't even run yet.
26
32
  def delete!
27
- Familia.trace :DELETE!, nil, uri if Familia.debug?
28
- ret = dbclient.del dbkey
29
- ret.positive?
33
+ Familia.trace :DELETE!, nil, self.class.uri if Familia.debug?
34
+ dbclient.del dbkey
30
35
  end
31
36
  alias clear delete!
32
37
 
@@ -1,4 +1,6 @@
1
1
  # lib/familia/data_type/serialization.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class DataType
@@ -32,13 +34,13 @@ module Familia
32
34
 
33
35
  if opts[:class]
34
36
  prepared = Familia.identifier_extractor(opts[:class])
35
- Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared || '<nil>'}"
37
+ Familia.debug " from opts[class] <#{opts[:class]}>: #{prepared || '<nil>'}"
36
38
  end
37
39
 
38
40
  if prepared.nil?
39
41
  # Enforce strict values when no class option is specified
40
42
  prepared = Familia.identifier_extractor(val)
41
- Familia.ld " from <#{val.class}> => <#{prepared.class}>"
43
+ Familia.debug " from <#{val.class}> => <#{prepared.class}>"
42
44
  end
43
45
 
44
46
  if Familia.debug?
@@ -77,7 +79,7 @@ module Familia
77
79
  # replaced with nil.
78
80
  #
79
81
  def deserialize_values_with_nil(*values)
80
- Familia.ld "deserialize_values: (#{@opts}) #{values}"
82
+ Familia.debug "deserialize_values: (#{@opts}) #{values}"
81
83
  return [] if values.empty?
82
84
  return values.flatten unless @opts[:class]
83
85
 
@@ -89,7 +91,7 @@ module Familia
89
91
  next if obj.nil?
90
92
 
91
93
  val = @opts[:class].send load_method, obj
92
- Familia.ld "[#{self.class}#deserialize_values] nil returned for #{@opts[:class]}##{name}" if val.nil?
94
+ Familia.debug "[#{self.class}#deserialize_values] nil returned for #{@opts[:class]}##{name}" if val.nil?
93
95
 
94
96
  val
95
97
  rescue StandardError => e
@@ -117,7 +119,11 @@ module Familia
117
119
  # for serialization since everything becomes a string in Valkey.
118
120
  #
119
121
  def deserialize_value(val)
122
+ # Handle Redis::Future objects during transactions first
123
+ return val if val.is_a?(Redis::Future)
124
+
120
125
  return @opts[:default] if val.nil?
126
+
121
127
  return val unless @opts[:class]
122
128
 
123
129
  ret = deserialize_values val
@@ -1,4 +1,6 @@
1
1
  # lib/familia/data_type/settings.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class DataType
@@ -1,4 +1,6 @@
1
1
  # lib/familia/data_type/types/counter.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class Counter < StringKey
@@ -1,4 +1,6 @@
1
1
  # lib/familia/data_type/types/hashkey.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class HashKey < DataType
@@ -22,8 +24,8 @@ module Familia
22
24
  update_expiration
23
25
  ret
24
26
  rescue TypeError => e
25
- Familia.le "[hset]= #{e.message}"
26
- Familia.ld "[hset]= #{dbkey} #{field}=#{val}" if Familia.debug
27
+ Familia.error "[hset]= #{e.message}"
28
+ Familia.debug "[hset]= #{dbkey} #{field}=#{val}"
27
29
  echo :hset, Familia.pretty_stack(limit: 1) if Familia.debug # logs via echo to the db and back
28
30
  klass = val.class
29
31
  msg = "Cannot store #{field} => #{val.inspect} (#{klass}) in #{dbkey}"
@@ -74,8 +76,8 @@ module Familia
74
76
  update_expiration if ret == 1
75
77
  ret
76
78
  rescue TypeError => e
77
- Familia.le "[hsetnx] #{e.message}"
78
- Familia.ld "[hsetnx] #{dbkey} #{field}=#{val}" if Familia.debug
79
+ Familia.error "[hsetnx] #{e.message}"
80
+ Familia.debug "[hsetnx] #{dbkey} #{field}=#{val}"
79
81
  echo :hsetnx, Familia.pretty_stack(limit: 1) if Familia.debug # logs via echo to the db and back
80
82
  klass = val.class
81
83
  msg = "Cannot store #{field} => #{val.inspect} (#{klass}) in #{dbkey}"
@@ -152,11 +154,11 @@ module Familia
152
154
  # puts "Oops! Our hash seems to have vanished into the Database void!"
153
155
  # end
154
156
  def refresh!
155
- Familia.trace :REFRESH, nil, uri if Familia.debug?
157
+ Familia.trace :REFRESH, nil, self.class.uri if Familia.debug?
156
158
  raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
157
159
 
158
160
  fields = hgetall
159
- Familia.ld "[refresh!] #{self.class} #{dbkey} #{fields.keys}"
161
+ Familia.debug "[refresh!] #{self.class} #{dbkey} #{fields.keys}"
160
162
 
161
163
  # For HashKey, we update by merging the fresh data
162
164
  update(fields)
@@ -1,4 +1,6 @@
1
1
  # lib/familia/data_type/types/listkey.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class ListKey < DataType
@@ -1,4 +1,6 @@
1
1
  # lib/familia/data_type/types/lock.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class Lock < StringKey
@@ -1,4 +1,6 @@
1
1
  # lib/familia/data_type/types/sorted_set.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class SortedSet < DataType
@@ -1,4 +1,6 @@
1
1
  # lib/familia/data_type/types/stringkey.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  class StringKey < DataType
@@ -1,4 +1,6 @@
1
1
  # lib/familia/data_type/types/unsorted_set.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  # Familia::UnsortedSet
@@ -1,4 +1,6 @@
1
1
  # lib/familia/data_type.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'data_type/class_methods'
4
6
  require_relative 'data_type/settings'
@@ -1,4 +1,6 @@
1
1
  # lib/familia/encryption/encrypted_data.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  module Encryption
@@ -15,10 +17,10 @@ module Familia
15
17
  # Check for required fields
16
18
  required_fields = %i[algorithm nonce ciphertext auth_tag key_version]
17
19
  result = required_fields.all? { |field| parsed.key?(field) }
18
- Familia.ld "[valid?] result: #{result}, parsed: #{parsed}, required: #{required_fields}"
20
+ Familia.debug "[valid?] result: #{result}, parsed: #{parsed}, required: #{required_fields}"
19
21
  result
20
22
  rescue Familia::SerializerError => e
21
- Familia.ld "[valid?] JSON error: #{e.message}"
23
+ Familia.debug "[valid?] JSON error: #{e.message}"
22
24
  false
23
25
  end
24
26
  end
@@ -1,4 +1,6 @@
1
1
  # lib/familia/encryption/manager.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  module Encryption
@@ -1,4 +1,6 @@
1
1
  # lib/familia/encryption/provider.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  module Encryption
@@ -1,4 +1,6 @@
1
1
  # lib/familia/encryption/providers/aes_gcm_provider.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # ⚠️ RUBY MEMORY SAFETY WARNING ⚠️
4
6
  #
@@ -1,4 +1,6 @@
1
1
  # lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # ⚠️ PROTOTYPE IMPLEMENTATION - NOT FOR PRODUCTION USE ⚠️
4
6
  #
@@ -1,4 +1,6 @@
1
1
  # lib/familia/encryption/providers/xchacha20_poly1305_provider.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # ⚠️ RUBY MEMORY SAFETY WARNING ⚠️
4
6
  #
@@ -1,4 +1,6 @@
1
1
  # lib/familia/encryption/registry.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  module Encryption
@@ -1,5 +1,7 @@
1
1
  # lib/familia/encryption/request_cache.rb
2
2
  #
3
+ # frozen_string_literal: true
4
+
3
5
  # Request-scoped caching for encryption keys (if needed for performance)
4
6
  # This should ONLY be enabled if performance testing shows it's necessary
5
7
  #
@@ -1,4 +1,6 @@
1
1
  # lib/familia/encryption.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require 'base64'
4
6
  require 'oj'
@@ -60,9 +62,14 @@ module Familia
60
62
  # end
61
63
  class << self
62
64
  # Get or create a manager with specific algorithm
65
+ #
66
+ # Thread-safe lazy initialization using Concurrent::Map to ensure
67
+ # only a single Manager instance is created per algorithm even under
68
+ # concurrent encryption/decryption requests.
69
+ #
63
70
  def manager(algorithm: nil)
64
- @managers ||= {}
65
- @managers[algorithm] ||= Manager.new(algorithm: algorithm)
71
+ @managers ||= Concurrent::Map.new
72
+ @managers.fetch_or_store(algorithm) { Manager.new(algorithm: algorithm) }
66
73
  end
67
74
 
68
75
  # Quick encryption with auto-selected best provider
@@ -1,19 +1,54 @@
1
1
  # lib/familia/errors.rb
2
2
  #
3
+ # frozen_string_literal: true
4
+
3
5
  module Familia
6
+ # Base exception class for all Familia errors
4
7
  class Problem < RuntimeError; end
5
- class NoIdentifier < Problem; end
6
- class NonUniqueKey < Problem; end
7
8
 
8
- class FieldTypeError < Problem; end
9
- class AutoloadError < Problem; end
9
+ # Base exception class for Redis/persistence-related errors
10
+ class PersistenceError < Problem; end
11
+
12
+ # Base exception class for Horreum models
13
+ class HorreumError < Problem; end
14
+
15
+ # Raised when an object creation fails (e.g. the identifier
16
+ # is already in use)
17
+ class CreationError < HorreumError; end
18
+
19
+ # Raised when an object lacks a required identifier
20
+ class NoIdentifier < HorreumError; end
21
+
22
+ # Raised when a key is expected to be unique but isn't
23
+ class NonUniqueKey < PersistenceError; end
24
+
25
+ # Raised when watch failed (e.g. key was modified), typically
26
+ # retry
27
+ class OptimisticLockError < PersistenceError; end
28
+
29
+ # Raised when a field type is invalid or unexpected
30
+ class FieldTypeError < HorreumError; end
31
+
32
+ # Raised when autoloading fails
33
+ class AutoloadError < HorreumError; end
34
+
35
+ # Raised when serialization or deserialization fails
36
+ class SerializerError < HorreumError; end
37
+
38
+ # Raised when attempting to start transactions or pipelines on
39
+ # connection types that don't support them
40
+ class OperationModeError < PersistenceError; end
10
41
 
11
- class SerializerError < Problem; end
42
+ # Raised when attempting to call a major method like save when
43
+ # inside a transaction or pipeline
44
+ class NestedTransactionError < OperationModeError; end
12
45
 
13
- # Raised when attempting to start transactions or pipelines on connection types that don't support them
14
- class OperationModeError < Problem; end
46
+ # Raised when attempting to reference a field that doesn't exist
47
+ class UnknownFieldError < HorreumError; end
15
48
 
16
- class NotDistinguishableError < Problem
49
+ # Raised when a value cannot be converted to a distinguishable
50
+ # string representation
51
+ class NotDistinguishableError < HorreumError
17
52
  attr_reader :value
18
53
 
19
54
  def initialize(value)
@@ -26,7 +61,8 @@ module Familia
26
61
  end
27
62
  end
28
63
 
29
- class NotConnected < Problem
64
+ # Raised when no connection is available for a given URI
65
+ class NotConnected < PersistenceError
30
66
  attr_reader :uri
31
67
 
32
68
  def initialize(uri)
@@ -39,13 +75,15 @@ module Familia
39
75
  end
40
76
  end
41
77
 
42
- # UnsortedSet Familia.connection_provider or use middleware to provide connections.
43
- class NoConnectionAvailable < Problem; end
78
+ # UnsortedSet Familia.connection_provider or use middleware
79
+ # to provide connections.
80
+ class NoConnectionAvailable < PersistenceError; end
44
81
 
45
82
  # Raised when a load method fails to find the requested object
46
- class NotFound < Problem; end
83
+ class NotFound < PersistenceError; end
47
84
 
48
- # Raised when attempting to refresh an object whose key doesn't exist in the database
85
+ # Raised when attempting to refresh an object whose key
86
+ # doesn't exist in the database
49
87
  class KeyNotFoundError < NonUniqueKey
50
88
  attr_reader :key
51
89
 
@@ -59,7 +97,8 @@ module Familia
59
97
  end
60
98
  end
61
99
 
62
- # Raised when attempting to create an object that already exists in the database
100
+ # Raised when attempting to create an object that already
101
+ # exists in the database
63
102
  class RecordExistsError < NonUniqueKey
64
103
  attr_reader :key
65
104
 
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/autoloader.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # rubocop:disable Style/ClassAndModuleChildren
4
6
  module Familia::Features
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/encrypted_fields/concealed_string.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  # ConcealedString
4
6
  #
@@ -1,3 +1,7 @@
1
+ # lib/familia/features/encrypted_fields/encrypted_field_type.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # lib/familia/field_types/encrypted_field_type.rb
2
6
 
3
7
  require_relative '../../field_type'
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/encrypted_fields.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'encrypted_fields/encrypted_field_type'
4
6
 
@@ -341,8 +343,6 @@ module Familia
341
343
  # Check if this instance has any encrypted fields with values
342
344
  #
343
345
  # @return [Boolean] true if any encrypted fields have values
344
- #
345
- # TODO: Missing test coverage
346
346
  def encrypted_data?
347
347
  self.class.encrypted_fields.any? do |field_name|
348
348
  field_value = instance_variable_get("@#{field_name}")
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/expiration/extensions.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  module Familia
4
6
  # Add a default update_expiration method for all classes that include
@@ -10,25 +12,23 @@ module Familia
10
12
  # with the :expiration feature's implementation.
11
13
  #
12
14
  # This is a no-op implementation that gets overridden by the :expiration
13
- # feature. It accepts an optional default_expiration parameter to maintain
14
- # interface compatibility with the overriding implementations.
15
+ # feature when it is enabled. This allows for calling this method on any
16
+ # horreum model regardless of the feature status. It accepts an optional
17
+ # expiration parameter to maintain interface compatibility with
18
+ # the overriding implementations.
15
19
  #
16
- # @param default_expiration [Numeric, nil] Time To Live in seconds
20
+ # @param expiration [Numeric, nil] Time To Live in seconds
17
21
  # @return [nil] Always returns nil for the base implementation
18
22
  #
19
23
  # @note This is a no-op implementation. Classes that need expiration
20
24
  # functionality should include the :expiration feature.
21
25
  #
22
- # @example Enable expiration feature
23
- # class MyModel < Familia::Horreum
24
- # feature :expiration
25
- # default_expiration 1.hour
26
- # end
26
+ # @example MyModel.new.update_expiration(expiration: 3600) # => nothing happens
27
27
  #
28
- def update_expiration(default_expiration: nil)
29
- Familia.ld <<~LOG
28
+ def update_expiration(expiration: nil)
29
+ Familia.debug <<~LOG
30
30
  [update_expiration] Expiration feature not enabled for #{self.class}.
31
- Key: #{dbkey} Arg: #{default_expiration} (caller: #{caller(1..1)})
31
+ Key: #{dbkey} Arg: #{expiration} (caller: #{caller(1..1)})
32
32
  LOG
33
33
  nil
34
34
  end
@@ -1,4 +1,6 @@
1
1
  # lib/familia/features/expiration.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'expiration/extensions'
4
6
 
@@ -36,7 +38,7 @@ module Familia
36
38
  # session.ttl # => 1799
37
39
  #
38
40
  # # UnsortedSet custom expiration for new objects
39
- # session.update_expiration(default_expiration: 2.hours)
41
+ # session.update_expiration(expiration: 2.hours)
40
42
  #
41
43
  # Class-Level Configuration:
42
44
  #
@@ -89,7 +91,7 @@ module Familia
89
91
  #
90
92
  # Setting expiration to 0 (zero) disables TTL, making data persist indefinitely:
91
93
  #
92
- # session.update_expiration(default_expiration: 0) # No expiration
94
+ # session.update_expiration(expiration: 0) # No expiration
93
95
  #
94
96
  # TTL Querying:
95
97
  #
@@ -115,7 +117,7 @@ module Familia
115
117
  # when 'free'
116
118
  # update_expiration(1.hour)
117
119
  # else
118
- # update_expiration(default_expiration)
120
+ # update_expiration(expiration)
119
121
  # end
120
122
  # end
121
123
  # end
@@ -135,10 +137,10 @@ module Familia
135
137
  #
136
138
  # The feature validates expiration values and raises descriptive errors:
137
139
  #
138
- # session.update_expiration(default_expiration: "invalid")
140
+ # session.update_expiration(expiration: "invalid")
139
141
  # # => Familia::Problem: Default expiration must be a number
140
142
  #
141
- # session.update_expiration(default_expiration: -1)
143
+ # session.update_expiration(expiration: -1)
142
144
  # # => Familia::Problem: Default expiration must be non-negative
143
145
  #
144
146
  # Performance Considerations:
@@ -226,20 +228,20 @@ module Familia
226
228
  # after which it will be automatically removed. The method also handles
227
229
  # cascading expiration to related data structures when applicable.
228
230
  #
229
- # @param default_expiration [Numeric, nil] The Time To Live in seconds. If nil,
231
+ # @param expiration [Numeric, nil] The Time To Live in seconds. If nil,
230
232
  # the default TTL will be used.
231
233
  #
232
234
  # @return [Boolean] Returns true if the expiration was set successfully,
233
235
  # false otherwise.
234
236
  #
235
237
  # @example Setting an expiration of one day
236
- # object.update_expiration(default_expiration: 86400)
238
+ # object.update_expiration(expiration: 86400)
237
239
  #
238
240
  # @example Using default expiration
239
241
  # object.update_expiration # Uses class default_expiration
240
242
  #
241
243
  # @example Removing expiration (persist indefinitely)
242
- # object.update_expiration(default_expiration: 0)
244
+ # object.update_expiration(expiration: 0)
243
245
  #
244
246
  # @note If default expiration is set to zero, the expiration will be removed,
245
247
  # making the data persist indefinitely.
@@ -247,19 +249,19 @@ module Familia
247
249
  # @raise [Familia::Problem] Raises an error if the default expiration is not
248
250
  # a non-negative number.
249
251
  #
250
- def update_expiration(default_expiration: nil)
251
- default_expiration ||= self.default_expiration
252
+ def update_expiration(expiration: nil)
253
+ expiration ||= default_expiration
252
254
 
253
255
  # Handle cascading expiration to related data structures
254
256
  if self.class.relations?
255
- Familia.ld "[update_expiration] #{self.class} has relations: #{self.class.related_fields.keys}"
257
+ Familia.debug "[update_expiration] #{self.class} has relations: #{self.class.related_fields.keys}"
256
258
  self.class.related_fields.each do |name, definition|
257
259
  # Skip relations that don't have their own expiration settings
258
260
  next if definition.opts[:default_expiration].nil?
259
261
 
260
262
  obj = send(name)
261
- Familia.ld "[update_expiration] Updating expiration for #{name} (#{obj.dbkey}) to #{default_expiration}"
262
- obj.update_expiration(default_expiration: default_expiration)
263
+ Familia.debug "[update_expiration] Updating expiration for #{name} (#{obj.dbkey}) to #{expiration}"
264
+ obj.update_expiration(expiration: expiration)
263
265
  end
264
266
  end
265
267
 
@@ -267,29 +269,35 @@ module Familia
267
269
  # It's important to raise exceptions here and not just log warnings. We
268
270
  # don't want to silently fail at setting expirations and cause data
269
271
  # retention issues (e.g. not removed in a timely fashion).
270
- unless default_expiration.is_a?(Numeric)
272
+ unless expiration.is_a?(Numeric)
271
273
  raise Familia::Problem,
272
- "Default expiration must be a number (#{default_expiration.class} given for #{self.class})"
274
+ "Default expiration must be a number (#{expiration.class} given for #{self.class})"
273
275
  end
274
276
 
275
- unless default_expiration >= 0
277
+ unless expiration >= 0
276
278
  raise Familia::Problem,
277
- "Default expiration must be non-negative (#{default_expiration} given for #{self.class})"
279
+ "Default expiration must be non-negative (#{expiration} given for #{self.class})"
278
280
  end
279
281
 
280
282
  # If zero, simply skip setting an expiry for this key. If we were to set
281
283
  # 0, Valkey/Redis would drop the key immediately.
282
- if default_expiration.zero?
283
- Familia.ld "[update_expiration] No expiration for #{self.class} (#{dbkey})"
284
+ if expiration.zero?
285
+ Familia.debug "[update_expiration] No expiration for #{self.class} (#{dbkey})"
284
286
  return true
285
287
  end
286
288
 
287
- Familia.ld "[update_expiration] Expires #{dbkey} in #{default_expiration} seconds"
289
+ # Structured TTL operation logging
290
+ Familia.debug "TTL updated",
291
+ operation: :expire,
292
+ key: dbkey,
293
+ ttl_seconds: expiration,
294
+ class: self.class.name,
295
+ identifier: (identifier rescue nil)
288
296
 
289
297
  # The Valkey/Redis' EXPIRE command returns 1 if the timeout was set, 0
290
298
  # if key does not exist or the timeout could not be set. Via redis-rb,
291
299
  # it's a boolean.
292
- expire(default_expiration)
300
+ expire(expiration)
293
301
  end
294
302
 
295
303
  # Get the remaining time to live for this object's data