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
@@ -8,6 +8,8 @@ module Familia
8
8
  dbclient.zcard dbkey
9
9
  end
10
10
  alias size element_count
11
+ alias length element_count
12
+ alias count element_count
11
13
 
12
14
  def empty?
13
15
  element_count.zero?
@@ -24,14 +26,13 @@ module Familia
24
26
  # @return [Integer] Returns 1 if the element is new and added, 0 if the
25
27
  # element already existed and the score was updated.
26
28
  #
27
- # @example
28
- # sorted_set << "new_element"
29
+ # @example sorted_set << "new_element"
29
30
  #
30
31
  # @note This is a non-standard operation for sorted sets as it doesn't allow
31
32
  # specifying a custom score. Use `add` or `[]=` for more control.
32
33
  #
33
34
  def <<(val)
34
- add(Time.now.to_i, val)
35
+ add(val)
35
36
  end
36
37
 
37
38
  # NOTE: The argument order is the reverse of #add. We do this to
@@ -42,14 +43,88 @@ module Familia
42
43
  # obj.metrics[VALUE] # => SCORE
43
44
  #
44
45
  def []=(val, score)
45
- add score, val
46
+ add val, score
46
47
  end
47
48
 
48
- def add(score, val)
49
- ret = dbclient.zadd dbkey, score, serialize_value(val)
49
+ # Adds an element to the sorted set with an optional score and ZADD options.
50
+ #
51
+ # This method supports Redis ZADD options for conditional adds and updates:
52
+ # - **NX**: Only add new elements (don't update existing)
53
+ # - **XX**: Only update existing elements (don't add new)
54
+ # - **GT**: Only update if new score > current score
55
+ # - **LT**: Only update if new score < current score
56
+ # - **CH**: Return changed count (new + updated) instead of just new count
57
+ #
58
+ # @param val [Object] The value to add to the sorted set
59
+ # @param score [Numeric, nil] The score for ranking (defaults to current timestamp)
60
+ # @param nx [Boolean] Only add new elements, don't update existing (default: false)
61
+ # @param xx [Boolean] Only update existing elements, don't add new (default: false)
62
+ # @param gt [Boolean] Only update if new score > current score (default: false)
63
+ # @param lt [Boolean] Only update if new score < current score (default: false)
64
+ # @param ch [Boolean] Return changed count instead of added count (default: false)
65
+ #
66
+ # @return [Boolean] Returns the return value from the redis gem's ZADD
67
+ # command. Returns true if element was added or changed (with CH option),
68
+ # false if element score was updated without change tracking or no
69
+ # operation occurred due to option constraints (NX, XX, GT, LT).
70
+ #
71
+ # @raise [ArgumentError] If mutually exclusive options are specified together
72
+ # (NX+XX, GT+LT, NX+GT, NX+LT)
73
+ #
74
+ # @example Add new element with timestamp
75
+ # metrics.add('pageview', Time.now.to_f) #=> true
76
+ #
77
+ # @example Preserve original timestamp on subsequent saves
78
+ # index.add(email, Time.now.to_f, nx: true) #=> true
79
+ # index.add(email, Time.now.to_f, nx: true) #=> false (unchanged)
80
+ #
81
+ # @example Update timestamp only for existing entries
82
+ # index.add(email, Time.now.to_f, xx: true) #=> false (if doesn't exist)
83
+ #
84
+ # @example Only update if new score is higher (leaderboard)
85
+ # scores.add(player, 1000, gt: true) #=> true (new entry)
86
+ # scores.add(player, 1500, gt: true) #=> false (updated)
87
+ # scores.add(player, 1200, gt: true) #=> false (not updated, score lower)
88
+ #
89
+ # @example Track total changes for analytics
90
+ # changed = metrics.add(user, score, ch: true) #=> true (new or updated)
91
+ #
92
+ # @example Combined options: only update existing, only if score increases
93
+ # index.add(key, new_score, xx: true, gt: true)
94
+ #
95
+ # @note GT and LT options do NOT prevent adding new elements, they only
96
+ # affect update behavior for existing elements.
97
+ #
98
+ # @note Default behavior (no options) adds new elements and updates existing
99
+ # ones unconditionally, matching standard Redis ZADD semantics.
100
+ #
101
+ # @note INCR option is not supported. Use the increment method for ZINCRBY operations.
102
+ #
103
+ def add(val, score = nil, nx: false, xx: false, gt: false, lt: false, ch: false)
104
+ score ||= Familia.now
105
+
106
+ # Validate mutual exclusivity
107
+ validate_zadd_options!(nx: nx, xx: xx, gt: gt, lt: lt)
108
+
109
+ # Build options hash for redis gem
110
+ opts = {}
111
+ opts[:nx] = true if nx
112
+ opts[:xx] = true if xx
113
+ opts[:gt] = true if gt
114
+ opts[:lt] = true if lt
115
+ opts[:ch] = true if ch
116
+
117
+ # Pass options to ZADD
118
+ ret = if opts.empty?
119
+ dbclient.zadd(dbkey, score, serialize_value(val))
120
+ else
121
+ dbclient.zadd(dbkey, score, serialize_value(val), **opts)
122
+ end
123
+
50
124
  update_expiration
