familia 2.0.0.pre16 → 2.0.0.pre18

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 (250) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/{code-smellage.yml → code-smells.yml} +3 -63
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +6 -0
  6. data/CHANGELOG.rst +82 -0
  7. data/CLAUDE.md +47 -2
  8. data/Gemfile.lock +1 -1
  9. data/README.md +13 -0
  10. data/bin/irb +1 -1
  11. data/docs/archive/FAMILIA_TECHNICAL.md +1 -1
  12. data/docs/guides/core-field-system.md +48 -26
  13. data/docs/migrating/v2.0.0-pre18.md +58 -0
  14. data/docs/overview.md +2 -2
  15. data/docs/qodo-merge-compliance.md +96 -0
  16. data/docs/reference/api-technical.md +1 -1
  17. data/examples/encrypted_fields.rb +1 -1
  18. data/examples/safe_dump.rb +1 -1
  19. data/lib/familia/base.rb +6 -6
  20. data/lib/familia/connection/middleware.rb +58 -4
  21. data/lib/familia/connection.rb +1 -1
  22. data/lib/familia/data_type/class_methods.rb +63 -0
  23. data/lib/familia/data_type/connection.rb +83 -0
  24. data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
  25. data/lib/familia/data_type/serialization.rb +5 -5
  26. data/lib/familia/data_type/settings.rb +96 -0
  27. data/lib/familia/data_type/types/hashkey.rb +2 -1
  28. data/lib/familia/data_type/types/sorted_set.rb +113 -10
  29. data/lib/familia/data_type/types/stringkey.rb +0 -4
  30. data/lib/familia/data_type.rb +8 -195
  31. data/lib/familia/encryption/encrypted_data.rb +12 -2
  32. data/lib/familia/encryption/manager.rb +11 -4
  33. data/lib/familia/features/autoloader.rb +3 -1
  34. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
  35. data/lib/familia/features/encrypted_fields.rb +5 -2
  36. data/lib/familia/features/external_identifier.rb +49 -8
  37. data/lib/familia/features/object_identifier.rb +84 -12
  38. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
  39. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +45 -26
  40. data/lib/familia/features/relationships/indexing.rb +7 -1
  41. data/lib/familia/features/relationships/participation/participant_methods.rb +6 -2
  42. data/lib/familia/features/safe_dump.rb +2 -3
  43. data/lib/familia/features/transient_fields.rb +7 -2
  44. data/lib/familia/features.rb +6 -1
  45. data/lib/familia/field_type.rb +0 -18
  46. data/lib/familia/horreum/{core/connection.rb → connection.rb} +21 -0
  47. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +1 -1
  48. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +102 -56
  49. data/lib/familia/horreum/{subclass/management.rb → management.rb} +18 -15
  50. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +73 -170
  51. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +22 -2
  52. data/lib/familia/horreum/serialization.rb +190 -0
  53. data/lib/familia/horreum.rb +39 -14
  54. data/lib/familia/identifier_extractor.rb +60 -0
  55. data/lib/familia/logging.rb +271 -112
  56. data/lib/familia/refinements.rb +0 -1
  57. data/lib/familia/version.rb +1 -1
  58. data/lib/familia.rb +2 -2
  59. data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -14
  60. data/pr_agent.toml +31 -0
  61. data/pr_compliance_checklist.yaml +45 -0
  62. data/try/edge_cases/empty_identifiers_try.rb +1 -1
  63. data/try/edge_cases/hash_symbolization_try.rb +31 -31
  64. data/try/edge_cases/json_serialization_try.rb +2 -2
  65. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
  66. data/try/edge_cases/race_conditions_try.rb +1 -1
  67. data/try/edge_cases/reserved_keywords_try.rb +1 -1
  68. data/try/edge_cases/string_coercion_try.rb +1 -1
  69. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  70. data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
  71. data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
  72. data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
  73. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  74. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
  75. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
  76. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
  77. data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
  78. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
  79. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  80. data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
  81. data/try/features/encrypted_fields/memory_security_try.rb +1 -1
  82. data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
  83. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  84. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
  85. data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
  86. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
  87. data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
  88. data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
  89. data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
  90. data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
  91. data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
  92. data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
  93. data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
  94. data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
  95. data/try/features/expiration/expiration_try.rb +1 -1
  96. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  97. data/try/features/feature_dependencies_try.rb +1 -1
  98. data/try/features/feature_improvements_try.rb +1 -1
  99. data/try/features/field_groups_try.rb +244 -0
  100. data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
  101. data/try/features/object_identifier/object_identifier_try.rb +1 -1
  102. data/try/features/quantization/quantization_try.rb +1 -1
  103. data/try/features/real_feature_integration_try.rb +17 -14
  104. data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
  105. data/try/features/relationships/indexing_try.rb +16 -1
  106. data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
  107. data/try/features/relationships/participation_commands_verification_try.rb +4 -4
  108. data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
  109. data/try/features/relationships/participation_reverse_index_try.rb +1 -1
  110. data/try/features/relationships/relationships_api_changes_try.rb +1 -1
  111. data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
  112. data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
  113. data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
  114. data/try/features/relationships/relationships_performance_try.rb +1 -1
  115. data/try/features/relationships/relationships_performance_working_try.rb +1 -1
  116. data/try/features/relationships/relationships_try.rb +1 -1
  117. data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
  118. data/try/features/safe_dump/safe_dump_try.rb +1 -1
  119. data/try/features/transient_fields/redacted_string_try.rb +1 -1
  120. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  121. data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
  122. data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
  123. data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
  124. data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +1 -1
  125. data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
  126. data/try/{core → integration/connection}/isolated_dbclient_try.rb +3 -3
  127. data/try/integration/connection/middleware_reconnect_try.rb +87 -0
  128. data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
  129. data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
  130. data/try/{core → integration/connection}/pools_try.rb +1 -1
  131. data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
  132. data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
  133. data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
  134. data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
  135. data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
  136. data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
  137. data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
  138. data/try/{core → integration}/create_method_try.rb +1 -1
  139. data/try/integration/cross_component_try.rb +1 -1
  140. data/try/{core → integration}/database_consistency_try.rb +12 -8
  141. data/try/{core → integration}/familia_extended_try.rb +1 -1
  142. data/try/{core → integration}/familia_members_methods_try.rb +1 -1
  143. data/try/{models → integration/models}/customer_safe_dump_try.rb +1 -1
  144. data/try/{models → integration/models}/customer_try.rb +6 -6
  145. data/try/{models → integration/models}/datatype_base_try.rb +1 -1
  146. data/try/{models → integration/models}/familia_object_try.rb +1 -1
  147. data/try/{core → integration}/persistence_operations_try.rb +1 -1
  148. data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
  149. data/try/{configuration → integration}/scenarios_try.rb +2 -2
  150. data/try/{core → integration}/secure_identifier_try.rb +1 -1
  151. data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
  152. data/try/performance/benchmarks_try.rb +2 -2
  153. data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
  154. data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
  155. data/try/{helpers → support/helpers}/test_helpers.rb +15 -7
  156. data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +1 -1
  157. data/try/{core → unit/core}/autoloader_try.rb +1 -1
  158. data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
  159. data/try/{core → unit/core}/connection_try.rb +5 -5
  160. data/try/{core → unit/core}/errors_try.rb +4 -4
  161. data/try/{core → unit/core}/extensions_try.rb +1 -1
  162. data/try/unit/core/familia_logger_try.rb +110 -0
  163. data/try/{core → unit/core}/familia_try.rb +2 -2
  164. data/try/{core → unit/core}/middleware_try.rb +41 -1
  165. data/try/{core → unit/core}/settings_try.rb +1 -1
  166. data/try/{core → unit/core}/time_utils_try.rb +1 -1
  167. data/try/{core → unit/core}/tools_try.rb +3 -3
  168. data/try/{core → unit/core}/utils_try.rb +17 -14
  169. data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
  170. data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
  171. data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
  172. data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
  173. data/try/{data_types → unit/data_types}/list_try.rb +1 -1
  174. data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
  175. data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
  176. data/try/unit/data_types/sorted_set_zadd_options_try.rb +625 -0
  177. data/try/{data_types → unit/data_types}/string_try.rb +1 -1
  178. data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
  179. data/try/unit/horreum/auto_indexing_on_save_try.rb +212 -0
  180. data/try/{horreum → unit/horreum}/base_try.rb +3 -3
  181. data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
  182. data/try/{horreum → unit/horreum}/commands_try.rb +3 -1
  183. data/try/unit/horreum/defensive_initialization_try.rb +86 -0
  184. data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +3 -1
  185. data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
  186. data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
  187. data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
  188. data/try/{horreum → unit/horreum}/initialization_try.rb +2 -2
  189. data/try/unit/horreum/json_type_preservation_try.rb +248 -0
  190. data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
  191. data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
  192. data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
  193. data/try/{horreum → unit/horreum}/settings_try.rb +3 -1
  194. data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
  195. data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
  196. data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
  197. data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
  198. data/try/valkey.conf +26 -0
  199. metadata +149 -132
  200. data/lib/familia/distinguisher.rb +0 -85
  201. data/lib/familia/horreum/core.rb +0 -21
  202. data/lib/familia/refinements/logger_trace.rb +0 -60
  203. data/try/refinements/logger_trace_methods_try.rb +0 -44
  204. /data/lib/familia/horreum/{shared/settings.rb → settings.rb} +0 -0
  205. /data/lib/familia/horreum/{core/utils.rb → utils.rb} +0 -0
  206. /data/try/{debugging → support/debugging}/README.md +0 -0
  207. /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
  208. /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
  209. /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
  210. /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
  211. /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
  212. /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
  213. /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
  214. /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
  215. /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
  216. /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
  217. /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
  218. /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
  219. /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
  220. /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
  221. /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
  222. /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
  223. /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
  224. /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
  225. /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
  226. /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
  227. /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
  228. /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
  229. /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
  230. /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
  231. /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
  232. /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
  233. /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
  234. /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
  235. /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
  236. /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
  237. /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
  238. /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
  239. /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
  240. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
  241. /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  242. /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
  243. /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
  244. /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  245. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
  246. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
  247. /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
  248. /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
  249. /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
  250. /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -1,6 +1,9 @@
