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
@@ -2,636 +2,608 @@
2
2
 
3
3
  > **💡 Quick Reference**
4
4
  >
5
- > Enable integration with external systems and legacy databases:
5
+ > Generate deterministic, public-facing identifiers from internal objid:
6
6
  > ```ruby
7
- > class ExternalUser < Familia::Horreum
7
+ > class User < Familia::Horreum
8
+ > feature :object_identifier
8
9
  > feature :external_identifier
9
- > field :internal_id, :external_id, :name, :sync_status
10
+ > field :email, :name
10
11
  > end
11
12
  > ```
12
13
 
13
14
  ## Overview
14
15
 
15
- The External Identifier feature provides seamless integration between Familia objects and external systems. Whether you're migrating from a legacy database, integrating with third-party APIs, or maintaining bidirectional synchronization with external services, this feature handles identifier mapping, validation, and sync status tracking.
16
+ The External Identifier feature provides deterministic, public-facing identifiers that are securely derived from internal `objid` values. These shorter, URL-safe identifiers are perfect for APIs, public URLs, and external integrations where you want to hide internal implementation details while maintaining deterministic lookups.
16
17
 
17
18
  ## Why Use External Identifiers?
18
19
 
19
- **Legacy Integration**: Migrate existing systems while maintaining references to original identifiers.
20
+ **Security Through Obscurity**: Hide internal UUID structure and potential timestamp information from public interfaces.
20
21
 
21
- **API Synchronization**: Keep local objects synchronized with external services using their native identifiers.
22
+ **URL-Friendly**: Generate compact, base36-encoded identifiers safe for use in URLs and APIs.
22
23
 
23
- **Dual-Key Strategy**: Maintain both internal Familia identifiers and external system identifiers for robust integration.
24
+ **Deterministic Generation**: Same `objid` always produces the same `extid` for reliable lookups.
24
25
 
25
- **Sync Tracking**: Built-in status tracking for synchronization operations and failure handling.
26
+ **Bidirectional Mapping**: Automatic lookup tables enable finding objects by external ID.
26
27
 
27
- **Validation**: Ensure external identifiers meet format requirements and business rules.
28
+ **Customizable Format**: Configure prefix and format patterns to match your naming conventions.
28
29
 
29
- ## Quick Start
30
+ ## Dependencies
30
31
 
31
- ### Basic External ID Mapping
32
+ External identifiers require the Object Identifier feature:
32
33
 
33
34
  ```ruby
34
- class Customer < Familia::Horreum
35
+ class MyModel < Familia::Horreum
36
+ feature :object_identifier # Required first
37
+ feature :external_identifier # Then enable external IDs
38
+ end
39
+ ```
40
+
41
+ ## Basic Usage
42
+
43
+ ### Default External Identifier
44
+
45
+ ```ruby
46
+ class User < Familia::Horreum
47
+ feature :object_identifier
35
48
  feature :external_identifier
36
49
 
37
- identifier_field :internal_id
38
- field :internal_id, :external_id, :name, :email, :sync_status
50
+ field :email, :name
39
51
  end
40
52
 
41
- # Create with external mapping
42
- customer = Customer.new(
43
- internal_id: SecureRandom.uuid,
44
- external_id: "ext_customer_12345",
45
- name: "Acme Corporation",
46
- email: "contact@acme.com"
47
- )
48
- customer.save # Automatically creates bidirectional mapping
53
+ user = User.new(email: 'alice@example.com', name: 'Alice')
54
+ user.save
55
+
56
+ # Internal identifier (long, detailed)
57
+ user.objid # => "01234567-89ab-cdef-1234-567890abcdef"
49
58
 
50
- # Find by external ID
51
- found_customer = Customer.find_by_external_id("ext_customer_12345")
52
- puts found_customer.name # => "Acme Corporation"
59
+ # External identifier (short, public-safe, 29 chars total: 4 prefix + 25 ID)
60
+ user.extid # => "ext_abc123def456ghi789jkl012" (deterministic from objid)
61
+
62
+ # Always produces the same extid from the same objid
63
+ user2 = User.new(objid: user.objid, email: 'alice@example.com')
64
+ user2.extid # => "ext_abc123def456ghi789jkl012" (identical)
53
65
  ```
54
66
 
55
- ### Legacy Database Migration
67
+ ### Finding by External ID
56
68
 
