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,674 @@
1
+ # Optimized Loading Guide
2
+
3
+ > **💡 Quick Reference**
4
+ >
5
+ > Reduce Redis commands by 50-96% for bulk object loading:
6
+ > ```ruby
7
+ > # Single object (50% reduction)
8
+ > user = User.find_by_id(123, check_exists: false)
9
+ >
10
+ > # Bulk loading (96% reduction)
11
+ > users = User.load_multi([123, 456, 789])
12
+ > ```
13
+
14
+ ## Overview
15
+
16
+ Familia's optimized loading provides two complementary strategies to dramatically reduce Redis command overhead when loading objects. These optimizations are particularly valuable for applications loading collections of related objects, processing query results, or operating in high-throughput environments.
17
+
18
+ **Default behavior**: Each object load requires 2 Redis commands (EXISTS + HGETALL)
19
+
20
+ **Optimized approaches**:
21
+ 1. **Skip EXISTS check** (`check_exists: false`) - 50% reduction, 1 command per object
22
+ 2. **Pipelined bulk loading** (`load_multi`) - Up to 96% reduction, 1 round trip for N objects
23
+
24
+ ## Why Optimize Object Loading?
25
+
26
+ **Network Overhead**: Each Redis command incurs network round-trip latency. For 14 objects, default loading requires 28 round trips.
27
+
28
+ **Bulk Operations**: Loading collections of related objects (e.g., metadata for a customer, domains for a team) compounds the overhead.
29
+
30
+ **High Throughput**: APIs serving thousands of requests per second benefit significantly from reduced Redis commands.
31
+
32
+ **Cost Efficiency**: Fewer commands mean lower Redis server load and reduced infrastructure costs in cloud environments.
33
+
34
+ ## The Problem
35
+
36
+ Consider loading metadata objects for a customer:
37
+
38
+ ```ruby
39
+ # Get metadata IDs from sorted set
40
+ metadata_ids = customer.metadata.rangebyscore(start_time, end_time)
41
+ # => ["id1", "id2", "id3", ..., "id14"] # 14 metadata objects
42
+
43
+ # Traditional approach
44
+ metadata = metadata_ids.map { |id| Metadata.find_by_id(id) }
45
+ ```
46
+
47
+ **Redis commands generated**:
48
+ ```
49
+ exists metadata:id1:object # Check 1
50
+ hgetall metadata:id1:object # Load 1
51
+ exists metadata:id2:object # Check 2
52
+ hgetall metadata:id2:object # Load 2
53
+ ... (repeated 14 times)
54
+ # Total: 28 commands across 28 network round trips
55
+ ```
56
+
57
+ ## Quick Start
58
+
59
+ ### Optimization 1: Skip EXISTS Check
60
+
61
+ For single object loads or when iterating over collections:
62
+
63
+ ```ruby
64
+ # Default behavior (2 commands)
65
+ user = User.find_by_id(123)
66
+
67
+ # Optimized (1 command)
68
+ user = User.find_by_id(123, check_exists: false)
69
+
70
+ # Still returns nil for non-existent objects
71
+ missing = User.find_by_id(999, check_exists: false) # => nil
72
+ ```
73
+
74
+ **When to use**:
75
+ - Loading objects from known-to-exist references (sorted set members, etc.)
76
+ - Performance-critical paths where 50% reduction matters
77
+ - Iterating over collections with `.map`
78
+
79
+ **Performance**: 14 objects → 14 commands instead of 28 (50% reduction)
80
+
81
+ ### Optimization 2: Pipelined Bulk Loading
82
+
83
+ For loading multiple objects at once:
84
+
85
+ ```ruby
86
+ # Optimized bulk loading (1 round trip)
87
+ users = User.load_multi([123, 456, 789])
88
+
89
+ # With metadata example from above
90
+ metadata = Metadata.load_multi(metadata_ids)
91
+
92
+ # Filter out nils for missing objects
93
+ existing_metadata = Metadata.load_multi(metadata_ids).compact
94
+ ```
95
+
96
+ **When to use**:
97
+ - Loading collections of related objects
98
+ - Processing query results (ZRANGEBYSCORE, SMEMBERS, etc.)
99
+ - Batch operations
100
+ - Any scenario requiring multiple object lookups
101
+
102
+ **Performance**: 14 objects → 1 pipelined batch with 14 HGETALL commands (96% reduction in round trips)
103
+
104
+ ## Detailed Usage
105
+
106
+ ### check_exists Parameter
107
+
108
+ The `check_exists` parameter is available on all finder methods:
109
+
110
+ ```ruby
111
+ # find_by_dbkey
112
+ user = User.find_by_dbkey("user:123:object", check_exists: false)
113
+
114
+ # find_by_identifier
115
+ user = User.find_by_identifier(123, check_exists: false)
116
+
117
+ # Aliases (find_by_id, find, load)
118
+ user = User.find_by_id(123, check_exists: false)
119
+ user = User.find(123, check_exists: false)
120
+ user = User.load(123, check_exists: false)
121
+
122
+ # Custom suffix
123
+ session = Session.find_by_identifier('abc123', :session, check_exists: false)
124
+ ```
125
+
126
+ **How it works**:
127
+
128
+ **Safe mode** (`check_exists: true`, default):
129
+ 1. Send `EXISTS user:123:object`
130
+ 2. If key doesn't exist, return nil immediately
131
+ 3. If exists, send `HGETALL user:123:object`
132
+ 4. Instantiate object from hash
133
+
134
+ **Optimized mode** (`check_exists: false`):
135
+ 1. Send `HGETALL user:123:object` directly
136
+ 2. If hash is empty (key doesn't exist), return nil
137
+ 3. Otherwise instantiate object from hash
138
+
139
+ **Safety**: Both modes return nil for non-existent keys. Optimized mode detects non-existence via empty hash response.
140
+
141
+ ### Pipelined Bulk Loading
142
+
143
+ #### load_multi
144
+
145
+ Load multiple objects by their identifiers:
146
+
147
+ ```ruby
148
+ # Basic usage
149
+ users = User.load_multi([123, 456, 789])
150
+
151
+ # Returns array with nils for missing objects
152
+ results = User.load_multi(['id1', 'missing', 'id3'])
153
+ # => [<User:id1>, nil, <User:id3>]
154
+
155
+ # Filter out nils
156
+ existing = User.load_multi(ids).compact
157
+
158
+ # Empty array handling
159
+ User.load_multi([]) # => []
160
+
161
+ # Preserves order
162
+ users = User.load_multi([789, 123, 456])
163
+ users.map(&:user_id) # => [789, 123, 456] (same order)
164
+ ```
165
+
166
+ **Parameters**:
167
+ - `identifiers` - Array of identifiers (Strings or Integers)
168
+ - `suffix` - Optional suffix (default: class suffix)
169
+
170
+ **Returns**: Array of objects in same order as input, with nils for non-existent keys
171
+
172
+ #### load_multi_by_keys
173
+
174
+ Load objects by full dbkeys (lower-level variant):
175
+
176
+ ```ruby
177
+ # When you already have full keys
178
+ keys = [
179
+ "user:123:object",
180
+ "user:456:object",
181
+ "user:789:object"
182
+ ]
183
+ users = User.load_multi_by_keys(keys)
184
+
185
+ # Mixed existing and non-existent keys
186
+ keys = ["user:123:object", "user:missing:object"]
187
+ results = User.load_multi_by_keys(keys)
188
+ # => [<User:123>, nil]
189
+ ```
190
+
191
+ **When to use**: When working directly with dbkeys rather than identifiers (rare).
192
+
193
+ #### load_batch Alias
194
+
195
+ `load_batch` is an alias for `load_multi`:
196
+
197
+ ```ruby
198
+ users = User.load_batch([123, 456, 789])
199
+ # Identical to load_multi
200
+ ```
201
+
202
+ ### Handling Edge Cases
203
+
204
+ ```ruby
205
+ # Nil identifiers
206
+ results = User.load_multi(['id1', nil, 'id3'])
207
+ # => [<User:id1>, nil, <User:id3>]
208
+
209
+ # Empty string identifiers
210
+ results = User.load_multi(['id1', '', 'id3'])
211
+ # => [<User:id1>, nil, <User:id3>]
212
+
213
+ # All missing
214
+ results = User.load_multi(['missing1', 'missing2'])
215
+ results.compact # => []
216
+
217
+ # Mixed with compact
218
+ existing = User.load_multi(ids).compact
219
+ # Only non-nil objects
220
+ ```
221
+
222
+ ## Performance Comparison
223
+
224
+ ### Single Object Loading
225
+
226
+ | Method | Commands | Round Trips | Use Case |
227
+ |--------|----------|-------------|----------|
228
+ | `find_by_id(id)` (default) | 2 | 2 | Safe, defensive code |
229
+ | `find_by_id(id, check_exists: false)` | 1 | 1 | Performance-critical |
230
+ | `load_multi([id])` | 1 | 1 | Bulk API consistency |
231
+
232
+ ### Bulk Loading (14 Objects)
233
+
234
+ | Method | Commands | Round Trips | Improvement |
235
+ |--------|----------|-------------|-------------|
236
+ | `ids.map { \|id\| find(id) }` | 28 | 28 | Baseline |
237
+ | `ids.map { \|id\| find(id, check_exists: false) }` | 14 | 14 | 50% reduction |
238
+ | `load_multi(ids)` | 14 | 1 | 96% reduction |
239
+
240
+ ### Real-World Example
241
+
242
+ Loading customer metadata (your use case):
243
+
244
+ ```ruby
245
+ # Get metadata IDs from sorted set (1 command)
246
+ metadata_ids = customer.metadata.rangebyscore(start_time, end_time)
247
+ # => 14 metadata IDs
248
+
249
+ # ❌ Traditional approach: 28 commands, 28 round trips
250
+ metadata = metadata_ids.map { |id| Metadata.find_by_id(id) }
251
+
252
+ # ✅ Optimized approach: 14 commands, 14 round trips (50% reduction)
253
+ metadata = metadata_ids.map { |id| Metadata.find_by_id(id, check_exists: false) }
254
+
255
+ # ✅✅ Best approach: 14 commands, 1 round trip (96% reduction)
256
+ metadata = Metadata.load_multi(metadata_ids).compact
257
+
258
+ # Total commands for full operation:
259
+ # Traditional: 1 (ZRANGEBYSCORE) + 28 (loading) = 29 commands
260
+ # Optimized: 1 (ZRANGEBYSCORE) + 1 (pipelined batch) = 2 commands
261
+ # Improvement: 93% reduction
262
+ ```
263
+
264
+ ## Best Practices
265
+
266
+ ### 1. Use load_multi for Bulk Operations
267
+
268
+ **Always prefer** `load_multi` when loading multiple objects:
269
+
270
+ ```ruby
271
+ # ❌ Avoid
272
+ domain_ids = team.domains.members
273
+ domains = domain_ids.map { |id| Domain.find_by_id(id) }
274
+
275
+ # ✅ Better
276
+ domain_ids = team.domains.members
277
+ domains = Domain.load_multi(domain_ids).compact
278
+ ```
279
+
280
+ ### 2. Use check_exists: false for Trusted References
281
+
282
+ When loading objects from known-to-exist references:
283
+
284
+ ```ruby
285
+ # Objects from sorted set members
286
+ participant_ids = event.participants.members
287
+ participants = participant_ids.map { |id|
288
+ User.find_by_id(id, check_exists: false)
289
+ }
290
+
291
+ # Even better with load_multi
292
+ participants = User.load_multi(participant_ids).compact
293
+ ```
294
+
295
+ ### 3. Keep Default Behavior for Defensive Code
296
+
297
+ Use default `check_exists: true` when:
298
+ - Loading from user input
299
+ - Defensive/paranoid code paths
300
+ - Single object lookups where optimization doesn't matter
301
+ - Initial development before optimization phase
302
+
303
+ ```ruby
304
+ # User input - keep safe mode
305
+ user = User.find_by_id(params[:user_id])
306
+
307
+ # Internal lookup - optimize
308
+ user = User.find_by_id(session.user_id, check_exists: false)
309
+ ```
310
+
311
+ ### 4. Compact Results Appropriately
312
+
313
+ Handle nils based on your requirements:
314
+
315
+ ```ruby
316
+ # When all objects should exist (raise on missing)
317
+ users = User.load_multi(ids)
318
+ if users.any?(&:nil?)
319
+ raise "Missing users: #{ids.zip(users).select { |_, u| u.nil? }}"
320
+ end
321
+
322
+ # When missing objects are acceptable
323
+ existing_users = User.load_multi(ids).compact
324
+
325
+ # When you need to track which are missing
326
+ results = ids.zip(User.load_multi(ids))
327
+ results.each do |id, user|
328
+ if user.nil?
329
+ logger.warn "User #{id} not found"
330
+ else
331
+ process_user(user)
332
+ end
333
+ end
334
+ ```
335
+
336
+ ### 5. Measure Before Optimizing
337
+
338
+ Profile your application to identify bottlenecks:
339
+
340
+ ```ruby
341
+ # Add timing to measure impact
342
+ require 'benchmark'
343
+
344
+ ids = (1..100).to_a
345
+
346
+ # Traditional
347
+ traditional_time = Benchmark.realtime do
348
+ users = ids.map { |id| User.find_by_id(id) }
349
+ end
350
+
351
+ # Optimized
352
+ optimized_time = Benchmark.realtime do
353
+ users = User.load_multi(ids)
354
+ end
355
+
356
+ puts "Traditional: #{traditional_time}s"
357
+ puts "Optimized: #{optimized_time}s"
358
+ puts "Speedup: #{(traditional_time / optimized_time).round(1)}x"
359
+ ```
360
+
361
+ ## Implementation Details
362
+
363
+ ### Empty Hash Detection
364
+
365
+ When `check_exists: false`, non-existent keys are detected via empty hash:
366
+
367
+ ```ruby
368
+ # Non-existent key
369
+ hash = redis.hgetall("user:missing:object")
370
+ # => {} # Empty hash indicates key doesn't exist
371
+
372
+ # Existing key with no fields (edge case)
373
+ redis.hset("user:empty:object", "placeholder", "")
374
+ redis.hdel("user:empty:object", "placeholder")
375
+ hash = redis.hgetall("user:empty:object")
376
+ # => {} # Also empty, but key technically exists
377
+
378
+ # Both cases safely return nil
379
+ ```
380
+
381
+ **Note**: In practice, Familia objects always have fields, so empty hashes reliably indicate non-existent keys.
382
+
383
+ ### Pipelining vs Individual Commands
384
+
385
+ **Individual commands**:
386
+ ```ruby
387
+ # Each command is a separate round trip
388
+ ids.each do |id|
389
+ key = "user:#{id}:object"
390
+ redis.hgetall(key) # Round trip 1, 2, 3, ...
391
+ end
392
+ ```
393
+
394
+ **Pipelined commands**:
395
+ ```ruby
396
+ # All commands in single round trip
397
+ redis.pipelined do |pipeline|
398
+ ids.each do |id|
399
+ key = "user:#{id}:object"
400
+ pipeline.hgetall(key) # Queued locally
401
+ end
402
+ end # Single round trip with all commands
403
+ ```
404
+
405
+ ### Field Deserialization
406
+
407
+ All optimized methods use the same deserialization logic as standard loading:
408
+
409
+ ```ruby
410
+ # All field types are properly handled
411
+ user = User.load_multi([123]).first
412
+ user.age # Integer field correctly deserialized
413
+ user.active # Boolean field correctly deserialized
414
+ user.metadata # Hash field correctly deserialized
415
+ user.tags # Array field correctly deserialized
416
+ ```
417
+
418
+ **Technical details**:
419
+ - Uses `initialize_with_keyword_args_deserialize_value` internally
420
+ - JSON deserialization for all field values
421
+ - Proper type preservation (Integer, Boolean, Hash, Array, nil)
422
+
423
+ ## Migration Guide
424
+
425
+ ### Identifying Optimization Opportunities
426
+
427
+ Look for these patterns in your codebase:
428
+
429
+ ```ruby
430
+ # Pattern 1: Mapping over collection of IDs
431
+ ids.map { |id| Model.find_by_id(id) }
432
+ ids.map { |id| Model.find(id) }
433
+ ids.map { |id| Model.load(id) }
434
+
435
+ # Pattern 2: Loading from sorted set members
436
+ member_ids = sorted_set.members
437
+ members = member_ids.map { |id| Model.find(id) }
438
+
439
+ # Pattern 3: Loading from set members
440
+ tag_ids = set.members
441
+ tags = tag_ids.map { |id| Tag.find(id) }
442
+
443
+ # Pattern 4: Processing query results
444
+ user_ids = redis.zrangebyscore("users:active", start_score, end_score)
445
+ users = user_ids.map { |id| User.find(id) }
446
+ ```
447
+
448
+ ### Step-by-Step Migration
449
+
450
+ **Step 1**: Identify bulk loading patterns
451
+ ```bash
452
+ # Search your codebase
453
+ grep -r "\.map.*find_by_id" app/
454
+ grep -r "\.map.*\.find(" app/
455
+ ```
456
+
457
+ **Step 2**: Replace with `load_multi`
458
+ ```ruby
459
+ # Before
460
+ domains = domain_ids.map { |id| Domain.find(id) }
461
+
462
+ # After
463
+ domains = Domain.load_multi(domain_ids).compact
464
+ ```
465
+
466
+ **Step 3**: Profile the change
467
+ ```ruby
468
+ # Add logging temporarily
469
+ start = Time.now
470
+ domains = Domain.load_multi(domain_ids).compact
471
+ duration = Time.now - start
472
+ Rails.logger.info "Loaded #{domains.size} domains in #{duration}s"
473
+ ```
474
+
475
+ **Step 4**: Deploy and monitor
476
+ - Check error rates remain stable
477
+ - Monitor Redis command counts
478
+ - Verify response times improve
479
+
480
+ ### Backwards Compatibility
481
+
482
+ All changes are fully backwards compatible:
483
+
484
+ ```ruby
485
+ # Existing code continues to work
486
+ user = User.find_by_id(123) # Still works, still safe
487
+
488
+ # New optional parameter
489
+ user = User.find_by_id(123, check_exists: false) # Opt-in optimization
490
+
491
+ # New methods
492
+ users = User.load_multi(ids) # New method, doesn't break existing code
493
+ ```
494
+
495
+ ## Common Patterns
496
+
497
+ ### Pattern 1: Loading Related Objects
498
+
499
+ ```ruby
500
+ class Team < Familia::Horreum
501
+ identifier_field :team_id
502
+ field :team_id, :name
503
+ sorted_set :member_ids # Stores user IDs with scores
504
+ end
505
+
506
+ # Efficient member loading
507
+ def load_team_members(team)
508
+ member_ids = team.member_ids.members
509
+ User.load_multi(member_ids).compact
510
+ end
511
+
512
+ # With sorting by score
513
+ def load_recent_members(team, limit: 10)
514
+ member_ids = team.member_ids.revrange(0, limit - 1)
515
+ User.load_multi(member_ids).compact
516
+ end
517
+ ```
518
+
519
+ ### Pattern 2: Filtered Loading
520
+
521
+ ```ruby
522
+ # Load and filter in one pass
523
+ def load_active_users(user_ids)
524
+ User.load_multi(user_ids).compact.select(&:active?)
525
+ end
526
+
527
+ # Load with transformation
528
+ def load_user_emails(user_ids)
529
+ User.load_multi(user_ids).compact.map(&:email)
530
+ end
531
+
532
+ # Load with stats
533
+ def load_with_stats(user_ids)
534
+ users = User.load_multi(user_ids)
535
+ {
536
+ found: users.compact.size,
537
+ missing: users.count(&:nil?),
538
+ users: users.compact
539
+ }
540
+ end
541
+ ```
542
+
543
+ ### Pattern 3: Batch Processing
544
+
545
+ ```ruby
546
+ # Process in batches to avoid memory issues
547
+ def process_all_users(batch_size: 100)
548
+ user_ids = User.instances.members # Get all user IDs
549
+
550
+ user_ids.each_slice(batch_size) do |batch_ids|
551
+ users = User.load_multi(batch_ids).compact
552
+
553
+ users.each do |user|
554
+ process_user(user)
555
+ end
556
+ end
557
+ end
558
+ ```
559
+
560
+ ### Pattern 4: Multi-Model Loading
561
+
562
+ ```ruby
563
+ # Load related objects across different models
564
+ def load_dashboard_data(user_id)
565
+ user = User.find_by_id(user_id, check_exists: false)
566
+
567
+ # Load user's teams and domains in parallel
568
+ team_ids = user.team_ids.members
569
+ domain_ids = user.domain_ids.members
570
+
571
+ teams = Team.load_multi(team_ids).compact
572
+ domains = Domain.load_multi(domain_ids).compact
573
+
574
+ {
575
+ user: user,
576
+ teams: teams,
577
+ domains: domains
578
+ }
579
+ end
580
+ ```
581
+
582
+ ## Troubleshooting
583
+
584
+ ### Issue: Unexpected nils in Results
585
+
586
+ **Problem**: `load_multi` returns more nils than expected
587
+
588
+ **Causes**:
589
+ 1. Objects genuinely don't exist
590
+ 2. Wrong identifier field being used
591
+ 3. Suffix mismatch
592
+
593
+ **Solution**:
594
+ ```ruby
595
+ # Debug which objects are missing
596
+ ids = [1, 2, 3]
597
+ results = User.load_multi(ids)
598
+ missing_ids = ids.zip(results).select { |_, obj| obj.nil? }.map(&:first)
599
+ puts "Missing: #{missing_ids}"
600
+
601
+ # Check if keys exist
602
+ missing_ids.each do |id|
603
+ key = User.dbkey(id)
604
+ exists = Familia.redis.exists(key)
605
+ puts "#{key}: #{exists}"
606
+ end
607
+
608
+ # Verify correct suffix
609
+ User.suffix # Check what suffix the class uses
610
+ ```
611
+
612
+ ### Issue: Performance Not Improving
613
+
614
+ **Problem**: `load_multi` doesn't seem faster
615
+
616
+ **Causes**:
617
+ 1. Small dataset (overhead of pipelining)
618
+ 2. Local Redis instance (network latency minimal)
619
+ 3. Not actually using `load_multi`
620
+
621
+ **Solution**:
622
+ ```ruby
623
+ # Benchmark with realistic dataset
624
+ require 'benchmark'
625
+
626
+ # Create test data
627
+ ids = (1..100).map do |i|
628
+ user = User.new(user_id: i, name: "User #{i}")
629
+ user.save
630
+ i
631
+ end
632
+
633
+ # Compare approaches
634
+ Benchmark.bm(20) do |x|
635
+ x.report("traditional:") { ids.map { |id| User.find(id) } }
636
+ x.report("check_exists:false:") { ids.map { |id| User.find(id, check_exists: false) } }
637
+ x.report("load_multi:") { User.load_multi(ids) }
638
+ end
639
+ ```
640
+
641
+ ### Issue: Order Not Preserved
642
+
643
+ **Problem**: Results appear in wrong order
644
+
645
+ **Cause**: Using `compact` changes indices
646
+
647
+ **Solution**:
648
+ ```ruby
649
+ # ❌ Loses position information
650
+ ids = [1, 2, 3]
651
+ users = User.load_multi(ids).compact # [<User:1>, <User:3>] if 2 is missing
652
+
653
+ # ✅ Preserve positions with zip
654
+ ids.zip(User.load_multi(ids)).each do |id, user|
655
+ if user
656
+ puts "Processing user #{id}"
657
+ process_user(user)
658
+ else
659
+ puts "User #{id} not found"
660
+ end
661
+ end
662
+
663
+ # ✅ Or track original indices
664
+ users_with_ids = User.load_multi(ids).map.with_index do |user, idx|
665
+ [ids[idx], user]
666
+ end
667
+ ```
668
+
669
+ ## See Also
670
+
671
+ - [Core Field System](core-field-system.md) - Understanding Familia's field types
672
+ - [Relationships Guide](feature-relationships.md) - Loading related objects
673
+ - [Time Utilities](time-utilities.md) - For score-based queries with timestamps
674
+ - [Implementation Guide](implementation.md) - Advanced Familia internals