familia 2.0.0.pre15 → 2.0.0.pre17

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 (288) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.github/workflows/code-quality.yml +138 -0
  4. data/.github/workflows/code-smells.yml +85 -0
  5. data/.github/workflows/docs.yml +31 -8
  6. data/.gitignore +3 -1
  7. data/.pre-commit-config.yaml +7 -1
  8. data/.reek.yml +98 -0
  9. data/.rubocop.yml +54 -10
  10. data/.talismanrc +9 -0
  11. data/.yardopts +18 -13
  12. data/CHANGELOG.rst +86 -4
  13. data/CLAUDE.md +39 -1
  14. data/Gemfile +6 -5
  15. data/Gemfile.lock +99 -23
  16. data/LICENSE.txt +1 -1
  17. data/README.md +285 -85
  18. data/changelog.d/README.md +2 -2
  19. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  20. data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
  21. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  22. data/docs/archive/README.md +3 -2
  23. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  24. data/docs/conf.py +29 -0
  25. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  26. data/docs/guides/feature-encrypted-fields.md +785 -0
  27. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  28. data/docs/guides/feature-external-identifiers.md +637 -0
  29. data/docs/guides/feature-object-identifiers.md +435 -0
  30. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  31. data/docs/guides/feature-relationships-methods.md +684 -0
  32. data/docs/guides/feature-relationships.md +200 -0
  33. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  34. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  35. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  36. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  37. data/docs/guides/index.md +176 -0
  38. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  39. data/docs/migrating/v2.0.0-pre.md +1 -1
  40. data/docs/migrating/v2.0.0-pre11.md +2 -2
  41. data/docs/migrating/v2.0.0-pre12.md +2 -2
  42. data/docs/migrating/v2.0.0-pre5.md +33 -12
  43. data/docs/migrating/v2.0.0-pre6.md +2 -2
  44. data/docs/migrating/v2.0.0-pre7.md +8 -8
  45. data/docs/overview.md +624 -20
  46. data/docs/reference/api-technical.md +1365 -0
  47. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  48. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  49. data/examples/autoloader/mega_customer.rb +3 -1
  50. data/examples/encrypted_fields.rb +378 -0
  51. data/examples/json_usage_patterns.rb +144 -0
  52. data/examples/relationships.rb +13 -13
  53. data/examples/safe_dump.rb +7 -7
  54. data/examples/single_connection_transaction_confusions.rb +379 -0
  55. data/lib/familia/base.rb +51 -10
  56. data/lib/familia/connection/handlers.rb +223 -0
  57. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  58. data/lib/familia/connection/middleware.rb +75 -0
  59. data/lib/familia/connection/operation_core.rb +93 -0
  60. data/lib/familia/connection/operations.rb +277 -0
  61. data/lib/familia/connection/pipeline_core.rb +87 -0
  62. data/lib/familia/connection/transaction_core.rb +100 -0
  63. data/lib/familia/connection.rb +60 -186
  64. data/lib/familia/data_type/class_methods.rb +63 -0
  65. data/lib/familia/data_type/commands.rb +53 -51
  66. data/lib/familia/data_type/connection.rb +83 -0
  67. data/lib/familia/data_type/serialization.rb +108 -107
  68. data/lib/familia/data_type/settings.rb +96 -0
  69. data/lib/familia/data_type/types/counter.rb +1 -1
  70. data/lib/familia/data_type/types/hashkey.rb +15 -11
  71. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  72. data/lib/familia/data_type/types/lock.rb +3 -2
  73. data/lib/familia/data_type/types/sorted_set.rb +128 -14
  74. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
  75. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  76. data/lib/familia/data_type.rb +12 -171
  77. data/lib/familia/distinguisher.rb +85 -0
  78. data/lib/familia/encryption/encrypted_data.rb +15 -24
  79. data/lib/familia/encryption/manager.rb +6 -4
  80. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  81. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  82. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  83. data/lib/familia/encryption/request_cache.rb +7 -7
  84. data/lib/familia/encryption.rb +2 -3
  85. data/lib/familia/errors.rb +9 -3
  86. data/lib/familia/features/autoloader.rb +30 -12
  87. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  88. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  89. data/lib/familia/features/encrypted_fields.rb +71 -66
  90. data/lib/familia/features/expiration/extensions.rb +1 -1
  91. data/lib/familia/features/expiration.rb +31 -26
  92. data/lib/familia/features/external_identifier.rb +57 -19
  93. data/lib/familia/features/object_identifier.rb +134 -25
  94. data/lib/familia/features/quantization.rb +16 -21
  95. data/lib/familia/features/relationships/README.md +97 -0
  96. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  97. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  98. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
  99. data/lib/familia/features/relationships/indexing.rb +182 -256
  100. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  101. data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
  102. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  103. data/lib/familia/features/relationships/participation.rb +656 -0
  104. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  105. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  106. data/lib/familia/features/relationships.rb +65 -266
  107. data/lib/familia/features/safe_dump.rb +127 -130
  108. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  109. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  110. data/lib/familia/features/transient_fields.rb +10 -7
  111. data/lib/familia/features.rb +10 -14
  112. data/lib/familia/field_type.rb +6 -4
  113. data/lib/familia/horreum/connection.rb +297 -0
  114. data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
  115. data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
  116. data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
  117. data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
  118. data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
  119. data/lib/familia/horreum/serialization.rb +172 -0
  120. data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
  121. data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
  122. data/lib/familia/horreum.rb +222 -119
  123. data/lib/familia/json_serializer.rb +0 -1
  124. data/lib/familia/logging.rb +11 -114
  125. data/lib/familia/refinements/dear_json.rb +122 -0
  126. data/lib/familia/refinements/logger_trace.rb +20 -17
  127. data/lib/familia/refinements/stylize_words.rb +65 -0
  128. data/lib/familia/refinements/time_literals.rb +60 -52
  129. data/lib/familia/refinements.rb +2 -1
  130. data/lib/familia/secure_identifier.rb +60 -28
  131. data/lib/familia/settings.rb +83 -7
  132. data/lib/familia/utils.rb +5 -87
  133. data/lib/familia/verifiable_identifier.rb +4 -4
  134. data/lib/familia/version.rb +1 -1
  135. data/lib/familia.rb +72 -14
  136. data/lib/middleware/database_middleware.rb +56 -14
  137. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  138. data/try/configuration/scenarios_try.rb +2 -2
  139. data/try/connection/fiber_context_preservation_try.rb +250 -0
  140. data/try/connection/handler_constraints_try.rb +59 -0
  141. data/try/connection/operation_mode_guards_try.rb +208 -0
  142. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  143. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  144. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  145. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  146. data/try/connection/transaction_mode_strict_try.rb +98 -0
  147. data/try/connection/transaction_mode_warn_try.rb +131 -0
  148. data/try/connection/transaction_modes_try.rb +249 -0
  149. data/try/core/autoloader_try.rb +120 -2
  150. data/try/core/connection_try.rb +10 -10
  151. data/try/core/conventional_inheritance_try.rb +130 -0
  152. data/try/core/create_method_try.rb +15 -23
  153. data/try/core/database_consistency_try.rb +11 -10
  154. data/try/core/errors_try.rb +11 -14
  155. data/try/core/familia_extended_try.rb +2 -2
  156. data/try/core/familia_members_methods_try.rb +76 -0
  157. data/try/core/familia_try.rb +1 -1
  158. data/try/core/isolated_dbclient_try.rb +165 -0
  159. data/try/core/middleware_try.rb +16 -16
  160. data/try/core/persistence_operations_try.rb +4 -4
  161. data/try/core/pools_try.rb +42 -26
  162. data/try/core/secure_identifier_try.rb +28 -24
  163. data/try/core/time_utils_try.rb +10 -10
  164. data/try/core/tools_try.rb +3 -3
  165. data/try/core/utils_try.rb +2 -2
  166. data/try/data_types/boolean_try.rb +4 -4
  167. data/try/data_types/datatype_base_try.rb +0 -2
  168. data/try/data_types/list_try.rb +10 -10
  169. data/try/data_types/sorted_set_try.rb +5 -5
  170. data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
  171. data/try/data_types/string_try.rb +12 -12
  172. data/try/data_types/unsortedset_try.rb +33 -0
  173. data/try/debugging/cache_behavior_tracer.rb +7 -7
  174. data/try/debugging/debug_aad_process.rb +1 -1
  175. data/try/debugging/debug_concealed_internal.rb +1 -1
  176. data/try/debugging/debug_cross_context.rb +1 -1
  177. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  178. data/try/debugging/encryption_method_tracer.rb +10 -10
  179. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  180. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  181. data/try/encryption/config_persistence_try.rb +2 -2
  182. data/try/encryption/encryption_core_try.rb +19 -19
  183. data/try/encryption/instance_variable_scope_try.rb +1 -1
  184. data/try/encryption/module_loading_try.rb +2 -2
  185. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  186. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  187. data/try/encryption/secure_memory_handling_try.rb +1 -1
  188. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  189. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  190. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  191. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  192. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  193. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  194. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  195. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  196. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  197. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  198. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  199. data/try/features/feature_dependencies_try.rb +3 -3
  200. data/try/features/field_groups_try.rb +244 -0
  201. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  202. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  203. data/try/features/quantization/quantization_try.rb +1 -1
  204. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  205. data/try/features/relationships/indexing_try.rb +443 -0
  206. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  207. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  208. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  209. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  210. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  211. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  212. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  213. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  214. data/try/features/relationships/relationships_performance_try.rb +20 -20
  215. data/try/features/relationships/relationships_try.rb +27 -38
  216. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  217. data/try/features/transient_fields/refresh_reset_try.rb +3 -1
  218. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  219. data/try/helpers/test_cleanup.rb +86 -0
  220. data/try/helpers/test_helpers.rb +6 -7
  221. data/try/horreum/auto_indexing_on_save_try.rb +212 -0
  222. data/try/horreum/base_try.rb +3 -2
  223. data/try/horreum/commands_try.rb +3 -1
  224. data/try/horreum/defensive_initialization_try.rb +86 -0
  225. data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
  226. data/try/horreum/initialization_try.rb +11 -7
  227. data/try/horreum/relations_try.rb +21 -13
  228. data/try/horreum/serialization_try.rb +12 -11
  229. data/try/horreum/settings_try.rb +2 -0
  230. data/try/integration/cross_component_try.rb +3 -3
  231. data/try/memory/memory_basic_test.rb +1 -1
  232. data/try/memory/memory_docker_ruby_dump.sh +2 -2
  233. data/try/models/customer_safe_dump_try.rb +1 -1
  234. data/try/models/customer_try.rb +13 -15
  235. data/try/models/datatype_base_try.rb +3 -3
  236. data/try/models/familia_object_try.rb +9 -8
  237. data/try/performance/benchmarks_try.rb +2 -2
  238. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  239. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  240. data/try/prototypes/atomic_saves_v4.rb +1 -1
  241. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  242. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  243. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  244. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  245. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  246. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  247. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  248. data/try/prototypes/pooling/pool_siege.rb +11 -11
  249. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  250. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  251. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  252. data/try/refinements/logger_trace_methods_try.rb +44 -0
  253. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  254. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  255. data/try/valkey.conf +26 -0
  256. metadata +92 -52
  257. data/.rubocop_todo.yml +0 -208
  258. data/docs/connection_pooling.md +0 -192
  259. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  260. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  261. data/docs/guides/Feature-System-Autoloading.md +0 -198
  262. data/docs/guides/Home.md +0 -116
  263. data/docs/guides/Relationships-Guide.md +0 -737
  264. data/docs/guides/relationships-methods.md +0 -266
  265. data/docs/reference/auditing_database_commands.rb +0 -228
  266. data/examples/permissions.rb +0 -240
  267. data/lib/familia/features/relationships/cascading.rb +0 -437
  268. data/lib/familia/features/relationships/membership.rb +0 -497
  269. data/lib/familia/features/relationships/permission_management.rb +0 -264
  270. data/lib/familia/features/relationships/querying.rb +0 -615
  271. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  272. data/lib/familia/features/relationships/tracking.rb +0 -418
  273. data/lib/familia/horreum/core/connection.rb +0 -73
  274. data/lib/familia/horreum/core.rb +0 -21
  275. data/lib/familia/refinements/snake_case.rb +0 -40
  276. data/lib/familia/validation/command_recorder.rb +0 -336
  277. data/lib/familia/validation/expectations.rb +0 -519
  278. data/lib/familia/validation/validation_helpers.rb +0 -443
  279. data/lib/familia/validation/validator.rb +0 -412
  280. data/lib/familia/validation.rb +0 -140
  281. data/try/data_types/set_try.rb +0 -33
  282. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  283. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  284. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  285. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  286. data/try/validation/command_validation_try.rb.disabled +0 -207
  287. data/try/validation/performance_validation_try.rb.disabled +0 -324
  288. data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -1,7 +1,6 @@