51
125
  ret
52
126
  end
127
+ alias add_element add
53
128
 
54
129
  def score(val)
55
130
  ret = dbclient.zscore dbkey, serialize_value(val, strict_values: false)
@@ -58,7 +133,7 @@ module Familia
58
133
  alias [] score
59
134
 
60
135
  def member?(val)
61
- Familia.trace :MEMBER, dbclient, "#{val}<#{val.class}>", caller(1..1) if Familia.debug?
136
+ Familia.trace :MEMBER, nil, "#{val}<#{val.class}>" if Familia.debug?
62
137
  !rank(val).nil?
63
138
  end
64
139
  alias include? member?
@@ -132,7 +207,7 @@ module Familia
132
207
  end
133
208
 
134
209
  def range(sidx, eidx, opts = {})
135
- echo :range, caller(1..1).first if Familia.debug
210
+ echo :range, Familia.pretty_stack(limit: 1) if Familia.debug
136
211
  elements = rangeraw(sidx, eidx, opts)
137
212
  deserialize_values(*elements)
138
213
  end
@@ -149,7 +224,7 @@ module Familia
149
224
  end
150
225
 
151
226
  def revrange(sidx, eidx, opts = {})
152
- echo :revrange, caller(1..1).first if Familia.debug
227
+ echo :revrange, Familia.pretty_stack(limit: 1) if Familia.debug
153
228
  elements = revrangeraw(sidx, eidx, opts)
154
229
  deserialize_values(*elements)
155
230
  end
@@ -160,25 +235,25 @@ module Familia
160
235
 
161
236
  # e.g. obj.metrics.rangebyscore (now-12.hours), now, :limit => [0, 10]
162
237
  def rangebyscore(sscore, escore, opts = {})
163
- echo :rangebyscore, caller(1..1).first if Familia.debug
238
+ echo :rangebyscore, Familia.pretty_stack(limit: 1) if Familia.debug
164
239
  elements = rangebyscoreraw(sscore, escore, opts)
165
240
  deserialize_values(*elements)
166
241
  end
167
242
 
168
243
  def rangebyscoreraw(sscore, escore, opts = {})
169
- echo :rangebyscoreraw, caller(1..1).first if Familia.debug
244
+ echo :rangebyscoreraw, Familia.pretty_stack(limit: 1) if Familia.debug
170
245
  dbclient.zrangebyscore(dbkey, sscore, escore, **opts)
171
246
  end
172
247
 
173
248
  # e.g. obj.metrics.revrangebyscore (now-12.hours), now, :limit => [0, 10]
174
249
  def revrangebyscore(sscore, escore, opts = {})
175
- echo :revrangebyscore, caller(1..1).first if Familia.debug
250
+ echo :revrangebyscore, Familia.pretty_stack(limit: 1) if Familia.debug
176
251
  elements = revrangebyscoreraw(sscore, escore, opts)
177
252
  deserialize_values(*elements)
178
253
  end
179
254
 
180
255
  def revrangebyscoreraw(sscore, escore, opts = {})