1
1
  # lib/familia/data_type.rb
2
2
 
3
- require_relative 'data_type/commands'
3
+ require_relative 'data_type/class_methods'
4
+ require_relative 'data_type/settings'
5
+ require_relative 'data_type/connection'
6
+ require_relative 'data_type/database_commands'
4
7
  require_relative 'data_type/serialization'
5
8
 
6
9
  # Familia
@@ -14,6 +17,7 @@ module Familia
14
17
  # @abstract Subclass and implement Database data type specific methods
15
18
  class DataType
16
19
  include Familia::Base
20
+ extend ClassMethods
17
21
  extend Familia::Features
18
22
 
19
23
  using Familia::Refinements::TimeLiterals
@@ -29,60 +33,6 @@ module Familia
29
33
  attr_reader :registered_types, :valid_options, :has_related_fields
30
34
  end
31
35
 
32
- # DataType::ClassMethods
33
- #
34
- module ClassMethods
35
- attr_accessor :parent, :suffix, :prefix, :uri
36
- attr_writer :logical_database
37
-
38
- # To be called inside every class that inherits DataType
39
- # +methname+ is the term used for the class and instance methods
40
- # that are created for the given +klass+ (e.g. set, list, etc)
41
- def register(klass, methname)
42
- Familia.trace :REGISTER, nil, "[#{self}] Registering #{klass} as #{methname.inspect}" if Familia.debug?
43
-
44
- @registered_types[methname] = klass
45
- end
46
-
47
- # Get the registered type class from a given method name
48
- # +methname+ is the method name used to register the class (e.g. :set, :list, etc)
49
- # Returns the registered class or nil if not found
50
- def registered_type(methname)
51
- @registered_types[methname]
52
- end
53
-
54
- def logical_database(val = nil)
55
- @logical_database = val unless val.nil?
56
- @logical_database || parent&.logical_database
57
- end
58
-
59
- def uri(val = nil)
60
- @uri = val unless val.nil?
61
- @uri || (parent ? parent.uri : Familia.uri)
62
- end
63
-
64
- def inherited(obj)
65
- Familia.trace :DATATYPE, nil, "#{obj} is my kinda type" if Familia.debug?
66
- obj.logical_database = logical_database
67
- obj.default_expiration = default_expiration # method added via Features::Expiration
68
- obj.uri = uri
69
- super
70
- end
71
-
72
- def valid_keys_only(opts)
73
- opts.slice(*DataType.valid_options)
74
- end
75
-
76
- def relations?
77
- @has_related_fields ||= false
78
- end
79
- end
80
- extend ClassMethods
81
-
82
- attr_reader :keystring, :opts, :uri, :logical_database
83
-
84
- alias url uri
85
-
86
36
  # +keystring+: If parent is set, this will be used as the suffix