1
- # lib/familia/horreum/subclass/definition.rb
1
+ # lib/familia/horreum/definition.rb
2
2
 
3
- require_relative 'related_fields_management'
4
- require_relative '../shared/settings'
3
+ require_relative 'settings'
5
4
 
6
5
  module Familia
7
6
  VALID_STRATEGIES = %i[raise skip ignore warn overwrite].freeze
@@ -32,21 +31,103 @@ module Familia
32
31
  @dump_method = nil
33
32
  @load_method = nil
34
33
 
35
- # DefinitionMethods: Provides class-level functionality for Horreum subclasses
34
+ # Field groups
35
+ @field_groups = nil
36
+ @current_field_group = nil
37
+
38
+ # DefinitionMethods - Class-level DSL methods for defining Horreum model structure
36
39
  #
37
40
  # This module is extended into classes that include Familia::Horreum,
38
- # providing methods for Database operations and object management.
41
+ # providing class methods for defining model structure and configuration
42
+ # (e.g., Customer.field :name, Customer.identifier_field :custid).
39
43
  #
40
44
  # Key features:
41
- # * Includes RelatedFieldsManagement for DataType field handling
42
- # * Defines methods for managing fields, identifiers, and dbkeys
43
- # * Provides utility methods for working with Database objects
45
+ # * Defines DSL methods for field definitions (field, identifier_field)
46
+ # * Includes RelatedFieldsManagement for DataType field DSL (list, set, zset, etc.)
47
+ # * Provides class-level configuration (prefix, suffix, logical_database)
48
+ # * Manages field metadata and inheritance
44
49
  #