57
69
  ```ruby
58
- class LegacyAccount < Familia::Horreum
59
- feature :external_identifier, prefix: "legacy"
70
+ # Create and save user
71
+ user = User.new(email: 'bob@example.com')
72
+ user.save
73
+ puts user.extid # => "ext_xyz789abc123def456ghi123"
60
74
 
61
- identifier_field :familia_id
62
- field :familia_id, :legacy_account_id, :username, :migration_status
75
+ # Find by external identifier
76
+ found_user = User.find_by_extid('ext_xyz789abc123def456ghi123')
77
+ found_user.email # => "bob@example.com"
63
78
 
64
- # External ID validation
65
- def valid_external_id?
66
- legacy_account_id.present? &&
67
- legacy_account_id.match?(/^LAC[A-Z]{2}\d{8}$/)
68
- end
79
+ # Returns nil if not found
80
+ missing = User.find_by_extid('ext_nonexistent') # => nil
81
+ ```
82
+
83
+ ### Long-form Methods
84
+
85
+ ```ruby
86
+ # Aliases for clarity
87
+ user.external_identifier # Same as user.extid
88
+ user.external_identifier = 'new' # Same as user.extid = 'new'
89
+ ```
90
+
91
+ ## Custom Format Templates
92
+
93
+ ### Custom Prefix
94
+
95
+ ```ruby
96
+ class Customer < Familia::Horreum
97
+ feature :object_identifier
98
+ feature :external_identifier, format: 'cust_%{id}'
99
+
100
+ field :company_name, :email
69
101
  end
70
102
 
71
- # Migrate legacy data
72
- legacy_user = LegacyAccount.new(
73
- familia_id: SecureRandom.uuid,
74
- legacy_account_id: "LACUS12345678",
75
- username: "john_doe"
76
- )
103
+ customer = Customer.new(company_name: 'Acme Corp')
104
+ customer.save
105
+ customer.extid # => "cust_abc123def456ghi789jkl012" (30 chars: 5 prefix + 25 ID)
106
+ ```
107
+
108
+ ### Hyphen Separator
109
+
110
+ ```ruby
111
+ class APIKey < Familia::Horreum
112
+ feature :object_identifier
113
+ feature :external_identifier, format: 'api-%{id}'
77
114
 
78
- if legacy_user.valid_external_id?
79
- legacy_user.save
80
- legacy_user.mark_migration_completed
115
+ field :name, :permissions
81
116
  end
117
+
118
+ key = APIKey.new(name: 'Production API Key')
119
+ key.save
120
+ key.extid # => "api-abc123def456ghi789jkl012" (29 chars: 4 prefix + 25 ID)
82
121
  ```
83
122
 
84
- ## Configuration Options
123
+ ### Version Prefix
124
+
125
+ ```ruby
126
+ class Resource < Familia::Horreum
127
+ feature :object_identifier
128
+ feature :external_identifier, format: 'v2/%{id}'
129
+
130
+ field :resource_type, :data
131
+ end
132
+
133
+ resource = Resource.new(resource_type: 'document')
134
+ resource.save
135
+ resource.extid # => "v2/abc123def456ghi789jkl012" (28 chars: 3 prefix + 25 ID)
136
+ ```
85
137
 
86
- ### Basic Configuration
138
+ ### No Prefix
87
139
 
88
140
  ```ruby
89
- class ExternalResource < Familia::Horreum
90
- feature :external_identifier,
91
- validation_pattern: /^ext_\d{6,}$/,
92
- source_system: "CustomerAPI",
93
- bidirectional: true # Default
141
+ class SimpleModel < Familia::Horreum
142
+ feature :object_identifier
143
+ feature :external_identifier, format: '%{id}'
94
144
 
95
- field :resource_id, :external_id, :data
145
+ field :data
96
146
  end
147
+
148
+ model = SimpleModel.new(data: 'test')
149
+ model.save
150
+ model.extid # => "abc123def456ghi789jkl012" (25 chars: just the ID)
97
151
  ```
98
152
 
99
- **Configuration Parameters:**
100
- - `validation_pattern`: Regex pattern for external ID validation
101
- - `source_system`: Name of the external system (for logging/debugging)
102
- - `bidirectional`: Enable bidirectional mapping (default: true)
103
- - `prefix`: Optional prefix for mapping keys
153
+ ## Security Model
104
154
 
105
- ### Advanced Validation
155
+ ### Deterministic but Obscured
106
156
 