181
- echo :revrangebyscoreraw, caller(1..1).first if Familia.debug
256
+ echo :revrangebyscoreraw, Familia.pretty_stack(limit: 1) if Familia.debug
182
257
  opts[:with_scores] = true if opts[:withscores]
183
258
  dbclient.zrevrangebyscore(dbkey, sscore, escore, opts)
184
259
  end
@@ -207,7 +282,7 @@ module Familia
207
282
  # @param value The value to remove from the sorted set
208
283
  # @return [Integer] The number of members that were removed (0 or 1)
209
284
  def remove_element(value)
210
- Familia.trace :REMOVE_ELEMENT, dbclient, "#{value}<#{value.class}>", caller(1..1) if Familia.debug?
285
+ Familia.trace :REMOVE_ELEMENT, nil, "#{value}<#{value.class}>" if Familia.debug?
211
286
  # We use `strict_values: false` here to allow for the deletion of values
212
287
  # that are in the sorted set. If it's a horreum object, the value is
213
288
  # the identifier and not a serialized version of the object. So either
@@ -231,6 +306,45 @@ module Familia
231
306
  at(-1)
232
307
  end
233
308
 
309
+
310
+ private
311
+
312
+ # Validates that mutually exclusive ZADD options are not specified together.
313
+ #
314
+ # @param nx [Boolean] NX option flag
315
+ # @param xx [Boolean] XX option flag
316
+ # @param gt [Boolean] GT option flag
317
+ # @param lt [Boolean] LT option flag
318
+ #
319
+ # @raise [ArgumentError] If mutually exclusive options are specified
320
+ #
321
+ # @note Valid combinations: XX+GT, XX+LT
322
+ # @note Invalid combinations: NX+XX, GT+LT, NX+GT, NX+LT
323
+ #
324
+ def validate_zadd_options!(nx:, xx:, gt:, lt:)
325
+ # NX and XX are mutually exclusive
326
+ if nx && xx
327
+ raise ArgumentError, "ZADD options NX and XX are mutually exclusive"
328
+ end
329
+
330
+ # GT and LT are mutually exclusive
331
+ if gt && lt
332
+ raise ArgumentError, "ZADD options GT and LT are mutually exclusive"
333
+ end
334
+
335
+ # NX is mutually exclusive with GT
336
+ if nx && gt
337
+ raise ArgumentError, "ZADD options NX and GT are mutually exclusive"
338
+ end
339
+
340
+ # NX is mutually exclusive with LT
341
+ if nx && lt
342
+ raise ArgumentError, "ZADD options NX and LT are mutually exclusive"
343
+ end
344
+
345
+ # Note: XX + GT and XX + LT are valid combinations
346
+ end
347
+
234
348
  Familia::DataType.register self, :sorted_set
235
349
  Familia::DataType.register self, :zset
236
350
  end
@@ -1,7 +1,7 @@
1
- # lib/familia/data_type/types/string.rb
1
+ # lib/familia/data_type/types/stringkey.rb
2
2
 
3
3
  module Familia
4
- class String < DataType
4
+ class StringKey < DataType
5
5
  def init; end
6
6
 
7
7
  # Returns the number of elements in the list
@@ -10,13 +10,14 @@ module Familia
10
10
  to_s.size
11
11
  end
12
12
  alias size char_count
13
+ alias length char_count
13
14
 
14
15
  def empty?
15
16
  char_count.zero?
16
17
  end
17
18
 
18
19
  def value
19
- echo :value, caller(0..0) if Familia.debug
20
+ echo :value, Familia.pretty_stack(limit: 1) if Familia.debug
20
21
  dbclient.setnx dbkey, @opts[:default] if @opts[:default]
21
22
  deserialize_value dbclient.get(dbkey)
22
23
  end
@@ -30,7 +31,7 @@ module Familia
30
31
  end
31
32
 
32
33
  def to_i
33
- value&.to_i || 0
34
+ value.to_i
34
35
  end
35
36
 
36
37
  def value=(val)
@@ -113,14 +114,11 @@ module Familia
113
114
  ret.positive?
114
115
  end
115
116
 
116
- def nil?
117
- value.nil?
118
- end
119
-
120
117
  Familia::DataType.register self, :string