45
50
  module DefinitionMethods
46
51
  include Familia::Settings
47
52
  include Familia::Horreum::RelatedFieldsManagement # Provides DataType field methods
48
53
 
49
- using Familia::Refinements::SnakeCase
54
+ # Defines a field group to organize related fields.
55
+ #
56
+ # Field groups provide a way to categorize and query fields by purpose or feature.
57
+ # When a block is provided, fields defined within the block are automatically
58
+ # added to the group. Without a block, an empty group is initialized.
59
+ #
60
+ # @param name [Symbol, String] the name of the field group
61
+ # @yield optional block for defining fields within the group
62
+ # @return [Array<Symbol>] the array of field names in the group
63
+ #
64
+ # @raise [Familia::Problem] if attempting to nest field groups
65
+ #
66
+ # @example Manual field grouping
67
+ # class User < Familia::Horreum
68
+ # field_group :personal_info do
69
+ # field :name
70
+ # field :email
71
+ # end
72
+ # end
73
+ #
74
+ # User.personal_info # => [:name, :email]
75
+ #
76
+ # @example Initialize empty group
77
+ # class User < Familia::Horreum
78
+ # field_group :placeholder
79
+ # end
80
+ #
81
+ # User.placeholder # => []
82
+ #
83
+ def field_group(name, &block)
84
+
85
+ # Prevent nested field groups
86
+ if @current_field_group
87
+ raise Familia::Problem,
88
+ "Cannot define field group :#{name} while :#{@current_field_group} is being defined. " \
89
+ "Nested field groups are not supported."
90
+ end
91
+
92
+ # Initialize group
93
+ field_groups[name.to_sym] ||= []
94
+
95
+ if block_given?
96
+ @current_field_group = name.to_sym
97
+ begin
98
+ instance_eval(&block)
99
+ ensure
100
+ @current_field_group = nil
101
+ end
102
+ else
103
+ Familia.ld "[field_group] Created field group :#{name} but no block given" if Familia.debug?
104
+ end
105
+
106
+ field_groups[name.to_sym]
107
+ end
108
+
109
+ # Returns the list of all field group names defined for the class.
110
+ #
111
+ # @return [Array<Symbol>] array of field group names
112
+ #
113
+ # @example
114
+ # class User < Familia::Horreum
115
+ # field_group :personal_info do
116
+ # field :name
117
+ # end
118
+ # field_group :metadata do
119
+ # field :created_at
120
+ # end
121
+ # end
122
+ #
123
+ # User.field_groups # => [
124
+ # :personal_info => [...],
125
+ # :metadata => [..]
126
+ # ]
127
+ #
128
+ def field_groups
129
+ @field_groups ||= {}
130
+ end
50
131
 