107
- ```ruby
108
- class StrictExternalUser < Familia::Horreum
109
- feature :external_identifier,
110
- validation_pattern: /^user_[a-z0-9]{8,16}$/,
111
- source_system: "AuthService"
157
+ External identifiers use cryptographic techniques to obscure the relationship between `objid` and `extid`:
112
158
 
113
- field :user_id, :external_id, :username, :permissions
159
+ 1. **SHA-256 Seeding**: The `objid` is hashed to create a uniform seed
160
+ 2. **PRNG Function**: A pseudorandom number generator acts as a deterministic transformation
161
+ 3. **Base36 Encoding**: Result is encoded as URL-safe, compact string
114
162
 
115
- # Custom validation beyond pattern matching
116
- def validate_external_id!
117
- return false unless valid_external_id_format?
163
+ This ensures:
164
+ - Same `objid` always produces same `extid` (deterministic)
165
+ - No discernible mathematical correlation between `objid` and `extid` (secure)
166
+ - Cannot reverse-engineer `objid` from `extid` (one-way)
118
167
 
119
- # Check against blacklist
120
- blacklisted_ids = ["user_test", "user_admin", "user_system"]
121
- return false if blacklisted_ids.include?(external_id)
168
+ ### Generated Format Details
122
169
 
123
- # Verify with external service
124
- external_service_response = AuthService.verify_user_id(external_id)
125
- external_service_response['valid'] == true
126
- end
170
+ The ID portion (after any prefix) is always:
171
+ - **Length**: Exactly 25 characters
172
+ - **Characters**: Lowercase alphanumeric only (`0-9a-z`)
173
+ - **Encoding**: Base36 representation of 128-bit random data
174
+ - **Pattern**: `/[0-9a-z]{25}/`
127
175
 
128
- private
176
+ ### Provenance Validation
129
177
 
130
- def valid_external_id_format?
131
- external_id.present? && external_id.match?(self.class.validation_pattern)
132
- end
133
- end
178
+ External identifiers can only be derived from `objid` values with known provenance:
179
+
180
+ ```ruby
181
+ # ✅ Valid - objid from ObjectIdentifier feature
182
+ user = User.new # objid generated by UUID v7
183
+ user.extid # => Works fine
184
+
185
+ # ❌ Invalid - objid of unknown origin
186
+ user = User.new
187
+ user.instance_variable_set(:@objid, 'unknown-source-id')
188
+ user.extid # => ExternalIdentifierError: objid provenance unknown
134
189
  ```
135
190
 
136
- ## Mapping and Lookup Operations
191
+ ## Automatic Lookup Management
137
192
 
138
193
  ### Bidirectional Mapping
139
194
 
140
- External identifiers automatically maintain bidirectional mappings for efficient lookups:
195
+ The feature automatically maintains lookup tables:
141
196
 
142
197
  ```ruby
143
198
  class Product < Familia::Horreum
199
+ feature :object_identifier
144
200
  feature :external_identifier
145
- field :product_id, :external_sku, :name, :price
201
+
202
+ field :name, :price
146
203
  end
147
204
 
148
- product = Product.create(
149
- product_id: "familia_prod_123",
150
- external_sku: "SKU-ABC-789",
151
- name: "Widget Pro"
152
- )
205
+ product = Product.new(name: 'Widget', price: 29.99)
206
+ product.save
207
+
208
+ # Lookup table is automatically updated
209
+ Product.extid_lookup.class # => Familia::DataType::HashKey
210
+ Product.extid_lookup.length # => 1
211
+
212
+ # Mapping: extid -> objid
213
+ extid = product.extid
214
+ Product.extid_lookup[extid] # => product.objid
215
+ ```
153
216
 
154
- # Automatic bidirectional mapping is created:
155
- # external_id_mapping["SKU-ABC-789"] = "familia_prod_123"
156
- # internal_id_mapping["familia_prod_123"] = "SKU-ABC-789"
217
+ ### Cleanup on Destroy
157
218
 
158
- # Fast lookups in both directions
159
- by_external = Product.find_by_external_id("SKU-ABC-789")
160
- by_internal = Product.load("familia_prod_123")
219
+ ```ruby
220
+ product.destroy!
161
221
 
162
- # Both return the same object
163
- by_external.product_id == by_internal.product_id # => true
222
+ # Lookup entry is automatically cleaned up
223
+ Product.extid_lookup[extid] # => nil
164
224
  ```
165
225
 
166
- ### Batch Operations
226
+ ## Implementation Details
167
227
 
168
- Efficiently handle multiple external identifier operations:
228
+ ### Lazy Generation
229
+
230
+ External identifiers are generated lazily when first accessed:
169
231
 
170
232
  ```ruby
171
- class BulkImporter
172
- def self.import_external_users(external_data_array)
173
- external_ids = external_data_array.map { |data| data['external_id'] }
233
+ user = User.new(email: 'test@example.com')
174
234
 
175
- # Batch lookup existing users
176
- existing_users = ExternalUser.multiget_by_external_ids(external_ids)
177
- existing_external_ids = existing_users.compact.map(&:external_id)
235
+ # extid is not generated until accessed
236
+ user.instance_variable_get(:@extid) # => nil
178
237
 
179
- # Process only new users
180
- new_data = external_data_array.reject do |data|
181
- existing_external_ids.include?(data['external_id'])
182
- end
238
+ # First access triggers generation
239
+ user.extid # => "ext_abc123..." (generated from objid)
183
240
 