87
37
  # for dbkey. Otherwise this becomes the value of the key.
88
38
  # If this is an Array, the elements will be joined.
@@ -125,146 +75,9 @@ module Familia
125
75
  init if respond_to? :init
126
76
  end
127
77
 
128
- # TODO: Replace with Chain of Responsibility pattern
129
- def dbclient
130
- return Fiber[:familia_transaction] if Fiber[:familia_transaction]
131
- return @dbclient if @dbclient
132
-
133
- # Delegate to parent if present, otherwise fall back to Familia
134
- parent ? parent.dbclient : Familia.dbclient(opts[:logical_database])
135
- end
136
-
137
- # Produces the full dbkey for this object.
138
- #
139
- # @return [String] The full dbkey.
140
- #
141
- # This method determines the appropriate dbkey based on the context of the DataType object:
142
- #
143
- # 1. If a hardcoded key is set in the options, it returns that key.
144
- # 2. For instance-level DataType objects, it uses the parent instance's dbkey method.
145
- # 3. For class-level DataType objects, it uses the parent class's dbkey method.
146
- # 4. For standalone DataType objects, it uses the keystring as the full dbkey.
147
- #
148
- # For class-level DataType objects (parent_class? == true):
149
- # - The suffix is optional and used to differentiate between different types of objects.
150
- # - If no suffix is provided, the class's default suffix is used (via the self.suffix method).
151
- # - If a nil suffix is explicitly passed, it won't appear in the resulting dbkey.
152
- # - Passing nil as the suffix is how class-level DataType objects are created without
153
- # the global default 'object' suffix.
154
- #
155
- # @example Instance-level DataType
156
- # user_instance.some_datatype.dbkey # => "user:123:some_datatype"
157
- #
158
- # @example Class-level DataType
159
- # User.some_datatype.dbkey # => "user:some_datatype"
160
- #
161
- # @example Standalone DataType
162
- # DataType.new("mykey").dbkey # => "mykey"
163
- #
164
- # @example Class-level DataType with explicit nil suffix
165
- # User.dbkey("123", nil) # => "user:123"
166
- #
167
- def dbkey
168
- # Return the hardcoded key if it's set. This is useful for
169
- # support legacy keys that aren't derived in the same way.
170
- return opts[:dbkey] if opts[:dbkey]
171
-
172
- if parent_instance?
173
- # This is an instance-level datatype object so the parent instance's
174
- # dbkey method is defined in Familia::Horreum::InstanceMethods.
175
- parent.dbkey(keystring)
176
- elsif parent_class?
177
- # This is a class-level datatype object so the parent class' dbkey
178
- # method is defined in Familia::Horreum::DefinitionMethods.
179
- parent.dbkey(keystring, nil)
180
- else
181
- # This is a standalone DataType object where it's keystring
182
- # is the full database key (dbkey).
183
- keystring
184
- end
185
- end
186
-
187
- def class?
188
- !@opts[:class].to_s.empty? && @opts[:class].is_a?(Familia)
189
- end
190
-
191
- # Provides a structured way to "gear down" to run db commands that are
192
- # not implemented in our DataType classes since we intentionally don't
193
- # have a method_missing method.
194
- def direct_access
195
- yield(dbclient, dbkey)
196
- end
197
-
198
- def parent_instance?
199
- parent&.is_a?(Horreum::ParentDefinition)
200
- end
201
-
202
- def parent_class?
203
- parent.is_a?(Class) && parent.ancestors.include?(Familia::Horreum)
204
- end
205
-
206
- def parent?
207
- parent_class? || parent_instance?
208
- end
209
-
210
- def parent
211
- # Return cached ParentDefinition if available
212
- return @parent if @parent
213
-
214
- # Return class-level parent if no instance parent
215
- return self.class.parent unless @parent_ref
216
-
217
- # Create ParentDefinition dynamically from stored reference.
218
- # This ensures we get the current identifier value (available after initialization)
219
- # rather than a stale nil value from initialization time. Cannot cache due to frozen object.
220
- Horreum::ParentDefinition.from_parent(@parent_ref)
221
- end
222
-
223
- def parent=(value)
224
- case value
225
- when Horreum::ParentDefinition
226
- @parent = value
227
- when nil
228
- @parent = nil
229
- @parent_ref = nil
230
- else
231
- # Store parent instance reference for lazy ParentDefinition creation.
232
- # During initialization, the parent's identifier may not be available yet,
233
- # so we defer ParentDefinition creation until first access for memory efficiency.
234
- # Note: @parent_ref is not cleared after use because DataType objects are frozen.
235
- @parent_ref = value
236
- @parent = nil # Will be created dynamically in parent method
237
- end
238
- end
239
-
240
- def uri
241
- # Return explicit instance URI if set
242
- return @uri if @uri
243
-
244
- # If we have a parent with logical_database, build URI with that database
245
- if parent && parent.respond_to?(:logical_database) && parent.logical_database
246
- new_uri = (self.class.uri || Familia.uri).dup
247
- new_uri.db = parent.logical_database
248
- new_uri
249
- else
250
- # Fall back to class-level URI or global Familia.uri
251
- self.class.uri || Familia.uri
252
- end
253
- end
254
-
255
- def uri=(value)
256
- @uri = value
257
- end
258
-
259
- def dump_method
260
- self.class.dump_method
261
- end
262
-
263
- def load_method
264
- self.class.load_method
265
- end
266
-
267
- include Commands
78
+ include Settings
79
+ include Connection
80
+ include DatabaseCommands
268
81
  include Serialization