118
+ Familia::DataType.register self, :stringkey
121
119
  end
122
120
  end
123
121
 
124
- # Both subclass String
122
+ # Both subclass StringKey
125
123
  require_relative 'lock'
126
124
  require_relative 'counter'
@@ -1,13 +1,17 @@
1
1
  # lib/familia/data_type/types/unsorted_set.rb
2
2
 
3
3
  module Familia
4
- class Set < DataType
4
+ # Familia::UnsortedSet
5
+ #
6
+ class UnsortedSet < DataType
5
7
  # Returns the number of elements in the unsorted set
6
8
  # @return [Integer] number of elements
7
9
  def element_count
8
10
  dbclient.scard dbkey
9
11
  end
10
12
  alias size element_count
13
+ alias length element_count
14
+ alias count element_count
11
15
 
12
16
  def empty?
13
17
  element_count.zero?
@@ -18,13 +22,14 @@ module Familia
18
22
  update_expiration
19
23
  self
20
24
  end
25
+ alias add_element add
21
26
 
22
27
  def <<(v)
23
28
  add v
24
29
  end
25
30
 
26
31
  def members
27
- echo :members, caller(1..1).first if Familia.debug
32
+ echo :members, Familia.pretty_stack(limit: 1) if Familia.debug
28
33
  elements = membersraw
29
34
  deserialize_values(*elements)
30
35
  end
@@ -92,35 +97,23 @@ module Familia
92
97
  dbclient.smove dbkey, dstkey, val
93
98
  end
94
99
 
95
- def random
96
- deserialize_value randomraw
100
+ # Get one or more random members from the set
101
+ # @param count [Integer] Number of random members to return (default: 1)
102
+ # @return [Array] Array of deserialized random members
103
+ def sample(count = 1)
104
+ deserialize_values(*sampleraw(count))
97
105
  end
106
+ alias random sample
98
107
 
99
- def randomraw
100
- dbclient.srandmember(dbkey)
108
+ # Get one or more random members from the set without deserialization
109
+ # @param count [Integer] Number of random members to return (default: 1)
110
+ # @return [Array] Array of raw random members
111
+ def sampleraw(count = 1)
112
+ dbclient.srandmember(dbkey, count) || []
101
113
  end
102
-
103
- ## Make the value stored at KEY identical to the given list
104
- # define_method :"#{name}_sync" do |*latest|
105
- # latest = latest.flatten.compact
106
- # # Do nothing if we're given an empty Array.
107
- # # Otherwise this would clear all current values
108
- # if latest.empty?
109
- # false
110
- # else
111
- # # Convert to a list of index values if we got the actual objects
112
- # latest = latest.collect { |obj| obj.index } if klass === latest.first
113
- # current = send("#{name_plural}raw")
114
- # added = latest-current
115
- # removed = current-latest
116
- # #Familia.info "#{self.index}: adding: #{added}"
117
- # added.each { |v| self.send("add_#{name_singular}", v) }
118
- # #Familia.info "#{self.index}: removing: #{removed}"
119
- # removed.each { |v| self.send("remove_#{name_singular}", v) }
120
- # true
121
- # end
122
- # end
114
+ alias random sampleraw
123
115
 
124
116
  Familia::DataType.register self, :set
117
+ Familia::DataType.register self, :unsorted_set
125
118
  end
126
119
  end
@@ -1,5 +1,8 @@
1
1
  # lib/familia/data_type.rb
2
2
 
3
+ require_relative 'data_type/class_methods'
4
+ require_relative 'data_type/settings'
5
+ require_relative 'data_type/connection'
3
6
  require_relative 'data_type/commands'
4
7
  require_relative 'data_type/serialization'
5
8
 
@@ -9,72 +12,27 @@ module Familia
9
12
  # DataType - Base class for Database data type wrappers
10
13
  #
11
14
  # This class provides common functionality for various Database data types
12
- # such as String, List, Set, SortedSet, and HashKey.
15
+ # such as String, List, UnsortedSet, SortedSet, and HashKey.
13
16
  #
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
20
24
 
21
25
  @registered_types = {}