184
- # Batch create new users
185
- new_users = new_data.map do |data|
186
- ExternalUser.new(
187
- internal_id: SecureRandom.uuid,
188
- external_id: data['external_id'],
189
- name: data['name'],
190
- email: data['email']
191
- )
192
- end
241
+ # Subsequent access returns cached value
242
+ user.extid # => "ext_abc123..." (same value)
243
+ ```
193
244
 
194
- # Batch save with transaction
195
- ExternalUser.transaction do |redis|
196
- new_users.each(&:save)
197
- end
245
+ ### Preservation During Initialization
198
246
 
199
- new_users
200
- end
201
- end
247
+ Values provided during initialization are preserved:
248
+
249
+ ```ruby
250
+ # Loading from database with existing extid
251
+ user = User.new(
252
+ objid: existing_objid,
253
+ extid: existing_extid,
254
+ email: 'test@example.com'
255
+ )
256
+
257
+ # Existing extid is preserved, not regenerated
258
+ user.extid # => existing_extid (not derived)
202
259
  ```
203
260
 
204
- ## Synchronization Status Tracking
261
+ ### Database Persistence
205
262
 
206
- ### Built-in Sync Status Management
263
+ External identifiers are automatically stored in the object's hash:
207
264
 
208
265
  ```ruby
209
- class SyncableResource < Familia::Horreum
210
- feature :external_identifier
266
+ user = User.new(email: 'test@example.com')
267
+ user.save
211
268
 
212
- field :resource_id, :external_id, :data, :sync_status, :last_sync_at, :sync_error
213
-
214
- def sync_to_external!
215
- mark_sync_pending
216
-
217
- begin
218
- # Simulate external API call
219
- response = ExternalAPI.update_resource(external_id, data: self.data)
220
-
221
- if response.success?
222
- mark_sync_completed
223
- self.last_sync_at = Familia.now.to_i
224
- save
225
- else
226
- mark_sync_failed(response.error_message)
227
- end
228
- rescue => e
229
- mark_sync_failed(e.message)
230
- raise
231
- end
232
- end
233
-
234
- def sync_from_external!
235
- mark_sync_pending
236
-
237
- begin
238
- external_data = ExternalAPI.get_resource(external_id)
239
- self.data = external_data['data']
240
- mark_sync_completed
241
- save
242
- rescue => e
243
- mark_sync_failed(e.message)
244
- raise
245
- end
246
- end
269
+ # extid is stored alongside other fields
270
+ user.to_h # => { objid: "...", extid: "ext_abc123...", email: "..." }
271
+ ```
247
272
 
248
- def needs_sync?
249
- sync_status != 'completed' ||
250
- (last_sync_at && (Familia.now.to_i - last_sync_at) > 1.hour)
251
- end
252
- end
273
+ ## Error Handling
253
274
 
254
- # Usage
255
- resource = SyncableResource.find_by_external_id("ext_123")
275
+ ### Missing ObjectIdentifier Feature
256
276
 
257
- if resource.needs_sync?
258
- resource.sync_from_external!
277
+ ```ruby
278
+ class BadModel < Familia::Horreum
279
+ feature :external_identifier # Missing :object_identifier dependency
259
280
  end
260
281
 
261
- puts resource.sync_status # => "completed", "pending", "failed"
282
+ # => Error during class definition
262
283
  ```
263
284
 
264
- ### Sync Status Methods
265
-
266
- The external identifier feature provides these built-in status methods:
285
+ ### Unknown Provenance
267
286
 
268
287
  ```ruby
269
- # Status management
270
- object.mark_sync_pending
271
- object.mark_sync_completed
272
- object.mark_sync_failed(error_message)
288
+ user = User.new
289
+ # Manually set objid with unknown provenance
290
+ user.instance_variable_set(:@objid, 'manual-id')
273
291
 
274
- # Status checking
275
- object.sync_pending? # => true/false
276
- object.sync_completed? # => true/false
277
- object.sync_failed? # => true/false
292
+ user.extid
293
+ # => ExternalIdentifierError: Cannot derive external identifier: objid provenance unknown
294
+ ```
295
+
296
+ ### Invalid Objid Format
278
297
 
279
- # Error handling
280
- object.sync_error # => error message if failed
281
- object.clear_sync_error # Reset error state
298
+ ```ruby
299
+ # For custom generators, objid must be hexadecimal
300
+ user.instance_variable_set(:@objid, 'not-hex-format!')
301
+ user.extid
302
+ # => ExternalIdentifierError: Cannot normalize objid from custom generator
282
303
  ```
283
304
 
284
- ## Integration Patterns
305
+ ## Testing Strategies
285
306
 
286
- ### API Integration with Webhooks
307
+ ### Basic Functionality
287
308
 
288
309
  ```ruby
289
- class WebhookHandler
290
- def self.handle_external_update(webhook_data)
291
- external_id = webhook_data['resource_id']
292
- resource = ExternalResource.find_by_external_id(external_id)
293
-
294
- if resource
295
- # Update existing resource
296
- resource.data = webhook_data['data']
297
- resource.mark_sync_completed
298
- resource.save
299
- else
300
- # Create new resource from webhook
301
- resource = ExternalResource.create(
302
- internal_id: SecureRandom.uuid,
303
- external_id: external_id,
304
- data: webhook_data['data']
305
- )
306
- resource.mark_sync_completed
307
- end
310
+ class ExternalIdentifierTest < Minitest::Test
311
+ def setup
312
+ @user = User.new(email: 'test@example.com')
313
+ @user.save
314
+ end
315
+
316
+ def test_deterministic_generation
317
+ extid1 = @user.extid
318
+ extid2 = @user.extid
308
319
 