269
82
  end
270
83
 
@@ -44,8 +44,18 @@ module Familia
44
44
  new(**parsed)
45
45
  end
46
46
 
47
- def self.from_json(json_string)
48
- validate!(json_string)
47
+ def self.from_json(json_string_or_hash)
48
+ # Support both JSON strings (legacy) and already-parsed Hashes (v2.0 deserialization)
49
+ if json_string_or_hash.is_a?(Hash)
50
+ # Already parsed - use directly
51
+ parsed = json_string_or_hash
52
+ # Symbolize keys if they're strings
53
+ parsed = parsed.transform_keys(&:to_sym) if parsed.keys.first.is_a?(String)
54
+ new(**parsed)
55
+ else
56
+ # JSON string - validate and parse
57
+ validate!(json_string_or_hash)
58
+ end
49
59
  end
50
60
 
51
61
  # Instance methods for decryptability validation
@@ -32,15 +32,22 @@ module Familia
32
32
  Familia::Encryption.secure_wipe(key) if key
33
33
  end
34
34
 
35
- def decrypt(encrypted_json, context:, additional_data: nil)
36
- return nil if encrypted_json.nil? || encrypted_json.empty?
35
+ def decrypt(encrypted_json_or_hash, context:, additional_data: nil)
36
+ return nil if encrypted_json_or_hash.nil? || (encrypted_json_or_hash.respond_to?(:empty?) && encrypted_json_or_hash.empty?)
37
37
 
