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,73 +0,0 @@
1
- # lib/familia/horreum/connection.rb
2
-
3
- module Familia
4
- class Horreum
5
- # Connection: Valkey connection management for Horreum instances
6
- # Provides both instance and class-level connection methods
7
- module Connection
8
- attr_reader :uri
9
-
10
- # Returns the Database connection for the class.
11
- #
12
- # This method retrieves the Database connection instance for the class. If no
13
- # connection is set, it initializes a new connection using the provided URI
14
- # or database configuration.
15
- #
16
- # @return [Redis] the Database connection instance.
17
- #
18
- def dbclient
19
- Fiber[:familia_transaction] || @dbclient || Familia.dbclient(uri || logical_database)
20
- end
21
-
22
- def connect(*)
23
- Familia.connect(*)
24
- end
25
-
26
- def uri=(uri)
27
- @uri = normalize_uri(uri)
28
- end
29
- alias url uri
30
- alias url= uri=
31
-
32
- # Perform a sacred Database transaction ritual.
33
- #
34
- # This method creates a protective circle around your Database operations,
35
- # ensuring they all succeed or fail together. It's like a group hug for your
36
- # data operations, but with more ACID properties.
37
- #
38
- # @yield [conn] A block where you can perform your Database incantations.
39
- # @yieldparam conn [Redis] A Database connection in multi mode.
40
- #
41
- # @example Performing a Database rain dance
42
- # transaction do |conn|
43
- # conn.set("weather", "rainy")
44
- # conn.set("mood", "melancholic")
45
- # end
46
- #
47
- # @note This method works with the global Familia.transaction context when available
48
- #
49
- def transaction(&)
50
- # If we're already in a Familia.transaction context, just yield the multi connection
51
- if Fiber[:familia_transaction]
52
- yield(Fiber[:familia_transaction])
53
- else
54
- # Otherwise, create a local transaction
55
- block_result = dbclient.multi(&)
56
- end
57
- block_result
58
- end
59
- alias multi transaction
60
-
61
- def pipeline(&)
62
- # If we're already in a Familia.pipeline context, just yield the pipeline connection
63
- if Fiber[:familia_pipeline]
64
- yield(Fiber[:familia_pipeline])
65
- else
66
- # Otherwise, create a local transaction
67
- block_result = dbclient.pipeline(&)
68
- end
69
- block_result
70
- end
71
- end
72
- end
73
- end
@@ -1,21 +0,0 @@
1
- # lib/familia/horreum/core.rb
2
-
3
- require_relative 'core/database_commands'
4
- require_relative 'core/serialization'
5
- require_relative 'core/connection'
6
- require_relative 'core/utils'
7
-
8
- module Familia
9
- class Horreum
10
- module Core
11
- include Familia::Horreum::DatabaseCommands
12
- include Familia::Horreum::Serialization
13
- # include for instance methods after it's loaded. Note that Horreum::Utils
14
- # are also included and at one time also has a uri method. This connection
15
- # module is also extended for the class level methods. It will require some
16
- # disambiguation at some point.
17
- include Familia::Horreum::Connection
18
- include Familia::Horreum::Utils
19
- end
20
- end
21
- end
@@ -1,40 +0,0 @@
1
- # lib/familia/refinements/snake_case.rb
2
-
3
- module Familia
4
- module Refinements
5
- module SnakeCase
6
- # We refine String rather than Class or Module because this method operates on
7
- # string representations of class names (like those from `Class#name`) rather
8
- # than the class objects themselves. Refining String is safer because it
9
- # limits its scope to only the subset string manipulation contexts where it is
10
- # used.
11
- #
12
- # Appropriate for converting Ruby class names to database table names, config
13
- # keys, part of a path or any other snake_case identifiers. The only situation
14
- # it is not appropriate for is investigating actual snakes.
15
- refine String do
16
- # Converts a string from PascalCase/camelCase to snake_case format.
17
- #
18
- # @return [String] the snake_case version of the string
19
- #
20
- # @example Converting simple CamelCase
21
- # "FirstName".snake_case #=> "first_name"
22
- #
23
- # @example Converting PascalCase with acronyms
24
- # XMLHttpRequest.name.snake_case #=> "xml_http_request"
25
- #
26
- # @example Converting namespaced class names
27
- # "MyApp::UserAccount".snake_case #=> "user_account"
28
- #
29
- # @example Handling mixed case with numbers
30
- # "parseHTML5Document".snake_case #=> "parse_html5_document"
31
- def snake_case
32
- split('::').last
33
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
34
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
35
- .downcase
36
- end
37
- end
38
- end
39
- end
40
- end
@@ -1,336 +0,0 @@
1
- # lib/familia/validation/command_recorder.rb
2
-
3
- require 'concurrent-ruby'
4
-
5
- module Familia
6
- module Validation
7
- # Enhanced command recorder that captures Redis commands with full context
8
- # for validation purposes. Extends the existing DatabaseLogger functionality
9
- # to provide detailed command tracking including transaction boundaries.
10
- #
11
- # @example Basic usage
12
- # CommandRecorder.start_recording
13
- # # ... perform Redis operations
14
- # commands = CommandRecorder.stop_recording
15
- # puts commands.map(&:to_s)
16
- #
17
- # @example Transaction recording
18
- # CommandRecorder.start_recording
19
- # Familia.transaction do |conn|
20
- # conn.hset("key", "field", "value")
21
- # conn.incr("counter")
22
- # end
23
- # commands = CommandRecorder.stop_recording
24
- # commands.transaction_blocks.length #=> 1
25
- # commands.transaction_blocks.first.commands.length #=> 2
26
- #
27
- module CommandRecorder
28
- extend self
29
-
30
- # Thread-safe recording state
31
- @recording_state = Concurrent::ThreadLocalVar.new { false }
32
- @recorded_commands = Concurrent::ThreadLocalVar.new { CommandSequence.new }
33
- @transaction_stack = Concurrent::ThreadLocalVar.new { [] }
34
- @pipeline_stack = Concurrent::ThreadLocalVar.new { [] }
35
-
36
- # Represents a single Redis command with full context
37
- class RecordedCommand
38
- attr_reader :command, :args, :result, :timestamp, :duration_us, :context, :command_type
39
-
40
- def initialize(command:, args:, result:, timestamp:, duration_us:, context: {})
41
- @command = command.to_s.upcase
42
- @args = args.dup.freeze
43
- @result = result
44
- @timestamp = timestamp
45
- @duration_us = duration_us
46
- @context = context.dup.freeze
47
- @command_type = determine_command_type
48
- end
49
-
50
- def to_s
51
- args_str = @args.map(&:inspect).join(', ')
52
- "#{@command}(#{args_str})"
53
- end
54
-
55
- def to_h
56
- {
57
- command: @command,
58
- args: @args,
59
- result: @result,
60
- timestamp: @timestamp,
61
- duration_us: @duration_us,
62
- context: @context,
63
- command_type: @command_type
64
- }
65
- end
66
-
67
- def transaction_command?
68
- %w[MULTI EXEC DISCARD].include?(@command)
69
- end
70
-
71
- def pipeline_command?
72
- @context[:pipeline] == true
73
- end
74
-
75
- def atomic_command?
76
- @context[:transaction] == true
77
- end
78
-
79
- private
80
-
81
- def determine_command_type
82
- case @command
83
- when 'MULTI', 'EXEC', 'DISCARD'
84
- :transaction_control
85
- when 'PIPELINE'
86
- :pipeline_control
87
- when /^H(GET|SET|DEL|EXISTS|KEYS|LEN|MGET|MSET)/
88
- :hash
89
- when /^(L|R)(PUSH|POP|LEN|RANGE|INDEX|SET|REM)/
90
- :list
91
- when /^S(ADD|REM|MEMBERS|CARD|ISMEMBER|DIFF|INTER|UNION)/
92
- :set
93
- when /^Z(ADD|REM|RANGE|SCORE|CARD|COUNT|RANK|INCR)/
94
- :sorted_set
95
- when /^(GET|SET|DEL|EXISTS|EXPIRE|TTL|TYPE|INCR|DECR)/
96
- :string
97
- else
98
- :other
99
- end
100
- end
101
- end
102
-
103
- # Represents a sequence of Redis commands with transaction boundaries
104
- class CommandSequence
105
- attr_reader :commands, :transaction_blocks, :pipeline_blocks
106
-
107
- def initialize
108
- @commands = []
109
- @transaction_blocks = []
110
- @pipeline_blocks = []
111
- end
112
-
113
- def add_command(recorded_command)
114
- @commands << recorded_command
115
- end
116
-
117
- def start_transaction(context = {})
118
- @transaction_blocks << TransactionBlock.new(context)
119
- end
120
-
121
- def end_transaction
122
- return unless current_transaction
123
-
124
- current_transaction.finalize(@commands)
125
- end
126
-
127
- def start_pipeline(context = {})
128
- @pipeline_blocks << PipelineBlock.new(context)
129
- end
130
-
131
- def end_pipeline
132
- return unless current_pipeline
133
-
134
- current_pipeline.finalize(@commands)
135
- end
136
-
137
- def current_transaction
138
- @transaction_blocks.last
139
- end
140
-
141
- def current_pipeline
142
- @pipeline_blocks.last
143
- end
144
-
145
- def command_count
146
- @commands.length
147
- end
148
-
149
- def transaction_count
150
- @transaction_blocks.length
151
- end
152
-
153
- def pipeline_count
154
- @pipeline_blocks.length
155
- end
156
-
157
- def to_a
158
- @commands
159
- end
160
-
161
- def clear
162
- @commands.clear
163
- @transaction_blocks.clear
164
- @pipeline_blocks.clear
165
- end
166
- end
167
-
168
- # Represents a transaction block (MULTI/EXEC)
169
- class TransactionBlock
170
- attr_reader :start_index, :end_index, :commands, :context, :started_at
171
-
172
- def initialize(context = {})
173
- @context = context
174
- @started_at = Time.now
175
- @start_index = nil
176
- @end_index = nil
177
- @commands = []
178
- end
179
-
180
- def finalize(all_commands)
181
- # Find MULTI and EXEC commands
182
- multi_index = all_commands.rindex { |cmd| cmd.command == 'MULTI' }
183
- exec_index = all_commands.rindex { |cmd| cmd.command == 'EXEC' }
184
-
185
- return unless multi_index && exec_index && exec_index > multi_index
186
-
187
- @start_index = multi_index
188
- @end_index = exec_index
189
- @commands = all_commands[(multi_index + 1)...exec_index]
190
- end
191
-
192
- def valid?
193
- @start_index && @end_index && @commands.any?
194
- end
195
-
196
- def command_count
197
- @commands.length
198
- end
199
- end
200
-
201
- # Represents a pipeline block
202
- class PipelineBlock
203
- attr_reader :commands, :context, :started_at
204
-
205
- def initialize(context = {})
206
- @context = context
207
- @started_at = Time.now
208
- @commands = []
209
- end
210
-
211
- def finalize(all_commands)
212
- # Pipeline commands are those executed within pipeline context
213
- @commands = all_commands.select(&:pipeline_command?)
214
- end
215
-
216
- def command_count
217
- @commands.length
218
- end
219
- end
220
-
221
- # Start recording Redis commands for the current thread
222
- def start_recording
223
- @recording_state.value = true
224
- @recorded_commands.value = CommandSequence.new
225
- @transaction_stack.value = []
226
- @pipeline_stack.value = []
227
- end
228
-
229
- # Stop recording and return the recorded command sequence
230
- def stop_recording
231
- @recording_state.value = false
232
- sequence = @recorded_commands.value
233
- @recorded_commands.value = CommandSequence.new
234
- sequence
235
- end
236
-
237
- # Check if currently recording
238
- def recording?
239
- @recording_state.value == true
240
- end
241
-
242
- # Record a Redis command with full context
243
- def record_command(command:, args:, result:, timestamp:, duration_us:, context: {})
244
- return unless recording?
245
-
246
- # Enhance context with transaction/pipeline state
247
- enhanced_context = context.merge(
248
- transaction: in_transaction?,
249
- pipeline: in_pipeline?,
250
- transaction_depth: transaction_depth,
251
- pipeline_depth: pipeline_depth
252
- )
253
-
254
- recorded_cmd = RecordedCommand.new(
255
- command: command,
256
- args: args,
257
- result: result,
258
- timestamp: timestamp,
259
- duration_us: duration_us,
260
- context: enhanced_context
261
- )
262
-
263
- sequence = @recorded_commands.value
264
- sequence.add_command(recorded_cmd)
265
-
266
- # Handle transaction boundaries
267
- case recorded_cmd.command
268
- when 'MULTI'
269
- sequence.start_transaction(enhanced_context)
270
- @transaction_stack.value.push(Time.now)
271
- when 'EXEC', 'DISCARD'
272
- sequence.end_transaction if sequence.current_transaction
273
- @transaction_stack.value.pop
274
- end
275
- end
276
-
277
- # Check if we're currently in a transaction
278
- def in_transaction?
279
- @transaction_stack.value.any?
280
- end
281
-
282
- # Check if we're currently in a pipeline
283
- def in_pipeline?
284
- @pipeline_stack.value.any?
285
- end
286
-
287
- # Get current transaction nesting depth
288
- def transaction_depth
289
- @transaction_stack.value.length
290
- end
291
-
292
- # Get current pipeline nesting depth
293
- def pipeline_depth
294
- @pipeline_stack.value.length
295
- end
296
-
297
- # Get the current command sequence (for inspection during recording)
298
- def current_sequence
299
- @recorded_commands.value
300
- end
301
-
302
- # Clear all recorded data
303
- def clear
304
- @recorded_commands.value.clear
305
- end
306
-
307
- # Enhanced middleware that integrates with DatabaseLogger
308
- module Middleware
309
- def self.call(command, config)
310
- return yield unless CommandRecorder.recording?
311
-
312
- timestamp = Time.now
313
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
314
-
315
- result = yield
316
-
317
- duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start_time
318
-
319
- CommandRecorder.record_command(
320
- command: command[0],
321
- args: command[1..-1],
322
- result: result,
323
- timestamp: timestamp,
324
- duration_us: duration,
325
- context: {
326
- config: config,
327
- thread_id: Thread.current.object_id
328
- }
329
- )
330
-
331
- result
332
- end
333
- end
334
- end
335
- end
336
- end