309
- resource
320
+ assert_equal extid1, extid2
310
321
  end
311
- end
312
322
 
313
- # Webhook endpoint
314
- post '/webhook/external_updates' do
315
- webhook_data = JSON.parse(request.body.read)
316
- WebhookHandler.handle_external_update(webhook_data)
317
- status 200
323
+ def test_same_objid_produces_same_extid
324
+ user2 = User.new(objid: @user.objid, email: 'other@example.com')
325
+
326
+ assert_equal @user.extid, user2.extid
327
+ end
328
+
329
+ def test_find_by_extid
330
+ found_user = User.find_by_extid(@user.extid)
331
+
332
+ assert_equal @user.objid, found_user.objid
333
+ assert_equal @user.email, found_user.email
334
+ end
335
+
336
+ def test_format_validation
337
+ # Default format: 'ext_' prefix + 25 character base36 ID
338
+ assert_match(/\Aext_[0-9a-z]{25}\z/, @user.extid)
339
+ end
318
340
  end
319
341
  ```
320
342
 
321
- ### Legacy Database Migration
343
+ ### Custom Format Testing
322
344
 
323
345
  ```ruby
324
- class LegacyMigration
325
- def self.migrate_customers_from_legacy_db
326
- # Connect to legacy database
327
- legacy_db = Sequel.connect(ENV['LEGACY_DATABASE_URL'])
346
+ class CustomFormatTest < Minitest::Test
347
+ def test_custom_prefix
348
+ customer = Customer.new(company_name: 'Test Corp')
349
+ customer.save
328
350
 
329
- legacy_db[:customers].each do |legacy_row|
330
- # Check if already migrated
331
- existing = Customer.find_by_external_id(legacy_row[:customer_id])
332
- next if existing
351
+ # Custom format: 'cust_' prefix + 25 character base36 ID
352
+ assert_match(/\Acust_[0-9a-z]{25}\z/, customer.extid)
353
+ end
333
354
 
334
- # Create new Familia object
335
- customer = Customer.new(
336
- internal_id: SecureRandom.uuid,
337
- external_id: legacy_row[:customer_id].to_s,
338
- name: legacy_row[:company_name],
339
- email: legacy_row[:email],
340
- created_at: legacy_row[:created_at].to_i
341
- )
355
+ def test_no_prefix_format
356
+ model = SimpleModel.new(data: 'test')
357
+ model.save
342
358
 
343
- if customer.valid_external_id?
344
- customer.save
345
- customer.mark_migration_completed
346
- puts "Migrated customer: #{customer.external_id}"
347
- else
348
- puts "Invalid external ID: #{legacy_row[:customer_id]}"
349
- end
350
- end
359
+ # No prefix format: just 25 character base36 ID
360
+ assert_match(/\A[0-9a-z]{25}\z/, model.extid)
361
+ end
362
+
363
+ def test_hyphen_separator
364
+ key = APIKey.new(name: 'Test Key')
365
+ key.save
366
+
367
+ # Hyphen format: 'api-' prefix + 25 character base36 ID
368
+ assert_match(/\Aapi-[0-9a-z]{25}\z/, key.extid)
351
369
  end
352
370
  end
353
371
  ```
354
372
 
355
- ### Multi-System Integration
373
+ ### Security Testing
356
374
 
357
375
  ```ruby
358
- class MultiSystemResource < Familia::Horreum
359
- feature :external_identifier
376
+ class SecurityTest < Minitest::Test
377
+ def test_no_correlation_between_objid_and_extid
378
+ users = 10.times.map do
379
+ User.new(email: "user#{rand(1000)}@example.com").tap(&:save)
380
+ end
360
381
 
361
- field :internal_id, :crm_id, :billing_id, :support_id, :name
382
+ objids = users.map(&:objid).sort
383
+ extids = users.map(&:extid).sort
362
384
 
363
- # Multiple external system mappings
364
- def crm_mapping
365
- @crm_mapping ||= ExternalIdMapping.new(self, :crm_id, "CRM_System")
366
- end
385
+ # Sorting should not preserve any correlation
386
+ # (This is a statistical test - might rarely fail by chance)
387
+ correlations = objids.zip(extids).count { |objid, extid|
388
+ objid[0..2] == extid[-3..-1] # Check if patterns match
389
+ }
367
390
 
368
- def billing_mapping
369
- @billing_mapping ||= ExternalIdMapping.new(self, :billing_id, "Billing_System")
391
+ assert correlations < 3, "Too many correlations detected: #{correlations}"
370
392
  end
371
393
 
372
- def support_mapping
373
- @support_mapping ||= ExternalIdMapping.new(self, :support_id, "Support_System")
374
- end
394
+ def test_cannot_reverse_engineer_objid
395
+ user = User.new(email: 'test@example.com')
396
+ user.save
375
397
 