22
- @valid_options = %i[class parent default_expiration default logical_database dbkey dbclient suffix prefix]
26
+ @valid_options = %i[class parent default_expiration default logical_database dbkey dbclient suffix prefix].freeze
23
27
  @logical_database = nil
24
28
 
25
29
  feature :expiration
26
30
  feature :quantization
27
31
 
28
32
  class << self
29
- attr_reader :registered_types, :valid_options, :has_relations
30
- attr_accessor :parent
31
- attr_writer :logical_database, :uri
33
+ attr_reader :registered_types, :valid_options, :has_related_fields
32
34
  end
33
35
 
34
- # DataType::ClassMethods
35
- #
36
- module ClassMethods
37
- # To be called inside every class that inherits DataType
38
- # +methname+ is the term used for the class and instance methods
39
- # that are created for the given +klass+ (e.g. set, list, etc)
40
- def register(klass, methname)
41
- Familia.trace :REGISTER, nil, "[#{self}] Registering #{klass} as #{methname.inspect}", caller(1..1) if Familia.debug?
42
-
43
- @registered_types[methname] = klass
44
- end
45
-
46
- def logical_database(val = nil)
47
- @logical_database = val unless val.nil?
48
- @logical_database || parent&.logical_database
49
- end
50
-
51
- def uri(val = nil)
52
- @uri = val unless val.nil?
53
- @uri || (parent ? parent.uri : Familia.uri)
54
- end
55
-
56
- def inherited(obj)
57
- Familia.trace :DATATYPE, nil, "#{obj} is my kinda type", caller(1..1) if Familia.debug?
58
- obj.logical_database = logical_database
59
- obj.default_expiration = default_expiration # method added via Features::Expiration
60
- obj.uri = uri
61
- obj.parent = self
62
- super
63
- end
64
-
65
- def valid_keys_only(opts)
66
- opts.slice(*DataType.valid_options)
67
- end
68
-
69
- def relations?
70
- @has_relations ||= false # rubocop:disable ThreadSafety/ClassInstanceVariable
71
- end
72
- end
73
- extend ClassMethods
74
-
75
- attr_reader :keystring, :opts
76
- attr_writer :dump_method, :load_method
77
-
78
36
  # +keystring+: If parent is set, this will be used as the suffix
79
37
  # for dbkey. Otherwise this becomes the value of the key.
80
38
  # If this is an Array, the elements will be joined.
@@ -94,10 +52,6 @@ module Familia
94
52
  #
95
53
  # :default => the default value (String-only)
96
54
  #
97
- # :logical_database => the logical database index to use (ignored if :dbclient is used).
98
- #
99
- # :dbclient => an instance of database client.
100
- #
101
55
  # :dbkey => a hardcoded key to use instead of the deriving the from
102
56
  # the name and parent (e.g. a derived key: customer:custid:secret_counter).
103
57
  #
@@ -111,138 +65,25 @@ module Familia
111
65
  @keystring = @keystring.join(Familia.delim) if @keystring.is_a?(Array)
112
66
 
113
67
  # Remove all keys from the opts that are not in the allowed list
114
- @opts = opts || {}
115
- @opts = DataType.valid_keys_only(@opts)
68
+ @opts = DataType.valid_keys_only(opts || {})
116
69
 
117
70
  # Apply the options to instance method setters of the same name
118
71
  @opts.each do |k, v|
119
- # Bewarde logging :parent instance here implicitly calls #to_s which for
120
- # some classes could include the identifier which could still be nil at
121
- # this point. This would result in a Familia::Problem being raised. So
122
- # to be on the safe-side here until we have a better understanding of
123
- # the issue, we'll just log the class name for each key-value pair.
124
- Familia.trace :SETTING, nil, " [setting] #{k} #{v.class}", caller(1..1) if Familia.debug?
125
72
  send(:"#{k}=", v) if respond_to? :"#{k}="
126
73
  end
127
74
 
128
75
  init if respond_to? :init
129
76
  end
130
77
 