38
38
  # Increment counter immediately to track all decryption attempts, even failed ones
39
39
  Familia::Encryption.derivation_count.increment
40
40
 
41
41
  begin
42
- data = Familia::Encryption::EncryptedData.new(**Familia::JsonSerializer.parse(encrypted_json,
43
- symbolize_names: true))
42
+ # Delegate parsing and instantiation to EncryptedData.from_json
43
+ # Wrap validation errors for security (don't expose internal structure details)
44
+ begin
45
+ data = Familia::Encryption::EncryptedData.from_json(encrypted_json_or_hash)
46
+ raise EncryptionError, 'Failed to parse encrypted data' unless data
47
+ rescue EncryptionError => e
48
+ # Re-wrap validation errors with generic message for security
49
+ raise EncryptionError, "Decryption failed: #{e.message}"
50
+ end
44
51
 
45
52
  # Validate algorithm support
46
53
  provider = Registry.get(data.algorithm)
@@ -28,7 +28,9 @@ module Familia::Features
28
28
  ]
29
29
 
30
30
  # Ensure the Features module exists within the base module
31
- base.const_set(:Features, Module.new) unless base.const_defined?(:Features) || config_name.eql?('features')
31
+ unless base.const_defined?(:Features) || config_name.eql?('features')
32
+ base.const_set(:Features, Module.new)
33
+ end
32
34
 
33
35
  # Use the shared autoload_files method
34
36
  autoload_files(dir_patterns, log_prefix: "Autoloader[#{config_name}]")
@@ -29,8 +29,10 @@ module Familia
29
29
  # Already concealed, store as-is
30
30
  instance_variable_set(:"@#{field_name}", value)
31
31
  elsif field_type.encrypted_json?(value)
32
- # Already encrypted JSON from database - wrap in ConcealedString without re-encrypting
33
- concealed = ConcealedString.new(value, self, field_type)
32
+ # Already encrypted (JSON string or Hash from database) - wrap in ConcealedString without re-encrypting
33
+ # Convert Hash back to JSON string if needed (v2.0 deserialization returns Hash)
34
+ encrypted_string = value.is_a?(Hash) ? Familia::JsonSerializer.dump(value) : value
35
+ concealed = ConcealedString.new(encrypted_string, self, field_type)
34
36
  instance_variable_set(:"@#{field_name}", concealed)
35
37
  else
36
38
  # Encrypt plaintext and wrap in ConcealedString
@@ -138,7 +140,13 @@ module Familia
138
140
 