51
132
  # Sets or retrieves the unique identifier field for the class.
52
133
  #
@@ -77,13 +158,13 @@ module Familia
77
158
  #
78
159
  # This method defines a new field for the class, creating getter and setter
79
160
  # instance methods similar to `attr_accessor`. It also generates a fast
80
- # writer method for immediate persistence to Redis.
161
+ # writer method for immediate persistence to the database.
81
162
  #
82
163
  # @param name [Symbol, String] the name of the field to define. If a method
83
164
  # with the same name already exists, an error is raised.
84
165
  # @param as [Symbol, String, false, nil] as the name to use for the accessor method (defaults to name).
85
166
  # If false or nil, no accessor methods are created.
86
- # @param fast_method [Symbol, false, nil] the name to use for the fast writer method (defaults to :"#{name}!").
167
+ # @param fast_method [Symbol, false, nil] the name to use for the fast writer method (defaults to :`"#{name}!"`).
87
168
  # If false or nil, no fast writer method is created.
88
169
  # @param on_conflict [Symbol] conflict resolution strategy when method already exists:
89
170
  # - :raise - raise error if method exists (default)
@@ -98,37 +179,37 @@ module Familia
98
179
  #
99
180
  def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, category: nil)
100
181
  # Use field type system for consistency
101
- require_relative '../../field_type'
182
+ require_relative '../field_type'
102
183
 