131
- def dbclient
132
- return Fiber[:familia_transaction] if Fiber[:familia_transaction]
133
- return @dbclient if @dbclient
134
-
135
- parent? ? parent.dbclient : Familia.dbclient(opts[:logical_database])
136
- end
137
-
138
- # Produces the full dbkey for this object.
139
- #
140
- # @return [String] The full dbkey.
141
- #
142
- # This method determines the appropriate dbkey based on the context of the DataType object:
143
- #
144
- # 1. If a hardcoded key is set in the options, it returns that key.
145
- # 2. For instance-level DataType objects, it uses the parent instance's dbkey method.
146
- # 3. For class-level DataType objects, it uses the parent class's dbkey method.
147
- # 4. For standalone DataType objects, it uses the keystring as the full dbkey.
148
- #
149
- # For class-level DataType objects (parent_class? == true):
150
- # - The suffix is optional and used to differentiate between different types of objects.
151
- # - If no suffix is provided, the class's default suffix is used (via the self.suffix method).
152
- # - If a nil suffix is explicitly passed, it won't appear in the resulting dbkey.
153
- # - Passing nil as the suffix is how class-level DataType objects are created without
154
- # the global default 'object' suffix.
155
- #
156
- # @example Instance-level DataType
157
- # user_instance.some_datatype.dbkey # => "user:123:some_datatype"
158
- #
159
- # @example Class-level DataType
160
- # User.some_datatype.dbkey # => "user:some_datatype"
161
- #
162
- # @example Standalone DataType
163
- # DataType.new("mykey").dbkey # => "mykey"
164
- #
165
- # @example Class-level DataType with explicit nil suffix
166
- # User.dbkey("123", nil) # => "user:123"
167
- #
168
- def dbkey
169
- # Return the hardcoded key if it's set. This is useful for
170
- # support legacy keys that aren't derived in the same way.
171
- return opts[:dbkey] if opts[:dbkey]
172
-
173
- if parent_instance?
174
- # This is an instance-level datatype object so the parent instance's
175
- # dbkey method is defined in Familia::Horreum::InstanceMethods.
176
- parent.dbkey(keystring)
177
- elsif parent_class?
178
- # This is a class-level datatype object so the parent class' dbkey
179
- # method is defined in Familia::Horreum::DefinitionMethods.
180
- parent.dbkey(keystring, nil)
181
- else
182
- # This is a standalone DataType object where it's keystring
183
- # is the full database key (dbkey).
184
- keystring
185
- end
186
- end
187
-
188
- def class?
189
- !@opts[:class].to_s.empty? && @opts[:class].is_a?(Familia)
190
- end
191
-
192
- def parent_instance?
193
- parent.is_a?(Familia::Horreum)
194
- end
195
-
196
- def parent_class?
197
- parent.is_a?(Class) && parent <= Familia::Horreum
198
- end
199
-
200
- def parent?
201
- parent_class? || parent_instance?
202
- end
203
-
204
- def parent
205
- @opts[:parent]
206
- end
207
-
208
-
209
- def logical_database
210
- @opts[:logical_database] || self.class.logical_database
211
- end
212
-
213
- def uri
214
- # If a specific URI is set in opts, use it
215
- return @opts[:uri] if @opts[:uri]
216
-
217
- # If parent has a DB set, create a URI with that DB
218
- if parent? && parent.respond_to?(:logical_database) && parent.logical_database
219
- base_uri = self.class.uri || Familia.uri
220
- if base_uri
221
- uri_with_db = base_uri.dup
222
- uri_with_db.db = parent.logical_database
223
- return uri_with_db
224
- end
225
- end
226
-
227
- # Otherwise fall back to class URI
228
- self.class.uri
229
- end
230
-
231
- def dump_method
232
- @dump_method || self.class.dump_method
233
- end
234
-
235
- def load_method
236
- @load_method || self.class.load_method
237
- end
238
-
78
+ include Settings
79
+ include Connection
239
80
  include Commands
240
81
  include Serialization
241
82
  end
242
83
 
243
- require_relative 'data_type/types/list'
84
+ require_relative 'data_type/types/listkey'
244
85
  require_relative 'data_type/types/unsorted_set'
245
86
  require_relative 'data_type/types/sorted_set'
246
87
  require_relative 'data_type/types/hashkey'