139
141
  # Check if a string looks like encrypted JSON data
140
142
  def encrypted_json?(data)
141
- Familia::Encryption::EncryptedData.valid?(data)
143
+ # Support both JSON strings (legacy) and Hashes (v2.0 deserialization)
144
+ if data.is_a?(Hash)
145
+ required_keys = %w[algorithm nonce ciphertext auth_tag key_version]
146
+ required_keys.all? { |key| data.key?(key) || data.key?(key.to_sym) }
147
+ else
148
+ Familia::Encryption::EncryptedData.valid?(data)
149
+ end
142
150
  end
143
151
 
144
152
  private
@@ -259,7 +259,7 @@ module Familia
259
259
  # - Insider threats with application access
260
260
  #
261
261
  module EncryptedFields
262
- Familia::Base.add_feature self, :encrypted_fields
262
+ Familia::Base.add_feature self, :encrypted_fields, depends_on: nil, field_group: :encrypted_fields
263
263
 
264
264
  def self.included(base)
265
265
  Familia.trace :LOADED, self, base if Familia.debug?
@@ -297,7 +297,10 @@ module Familia
297
297
  @encrypted_fields ||= []
298
298
  @encrypted_fields << name unless @encrypted_fields.include?(name)
299
299
 
300
- require_relative 'encrypted_fields/encrypted_field_type'
300
+ # Add to field_groups if the group exists
301
+ if field_groups&.key?(:encrypted_fields)
302
+ field_groups[:encrypted_fields] << name
303
+ end
301
304
 
302
305
  field_type = EncryptedFieldType.new(name, aad_fields: aad_fields, **)
303
306
  register_field_type(field_type)
@@ -10,6 +10,7 @@ module Familia
10
10
  def self.included(base)
11
11
  Familia.trace :LOADED, self, base if Familia.debug?
12
12
  base.extend ModelClassMethods
13
+ base.include ModelInstanceMethods
13
14
 
14
15
  # Ensure default prefix is set in feature options
15
16
  base.add_feature_options(:external_identifier, prefix: 'ext')
@@ -75,9 +76,6 @@ module Familia
75
76
 
76
77
  instance_variable_set(:"@#{field_name}", derived_extid)
77
78
 
78
- # Update mapping if we have an identifier (objid)
79
- self.class.extid_lookup[derived_extid] = identifier if respond_to?(:identifier) && identifier
80
-
81
79
  derived_extid
82
80
  end
83
81
  end
@@ -103,11 +101,6 @@ module Familia
103
101
 
104
102
  # Set the new value
105
103
  instance_variable_set(:"@#{field_name}", value)
106
-
107
- # Update mapping if we have both extid and identifier
108
- return unless value && respond_to?(:identifier) && identifier
109
-
110
- self.class.extid_lookup[value] = identifier
111
104
  end
112
105
  end
113
106
  end
@@ -159,6 +152,54 @@ module Familia
159
152
  end
160
153
  end
161
154
 
155
+ # Instance methods for external identifier management
156
+ module ModelInstanceMethods
157
+ # Override save to update extid_lookup mapping
158
+ #
159
+ # This ensures the extid_lookup index is populated during save operations
160
+ # rather than during object initialization, preventing unwanted database
161
+ # writes when calling .new()
162
+ #
163
+ # @param update_expiration [Boolean] Whether to update key expiration
164
+ # @return [Boolean] True if save was successful
165
+ #
166
+ def save(update_expiration: true)
167
+ result = super
168
+
169
+ # Update extid_lookup mapping after successful save
170
+ if result && respond_to?(:extid) && respond_to?(:identifier)
171
+ current_extid = extid # Triggers lazy generation if needed
172
+ if current_extid && identifier
173
+ self.class.extid_lookup[current_extid] = identifier
174
+ end
175
+ end
176
+
177
+ result
178
+ end
179
+
180
+ # Override save_if_not_exists to update extid_lookup mapping
181
+ #
182
+ # This ensures the extid_lookup index is populated during create operations
183
+ # which use save_if_not_exists instead of save.
184
+ #
185
+ # @param update_expiration [Boolean] Whether to update key expiration
186
+ # @return [Boolean] True if save was successful
187
+ #
188
+ def save_if_not_exists(update_expiration: true)
189
+ result = super
190
+
191
+ # Update extid_lookup mapping after successful save
192
+ if result && respond_to?(:extid) && respond_to?(:identifier)
193
+ current_extid = extid # Triggers lazy generation if needed
194
+ if current_extid && identifier
195
+ self.class.extid_lookup[current_extid] = identifier
196
+ end
197
+ end
198
+
199
+ result
200
+ end
201
+ end
202
+
162
203
  # Derives a deterministic, public-facing external identifier from the object's
