All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Version bump for minor updates
3.0.0 - 2025-11-11
All error classes simplified to standard Ruby exceptions:
All BetterModel errors have been refactored to remove complex Sentry-compatible data structures in favor of simple, idiomatic Ruby exception patterns. This is a breaking change for code that accessed error attributes beyond the message.
Before (v2.x):
# Complex error instantiation with named parameters
raise BetterModel::Errors::Searchable::InvalidPredicateError.new(
predicate_scope: :title_xxx,
value: "Rails",
available_predicates: [:title_eq, :title_cont],
model_class: Article
)
# Access to structured data
begin
article.search(params)
rescue BetterModel::Errors::Searchable::InvalidPredicateError => e
e.predicate_scope # => :title_xxx
e.available_predicates # => [:title_eq, :title_cont, ...]
e.tags # => {error_category: "invalid_predicate", ...}
e.context # => {model_class: "Article"}
e.extra # => {predicate_scope: :title_xxx, value: "Rails", ...}
endAfter (v3.0):
# Simple, idiomatic Ruby exception raising
raise BetterModel::Errors::Searchable::InvalidPredicateError,
"Invalid predicate scope: title_xxx. Available: title_eq, title_cont, ..."
raise BetterModel::Errors::Stateable::ConfigurationError, "Invalid configuration"
raise BetterModel::Errors::Validatable::NotEnabledError, "Module is not enabled"
# Only message attribute available
begin
article.search(params)
rescue BetterModel::Errors::Searchable::InvalidPredicateError => e
e.message # => "Invalid predicate scope: title_xxx. Available: title_eq, ..."
endWhy this change?
The v2.x approach added significant complexity for minimal benefit:
- ✅ Simpler: Standard Ruby exception patterns familiar to all developers
- ✅ Maintainable: Reduced codebase complexity (54 lines per error → 1 line)
- ✅ Idiomatic: Follows Ruby/Rails conventions for exception handling
- ✅ Compatible: Works with all monitoring tools (Sentry, Rollbar, etc.) using standard patterns
- ✅ Descriptive: Error messages remain detailed and helpful
Error Classes Affected (28 total):
All errors now use simple string messages:
- ConfigurationError (10 modules) - All modules
- NotEnabledError (4 modules) - Validatable, Stateable, Archivable, Statusable
- InvalidTransitionError, CheckFailedError, ValidationFailedError
- InvalidStateError, InvalidPredicateError, InvalidOrderError
- InvalidPaginationError, InvalidSecurityError
- AlreadyArchivedError, NotArchivedError
- And 16 more error classes across all modules
Removed complex error infrastructure in favor of standard Ruby exceptions:
Removed attributes:
tags- Structured metadata for groupingcontext- High-level structured dataextra- Detailed debug parameters- Domain-specific attributes (e.g.,
predicate_scope,event,state)
Removed concerns:
BetterModel::Errors::Concerns::SentryCompatiblemodule
Standard Sentry integration still works:
# v3.0 - Simple and idiomatic
begin
article.search(params)
rescue BetterModel::Errors::Searchable::InvalidPredicateError => e
# Standard Sentry capture (works with all monitoring tools)
Sentry.capture_exception(e)
# Or with custom context if needed
Sentry.capture_exception(e) do |scope|
scope.set_context("search", { params: params })
scope.set_tag("error_category", "search")
end
render json: { error: e.message }, status: :bad_request
endFor most applications, no changes are needed. If you only rescue exceptions and use their messages, your code continues to work unchanged.
Action required only if:
- You access error attributes like
.predicate_scope,.tags,.context,.extra - You instantiate errors with named parameters in tests
Example migrations:
# BEFORE (v2.x) - Accessing structured attributes
begin
article.search(params)
rescue BetterModel::Errors::Searchable::InvalidPredicateError => e
logger.error("Invalid predicate: #{e.predicate_scope}")
Sentry.capture_exception(e) do |scope|
scope.set_tags(e.tags)
scope.set_context("error_details", e.context)
end
render json: {
error: e.message,
available: e.available_predicates
}, status: :bad_request
end
# AFTER (v3.0) - Message-based handling
begin
article.search(params)
rescue BetterModel::Errors::Searchable::InvalidPredicateError => e
logger.error("Search error: #{e.message}")
Sentry.capture_exception(e) # Standard capture works
render json: { error: e.message }, status: :bad_request
end# Test migrations - if instantiating errors directly
# BEFORE (v2.x)
error = BetterModel::Errors::Searchable::ConfigurationError.new(
reason: "Invalid configuration",
model_class: Article
)
# AFTER (v3.0)
error = BetterModel::Errors::Searchable::ConfigurationError.new("Invalid configuration")- Version: Bumped to 3.0.0 for breaking changes in error API
- All error classes: Simplified to standard Ruby exceptions
- Error messages: Remain descriptive with contextual information
2.1.1 - 2025-11-09
- Validatable: Corrected error class names in documentation
- Fixed
BetterModel::ValidatableNotEnabledError→BetterModel::Errors::Validatable::NotEnabledError - Affected methods:
validate_group(group_name),errors_for_group(group_name)
- Fixed
- Validatable: Comprehensive error handling documentation
- New "Error Handling" section in
docs/validatable.md - Examples for all error scenarios (NotEnabledError, ConfigurationError, ArgumentError)
- Controller error handling patterns with rescue examples
- Best practices for testing error scenarios
- Error hierarchy diagram
- New "Error Handling" section in
- CLAUDE.md: Complete error handling guide for contributors
- Full error hierarchy for all BetterModel modules
- Module-specific error handling guidelines
- Best practices for error handling and testing
- Error metadata usage examples
- Debugging tips and integration patterns
- Aligned Validatable error documentation with Stateable and Searchable patterns
- Added comprehensive examples of error handling in controllers
- Added testing examples (Minitest and RSpec) for all error scenarios
2.1.0 - 2025-11-05
_present predicate now requires boolean parameter for non-string fields:
- Before (v1.x):
Article.view_count_present(no parameter) - After (v2.0):
Article.view_count_present(true)(boolean parameter required) - Rationale: Explicit parameters make intent clear and enable both presence/absence checks
- Affected field types: integer, decimal, float, bigint, boolean, date, datetime, time, timestamp
- Not affected: string/text fields retain parameterless
_present,_blank,_null
Boolean field predicates simplified:
- Removed redundant
_presentpredicate for boolean fields - Use
_eq(true)/_eq(false)or convenience methods_true/_falsefor boolean checks - Base predicates (_eq, _not_eq) now inherited from base predicate definitions
Date field predicates streamlined:
_presentnow requires boolean parameter:published_at_present(true)- Base predicates (_eq, _not_eq) inherited from base predicate definitions
- All other date-specific predicates unchanged (_lt, _lteq, _within, etc.)
Migration Guide:
| Old API (v1.x) | New API (v2.0) | Notes |
|---|---|---|
view_count_present |
view_count_present(true) |
Boolean parameter required |
view_count_present |
view_count_present(false) |
Check for absence/nil |
published_at_present |
published_at_present(true) |
Boolean parameter required |
featured_present |
featured_eq(true) or featured_true |
Use explicit true/false checks |
title_present |
title_present |
✅ Unchanged (string field) |
title_blank |
title_blank |
✅ Unchanged (string field) |
title_null |
title_null |
✅ Unchanged (string field) |
Why this change?
- Makes predicate behavior explicit and consistent across field types
- Enables absence checks:
field_present(false)equivalent tofield_null - Reduces API surface by removing redundant method definitions
- Follows principle of explicit over implicit
- Improves code maintainability by centralizing base predicate logic
- New folder:
context7/with 11 curated example files specifically designed for AI assistants - Purpose: Practical, copy-paste ready code examples for developers and AI tools like Context7
- Contents:
- Complete, working implementations for all 10 BetterModel features
- Self-contained examples with model setup, configuration, and usage
- Real-world use cases with progressive complexity
- Comprehensive inline comments and explanations
- Structured for optimal Context7 AI assistant integration
- Files included:
01_statusable.md- Dynamic boolean statuses02_permissible.md- Permission management03_predicable.md- Type-aware filtering04_sortable.md- Type-aware sorting05_searchable.md- Unified search interface06_archivable.md- Soft delete with tracking07_validatable.md- Declarative validation08_stateable.md- State machines09_traceable.md- Audit trail and versioning10_taggable.md- Tag management systemREADME.md- Overview and navigation guide
- Format: Each file includes setup, configuration examples, common usage patterns, advanced queries, and best practices
- Three-level redaction system for protecting sensitive data in version history:
:full: Complete redaction - all values replaced with"[REDACTED]":partial: Pattern-based masking with smart pattern detection:hash: SHA256 hashing for value verification without exposure
- Pattern detection for partial masking:
- Credit cards: Shows last 4 digits (e.g.,
"4532123456789012"→"****9012") - Emails: Shows first char + domain (e.g.,
"user@example.com"→"u***@example.com") - SSN: Shows last 4 digits (e.g.,
"123456789"→"***-**-6789") - Phone numbers: Shows last 4 digits (e.g.,
"5551234567"→"***-***-4567") - Unknown patterns: Shows character count (e.g.,
"random_text_123"→"[REDACTED:15chars]")
- Credit cards: Shows last 4 digits (e.g.,
- Rollback protection: Sensitive fields excluded from rollback by default
- Override with
allow_sensitive: trueoption (not recommended) - Warning: Rolling back sensitive fields will set them to redacted values
- Override with
- Configuration introspection:
traceable_sensitive_fieldsclass method - Thread-safe implementation with frozen configuration
- Refactored predicate generation to reduce code duplication:
- Base predicates (_eq, _not_eq, _present, _blank, _null) defined once and inherited by all field types
- String-specific presence checks override base definition for empty string handling
- Date/numeric/boolean predicates inherit base predicates automatically
- Improved code maintainability:
- Single source of truth for base predicate logic
- Reduced repetition across field type modules
- Clearer separation between base predicates and type-specific predicates
- Enhanced consistency: All field types now share identical base predicate behavior
- Context7 integration: Added new context7/ folder optimized for AI assistants and developers
- README updates: Added references to context7 documentation
- Predicate documentation: Updated to reflect new presence predicate API requirements
- Traceable guide updated with comprehensive sensitive fields section:
- Added "Sensitive Fields" subsection to Configuration
- Added redaction levels with practical examples
- Added rollback behavior with sensitive fields
- Added configuration introspection documentation
- Updated Overview with sensitive data protection feature
- Updated Table of Contents
- Added "Sensitive Data Protection" to Best Practices section
- Added guidance table for choosing redaction levels
- Updated "Don't track sensitive data" best practice to reference the
sensitive:option
- Examples of protecting passwords, PII, tokens, credit cards, and API keys
- Healthcare, e-commerce, and authentication system examples
- Total tests: 751 (+0 from v2.1.1)
- Added 14 comprehensive sensitive fields tests
- Removed ~150 unused variable assignments from test files
- Total assertions: 2392 (+26 from v2.1.1)
- Code coverage: 92.51% (1506/1628 lines)
- Pass rate: 100%
- 0 failures
- 0 errors
- 0 skips
- 0 warnings from project code
- Test execution time: ~7.5s for full test suite
- Full redaction: Complete value redaction and nil handling (2 tests)
- Hash redaction: SHA256 hashing with deterministic output (3 tests)
- Partial masking: Pattern detection for credit cards, emails, SSN, phone, unknown (4 tests)
- Rollback behavior: Default skipping and allow_sensitive option (2 tests)
- Integration: Mixed sensitive/normal fields, configuration introspection (2 tests)
- Edge cases: Nil values, empty strings, special characters (1 test)
- RuboCop: 100% compliant (0 offenses)
- Removed 151 unused variable assignments from test files
- All sensitive field code follows Rails Omakase style guide
- Thread-safe configuration with frozen objects
- Comprehensive error handling
- Predicates refactored with single inheritance chain for base predicates
- Base predicate definitions centralized in
define_base_predicatesmethod - String predicates override
_presentto handle empty string semantics - All configurations remain frozen and thread-safe
- Zero runtime performance impact from refactoring
1.3.0 - 2025-10-31
- New concern:
BetterModel::Taggablefor managing tags with normalization, validation, and statistics - Tag management methods:
tag_with(*tags): Add tags with automatic deduplicationuntag(*tags): Remove specific tagsretag(*tags): Replace all tagstagged_with?(tag): Check if record has a tag
- CSV interface:
tag_list: Export tags as comma-separated stringtag_list=: Import tags from string with custom delimiter support
- Normalization features:
- Automatic lowercase conversion (
normalize: true) - Whitespace stripping (
strip: true, default) - Minimum length enforcement (
min_length: 2) - Maximum length truncation (
max_length: 30)
- Automatic lowercase conversion (
- Validation system:
- Minimum/maximum tag count validation
- Whitelist validation (
allowed_tags) - Blacklist validation (
forbidden_tags)
- Statistics & analytics:
tag_counts: Count occurrences across all recordspopular_tags(limit:): Most frequently used tagsrelated_tags(tag, limit:): Co-occurrence analysis
- Predicable integration:
- Automatic
predicatesregistration for tag searches - Support for
tags_contains,tags_overlaps,tags_contains_all
- Automatic
- JSON serialization:
as_json(include_tag_list: true): Include CSV stringas_json(include_tag_stats: true): Include count and array
- Database support:
- PostgreSQL native arrays (recommended)
- SQLite/MySQL with JSON serialization
- Thread-safe frozen configuration
- Taggable guide (22 KB): Complete documentation with setup, configuration, and integration patterns
- Taggable examples (15+ KB): Progressive examples from basic to advanced with 8 complete scenarios
- Updated examples/README.md: Added Taggable as 10th module with navigation
- Updated main README.md:
- Added Taggable to Quick Start with configuration example
- Added tag management, query, and statistics examples
- Added Taggable to Features Overview section
- Updated "9 concerns" to "10 concerns"
- Added Taggable to individual concerns list
- Features Overview: 9 → 10 concerns
- Updated Quick Start with Taggable configuration block
- Added comprehensive tag usage examples (management, queries, statistics)
- Updated test coverage statistics: 92.57% (1507/1628 lines)
- Added Taggable to module compatibility matrix
- Renumbered example files:
11_use_cases.md→12_use_cases.md
- Total tests: 751 (+97 from v1.2.0)
- Added 83 comprehensive Taggable tests across 14 phases
- Coverage: Tag management, CSV, normalization, validation, statistics, integration
- Total assertions: 2366 (+105 from v1.2.0)
- Code coverage: 92.57% (1507/1628 lines, +1.12% from v1.2.0)
- Pass rate: 100% for Taggable tests
- 0 failures in Taggable
- 0 errors in Taggable
- 0 skips in Taggable
- Phase 1: Setup and configuration (6 tests)
- Phase 2: Tag management (tag_with, untag, retag, tagged_with?) (12 tests)
- Phase 3: CSV interface (tag_list, delimiter) (7 tests)
- Phase 4: Normalization (lowercase, strip, length) (7 tests)
- Phase 5: Validations (min, max, whitelist, blacklist) (8 tests)
- Phase 6: Statistics (tag_counts, popular_tags, related_tags) (9 tests)
- Phase 7: Predicable integration (1 test)
- Phase 8: JSON serialization (3 tests)
- Phase 9: Thread safety (4 tests)
- Phase 12: Edge cases (nil, empty, unicode, special chars) (12 tests)
- Phase 14: Integration with other modules (9 tests)
- Additional: Performance and error handling (5 tests)
- RuboCop: 100% compliant (0 offenses)
- All Taggable code follows Rails Omakase style guide
- Thread-safe configuration with frozen objects
- Comprehensive error handling
- Added
tagscolumn to test articles table (text/array type) - SQLite: Uses
serialize :tags, coder: JSON, type: Array - PostgreSQL: Native array column support recommended
- Taggable works seamlessly with:
- Predicable: Automatic tag search scopes
- Searchable: Unified search with tag filters
- Traceable: Audit trail for tag changes
- Statusable: Tag-based status conditions
- Permissible: Tag-based permissions
1.2.0 - 2025-10-30
- CLASS_CREATION_MUTEX: Thread-safe dynamic class creation in Traceable and Stateable
- Double-checked locking pattern: Optimal performance with thread safety
- Thread-safety test suite:
- Traceable: 6 comprehensive concurrency tests
- Stateable: 8 comprehensive concurrency tests
- Concurrent version/transition class creation tests
- Multiple table name handling tests
- PostgreSQL support: JSONB operators for optimal query performance in Traceable
- MySQL/Trilogy support: JSON_EXTRACT functions for change queries
- SQLite fallback: Graceful degradation with logging
- Adapter detection:
postgres?andmysql?methods in ChangeQuery - Cross-database compatibility for field change tracking
- Traceable guide (23.6 KB): Complete documentation with time-travel, rollback, and query scopes
- Stateable guide (25.8 KB): State machine documentation with guards, validations, and callbacks
- Integration guide (21.2 KB): Patterns for combining multiple concerns effectively
- Performance guide (19.5 KB): Database indexing, query optimization, N+1 prevention
- Migration guide (23.4 KB): Adding BetterModel to existing applications
- Total documentation: 113+ KB of comprehensive guides
- Updated README with Traceable examples and complete documentation links
- Features Overview section in README
- Complete Documentation section with links to all 12 guides
- Added Traceable to Quick Start section with time-travel example
- Added Traceable usage examples (audit trail, rollback, change queries)
- Created Features Overview section with all 9 concerns
- Added Complete Documentation section linking to all guides
- Enhanced Traceable section in detailed Features
- Improved exception handling in Traceable rollback methods
- Better error messages for version validation failures
- Enhanced ChangeQuery architecture with adapter-specific optimizations
- Removed inconsistent exception types in rollback validation tests
- Fixed method redefinition warnings in thread-safety tests
- Cleaned up redundant test files (documentation structure tests, adapter tests)
- Total tests: 654 (+26 from v1.1.0)
- Added 15 thread-safety tests (Traceable + Stateable)
- Removed 24 unnecessary tests (documentation structure, non-runnable adapter tests)
- Code coverage: 91.45% (1272/1391 lines covered)
- Pass rate: 100% - All tests passing
- 0 failures
- 0 errors
- 0 skips
- 0 warnings in project code
- Test execution time: ~6.6s for full test suite
- RuboCop: 100% compliant (0 offenses)
- Test files: 83 files inspected
- All auto-correctable style violations fixed
- Added test tables for thread-safety verification
- Enhanced schema with version/transition tracking tables
- Improved indexing strategy documentation
1.1.0 - 2025-10-30
- Opt-in activation: Enable with
validatable do...endblock - Basic validations: Clean DSL with
checkmethod for all ActiveModel validation types - Conditional validations: Use Rails
if:/unless:options for conditional logic - Complex validations:
register_complex_validation+check_complexfor cross-field and business rules - Validation groups: Partial validation for multi-step forms with
validation_group - Instance methods:
valid?(group),validate_group(group),errors_for_group(group) - Seamless integration with Statusable for status-based conditional validations
- Thread-safe with frozen immutable configuration
DEPRECATED in v2.0.0 (removed methods - see current API in docs/validatable.md):
→ Use Railsvalidate_if/validate_unlessif:/unless:options→ Usevalidate_orderregister_complex_validationfor cross-field comparisons→ Usevalidate_business_ruleregister_complex_validation+check_complex
- Flexible table naming: Per-model tables (default), custom names, or shared tables
- Generator option:
--table-nameparameter for custom table names - Default behavior: Automatic
{model}_versionstable naming (e.g.,article_versions) - DSL configuration:
versions_table 'custom_name'intraceable do...endblock - Dynamic Version classes: Automatic subclass creation per table with proper namespacing
- Updated README with Validatable quick start examples and usage
- Added comprehensive
docs/validatable.md(22KB) with API reference, real-world examples, best practices - Updated feature count: 7 powerful concerns (was 6)
- Enhanced Traceable documentation with table naming options and examples
- Total tests: 628 automated tests (+95 from v1.0.0, +17.8% increase)
- Unit tests: 628 with 1912 assertions
- New test suites:
- Searchable: +13 tests (OR conditions, security validation)
- Stateable: +7 tests (edge cases, guard exceptions, nil state handling)
- Validatable: +8 tests (nested conditions, validation groups, complex rules)
- Code coverage: 91.41% (1245/1362 lines covered, +6.06% from 85.35%)
- Pass rate: 100% - All tests passing (0 failures, 0 errors, 10 skips)
- Test execution time: ~7s for full test suite
- README enhancements: Strategic emoji placement for improved readability and scannability
- Section headers with category icons (📦 Installation, ⚡ Quick Start, 📚 Features, etc.)
- Code examples categorized with visual markers (✅, 🔐, ⬆️, 🔍, etc.)
- Step-by-step guides with numbered emojis (1️⃣, 2️⃣, 3️⃣)
- Key benefits highlighted with relevant icons
- Contributing workflow visualized with process emojis (🍴, 🌿, 🧪, 🎉)
1.0.0 - 2025-10-29
-
Statusable: Declarative status management with runtime evaluation
- Dynamic status definitions with lambda/block syntax
- Status checking with
is?(:status)andis_status?helper methods - Get all statuses with
statusesmethod - Thread-safe immutable registry
-
Permissible: Permission management based on model state
- Dynamic permission definitions with lambda/block syntax
- Permission checking with
permit?(:action)andpermit_action?helper methods - Get all permissions with
permissionsmethod - Integration with Statusable for status-based permissions
- Thread-safe immutable registry
-
Archivable: Soft-delete with archive management
- Soft delete with
archive!andrestore!methods - Archive tracking with
archived_by_idandarchive_reason - Status methods:
archived?andactive? - Scopes:
archived,not_archived,archived_only - Helper predicates:
archived_today,archived_this_week,archived_recently - Migration generator with flexible options (
--with-tracking,--with-by,--with-reason) - Optional default scope to hide archived records
- Seamless integration with Predicable, Sortable, and Searchable
- Thread-safe immutable configuration
- Soft delete with
-
Traceable: Change tracking with audit trail and time-travel
- Automatic change tracking on create/update/destroy
- Time-travel:
as_of(timestamp)reconstructs record state at any point - Rollback:
rollback_to(version)restores to previous versions - Audit trail with
versions,changes_for(field),audit_trail - Query changes:
changed_by(user_id),changed_between(start, end) - Field-specific queries:
Article.status_changed_from("draft").to("published") - Per-model versioning tables (e.g.,
article_versions) - Optional tracking:
updated_by_id,updated_reason - Migration generator with
--create-tableoption - Thread-safe immutable configuration
-
Sortable: Type-aware sorting scopes with NULL handling
- Automatic scope generation based on column type (string, numeric, datetime, boolean)
- Case-insensitive sorting for strings (
_asc_i,_desc_i) - Database-specific NULLS FIRST/LAST support (PostgreSQL, MySQL, SQLite)
- Multiple field sorting with chainable scopes
- Optimized queries with proper indexing support
-
Predicable: Advanced query scopes for filtering
- Comprehensive predicate coverage: string, numeric, datetime, boolean, null
- Type-safe predicates generated based on column type
- Case-insensitive string matching (
_i_cont) - Range queries with
_betweenpredicate - PostgreSQL-specific: array predicates (
_overlaps,_contains,_contained_by) - PostgreSQL-specific: JSONB predicates (
_has_key,_has_any_key,_has_all_keys,_jsonb_contains) - Chainable with standard ActiveRecord queries
-
Searchable: Unified search interface orchestrating Predicable and Sortable
- Single
search()method for filtering, sorting, and pagination - OR conditions support alongside AND predicates
- Built-in pagination with DoS protection (
max_per_page) - Security enforcement with required predicates
- Default ordering configuration at model level
- Strong parameters integration
- Type-safe validation of all parameters
- Comprehensive error handling with custom error classes
- Single
- Comprehensive test suite with 358 automated tests
- 84.98% code coverage with SimpleCov
- RuboCop Omakase code style enforcement (0 offenses)
- bundler-audit for dependency vulnerability scanning
- Complete README with quick start and examples
- Detailed documentation for each concern in
docs/directory - Real-world usage examples
- Database compatibility matrix
- Best practices and performance tips
- Rails 8.1+ support
- Ruby 3.0+ support
- Full ActiveRecord integration
- Support for PostgreSQL, MySQL/MariaDB, SQLite, SQL Server, Oracle
- Thread-safe implementations with immutable registries
- Zero runtime overhead with compile-time scope generation
- Efficient SQL generation using Arel
- Database-specific optimizations
- Strong parameters compatible
- Chainable with standard ActiveRecord methods