103
184
  # Create appropriate field type based on category
104
185
  field_type = if category == :transient
105
- require_relative '../../features/transient_fields/transient_field_type'
106
- TransientFieldType.new(name, as: as, fast_method: false, on_conflict: on_conflict)
107
- else
108
- # For regular fields and other categories, create custom field type with category override
109
- custom_field_type = Class.new(FieldType) do
110
- define_method :category do
111
- category || :field
112
- end
113
- end
114
- custom_field_type.new(name, as: as, fast_method: fast_method, on_conflict: on_conflict)
115
- end
186
+ require_relative '../features/transient_fields/transient_field_type'
187
+ TransientFieldType.new(name, as: as, fast_method: false, on_conflict: on_conflict)
188
+ else
189
+ # For regular fields and other categories, create custom field type with category override
190
+ custom_field_type = Class.new(FieldType) do
191
+ define_method :category do
192
+ category || :field
193
+ end
194
+ end
195
+ custom_field_type.new(name, as: as, fast_method: fast_method, on_conflict: on_conflict)
196
+ end
116
197
 
117
198
  register_field_type(field_type)
118
199
  end
119
200
 
120
- # Sets or retrieves the suffix for generating Redis keys.
201
+ # Sets or retrieves the suffix for generating Valkey/Redis keys.
121
202
  #
122
203
  # @param a [String, Symbol, nil] the suffix to set (optional).
123
204
  # @param blk [Proc] a block that returns the suffix (optional).
124
205
  # @return [String, Symbol] the current suffix or Familia.default_suffix if none is set.
125
206
  #
126
- def suffix(a = nil, &blk)
127
- @suffix = a || blk if a || !blk.nil?
207
+ def suffix(val = nil, &blk)
208
+ @suffix = val || blk if val || !blk.nil?
128
209
  @suffix || Familia.default_suffix
129
210
  end
130
211
 
131
- # Sets or retrieves the prefix for generating Redis keys.
212
+ # Sets or retrieves the prefix for generating Valkey/Redis keys.
132
213
  #
133
214
  # @param a [String, Symbol, nil] the prefix to set (optional).
134
215
  # @return [String, Symbol] the current prefix.
@@ -137,20 +218,20 @@ module Familia
137
218
  # which typically occurs with anonymous classes that haven't had their prefix
138
219
  # explicitly set.
139
220
  #
140
- def prefix(a = nil)
141
- @prefix = a if a
221
+ def prefix(val = nil)
222
+ @prefix = val if val
142
223
  @prefix || begin
143
224
  if name.nil?
144
225
  raise Problem, 'Cannot generate prefix for anonymous class. ' \
145
226
  'Use `prefix` method to set explicitly.'
146
227
  end
147
- name.downcase.gsub('::', Familia.delim).to_sym
228
+ config_name.to_sym
148
229
  end
149
230
  end
150
231
 
151
- def logical_database(v = nil)
152
- Familia.trace :DB, Familia.dbclient, "#{@logical_database} #{v.nil?}", caller(0..2) if Familia.debug?
153
- @logical_database = v unless v.nil?
232
+ def logical_database(num = nil)
233
+ Familia.trace :LOGICAL_DATABASE_DEF, "instvar:#{@logical_database}", num if Familia.debug?
234
+ @logical_database = num unless num.nil?
154
235
  @logical_database || parent&.logical_database
155
236
  end
156
237
 
@@ -172,20 +253,7 @@ module Familia
172
253
  end
173
254
 
174
255
  def relations?
175
- @has_relations ||= false
176
- end
177
-
178
- # Converts the class name into a string that can be used to look up
179
- # configuration values. This is particularly useful when mapping
180
- # familia models with specific database numbers in the configuration.
181
- #
182
- # Familia::Horreum::DefinitionMethods#config_name
183
- #
184
- # @example V2::Session.config_name => 'session'
185
- #
186
- # @return [String] The underscored class name as a string
187
- def config_name
188
- name.snake_case
256
+ @has_related_fields ||= false
189
257
  end
190
258
 
191
259
  def dump_method
@@ -234,6 +302,14 @@ module Familia
234
302
  # Complete the registration after installation. If we do this beforehand
235
303
  # we can run into issues where it looks like it's already installed.
236
304
  field_types[field_type.name] = field_type
305
+
306
+ # Add to current field group if one is active
307
+ if @current_field_group
308
+ @field_groups[@current_field_group] << field_type.name
309
+ end
310
+
311
+ # Freeze the field_type to ensure immutability (maintains Data class heritage)
312
+ field_type.freeze
237
313
  end