163
204
  # internal `objid`.
164
205
  #
@@ -88,6 +88,7 @@ module Familia
88
88
  def self.included(base)
89
89
  Familia.trace :LOADED, self, base if Familia.debug?
90
90
  base.extend ModelClassMethods
91
+ base.include ModelInstanceMethods
91
92
 
92
93
  # Ensure default generator is set in feature options
93
94
  base.add_feature_options(:object_identifier, generator: DEFAULT_GENERATOR)
@@ -160,9 +161,6 @@ module Familia
160
161
  generator = options[:generator] || DEFAULT_GENERATOR
161
162
  instance_variable_set(:"@#{field_name}_generator_used", generator)
162
163
 
163
- # Update mapping from objid to model primary key
164
- self.class.objid_lookup[generated_id] = identifier if respond_to?(:identifier) && identifier
165
-
166
164
  generated_id
167
165
  end
168
166
  end
@@ -198,14 +196,11 @@ module Familia
198
196
 
199
197
  instance_variable_set(:"@#{field_name}", value)
200
198
 
201
- # Update mapping from objid to this new identifier
202
- self.class.objid_lookup[value] = identifier unless value.nil? || identifier.nil?
203
-
204
199
  # When setting objid from external source (e.g., loading from Valkey/Redis),
205
- # we cannot determine the original generator, so we clear the provenance
206
- # tracking to indicate unknown origin. This prevents false assumptions
207
- # about the security properties of externally-provided identifiers.
208
- instance_variable_set(:"@#{field_name}_generator_used", nil)
200
+ # infer the generator type from the format to restore provenance tracking.
201
+ # This allows features like ExternalIdentifier to work correctly on loaded objects.
202
+ inferred_generator = infer_objid_generator(value)
203
+ instance_variable_set(:"@#{field_name}_generator_used", inferred_generator)
209
204
  end
210
205
  end
211
206
  end
@@ -284,6 +279,54 @@ module Familia
284
279
  end
285
280
  end
286
281
 
282
+ # Instance methods for object identifier management
283
+ module ModelInstanceMethods
284
+ # Override save to update objid_lookup mapping
285
+ #
286
+ # This ensures the objid_lookup index is populated during save operations
287
+ # rather than during object initialization, preventing unwanted database
288
+ # writes when calling .new()
289
+ #
290
+ # @param update_expiration [Boolean] Whether to update key expiration
291
+ # @return [Boolean] True if save was successful
292
+ #
293
+ def save(update_expiration: true)
294
+ result = super
295
+
296
+ # Update objid_lookup mapping after successful save
297
+ if result && respond_to?(:objid) && respond_to?(:identifier)
298
+ current_objid = objid # Triggers lazy generation if needed
299
+ if current_objid && identifier
300
+ self.class.objid_lookup[current_objid] = identifier
301
+ end
302
+ end
303
+
304
+ result
305
+ end
306
+
307
+ # Override save_if_not_exists to update objid_lookup mapping
308
+ #
309
+ # This ensures the objid_lookup index is populated during create operations
310
+ # which use save_if_not_exists instead of save.
311
+ #
312
+ # @param update_expiration [Boolean] Whether to update key expiration
313
+ # @return [Boolean] True if save was successful
314
+ #
315
+ def save_if_not_exists(update_expiration: true)
316
+ result = super
317
+
318
+ # Update objid_lookup mapping after successful save
319
+ if result && respond_to?(:objid) && respond_to?(:identifier)
320
+ current_objid = objid # Triggers lazy generation if needed
321
+ if current_objid && identifier
322
+ self.class.objid_lookup[current_objid] = identifier
323
+ end
324
+ end
325
+
326
+ result
327
+ end
328
+ end
329
+
287
330
  # Instance method for generating object identifier using configured strategy
288
331
  #
289
332
  # This method is called by the ObjectIdentifierFieldType when lazy generation
@@ -304,10 +347,39 @@ module Familia
304
347
  objid
305
348
  end
306
349
 
307
- # Full-length alias setter for objid
350
+ # Infers the generator type (:uuid_v7, :uuid_v4, :hex) from the format of an objid string.
308
351
  #
309
- # @param value [String] The object identifier to set
352
+ # This method analyzes the objid format to restore provenance tracking when loading
353
+ # objects from Redis, allowing dependent features like ExternalIdentifier to work correctly.
310
354
  #