247
- require_relative 'data_type/types/string'
88
+ require_relative 'data_type/types/stringkey'
248
89
  end
@@ -0,0 +1,85 @@
1
+ # lib/familia/distinguisher.rb
2
+
3
+ module Familia
4
+ module Distinguisher
5
+ # This method determines the appropriate transformation to apply based on
6
+ # the class of the input argument.
7
+ #
8
+ # @param [Object] value_to_distinguish The value to be processed. Keep in
9
+ # mind that all data is stored as a string so whatever the type
10
+ # of the value, it will be converted to a string.
11
+ # @param [Boolean] strict_values Whether to enforce strict value handling.
12
+ # Defaults to true.
13
+ # @return [String, nil] The processed value as a string or nil for unsupported
14
+ # classes.
15
+ #
16
+ # The method uses a case statement to handle different classes:
17
+ # - For `Symbol`, `String`, `Integer`, and `Float` classes, it traces the
18
+ # operation and converts the value to a string.
19
+ # - For `Familia::Horreum` class, it traces the operation and returns the
20
+ # identifier of the value.
21
+ # - For `TrueClass`, `FalseClass`, and `NilClass`, it traces the operation and
22
+ # converts the value to a string ("true", "false", or "").
23
+ # - For any other class, it traces the operation and returns nil.
24
+ #
25
+ # Alternative names for `value_to_distinguish` could be `input_value`, `value`,
26
+ # or `object`.
27
+ #
28
+ def distinguisher(value_to_distinguish, strict_values: true)
29
+ case value_to_distinguish
30
+ when ::Symbol, ::String, ::Integer, ::Float
31
+ Familia.trace :TOREDIS_DISTINGUISHER, nil, 'string' if Familia.debug?
32
+
33
+ # Symbols and numerics are naturally serializable to strings
34
+ # so it's a relatively low risk operation.
35
+ value_to_distinguish.to_s
36
+
37
+ when ::TrueClass, ::FalseClass, ::NilClass
38
+ Familia.trace :TOREDIS_DISTINGUISHER, nil, 'true/false/nil' if Familia.debug?
39
+
40
+ # TrueClass, FalseClass, and NilClass are considered high risk because their
41
+ # original types cannot be reliably determined from their serialized string
42
+ # representations. This can lead to unexpected behavior during deserialization.
43
+ # For instance, a TrueClass value serialized as "true" might be deserialized as
44
+ # a String, causing application errors. Even more problematic, a NilClass value
45
+ # serialized as an empty string makes it impossible to distinguish between a
46
+ # nil value and an empty string upon deserialization. Such scenarios can result
47
+ # in subtle, hard-to-diagnose bugs. To mitigate these risks, we raise an
48
+ # exception when encountering these types unless the strict_values option is
49
+ # explicitly set to false.
50
+ #
51
+ raise Familia::NotDistinguishableError, value_to_distinguish if strict_values
52
+
53
+ value_to_distinguish.to_s #=> "true", "false", ""
54
+
55
+ when Familia::Base, Class
56
+ Familia.trace :TOREDIS_DISTINGUISHER, nil, 'base' if Familia.debug?
57
+
58
+ # When called with a class we simply transform it to its name. For
59
+ # instances of Familia class, we store the identifier.
60
+ if value_to_distinguish.is_a?(Class)
61
+ value_to_distinguish.name
62
+ else
63
+ value_to_distinguish.identifier
64
+ end
65
+
66
+ else
67
+ Familia.trace :TOREDIS_DISTINGUISHER, nil, "else1 #{strict_values}" if Familia.debug?
68
+
69
+ if value_to_distinguish.class.ancestors.member?(Familia::Base)
70
+ Familia.trace :TOREDIS_DISTINGUISHER, nil, 'isabase' if Familia.debug?
71
+
72
+ value_to_distinguish.identifier
73
+
74
+ else
75
+ Familia.trace :TOREDIS_DISTINGUISHER, nil, "else2 #{strict_values}" if Familia.debug?
76
+ raise Familia::NotDistinguishableError, value_to_distinguish if strict_values
77
+
78
+ nil
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ extend Distinguisher
85
+ end