238
314
 
239
315
  # Retrieves feature options for the current class.
@@ -303,26 +379,15 @@ module Familia
303
379
  # end
304
380
  #
305
381
  def add_feature_options(feature_name, **options)
306
- @feature_options ||= {}
307
- @feature_options[feature_name.to_sym] ||= {}
308
-
309
- # Only set defaults for options that don't already exist
310
- options.each do |key, value|
311
- @feature_options[feature_name.to_sym][key] ||= value
312
- end
382
+ @feature_options ||= {}
383
+ @feature_options[feature_name.to_sym] ||= {}
313
384
 
314
- @feature_options[feature_name.to_sym]
315
- end
385
+ # Only set defaults for options that don't already exist
386
+ options.each do |key, value|
387
+ @feature_options[feature_name.to_sym][key] ||= value
388
+ end
316
389
 
317
- # Create and register a transient field type
318
- #
319
- # @param name [Symbol] The field name
320
- # @param options [Hash] Field options
321
- #
322
- def transient_field(name, **)
323
- require_relative '../../features/transient_fields/transient_field_type'
324
- field_type = TransientFieldType.new(name, **, fast_method: false)
325
- register_field_type(field_type)
390
+ @feature_options[feature_name.to_sym]
326
391
  end
327
392
 
328
393
  private
@@ -379,8 +444,8 @@ end
379
444
  # @raise [ArgumentError] if fast_method_name doesn't end with '!'
380
445
  #
381
446
  # @note Generated method behavior:
382
- # - Without args: Retrieves current value from Redis
383
- # - With value: Sets and immediately persists to Redis
447
+ # - Without args: Retrieves current value from Valkey/Redis
448
+ # - With value: Sets and immediately persists to Valkey/Redis
384
449
  # - Returns boolean indicating success for writes
385
450
  # - Bypasses object-level caching and expiration updates
386
451
  #
@@ -394,20 +459,20 @@ end
394
459
  handle_method_conflict(fast_method_name, on_conflict) do
395
460
  # Fast attribute accessor method for the '#{field_name}' attribute.
396
461
  # This method provides immediate read and write access to the attribute
397
- # in Redis.
462
+ # in the database.
398
463
  #
399
464
  # When called without arguments, it retrieves the current value of the
400
- # attribute from Redis.
465
+ # attribute from the database.
401
466
  # When called with an argument, it immediately persists the new value to
402
- # Redis.
467
+ # the database.
403
468
  #
404
469
  # @overload #{method_name}
405
- # Retrieves the current value of the attribute from Redis.
470
+ # Retrieves the current value of the attribute from the database.
406
471
  # @return [Object] the current value of the attribute.
407
472
  #
408
473
  # @overload #{method_name}(value)
409
474
  # Sets and immediately persists the new value of the attribute to
410
- # Redis.
475
+ # the database.
411
476
  # @param value [Object] the new value to set for the attribute.
412
477
  # @return [Object] the newly set value.
413
478
  #
@@ -416,7 +481,7 @@ end
416
481
  # the method.
417
482
  #
418
483
  # @note This method bypasses any object-level caching and interacts
419
- # directly with Redis. It does not trigger updates to other attributes
484
+ # directly with the database. It does not trigger updates to other attributes
420
485
  # or the object's expiration time.
421
486
  #
422
487
  # @example
@@ -437,7 +502,7 @@ end
437
502
 
438
503
  begin
439
504
  # Trace the operation if debugging is enabled.
440
- Familia.trace :FAST_WRITER, dbclient, "#{field_name}: #{val.inspect}", caller(1..1) if Familia.debug?
505
+ Familia.trace :FAST_WRITER, nil, "#{field_name}: #{val.inspect}" if Familia.debug?
441
506
 
442
507
  # Convert the provided value to a format suitable for Database storage.
443
508
  prepared = serialize_value(val)
@@ -1,14 +1,12 @@
1
- # lib/familia/horreum/subclass/management.rb
2
-
3
- require_relative 'related_fields_management'
1
+ # lib/familia/horreum/management.rb
4
2
 
5
3
  module Familia
6
4
  class Horreum
7
- # ManagementMethods: Provides class-level functionality for Horreum
8
- # records.
5
+ # ManagementMethods - Class-level methods for Horreum model management
9
6
  #
10
7
  # This module is extended into classes that include Familia::Horreum,
11
- # providing methods for Database operations and object management.
8
+ # providing class methods for database operations and object management
9
+ # (e.g., Customer.create, Customer.find_by_id)
12
10
  #
13
11
  # # Key features:
14
12
  # * Includes RelatedFieldsManagement for DataType field handling
