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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smells.yml +85 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +3 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +54 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +86 -4
- data/CLAUDE.md +39 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +624 -20
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +7 -7
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +51 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +15 -11
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +128 -14
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +12 -171
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +71 -66
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +57 -19
- data/lib/familia/features/object_identifier.rb +134 -25
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
- data/lib/familia/features/relationships/indexing.rb +182 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +10 -7
- data/lib/familia/features.rb +10 -14
- data/lib/familia/field_type.rb +6 -4
- data/lib/familia/horreum/connection.rb +297 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
- data/lib/familia/horreum/serialization.rb +172 -0
- data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
- data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
- data/lib/familia/horreum.rb +222 -119
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +2 -2
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +10 -10
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +11 -10
- data/try/core/errors_try.rb +11 -14
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/familia_try.rb +1 -1
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +3 -3
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/field_groups_try.rb +244 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +443 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +3 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +6 -7
- data/try/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +3 -1
- data/try/horreum/defensive_initialization_try.rb +86 -0
- data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/horreum/settings_try.rb +2 -0
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +2 -2
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +13 -15
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- data/try/valkey.conf +26 -0
- metadata +92 -52
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/horreum/core/connection.rb +0 -73
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- 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
|
data/lib/familia/horreum/core.rb
DELETED
@@ -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
|