355
+ # @param objid_value [String] The objid string to analyze
356
+ # @return [Symbol, nil] The inferred generator type or nil if unknown
357
+ def infer_objid_generator(objid_value)
358
+ return nil if objid_value.nil? || objid_value.to_s.empty?
359
+
360
+ objid_str = objid_value.to_s
361
+
362
+ # UUID format: xxxxxxxx-xxxx-Vxxx-xxxx-xxxxxxxxxxxx (36 chars with hyphens)
363
+ # where V is the version nibble at position 14
364
+ if objid_str.length == 36 && objid_str[8] == '-' && objid_str[13] == '-' && objid_str[18] == '-' && objid_str[23] == '-'
365
+ version_char = objid_str[14]
366
+ case version_char
367
+ when '7'
368
+ :uuid_v7
369
+ when '4'
370
+ :uuid_v4
371
+ else
372
+ nil # Unknown UUID version
373
+ end
374
+ # Hex format: pure hexadecimal without hyphens (32 or 64 chars typically)
375
+ elsif objid_str.match?(/\A[0-9a-fA-F]+\z/)
376
+ :hex
377
+ else
378
+ nil # Unknown format
379
+ end
380
+ end
381
+ private :infer_objid_generator
382
+
311
383
  def object_identifier=(value)
312
384
  self.objid = value
313
385
  end
@@ -75,7 +75,7 @@ module Familia
75
75
  actual_target_class.class_eval do
76
76
  # Helper method to get index set for a specific field value
77
77
  # This acts as a factory for field-value-specific DataTypes
78
- define_method("#{index_name}_for") do |field_value|
78
+ define_method(:"#{index_name}_for") do |field_value|
79
79
  # Return properly managed DataType instance with parameterized key
80
80
  index_key = "#{index_name}:#{field_value}"
81
81
  Familia::UnsortedSet.new(index_key, parent: self)
@@ -99,26 +99,26 @@ module Familia
99
99
  # Generate instance sampling method (e.g., company.sample_from_department)
100
100
  actual_target_class.class_eval do
101
101
 
102
- define_method("sample_from_#{field}") do |field_value, count = 1|
102
+ define_method(:"sample_from_#{field}") do |field_value, count = 1|
103
103
  index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet
104
104
 
105
105
  # Get random members efficiently (O(1) via SRANDMEMBER with count)
106
106
  # Returns array even for count=1 for consistent API
107
107
  index_set.sample(count).map do |id|
108
- indexed_class.new(index_set.deserialize_value(id))
108
+ indexed_class.find_by_identifier(id)
109
109
  end
110
110
  end
111
111
 
112
112
  # Generate bulk query method (e.g., company.find_all_by_department)
113
- define_method("find_all_by_#{field}") do |field_value|
113
+ define_method(:"find_all_by_#{field}") do |field_value|
114
114
  index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet
115
115
 
116
116
  # Get all members from set
117
- index_set.members.map { |id| indexed_class.new(id) }
117
+ index_set.members.map { |id| indexed_class.find_by_identifier(id) }
118
118
  end
119
119
 
120
120
  # Generate method to rebuild the index for this parent instance
121
- define_method("rebuild_#{index_name}") do
121
+ define_method(:"rebuild_#{index_name}") do
122
122
  # This would need to be implemented based on how you track which
123
123
  # objects belong to this parent instance
124
124
  # For now, just a placeholder
@@ -138,7 +138,7 @@ module Familia
138
138
  def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
139
139
  target_class_config = target_class.config_name
140
140
  indexed_class.class_eval do
141
- method_name = "add_to_#{target_class_config}_#{index_name}"
141
+ method_name = :"add_to_#{target_class_config}_#{index_name}"
142
142
  Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
143
143
 
144
144
  define_method(method_name) do |target_instance|
@@ -154,7 +154,7 @@ module Familia
154
154
  index_set.add(identifier)
155
155
  end
156
156
 
157
- method_name = "remove_from_#{target_class_config}_#{index_name}"
157
+ method_name = :"remove_from_#{target_class_config}_#{index_name}"
158
158
  Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
159
159
 
160
160
  define_method(method_name) do |target_instance|
@@ -170,7 +170,7 @@ module Familia
170
170
  index_set.remove(identifier)
171
171
  end
172
172
 
173
- method_name = "update_in_#{target_class_config}_#{index_name}"
173
+ method_name = :"update_in_#{target_class_config}_#{index_name}"
174
174
  Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
175
175
 
176
176
  define_method(method_name) do |target_instance, old_field_value = nil|