376
- def sync_to_all_systems!
377
- [crm_mapping, billing_mapping, support_mapping].each do |mapping|
378
- mapping.sync_to_external!
379
- end
398
+ # Should not be able to derive objid from extid
399
+ # This test ensures no obvious mathematical relationship
400
+ refute_match user.objid[0..8], user.extid
380
401
  end
381
402
 
382
- class ExternalIdMapping
383
- def initialize(resource, id_field, system_name)
384
- @resource = resource
385
- @id_field = id_field
386
- @system_name = system_name
387
- end
403
+ def test_base36_format_consistency
404
+ user = User.new(email: 'test@example.com')
405
+ user.save
388
406
 
389
- def sync_to_external!
390
- external_id = @resource.send(@id_field)
391
- return unless external_id
392
-
393
- case @system_name
394
- when "CRM_System"
395
- CRMApi.update_contact(external_id, @resource.to_crm_format)
396
- when "Billing_System"
397
- BillingApi.update_customer(external_id, @resource.to_billing_format)
398
- when "Support_System"
399
- SupportApi.update_user(external_id, @resource.to_support_format)
400
- end
401
- end
407
+ extid_suffix = user.extid.sub(/^ext_/, '')
408
+
409
+ # Must be exactly 25 characters of base36
410
+ assert_equal 25, extid_suffix.length
411
+ assert_match(/\A[0-9a-z]{25}\z/, extid_suffix)
402
412
  end
403
413
  end
404
414
  ```
405
415
 
406
416
  ## Performance Considerations
407
417
 
408
- ### Efficient Batch Lookups
418
+ ### Lookup Table Size
409
419
 
410
- ```ruby
411
- # Instead of individual lookups
412
- external_ids = ["ext_1", "ext_2", "ext_3"]
413
- users = external_ids.map { |id| User.find_by_external_id(id) }
420
+ Each class with external identifiers maintains its own lookup table:
414
421
 
415
- # Use batch operations
416
- users = User.multiget_by_external_ids(external_ids)
422
+ ```ruby
423
+ # Monitor lookup table growth
424
+ puts User.extid_lookup.length # Number of extid mappings
425
+ puts Customer.extid_lookup.length # Separate table per class
417
426
  ```
418
427
 
419
- ### Caching Strategies
428
+ ### Batch Operations
429
+
430
+ For bulk operations, consider the lookup table overhead:
420
431
 
421
432
  ```ruby
422
- class CachedExternalResource < Familia::Horreum
423
- feature :external_identifier
433
+ # Each save updates the lookup table
434
+ 1000.times do |i|
435
+ User.new(email: "user#{i}@example.com").save
436
+ end
424
437
 
425
- # Cache external ID mappings
426
- def self.find_by_external_id_cached(external_id)
427
- cache_key = "external_id_mapping:#{external_id}"
438
+ # Consider batch cleanup if needed
439
+ # User.extid_lookup.clear if rebuilding from scratch
440
+ ```
428
441
 
429
- cached_internal_id = Familia.redis.get(cache_key)
430
- if cached_internal_id
431
- return load(cached_internal_id)
432
- end
442
+ ## Integration Patterns
443
+
444
+ ### API Controllers
433
445
 
434
- # Fallback to database lookup
435
- resource = find_by_external_id(external_id)
436
- if resource
437
- Familia.redis.setex(cache_key, 300, resource.identifier)
446
+ ```ruby
447
+ class UsersController < ApplicationController
448
+ # Use external IDs in URLs
449
+ def show
450
+ @user = User.find_by_extid(params[:id])
451
+
452
+ if @user.nil?
453
+ render json: { error: 'User not found' }, status: 404
454
+ else
455
+ render json: user_json(@user)
438
456
  end
457
+ end
458
+
459
+ private
439
460
 
440
- resource
461
+ def user_json(user)
462
+ {
463
+ id: user.extid, # Use extid in API responses
464
+ email: user.email,
465
+ name: user.name
466
+ }
441
467
  end
442
468
  end
443
469
  ```
444
470
 
445
- ### Index Optimization
471
+ ### URL Generation
446
472
 
447
473
  ```ruby
448
- class OptimizedExternalResource < Familia::Horreum
449
- feature :external_identifier
474
+ # In views/helpers
475
+ def user_path(user)
476
+ "/users/#{user.extid}" # Short, clean URLs
477
+ end
450
478
 
451
- # Use dedicated sorted sets for each status with timestamp scores
452
- sorted_set :pending_sync_resources,
453
- score: ->(obj) { obj.last_sync_at&.to_i || 0 }
454
- sorted_set :completed_sync_resources,
455
- score: ->(obj) { obj.last_sync_at&.to_i || 0 }
456
- sorted_set :failed_sync_resources,
457
- score: ->(obj) { obj.last_sync_at&.to_i || 0 }
458
-
459
- def self.pending_sync_resources(limit: 100)
460
- # Query resources that need syncing, ordered by oldest first
461
- pending_sync_resources.range(0, limit - 1).map { |id| load(id) }.compact
462
- end
479
+ # Instead of:
480
+ # "/users/01234567-89ab-cdef-1234-567890abcdef"
481
+ #
482
+ # Generate:
483
+ # "/users/ext_abc123def456ghi789jkl012"
484
+ ```
463
485
 