@@ -17,11 +15,13 @@ module Familia
17
15
  module ManagementMethods
18
16
  include Familia::Horreum::RelatedFieldsManagement # Provides DataType query methods
19
17
 
18
+ using Familia::Refinements::StylizeWords
19
+
20
20
  # Creates and persists a new instance of the class.
21
21
  #
22
- # @param *args [Array] Variable number of positional arguments to be passed
22
+ # @param args [Array] Variable number of positional arguments to be passed
23
23
  # to the constructor.
24
- # @param **kwargs [Hash] Keyword arguments to be passed to the constructor.
24
+ # @param kwargs [Hash] Keyword arguments to be passed to the constructor.
25
25
  # @return [Object] The newly created and persisted instance.
26
26
  # @raise [Familia::Problem] If an instance with the same identifier already
27
27
  # exists.
@@ -68,10 +68,35 @@ module Familia
68
68
  ids.collect! { |objid| dbkey(objid) }
69
69
  return [] if ids.compact.empty?
70
70
 
71
- Familia.trace :MULTIGET, dbclient, "#{ids.size}: #{ids}", caller(1..1) if Familia.debug?
71
+ Familia.trace :MULTIGET, nil, "#{ids.size}: #{ids}" if Familia.debug?
72
72
  dbclient.mget(*ids)
73
73
  end
74
74
 
75
+ # Converts the class name into a string that can be used to look up
76
+ # configuration values. This is particularly useful when mapping
77
+ # familia models with specific database numbers in the configuration.
78
+ #
79
+ # Familia::Horreum::DefinitionMethods#config_name
80
+ #
81
+ # @example V2::Session.config_name => 'session'
82
+ #
83
+ # @return [String] The underscored class name as a string
84
+ def config_name
85
+ return nil if name.nil?
86
+
87
+ name.demodularize.snake_case
88
+ end
89
+
90
+ # Familia::Horreum::DefinitionMethods#familia_name
91
+ #
92
+ # @example V2::Session.config_name => 'Session'
93
+ #
94
+ def familia_name
95
+ return nil if name.nil?
96
+
97
+ name.demodularize
98
+ end
99
+
75
100
  # Retrieves and instantiates an object from Database using the full object
76
101
  # key.
77
102
  #
@@ -83,7 +108,7 @@ module Familia
83
108
  # This method performs a two-step process to safely retrieve and
84
109
  # instantiate objects:
85
110
  #
86
- # 1. It first checks if the key exists in Redis. This is crucial because:
111
+ # 1. It first checks if the key exists in the database. This is crucial because:
87
112
  # - It provides a definitive answer about the object's existence.
88
113
  # - It prevents ambiguity that could arise from `hgetall` returning an
89
114
  # empty hash for non-existent keys, which could lead to the creation
@@ -93,7 +118,7 @@ module Familia
93
118
  # it.
94
119
  #
95
120
  # This approach ensures that we only attempt to instantiate objects that
96
- # actually exist in Redis, improving reliability and simplifying
121
+ # actually exist in Valkey/Redis, improving reliability and simplifying
97
122
  # debugging.
98
123
  #
99
124
  # @example
@@ -108,17 +133,17 @@ module Familia
108
133
  does_exist = dbclient.exists(objkey).positive?
109
134
 
110
135
  Familia.ld "[.find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
111
- Familia.trace :FROM_KEY, dbclient, objkey, caller(1..1) if Familia.debug?
136
+ Familia.trace :FROM_KEY, nil, objkey if Familia.debug?
112
137
 
113
138
  # This is the reason for calling exists first. We want to definitively
114
- # and without any ambiguity know if the object exists in Redis. If it
139
+ # and without any ambiguity know if the object exists in the database. If it
115
140
  # doesn't, we return nil. If it does, we proceed to load the object.
116
141
  # Otherwise, hgetall will return an empty hash, which will be passed to
117
142
  # the constructor, which will then be annoying to debug.
118
143
  return unless does_exist
119
144
 
120
145
  obj = dbclient.hgetall(objkey) # horreum objects are persisted as database hashes
121
- Familia.trace :FROM_KEY2, dbclient, "#{objkey}: #{obj.inspect}", caller(1..1) if Familia.debug?
146
+ Familia.trace :FROM_KEY2, nil, "#{objkey}: #{obj.inspect}" if Familia.debug?
122
147
 
123
148
  new(**obj)
124
149
  end
@@ -150,33 +175,34 @@ module Familia
150
175
  objkey = dbkey(identifier, suffix)
151
176
 
152
177
  Familia.ld "[.find_by_id] #{self} from key #{objkey})"
