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
data/README.md
CHANGED
@@ -1,8 +1,56 @@
|
|
1
1
|
# Familia - 2.0
|
2
2
|
|
3
|
-
**Organize and store Ruby objects in Valkey/Redis
|
3
|
+
**Organize and store Ruby objects in Valkey/Redis using native database types (an ORM of sorts).**
|
4
4
|
|
5
|
-
Familia provides
|
5
|
+
Familia provides object-oriented access to Valkey/Redis using their database types. Unlike traditional ORMs that map objects to relational tables, Familia maps Ruby objects directly to Valkey's native data structures (strings, lists, sets, sorted sets, hashes) as instance variables.
|
6
|
+
|
7
|
+
> [!CAUTION]
|
8
|
+
> Familia 2 is in pre-release and not ready for production use. (October 2025)
|
9
|
+
## Traditional ORM vs Familia
|
10
|
+
|
11
|
+
**Traditional ORMs** convert your objects to SQL tables. A product with categories becomes two tables with a join table. Checking if a tag exists requires a query with joins.
|
12
|
+
|
13
|
+
**Familia** stores your objects using Valkey/Redis data structures directly. A product with categories uses an actual Valkey/Redis list. Checking if a tag exists is a native O(1) Valkey/Redis operation.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
# Traditional ORM - everything becomes SQL tables
|
17
|
+
class Product < ActiveRecord::Base
|
18
|
+
has_and_belongs_to_many :tags # Creates products_tags junction table
|
19
|
+
end
|
20
|
+
|
21
|
+
product.tags.include?(tag) # SELECT * FROM products_tags WHERE ...
|
22
|
+
|
23
|
+
# Familia - uses Valkey/Redis data types directly
|
24
|
+
class Product < Familia::Horreum
|
25
|
+
set :tags # Actual Valkey/Redis set
|
26
|
+
end
|
27
|
+
|
28
|
+
product.tags.include?("electronics") # Valkey/Redis SISMEMBER - O(1) operation
|
29
|
+
```
|
30
|
+
|
31
|
+
### What This Means in Practice
|
32
|
+
|
33
|
+
When you define a Familia model, each data type declaration creates the corresponding Valkey/Redis structure:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
class Product < Familia::Horreum
|
37
|
+
identifier_field :sku
|
38
|
+
|
39
|
+
field :name, :price # Stored in Valkey/Redis hash
|
40
|
+
list :categories # Actual Valkey/Redis list
|
41
|
+
set :tags # Actual Valkey/Redis set
|
42
|
+
zset :ratings # Actual Valkey/Redis sorted set
|
43
|
+
counter :views # Valkey/Redis string with atomic increment
|
44
|
+
end
|
45
|
+
|
46
|
+
# These are Valkey/Redis native operations, not ORM abstractions
|
47
|
+
product.categories.push("electronics") # LPUSH
|
48
|
+
product.tags.add("popular") # SADD
|
49
|
+
product.ratings.add(4.5, "user123") # ZADD with score
|
50
|
+
product.views.increment # INCR (atomic)
|
51
|
+
```
|
52
|
+
|
53
|
+
The performance characteristics you rely on in Valkey/Redis remain unchanged. Set membership is O(1). Sorted sets maintain order automatically. Counters increment atomically without read-modify-write cycles.
|
6
54
|
|
7
55
|
## Quick Start
|
8
56
|
|
@@ -10,7 +58,7 @@ Familia provides a flexible and feature-rich way to interact with Valkey using R
|
|
10
58
|
|
11
59
|
```bash
|
12
60
|
# Add to Gemfile
|
13
|
-
gem 'familia', '>= 2.0
|
61
|
+
gem 'familia', '>= 2.0'
|
14
62
|
|
15
63
|
# Or install directly
|
16
64
|
gem install familia
|
@@ -45,19 +93,49 @@ end
|
|
45
93
|
### 4. Basic Operations
|
46
94
|
|
47
95
|
```ruby
|
48
|
-
# Create
|
49
|
-
user = User.
|
50
|
-
user.save
|
96
|
+
# Create and save
|
97
|
+
user = User.create(email: 'alice@example.com', name: 'Alice', created_at: Time.now.to_i)
|
51
98
|
|
52
|
-
# Find
|
99
|
+
# Find by identifier
|
53
100
|
user = User.load('alice@example.com')
|
54
101
|
|
55
|
-
# Update
|
56
|
-
user.name = 'Alice
|
102
|
+
# Update and save
|
103
|
+
user.name = 'Alice Windows'
|
57
104
|
user.save
|
58
105
|
|
106
|
+
# Fast update (immediate persistence)
|
107
|
+
user.name!('Alice Smith') # Sets and saves immediately
|
108
|
+
|
59
109
|
# Check existence
|
60
110
|
User.exists?('alice@example.com') #=> true
|
111
|
+
|
112
|
+
# Delete
|
113
|
+
user.destroy
|
114
|
+
|
115
|
+
# Conditional save
|
116
|
+
user.save_if_not_exists # Only saves if object doesn't exist yet
|
117
|
+
```
|
118
|
+
|
119
|
+
### 5. Generated Method Patterns
|
120
|
+
|
121
|
+
Familia automatically generates methods for fields and data types:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class User < Familia::Horreum
|
125
|
+
field :name # → name, name=, name!
|
126
|
+
set :tags # → tags, tags=, tags?
|
127
|
+
list :history # → history, history=, history?
|
128
|
+
end
|
129
|
+
|
130
|
+
# Field methods
|
131
|
+
user.name # Get field value
|
132
|
+
user.name = 'Alice' # Set field value
|
133
|
+
user.name!('Alice') # Set and save immediately
|
134
|
+
|
135
|
+
# Data type methods
|
136
|
+
user.tags # Get Set instance
|
137
|
+
user.tags = new_set # Replace Set instance
|
138
|
+
user.tags? # Check if it's a Set type
|
61
139
|
```
|
62
140
|
|
63
141
|
## Prerequisites
|
@@ -66,13 +144,104 @@ User.exists?('alice@example.com') #=> true
|
|
66
144
|
- **Valkey/Redis**: 6.0+
|
67
145
|
- **Gems**: `redis` (automatically installed)
|
68
146
|
|
147
|
+
---
|
148
|
+
|
149
|
+
## Core Concepts
|
150
|
+
|
151
|
+
### Data Types
|
152
|
+
|
153
|
+
Familia provides direct mappings to Valkey/Redis native data structures:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
class BlogPost < Familia::Horreum
|
157
|
+
identifier_field :slug
|
158
|
+
|
159
|
+
# Basic fields
|
160
|
+
field :slug, :title, :content, :published_at
|
161
|
+
|
162
|
+
# Valkey/Redis data types as instance variables
|
163
|
+
string :view_count, default: '0' # Atomic counters
|
164
|
+
list :comments # Ordered, allows duplicates
|
165
|
+
set :tags # Unique values
|
166
|
+
zset :popularity_scores # Scored/ranked data
|
167
|
+
hashkey :metadata # Key-value pairs
|
168
|
+
|
169
|
+
# Advanced field types
|
170
|
+
counter :likes # Specialized atomic counter
|
171
|
+
end
|
172
|
+
|
173
|
+
post = BlogPost.create(slug: "hello-world", title: "Hello World")
|
174
|
+
|
175
|
+
# Work with Valkey/Redis data types naturally
|
176
|
+
post.view_count.increment # INCR view_count
|
177
|
+
post.comments.push("Great post!") # LPUSH comments
|
178
|
+
post.tags.add("ruby", "programming") # SADD tags
|
179
|
+
post.popularity_scores.add(4.5, "user123") # ZADD popularity_scores
|
180
|
+
post.metadata["author"] = "Alice" # HSET metadata
|
181
|
+
post.likes.increment(5) # INCRBY likes 5
|
182
|
+
```
|
183
|
+
|
184
|
+
### Features System
|
185
|
+
|
186
|
+
Enable advanced functionality with Familia's modular feature system:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
class User < Familia::Horreum
|
190
|
+
# Enable features as needed
|
191
|
+
feature :expiration # TTL management
|
192
|
+
feature :safe_dump # API-safe serialization
|
193
|
+
feature :encrypted_fields # Field-level encryption
|
194
|
+
feature :relationships # Object relationships
|
195
|
+
feature :object_identifier # Auto-generated IDs
|
196
|
+
feature :quantization # Time-based data bucketing
|
197
|
+
|
198
|
+
identifier_field :email
|
199
|
+
field :email, :name, :created_at
|
200
|
+
|
201
|
+
# Feature-specific functionality
|
202
|
+
encrypted_field :api_key # Automatically encrypted
|
203
|
+
safe_dump_field :email # Include in safe_dump
|
204
|
+
safe_dump_field :name # Include in safe_dump
|
205
|
+
default_expiration 30.days # Auto-expire inactive users
|
206
|
+
end
|
207
|
+
|
208
|
+
user = User.create(email: "alice@example.com", api_key: "secret123")
|
209
|
+
user.api_key.class # => ConcealedString
|
210
|
+
user.api_key.to_s # => "[CONCEALED]" (safe for logs)
|
211
|
+
user.safe_dump # => {email: "...", name: "..."}
|
212
|
+
```
|
213
|
+
|
214
|
+
### Querying and Finding
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
# Primary key lookup
|
218
|
+
user = User.load("alice@example.com")
|
219
|
+
|
220
|
+
# Existence checks
|
221
|
+
User.exists?("alice@example.com") # => true/false
|
222
|
+
|
223
|
+
# Bulk operations
|
224
|
+
users = User.multiget("alice@example.com", "bob@example.com")
|
225
|
+
|
226
|
+
# With relationships feature - indexed lookups
|
227
|
+
class User < Familia::Horreum
|
228
|
+
feature :relationships
|
229
|
+
field :email, :username
|
230
|
+
class_indexed_by :username, :username_lookup
|
231
|
+
end
|
232
|
+
|
233
|
+
# O(1) indexed finding
|
234
|
+
user = User.find_by_username("alice_doe") # Fast hash lookup
|
235
|
+
```
|
236
|
+
|
237
|
+
---
|
69
238
|
|
70
239
|
## Usage Examples
|
71
240
|
|
72
241
|
### Creating and Saving Objects
|
73
242
|
|
74
243
|
```ruby
|
75
|
-
flower = Flower.create(name: "Red Rose", token: "
|
244
|
+
flower = Flower.create(name: "Red Rose", token: "prose")
|
76
245
|
flower.owners.push("Alice", "Bob")
|
77
246
|
flower.tags.add("romantic")
|
78
247
|
flower.metrics.increment("views", 1)
|
@@ -83,7 +252,7 @@ flower.save
|
|
83
252
|
### Retrieving and Updating Objects
|
84
253
|
|
85
254
|
```ruby
|
86
|
-
rose = Flower.
|
255
|
+
rose = Flower.load("prose")
|
87
256
|
rose.name = "Pink Rose"
|
88
257
|
rose.save
|
89
258
|
```
|
@@ -91,9 +260,9 @@ rose.save
|
|
91
260
|
### Using Safe Dump
|
92
261
|
|
93
262
|
```ruby
|
94
|
-
user = User.create(username: "
|
263
|
+
user = User.create(username: "prosedog", first_name: "Rose", last_name: "Dog")
|
95
264
|
user.safe_dump
|
96
|
-
# => {id: "user:
|
265
|
+
# => {id: "user:prosedog", username: "prosedog", full_name: "Rose Dog"}
|
97
266
|
```
|
98
267
|
|
99
268
|
### Working with Time-based Data
|
@@ -106,7 +275,7 @@ metric.counter.increment # Increments the counter for the current hour
|
|
106
275
|
### Bulk Operations
|
107
276
|
|
108
277
|
```ruby
|
109
|
-
Flower.multiget("
|
278
|
+
Flower.multiget("prose", "tulip", "daisy")
|
110
279
|
```
|
111
280
|
|
112
281
|
### Transactional Operations
|
@@ -114,113 +283,125 @@ Flower.multiget("rrose", "tulip", "daisy")
|
|
114
283
|
```ruby
|
115
284
|
user.transaction do |conn|
|
116
285
|
conn.set("user:#{user.id}:status", "active")
|
117
|
-
conn.zadd("active_users",
|
286
|
+
conn.zadd("active_users", Familia.now.to_i, user.id)
|
118
287
|
end
|
119
288
|
```
|
120
289
|
|
121
|
-
###
|
122
|
-
|
123
|
-
Familia includes a powerful relationships system for managing object associations:
|
290
|
+
### Advanced Patterns
|
124
291
|
|
292
|
+
**Time-based Expiration:**
|
125
293
|
```ruby
|
126
|
-
class
|
127
|
-
feature :
|
128
|
-
|
129
|
-
identifier_field :custid
|
130
|
-
field :custid, :name, :email
|
131
|
-
set :domains # Collection for related objects
|
294
|
+
class Session < Familia::Horreum
|
295
|
+
feature :expiration
|
296
|
+
default_expiration 24.hours
|
132
297
|
|
133
|
-
|
134
|
-
class_indexed_by :email, :email_lookup
|
135
|
-
class_tracked_in :all_customers, score: :created_at
|
298
|
+
field :user_id, :token
|
136
299
|
end
|
137
300
|
|
138
|
-
|
139
|
-
|
301
|
+
session = Session.create(user_id: "123", token: "abc123")
|
302
|
+
session.ttl # Check remaining time
|
303
|
+
session.expire_in(1.hour) # Custom expiration
|
304
|
+
```
|
140
305
|
|
141
|
-
|
142
|
-
|
306
|
+
**Encrypted Fields:**
|
307
|
+
```ruby
|
308
|
+
class SecureData < Familia::Horreum
|
309
|
+
feature :encrypted_fields
|
143
310
|
|
144
|
-
|
145
|
-
|
311
|
+
field :name
|
312
|
+
encrypted_field :credit_card, :ssn
|
146
313
|
end
|
147
314
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
domain = Domain.new(domain_id: "dom456", name: "acme.com")
|
153
|
-
customer.domains << domain # Clean collection syntax
|
154
|
-
|
155
|
-
# Fast O(1) lookups
|
156
|
-
found_customer = Customer.find_by_email("admin@acme.com")
|
315
|
+
data = SecureData.create(name: "Alice", credit_card: "4111-1111-1111-1234")
|
316
|
+
data.credit_card.reveal # => "4111-1111-1111-1234"
|
317
|
+
data.credit_card.to_s # => "[CONCEALED]"
|
157
318
|
```
|
158
319
|
|
159
|
-
##
|
320
|
+
## Configuration
|
160
321
|
|
161
|
-
###
|
322
|
+
### Basic Setup
|
162
323
|
|
163
|
-
|
324
|
+
```ruby
|
325
|
+
# config/initializers/familia.rb (Rails)
|
326
|
+
require 'familia'
|
164
327
|
|
165
|
-
|
166
|
-
|
167
|
-
- **`tracked_in`** - Scored collections for rankings, time-series, and analytics
|
328
|
+
# Basic configuration
|
329
|
+
Familia.uri = 'redis://localhost:6379/0'
|
168
330
|
|
169
|
-
|
331
|
+
# Production configuration
|
332
|
+
Familia.configure do |config|
|
333
|
+
config.redis_uri = ENV['REDIS_URL']
|
334
|
+
config.debug = ENV['FAMILIA_DEBUG'] == 'true'
|
335
|
+
end
|
336
|
+
```
|
170
337
|
|
171
|
-
|
338
|
+
### Connection Pooling
|
172
339
|
|
173
|
-
|
340
|
+
```ruby
|
341
|
+
require 'connection_pool'
|
174
342
|
|
175
|
-
|
343
|
+
Familia.connection_provider = lambda do |uri|
|
344
|
+
ConnectionPool.new(size: 10, timeout: 5) do
|
345
|
+
Redis.new(url: uri)
|
346
|
+
end.with { |conn| yield conn if block_given?; conn }
|
347
|
+
end
|
348
|
+
```
|
176
349
|
|
177
|
-
|
350
|
+
### Encryption Setup
|
178
351
|
|
179
352
|
```ruby
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
# app/models/user/safe_dump_extensions.rb - Automatically discovered
|
187
|
-
class User
|
188
|
-
safe_dump_fields :name, :email # password excluded for security
|
353
|
+
Familia.configure do |config|
|
354
|
+
config.encryption_keys = {
|
355
|
+
v1: ENV['FAMILIA_ENCRYPTION_KEY_V1'],
|
356
|
+
v2: ENV['FAMILIA_ENCRYPTION_KEY_V2']
|
357
|
+
}
|
358
|
+
config.current_key_version = :v2
|
189
359
|
end
|
190
360
|
```
|
191
361
|
|
192
|
-
|
362
|
+
## Organizing Complex Models
|
193
363
|
|
194
|
-
|
364
|
+
For large applications, you can organize model complexity using custom features and the Feature Autoloading System:
|
195
365
|
|
196
|
-
|
197
|
-
# app/features/customer_management.rb
|
198
|
-
module MyApp::Features::CustomerManagement
|
199
|
-
Familia::Base.add_feature(self, :customer_management)
|
366
|
+
### Feature Organization with Autoloader
|
200
367
|
|
201
|
-
|
202
|
-
base.extend(ClassMethods)
|
203
|
-
end
|
368
|
+
For large applications, organize features into modular files using the autoloader:
|
204
369
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
370
|
+
```ruby
|
371
|
+
# app/models/customer.rb - Main model file
|
372
|
+
class Customer < Familia::Horreum
|
373
|
+
module Features
|
374
|
+
include Familia::Features::Autoloader
|
375
|
+
# Automatically loads all .rb files from app/models/customer/features/
|
209
376
|
end
|
210
377
|
|
211
|
-
|
212
|
-
|
378
|
+
identifier_field :custid
|
379
|
+
field :custid, :name, :email
|
380
|
+
feature :safe_dump # Feature configuration loaded automatically
|
381
|
+
end
|
382
|
+
|
383
|
+
# app/models/customer/features/notifications.rb - Automatically loaded
|
384
|
+
module Customer::Features::Notifications
|
385
|
+
def send_welcome_email
|
386
|
+
NotificationService.send_template(
|
387
|
+
email: email,
|
388
|
+
template: 'customer_welcome',
|
389
|
+
variables: { name: name, custid: custid }
|
390
|
+
)
|
213
391
|
end
|
214
392
|
end
|
215
393
|
|
216
|
-
# models/customer.rb
|
217
|
-
|
218
|
-
|
219
|
-
|
394
|
+
# app/models/customer/features/safe_dump_extensions.rb - Feature-specific config
|
395
|
+
module Customer::Features::SafeDumpExtensions
|
396
|
+
def self.included(base)
|
397
|
+
base.safe_dump_field :custid
|
398
|
+
base.safe_dump_field :name
|
399
|
+
base.safe_dump_field :email
|
400
|
+
end
|
220
401
|
end
|
221
402
|
```
|
222
403
|
|
223
|
-
|
404
|
+
This approach keeps complex models organized while maintaining clean, declarative style.
|
224
405
|
|
225
406
|
## AI Development Assistance
|
226
407
|
|
@@ -234,10 +415,29 @@ This version of Familia was developed with assistance from AI tools. The followi
|
|
234
415
|
|
235
416
|
I remain responsible for all design decisions and the final code. I believe in being transparent about development tools, especially as AI becomes more integrated into our workflows as developers.
|
236
417
|
|
237
|
-
##
|
418
|
+
## Links
|
238
419
|
|
239
|
-
For more information, visit:
|
240
420
|
- [Github Repository](https://github.com/delano/familia)
|
241
421
|
- [RubyGems Page](https://rubygems.org/gems/familia)
|
242
422
|
|
243
|
-
|
423
|
+
## Documentation
|
424
|
+
|
425
|
+
For comprehensive guides and detailed technical information:
|
426
|
+
|
427
|
+
- **[Overview Guide](docs/overview.md)** - Conceptual understanding and getting started
|
428
|
+
- **[Technical Reference](docs/reference/api-technical.md)** - Implementation details and advanced patterns
|
429
|
+
- **[Migration Guides](docs/migrating/)** - Upgrading from previous versions
|
430
|
+
|
431
|
+
## Contributing
|
432
|
+
|
433
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
434
|
+
|
435
|
+
1. Fork the repository
|
436
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
437
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
438
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
439
|
+
5. Open a Pull Request
|
440
|
+
|
441
|
+
## License
|
442
|
+
|
443
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
data/changelog.d/README.md
CHANGED
@@ -53,8 +53,8 @@ git commit
|
|
53
53
|
- **One Fragment Per Change:** Keep each fragment focused on a single feature, fix, or improvement.
|
54
54
|
- **Documenting AI Assistance:** If a change involved significant AI assistance, place it in its own fragment. This ensures the `### AI Assistance` section clearly corresponds to the single change described in that fragment.
|
55
55
|
- **Write for a Human Audience:** Describe the *impact* of the change, not just the implementation details.
|
56
|
-
- **Good:** "Improved the performance and stability of
|
57
|
-
- **Bad:** "Refactored the `
|
56
|
+
- **Good:** "Improved the performance and stability of Database connections under high load."
|
57
|
+
- **Bad:** "Refactored the `DatabaseManager`."
|
58
58
|
- **Be Specific:** Avoid generic messages like "fixed a bug." Clearly state what was fixed.
|
59
59
|
- **Include Context:** Reference issue or pull request numbers to provide a link to the discussion and implementation details. `scriv` will automatically create links for them.
|
60
60
|
- **Example:** `- Fixed a bug where users could not reset their passwords. PR #123`
|
@@ -16,11 +16,11 @@ Generated methods on Domain instances:
|
|
16
16
|
|
17
17
|
The method names follow the pattern: {action}_to_{lowercase_class_name}_{collection_name}
|
18
18
|
|
19
|
-
|
19
|
+
participates_in Relationships
|
20
20
|
|
21
21
|
When you declare:
|
22
22
|
class Customer < Familia::Horreum
|
23
|
-
|
23
|
+
participates_in :all_customers, type: :sorted_set, score: :created_at
|
24
24
|
end
|
25
25
|
|
26
26
|
Generated class methods:
|
@@ -29,18 +29,18 @@ Generated class methods:
|
|
29
29
|
- Customer.all_customers - Access the sorted set collection directly
|
30
30
|
|
31
31
|
For scoped tracking (with class prefix):
|
32
|
-
|
32
|
+
participates_in :global, :active_users, score: :last_seen
|
33
33
|
Generates: Customer.add_to_active_users(customer) and Customer.active_users
|
34
34
|
|
35
35
|
indexed_by Relationships
|
36
36
|
|
37
|
-
The `indexed_by` method creates Redis hash-based indexes for O(1) field lookups. The `context` parameter determines index ownership and scope.
|
37
|
+
The `indexed_by` method creates Valkey/Redis hash-based indexes for O(1) field lookups. The `context` parameter determines index ownership and scope.
|
38
38
|
|
39
39
|
**Global Context (Shared Index)**
|
40
40
|
When you declare:
|
41
41
|
```ruby
|
42
42
|
class Customer < Familia::Horreum
|
43
|
-
indexed_by :email, :email_lookup,
|
43
|
+
indexed_by :email, :email_lookup, target: :global
|
44
44
|
end
|
45
45
|
```
|
46
46
|
|
@@ -49,13 +49,13 @@ Generated class methods:
|
|
49
49
|
- Customer.remove_from_email_lookup(customer) - Remove customer from global email index
|
50
50
|
- Customer.email_lookup - Access the global hash index directly (supports .get(email))
|
51
51
|
|
52
|
-
Redis key pattern: `global:email_lookup`
|
52
|
+
Valkey/Redis key pattern: `global:email_lookup`
|
53
53
|
|
54
54
|
**Class Context (Per-Instance Index)**
|
55
55
|
When you declare:
|
56
56
|
```ruby
|
57
57
|
class Domain < Familia::Horreum
|
58
|
-
indexed_by :name, :domain_index,
|
58
|
+
indexed_by :name, :domain_index, target: Customer
|
59
59
|
end
|
60
60
|
```
|
61
61
|
|
@@ -63,7 +63,7 @@ Generated class methods on Customer:
|
|
63
63
|
- Customer.find_by_name(domain_name) - Find domain by name within this customer
|
64
64
|
- Customer.find_all_by_name(domain_names) - Find multiple domains by names
|
65
65
|
|
66
|
-
Redis key pattern: `customer:123:domain_index` (per customer instance)
|
66
|
+
Valkey/Redis key pattern: `customer:123:domain_index` (per customer instance)
|
67
67
|
|
68
68
|
**When to Use Each Context**
|
69
69
|
- **Global context (`:global`)**: Use for system-wide lookups where the field value should be unique across all instances
|
@@ -85,7 +85,7 @@ domain.add_to_customer_domains(customer.custid) # Add to relationship
|
|
85
85
|
domain.remove_from_customer_domains(customer.custid) # Remove from relationship
|
86
86
|
domain.in_customer_domains?(customer.custid) # Query membership
|
87
87
|
|
88
|
-
# For
|
88
|
+
# For participates_in relationships:
|
89
89
|
Customer.add_to_all_customers(customer) # Class method
|
90
90
|
Customer.all_customers.range(0, -1) # Direct collection access
|
91
91
|
|
@@ -97,10 +97,10 @@ Method Naming Conventions
|
|
97
97
|
|
98
98
|
The relationship system uses consistent naming patterns:
|
99
99
|
- member_of: {add_to|remove_from|in}_#{parent_class.downcase}_#{collection_name}
|
100
|
-
-
|
100
|
+
- participates_in: {add_to|remove_from}_#{collection_name} (class methods)
|
101
101
|
- indexed_by: {add_to|remove_from}_#{index_name} (class methods)
|
102
102
|
|
103
|
-
This automatic method generation creates a clean, predictable API that handles both the
|
103
|
+
This automatic method generation creates a clean, predictable API that handles both the db operations and maintains referential consistency
|
104
104
|
across related objects.
|
105
105
|
|
106
106
|
|
@@ -119,8 +119,8 @@ class User < Familia::Horreum
|
|
119
119
|
field :user_id, :email, :username
|
120
120
|
|
121
121
|
# System-wide unique email lookup
|
122
|
-
indexed_by :email, :email_lookup,
|
123
|
-
indexed_by :username, :username_lookup,
|
122
|
+
indexed_by :email, :email_lookup, target: :global
|
123
|
+
indexed_by :username, :username_lookup, target: :global
|
124
124
|
end
|
125
125
|
|
126
126
|
# Usage:
|
@@ -128,7 +128,7 @@ User.add_to_email_lookup(user)
|
|
128
128
|
found_user_id = User.email_lookup.get("john@example.com")
|
129
129
|
```
|
130
130
|
|
131
|
-
**Redis keys generated**: `global:email_lookup`, `global:username_lookup`
|
131
|
+
**Valkey/Redis keys generated**: `global:email_lookup`, `global:username_lookup`
|
132
132
|
|
133
133
|
### Class Context Pattern
|
134
134
|
Use `context: SomeClass` when field values are unique within a specific parent context:
|
@@ -149,8 +149,8 @@ class Domain < Familia::Horreum
|
|
149
149
|
field :domain_id, :name, :subdomain
|
150
150
|
|
151
151
|
# Domains are unique per customer (customer can't have duplicate domain names)
|
152
|
-
indexed_by :name, :domain_index,
|
153
|
-
indexed_by :subdomain, :subdomain_index,
|
152
|
+
indexed_by :name, :domain_index, target: Customer
|
153
|
+
indexed_by :subdomain, :subdomain_index, target: Customer
|
154
154
|
end
|
155
155
|
|
156
156
|
# Usage:
|
@@ -159,7 +159,7 @@ customer.find_by_name("example.com") # Find domain within this custome
|
|
159
159
|
customer.find_all_by_subdomain(["www", "api"]) # Find multiple subdomains
|
160
160
|
```
|
161
161
|
|
162
|
-
**Redis keys generated**: `customer:cust_123:domain_index`, `customer:cust_123:subdomain_index`
|
162
|
+
**Valkey/Redis keys generated**: `customer:cust_123:domain_index`, `customer:cust_123:subdomain_index`
|
163
163
|
|
164
164
|
### Mixed Pattern Example
|
165
165
|
A real-world example showing both patterns:
|
@@ -172,11 +172,11 @@ class ApiKey < Familia::Horreum
|
|
172
172
|
field :key_id, :key_hash, :name, :scope
|
173
173
|
|
174
174
|
# API key hashes must be globally unique
|
175
|
-
indexed_by :key_hash, :global_key_lookup,
|
175
|
+
indexed_by :key_hash, :global_key_lookup, target: :global
|
176
176
|
|
177
177
|
# But key names can be reused across different customers
|
178
|
-
indexed_by :name, :customer_key_lookup,
|
179
|
-
indexed_by :scope, :scope_lookup,
|
178
|
+
indexed_by :name, :customer_key_lookup, target: Customer
|
179
|
+
indexed_by :scope, :scope_lookup, target: Customer
|
180
180
|
end
|
181
181
|
|
182
182
|
# Usage examples:
|
@@ -197,10 +197,10 @@ If you have existing code with incorrect syntax, here's how to fix it:
|
|
197
197
|
indexed_by :email_lookup, field: :email
|
198
198
|
|
199
199
|
# ✅ New correct syntax - Global scope
|
200
|
-
indexed_by :email, :email_lookup,
|
200
|
+
indexed_by :email, :email_lookup, target: :global
|
201
201
|
|
202
202
|
# ✅ New correct syntax - Class scope
|
203
|
-
indexed_by :email, :customer_email_lookup,
|
203
|
+
indexed_by :email, :customer_email_lookup, target: Customer
|
204
204
|
```
|
205
205
|
|
206
206
|
**Key Differences**:
|