464
- def self.recently_synced(status:, limit: 100)
465
- # Get recently synced resources by status, newest first
466
- case status.to_s
467
- when 'pending'
468
- pending_sync_resources.revrange(0, limit - 1).map { |id| load(id) }.compact
469
- when 'completed'
470
- completed_sync_resources.revrange(0, limit - 1).map { |id| load(id) }.compact
471
- when 'failed'
472
- failed_sync_resources.revrange(0, limit - 1).map { |id| load(id) }.compact
473
- else
474
- []
475
- end
486
+ ### External System Integration
487
+
488
+ ```ruby
489
+ class WebhookHandler
490
+ def handle_external_callback(payload)
491
+ external_id = payload['user_id'] # From external system
492
+
493
+ user = User.find_by_extid(external_id)
494
+ return unless user
495
+
496
+ # Process callback for identified user
497
+ process_user_event(user, payload)
476
498
  end
477
499
  end
478
500
  ```
479
501
 
480
- ## Testing Strategies
502
+ ## Best Practices
481
503
 
482
- ### Test External ID Integration
504
+ ### Use in Public APIs
483
505
 
484
506
  ```ruby
485
- # test/models/external_user_test.rb
486
- require 'test_helper'
487
-
488
- class ExternalUserTest < Minitest::Test
489
- def test_bidirectional_mapping
490
- user = ExternalUser.create(
491
- internal_id: "test_123",
492
- external_id: "ext_456",
493
- name: "Test User"
494
- )
495
-
496
- # Test lookup by external ID
497
- found_by_external = ExternalUser.find_by_external_id("ext_456")
498
- assert_equal user.internal_id, found_by_external.internal_id
507
+ # ✅ Good - external IDs in public API
508
+ GET /api/users/ext_abc123def456ghi789jkl012
499
509
 
500
- # Test lookup by internal ID
501
- found_by_internal = ExternalUser.load("test_123")
502
- assert_equal user.external_id, found_by_internal.external_id
503
- end
510
+ # Avoid - internal UUIDs in public API
511
+ GET /api/users/01234567-89ab-cdef-1234-567890abcdef
512
+ ```
504
513
 
505
- def test_sync_status_tracking
506
- user = ExternalUser.create(
507
- internal_id: "test_123",
508
- external_id: "ext_456",
509
- name: "Test User"
510
- )
511
-
512
- # Test status transitions
513
- user.mark_sync_pending
514
- assert user.sync_pending?
515
- refute user.sync_completed?
516
-
517
- user.mark_sync_completed
518
- assert user.sync_completed?
519
- refute user.sync_pending?
520
-
521
- user.mark_sync_failed("Network error")
522
- assert user.sync_failed?
523
- assert_equal "Network error", user.sync_error
524
- end
514
+ ### Consistent Format Across Models
525
515
 
526
- def test_external_id_validation
527
- user = StrictExternalUser.new(
528
- user_id: "test_123",
529
- external_id: "invalid_format"
530
- )
516
+ ```ruby
517
+ class User < Familia::Horreum
518
+ feature :object_identifier
519
+ feature :external_identifier, format: 'user_%{id}'
520
+ end
531
521
 
532
- refute user.valid_external_id_format?
522
+ class Order < Familia::Horreum
523
+ feature :object_identifier
524
+ feature :external_identifier, format: 'order_%{id}'
525
+ end
533
526
 
534
- user.external_id = "user_validformat123"
535
- assert user.valid_external_id_format?
536
- end
527
+ class Product < Familia::Horreum
528
+ feature :object_identifier
529
+ feature :external_identifier, format: 'prod_%{id}'
537
530
  end
538
531
  ```
539
532
 
540
- ### Mock External Services
533
+ ### Error Handling in Controllers
541
534
 
542
535
  ```ruby
543
- # test/support/external_service_mock.rb
544
- class ExternalServiceMock
545
- def self.setup_mocks
546
- # Mock successful API responses
547
- stub_request(:get, /external-api\.com\/resource\/ext_\d+/)
548
- .to_return(
549
- status: 200,
550
- body: { data: "mocked_data", updated_at: Time.now.iso8601 }.to_json
551
- )
552
-
553
- stub_request(:post, /external-api\.com\/resource/)
554
- .to_return(
555
- status: 201,
556
- body: { id: "ext_#{rand(1000)}", status: "created" }.to_json
557
- )
558
- end
559
-
560
- def self.setup_error_mocks
561
- # Mock API errors for testing error handling
562
- stub_request(:get, /external-api\.com\/resource\/ext_error/)
563
- .to_return(status: 500, body: "Internal Server Error")
564
- end
536
+ def find_user_by_external_id(extid)
537
+ User.find_by_extid(extid) || raise(ActiveRecord::RecordNotFound)
538
+ rescue Familia::ExternalIdentifierError => e
539
+ Rails.logger.warn "Invalid external ID format for '#{extid}': #{e.message}"
540
+ raise(ActiveRecord::RecordNotFound)
565
541
  end
566
542
  ```