153
- Familia.trace :FIND_BY_ID, Familia.dbclient(uri), objkey, caller(1..1).first if Familia.debug?
178
+ Familia.trace :FIND_BY_ID, nil, objkey if Familia.debug?
154
179
  find_by_key objkey
155
180
  end
156
181
  alias find find_by_id
157
182
  alias load find_by_id # deprecated
158
183
  alias from_identifier find_by_id # deprecated
159
184
 
160
- # Checks if an object with the given identifier exists in Redis.
185
+ # Checks if an object with the given identifier exists in the database.
161
186
  #
162
187
  # @param identifier [String, Integer] The unique identifier for the object.
163
188
  # @param suffix [Symbol, nil] The suffix to use in the dbkey (default: class suffix).
164
189
  # @return [Boolean] true if the object exists, false otherwise.
165
190
  #
166
191
  # This method constructs the full dbkey using the provided identifier and suffix,
167
- # then checks if the key exists in Redis.
192
+ # then checks if the key exists in the database.
168
193
  #
169
194
  # @example
170
- # User.exists?(123) # Returns true if user:123:object exists in Redis
195
+ # User.exists?(123) # Returns true if user:123:object exists in Valkey/Redis
171
196
  #
172
197
  def exists?(identifier, suffix = nil)
173
- raise NoIdentifier, "Empty identifier" if identifier.to_s.empty?
198
+ raise NoIdentifier, 'Empty identifier' if identifier.to_s.empty?
199
+
174
200
  suffix ||= self.suffix
175
201
 
176
202
  objkey = dbkey identifier, suffix
177
203
 
178
204
  ret = dbclient.exists objkey
179
- Familia.trace :EXISTS, dbclient, "#{objkey} #{ret.inspect}", caller(1..1) if Familia.debug?
205
+ Familia.trace :EXISTS, nil, "#{objkey} #{ret.inspect}" if Familia.debug?
180
206
 
181
207
  ret.positive? # differs from Valkey API but I think it's okay bc `exists?` is a predicate method.
182
208
  end
@@ -193,17 +219,36 @@ module Familia
193
219
  # `delete!` when working directly with dbkeys.
194
220
  #
195
221
  # @example
196
- # User.destroy!(123) # Removes user:123:object from Redis
222
+ # User.destroy!(123) # Removes user:123:object from Valkey/Redis
197
223
  #
198
224
  def destroy!(identifier, suffix = nil)
199
225
  suffix ||= self.suffix
200
- return false if identifier.to_s.empty?
226
+ return MultiResult.new(false, []) if identifier.to_s.empty?
201
227
 
202
228
  objkey = dbkey identifier, suffix
203
229
 
204
- ret = dbclient.del objkey
205
- Familia.trace :DESTROY!, dbclient, "#{objkey} #{ret.inspect}", caller(1..1) if Familia.debug?
206
- ret.positive?
230
+ # Execute all deletion operations within a transaction
231
+ transaction do |conn|
232
+ # Clean up related fields first to avoid orphaned keys
233
+ if relations?
234
+ Familia.trace :DESTROY_RELATIONS!, nil, "#{self} has relations: #{related_fields.keys}" if Familia.debug?
235
+
236
+ # Create a temporary instance to access related fields.
237
+ # Pass identifier in constructor so init() sees it and can set dependent fields.
238
+ identifier_field_name = self.identifier_field
239
+ temp_instance = identifier_field_name ? new(identifier_field_name => identifier.to_s) : new
240
+
241
+ related_fields.each do |name, _definition|
242
+ obj = temp_instance.send(name)
243
+ Familia.trace :DESTROY_RELATION!, name, "Deleting related field #{name} (#{obj.dbkey})" if Familia.debug?
244
+ conn.del(obj.dbkey)
245
+ end
246
+ end
247
+
248
+ # Delete the main object key
249
+ ret = conn.del(objkey)
250
+ Familia.trace :DESTROY!, nil, "#{objkey} #{ret.inspect}" if Familia.debug?
251
+ end
207
252
  end
208
253
 
209
254
  # Finds all keys in Database matching the given suffix pattern.
@@ -249,7 +294,7 @@ module Familia
249
294
  end
250
295
 
251
296
  def any?(filter = '*')
252
- matching_keys_count(filter) > 0
297
+ matching_keys_count(filter).positive?
253
298
  end
254
299
 
255
300
  # Returns the number of dbkeys matching the given filter pattern
@@ -259,7 +304,8 @@ module Familia
259
304
  def matching_keys_count(filter = '*')
260
305
  dbclient.keys(dbkey(filter)).compact.size
261
306
  end
262
- alias size matching_keys_count # For backwards compatibility
307
+ alias size matching_keys_count
308
+ alias length matching_keys_count
263
309
  end
264
310
  end
265
311
  end