567
543
 
568
544
  ## Troubleshooting
569
545
 
570
- ### Common Issues
546
+ ### Extid Returns Nil
547
+
548
+ Check that object has valid objid and required features:
571
549
 
572
- **External ID Not Found**
573
550
  ```ruby
574
- # Debug external ID mappings
575
- puts ExternalUser.external_id_mapping.hgetall
576
- # Shows all external_id -> internal_id mappings
551
+ user = User.new(email: 'test@example.com')
577
552
 
578
- # Check reverse mapping
579
- puts ExternalUser.internal_id_mapping.hgetall
580
- # Shows all internal_id -> external_id mappings
553
+ # No objid yet - extid will be nil
554
+ user.extid # => nil
555
+
556
+ # ✅ Save first to generate objid
557
+ user.save
558
+ user.extid # => "ext_abc123..."
581
559
  ```
582
560
 
583
- **Sync Status Issues**
584
- ```ruby
585
- # Check sync status for all objects of a type
586
- ExternalUser.all.each do |user|
587
- puts "#{user.external_id}: #{user.sync_status} (#{user.sync_error})"
588
- end
561
+ ### Lookup Not Working
589
562
 
590
- # Reset failed sync statuses
591
- ExternalUser.all.select(&:sync_failed?).each(&:clear_sync_error)
592
- ```
563
+ Ensure object was saved to populate lookup table:
593
564
 
594
- **Validation Failures**
595
565
  ```ruby
596
- user = ExternalUser.new(external_id: "invalid")
566
+ user = User.new(email: 'test@example.com')
567
+ extid = user.extid # Generates extid but doesn't save lookup
597
568
 
598
- unless user.valid_external_id?
599
- puts "Validation failed for: #{user.external_id}"
600
- puts "Expected pattern: #{ExternalUser.validation_pattern}"
601
- end
602
- ```
569
+ User.find_by_extid(extid) # => nil (lookup not saved)
603
570
 
604
- ### Performance Debugging
571
+ user.save # Saves lookup mapping
572
+ User.find_by_extid(extid) # => user (now works)
573
+ ```
605
574
 
606
- ```ruby
607
- # Monitor external ID lookup performance
608
- def benchmark_external_lookups(external_ids)
609
- require 'benchmark'
575
+ ### Format Not Applied
610
576
 
611
- Benchmark.bm(20) do |x|
612
- x.report("Individual lookups:") do
613
- external_ids.each { |id| ExternalUser.find_by_external_id(id) }
614
- end
577
+ Check feature options syntax:
615
578
 
616
- x.report("Batch lookups:") do
617
- ExternalUser.multiget_by_external_ids(external_ids)
618
- end
619
- end
579
+ ```ruby
580
+ # ❌ Wrong - options after comma
581
+ class User < Familia::Horreum
582
+ feature :object_identifier
583
+ feature :external_identifier, { format: 'user_%{id}' }
620
584
  end
621
585
 
622
- # Check mapping Valkey/Redis key sizes
623
- mapping_keys = Familia.redis.keys("*external_id_mapping*")
624
- mapping_keys.each do |key|
625
- size = Familia.redis.hlen(key)
626
- puts "#{key}: #{size} mappings"
586
+ # Correct - options as keyword arguments
587
+ class User < Familia::Horreum
588
+ feature :object_identifier
589
+ feature :external_identifier, format: 'user_%{id}'
627
590
  end
628
591
  ```
629
592
 
630
- ---
593
+ ### Invalid Format Regex
594
+
595
+ When testing external ID format, use exact character counts:
596
+
597
+ ```ruby
598
+ # ❌ Wrong - uses + quantifier
599
+ extid.match(/\Aext_[0-9a-z]+\z/)
600
+
601
+ # ✅ Correct - uses exact count {25}
602
+ extid.match(/\Aext_[0-9a-z]{25}\z/)
603
+ ```
631
604
 
632
605
  ## See Also
633
606
 
634
- - **[Technical Reference](../reference/api-technical.md#external-identifier-feature-v200-pre7)** - Implementation details and advanced patterns
635
- - **[Object Identifiers Guide](feature-object-identifiers.md)** - Automatic ID generation strategies
636
- - **[Feature System Guide](feature-system.md)** - Understanding the feature architecture
637
- - **[Implementation Guide](implementation.md)** - Advanced configuration and migration patterns
607
+ - [Object Identifiers](feature-object-identifiers.md) - Required dependency for external identifiers
608
+ - [Feature System](feature-system.md) - Understanding Familia's feature architecture
609
+ - [Relationships](feature-relationships.md) - Using external IDs with relationships