Releases: tommyknocker/pdo-database-class
v2.9.1
Release 2.9.1 - Enhanced Error Diagnostics & ActiveRecord Improvements
Release Date: November 4, 2025
🎉 What's New
This release introduces Enhanced Error Diagnostics (roadmap item 1.1), significant ActiveRecord relationship improvements, and several developer experience enhancements.
✨ Major Features
Enhanced Error Diagnostics
- QueryDebugger class - Centralized debugging utilities for parameter sanitization and context formatting
- QueryException enhancements - Now includes
queryContextproperty with complete query builder state - Automatic context capture - Query context automatically captured and included in exceptions
- Parameter sanitization - Sensitive data (passwords, tokens, API keys) automatically masked in error messages
- Comprehensive documentation - New
error-diagnostics.mdguide with practical examples
ActiveRecord Relationship Enhancements
- Many-to-many relationships - New
viaTable()andvia()methods for junction table relationships - Yii2-like syntax - Support for Yii2-style relationship definitions for easier migration
- Enhanced relationship documentation and examples
QueryBuilder Convenience Methods
first()- Get first row from query resultlast()- Get last row from query resultindex()- Index query results by a specific column
CLI Migration Tool
- Yii2-style migration command-line tool
- Create, run, and rollback migrations via command line
- Yii2-compatible migration workflow
📚 Documentation Improvements
- MariaDB configuration examples - Complete setup guide in Getting Started documentation
- MariaDB connection examples - Step-by-step connection guide in first-connection.md
- Complete MariaDB setup guide for new users
🔄 Changes
- Fulltext search helper - Renamed
Db::fulltextMatch()toDb::match()for clarity - Composer script namespacing - All scripts now use
pdodb:prefix - PdoDb scope methods - Renamed for better readability
🐛 Fixes
- Fixed fatal error in
LoadBalancerTestsby implementing missingsetTempQueryContext()method - Fixed all PHPStan type errors
- Resolved all PHP-CS-Fixer code style issues
📊 Technical Details
- ✅ All tests passing - Comprehensive test coverage with improved ActiveQuery, EagerLoader, and CacheManager tests
- ✅ PHPStan Level 8 - Zero errors across entire codebase
- ✅ PHP-CS-Fixer - All code complies with PSR-12 standards
- ✅ 100% backward compatible - All existing code continues to work
- ✅ Code quality - Follows KISS, SOLID, DRY, YAGNI principles
📖 Documentation
🔗 Links
📦 Installation
composer require tommyknocker/pdo-database-class:^2.9.1Thank you for using PDOdb! 🚀
v2.9.0
🎉 Release v2.9.0 - MariaDB Support & Major Enhancements
Release Date: November 1, 2025
This release adds full MariaDB support as the fourth database dialect, along with significant enhancements for query building, debugging, and developer experience.
🚀 Major Features
✨ MariaDB Support
PDOdb now supports 4 database dialects: MySQL, MariaDB, PostgreSQL, and SQLite!
- Complete
MariaDBDialectclass with full SQL dialect implementation - 171 dedicated tests with 783 assertions for MariaDB-specific functionality
- All 51 examples work flawlessly on MariaDB (51/51 passing)
- GitHub Actions CI integration with dedicated MariaDB job and coverage reporting
- Full documentation updates across all README files
🔀 MERGE Statement Support
Standard SQL MERGE operations for complex data synchronization:
- PostgreSQL: Native MERGE with
MATCHED/NOT MATCHEDclauses - MySQL 8.0+: Enhanced UPSERT via
INSERT ... ON DUPLICATE KEY UPDATE - SQLite: Enhanced UPSERT support
- New
QueryBuilder::merge()method for merge operations
🎨 SQL Formatter/Pretty Printer
Enhanced debugging experience with human-readable SQL output:
QueryBuilder::toSQL(bool $isFormatted = false)- Optional formatted output- Proper indentation for nested queries, JOINs, and subqueries
- Multi-line formatting for complex queries
- Optional SQL keyword highlighting
- Standalone
SqlFormatterclass
💡 IDE Autocomplete Improvements
Better developer experience with enhanced type inference:
@templateannotations for better IDE support- More precise PHPDoc return type annotations
- Improved inheritance support with
staticreturn types
🔧 Improvements
Fluent Interface Return Types
- Replaced
selfwithstaticfor better inheritance support - Improved IDE autocomplete in subclass contexts
- Better extensibility for custom QueryBuilder classes
Documentation
- Replaced
Db::raw()with QueryBuilder helpers where possible - Cleaner, more maintainable documentation examples
- Better demonstrations of proper helper usage
🐛 Bug Fixes
MariaDB Compatibility
- Window Functions: LAG/LEAD with default values now use COALESCE wrapper (MariaDB doesn't support 3rd parameter)
- Type Casting: Fixed CAST operations - replaced REAL with DECIMAL(10,2) for MariaDB/MySQL
- JSON Operations: Fixed JSON_SET for nested paths and JSON_REMOVE for array indices
- Health Check: Fixed GitHub Actions MariaDB container health check configuration
Example Compatibility
- All examples updated to support 4 dialects (MySQL, MariaDB, PostgreSQL, SQLite)
- CREATE TABLE statements normalized for MariaDB compatibility
- Dialect-specific logic updated in all example files
📊 Statistics
- Tests: 1200 tests, 4856 assertions (+209 tests, +905 assertions from v2.8.0)
- Examples: 204/204 passing (51 files × 4 dialects)
- ✅ SQLite: 51/51
- ✅ MySQL: 51/51
- ✅ MariaDB: 51/51 (NEW!)
- ✅ PostgreSQL: 51/51
- Code Quality: PHPStan Level 8, PSR-12 compliant
- Backward Compatibility: 100% maintained
🔗 Links
📦 Installation
composer require tommyknocker/pdo-database-class:^2.9.0Full Changelog: v2.8.0...v2.9.0
v2.8.0
🚀 PDOdb v2.8.0 Release
Release Date: November 1, 2025
Overview
PDOdb v2.8.0 introduces major new features: ActiveRecord Pattern, Query Performance Profiling, Materialized CTEs, and PSR-14 Event Dispatcher Integration. Also includes improvements to exception handling, memory management, and code quality.
🎯 Major Features
✨ ActiveRecord Pattern (Optional ORM)
Lightweight ORM for object-based database operations:
- Magic attribute access -
$user->name,$user->email - Automatic CRUD -
save(),delete(),refresh() - Dirty tracking - Automatically tracks changed attributes
- Declarative validation - Rules-based validation with extensible validators
- Lifecycle events - PSR-14 events for save, insert, update, delete
- ActiveQuery builder - Full QueryBuilder API through
find()method
use tommyknocker\pdodb\orm\Model;
class User extends Model
{
public static function tableName(): string
{
return 'users';
}
public static function rules(): array
{
return [
[['name', 'email'], 'required'],
['email', 'email'],
];
}
}
$user = new User();
$user->name = 'Alice';
$user->email = 'alice@example.com';
$user->save();📚 Documentation: documentation/05-advanced-features/active-record.md
📖 Examples: examples/23-active-record/
📊 Query Performance Profiling
Built-in profiler for performance analysis:
- Automatic tracking of all query executions
- Execution time measurement (total, average, min, max)
- Memory usage tracking per query
- Slow query detection with configurable threshold
- Query grouping by SQL structure
- PSR-3 logger integration
$db->enableProfiling(0.5); // Threshold: 0.5 seconds
// Execute queries (automatically tracked)
$users = $db->find()->from('users')->get();
// Get statistics
$stats = $db->getProfilerStats(true);
echo "Avg time: " . round($stats['avg_time'] * 1000, 2) . " ms\n";
// Get slowest queries
$slowest = $db->getSlowestQueries(10);📚 Documentation: documentation/05-advanced-features/query-profiling.md
📖 Examples: examples/21-query-profiling/
⚡ Materialized CTE Support
Performance optimization for expensive CTE queries:
- PostgreSQL: Uses
MATERIALIZEDkeyword - MySQL: Uses optimizer hints
- Automatically caches expensive CTE computations
$results = $db->find()
->withMaterialized('customer_stats', function ($q) {
$q->from('orders')
->select([
'customer_id',
'order_count' => Db::count('*'),
'total_spent' => Db::sum('amount'),
])
->groupBy('customer_id');
})
->from('customers')
->join('customer_stats', 'customers.id = customer_stats.customer_id')
->where('customer_stats.total_spent', 1000, '>')
->get();📚 Documentation: documentation/03-query-builder/cte.md
📖 Examples: examples/17-cte/03-materialized-cte.php
🎪 PSR-14 Event Dispatcher Integration
Event-driven architecture for monitoring, auditing, and middleware:
ConnectionOpenedEvent- When connection is openedQueryExecutedEvent- After successful query executionQueryErrorEvent- When query error occursTransactionStartedEvent,TransactionCommittedEvent,TransactionRolledBackEvent
use tommyknocker\pdodb\events\QueryExecutedEvent;
$dispatcher->addListener(QueryExecutedEvent::class, function (QueryExecutedEvent $event) {
echo sprintf(
"Query: %s (%.2f ms, %d rows)\n",
substr($event->getSql(), 0, 50),
$event->getExecutionTime(),
$event->getRowsAffected()
);
});📖 Examples: examples/19-events/
🔧 Improvements
Exception Handling
- 113 new tests for exception handling (ExceptionTests, ErrorDetectionStrategyTests, ConstraintParserTests, ErrorCodeRegistryTests)
- Improved
ConstraintParserwith better pattern matching - Enhanced constraint name extraction from error messages
- Better handling of
FOREIGN KEYandschema.tableformats
Memory Management
- Fixed memory leaks by properly closing PDOStatement cursors
- All fetch methods automatically close cursors
- Exception-safe cleanup using
try/finallyblocks - Production-tested with 50,000+ queries without memory accumulation
Code Quality
- Infection mutation testing integrated for code quality assurance
- SHA-256 hashing replaces all MD5 usage for better security
- Simplified external reference detection (KISS/YAGNI refactoring)
- Query compilation cache improvements
- Zero skipped tests policy - all tests actively run
Bug Fixes
- SQLite cache parameter validation (prevents invalid DSN parameters)
- SQL identifier quoting in FileLoader for different dialects
- LATERAL JOIN improvements with better external reference detection
- Markdown EOF formatting consistency
📈 Statistics
- Tests: 991 tests, 3951 assertions (+417 tests, +1425 assertions from 2.7.1)
- Examples: 147/147 passing (49 files × 3 dialects each)
- PHPStan: Level 8, zero errors
- Backward Compatibility: 100% maintained
🔗 Links
- Full Changelog: CHANGELOG.md
- Documentation: documentation/
- Examples: examples/
- Comparison: v2.7.1...v2.8.0
⬇️ Installation
composer require tommyknocker/pdo-database-class:^2.8.0🙏 Thank You
Special thanks to all contributors and users who helped make this release possible!
Full Changelog: v2.7.1...v2.8.0
v2.7.1
🚀 Release v2.7.1 - Major Query Builder Enhancements
Overview
v2.7.1 introduces five major query builder features with full cross-database support, comprehensive testing, and extensive documentation. This release significantly expands the library's capabilities for complex SQL queries and advanced analytics.
✨ New Features
📊 Window Functions Support
Advanced analytics with SQL window functions - perfect for rankings, leaderboards, and time-series analysis.
Ranking Functions:
Db::rowNumber()- Sequential row numberingDb::rank()- Rank with gaps for tiesDb::denseRank()- Rank without gapsDb::ntile()- Divide rows into buckets
Value Access Functions:
Db::lag()- Access previous row valuesDb::lead()- Access next row valuesDb::firstValue()- First value in windowDb::lastValue()- Last value in windowDb::nthValue()- Nth value in window
Window Aggregates:
Db::windowAggregate()- Running totals, moving averages
Features:
- Support for
PARTITION BY,ORDER BY, and frame clauses (ROWS BETWEEN) - Cross-database support (MySQL 8.0+, PostgreSQL 9.4+, SQLite 3.25+)
- 11 helper methods in
WindowHelpersTrait - Complete documentation (900+ lines)
- 10 comprehensive use cases in examples
Documentation: documentation/03-query-builder/window-functions.md
Examples: examples/16-window-functions/
🔗 Common Table Expressions (CTEs)
WITH clauses for complex queries and recursive data processing.
Features:
QueryBuilder::with()- Basic CTEsQueryBuilder::withRecursive()- Recursive CTEs for hierarchical data- Support for
Closure,QueryBuilder, or raw SQL - Multiple CTEs with unique parameter scoping
- Explicit column lists for recursive CTEs
- Cross-database support (MySQL 8.0+, PostgreSQL 8.4+, SQLite 3.8.3+)
Use Cases:
- Tree structures (categories, comments, file systems)
- Organizational charts
- Recursive calculations
- Query organization and readability
Documentation: documentation/03-query-builder/cte.md
Examples: examples/17-cte/
🔀 Set Operations
UNION, INTERSECT, and EXCEPT operations for combining query results.
Features:
QueryBuilder::union()- Combine results (removes duplicates)QueryBuilder::unionAll()- Combine results (keeps duplicates)QueryBuilder::intersect()- Common rows in both queriesQueryBuilder::except()- Rows in first but not in second- Support for
ClosureandQueryBuilderinstances - Proper
ORDER BY/LIMIT/OFFSETplacement (SQL standard compliance) - Cross-database: MySQL 8.0+, PostgreSQL, SQLite 3.8.3+
Documentation: documentation/03-query-builder/set-operations.md
Examples: examples/18-set-operations/
🔍 DISTINCT and DISTINCT ON
Remove duplicates from result sets with advanced control.
Features:
QueryBuilder::distinct()- Remove duplicates (all databases)QueryBuilder::distinctOn()- PostgreSQL-specific distinct on columns- Runtime dialect validation with clear exceptions
DialectInterface::supportsDistinctOn()for feature detection
Documentation: documentation/03-query-builder/distinct.md
Examples: Extended examples/01-basic/05-ordering.php
🎯 FILTER Clause for Conditional Aggregates
SQL:2003 standard compliance with automatic dialect translation.
Features:
filter()method chainable after all aggregate functionsDb::count()->filter(...),Db::sum()->filter(...), etc.- Native
FILTER (WHERE ...)clause for PostgreSQL and SQLite 3.30+ - Automatic
CASE WHENfallback for MySQL DialectInterface::supportsFilterClause()for feature detection
Example:
$db->find()
->from('orders')
->select([
'total_orders' => Db::count('*'),
'paid_orders' => Db::count('*')->filter('status', 'paid'),
'pending_amount' => Db::sum('amount')->filter('status', 'pending'),
])
->groupBy('user_id')
->get();Documentation: documentation/03-query-builder/filter-clause.md
Examples: Extended examples/02-intermediate/02-aggregations.php
🧪 Testing
20 new edge-case tests covering critical scenarios:
- Empty result sets
- NULL value handling
- Boundary conditions
- Unsupported feature detection
Total Coverage:
- ✅ 574 tests with 2526 assertions
- ✅ All tests passing on MySQL, PostgreSQL, SQLite
- ✅ 108 examples (36 files × 3 dialects)
📚 Documentation
Added 6 new comprehensive documentation files (2,305+ lines):
- Window Functions (923 lines)
- CTEs (409 lines)
- Set Operations (204 lines)
- DISTINCT (320 lines)
- FILTER Clause (349 lines)
- Window Helpers Reference (550 lines)
🔧 Changes & Fixes
Changed
- Aggregate helpers now return
FilterValueinstead ofRawValueto supportfilter()chaining SelectQueryBuilderInterfaceextended withsetUnions(),setDistinct(),setDistinctOn()- Enhanced parameter management for UNION subqueries
Fixed
- MySQL recursive CTE string concatenation: Fixed test failure using
CONCAT()instead of||operator - All examples now work correctly on all three database dialects
📊 Statistics
- 39 files changed
- 6,507 insertions, 80 deletions
- PHPStan Level 9 (upgraded from Level 8)
- 100% backward compatible - no breaking changes
🔗 Links
- Full Changelog: CHANGELOG.md
- Documentation:
documentation/ - Examples:
examples/ - Comparison: v2.7.0...v2.7.1
📦 Installation
composer require tommyknocker/pdo-database-class:^2.7.1Enjoy building powerful database queries! 🎉
v2.7.0
🎉 Release v2.7.0 - Major Feature Update
We're excited to announce version 2.7.0 - our biggest release yet! This version adds 8 major new features, 57 comprehensive documentation files, and significant performance improvements.
🚀 What's New
🔍 Full-Text Search
Cross-database full-text search with unified API across MySQL, PostgreSQL, and SQLite:
$results = $db->find()
->from('articles')
->where(Db::fulltextMatch(['title', 'content'], 'search term'))
->get();⚡ Query Result Caching (PSR-16)
10-1000x faster repeated queries with built-in caching support:
$db->enableCache($cache);
// First call hits database, subsequent calls use cache
$users = $db->find()->from('users')->cache(300)->get();📄 Advanced Pagination
Three pagination types for any use case:
- Full pagination - Complete with page numbers and totals
- Simple pagination - Fast, no COUNT overhead
- Cursor-based - Perfect for real-time feeds and infinite scroll
$result = $db->find()->from('posts')->paginate(1, 20);
// Returns: items, total, currentPage, lastPage, perPage🔄 Read/Write Connection Splitting
Master-replica architecture with automatic query routing and load balancing:
$db->enableReadWriteSplitting(new RoundRobinLoadBalancer());
$db->addConnection('write', ['host' => 'master.db']);
$db->addConnection('read-1', ['host' => 'replica1.db', 'type' => 'read']);
$db->addConnection('read-2', ['host' => 'replica2.db', 'type' => 'read']);
// Automatically routes to replicas
$users = $db->find()->from('users')->get();📊 Schema Introspection
Query your database structure programmatically:
$indexes = $db->indexes('users');
$foreignKeys = $db->keys('orders');
$constraints = $db->constraints('products');📤 Export Helpers
Export results to JSON, CSV, or XML with ease:
$users = $db->find()->from('users')->get();
$json = Db::toJson($users, JSON_PRETTY_PRINT);
$csv = Db::toCsv($users);
$xml = Db::toXml($users, 'users', 'user');📥 JSON File Loading
Bulk load JSON data directly into tables:
$db->find()->table('products')
->loadJson('products.json', update: true);🔀 Enhanced orderBy()
Multiple new syntax options for flexible sorting:
// Array with directions
->orderBy(['name' => 'ASC', 'created_at' => 'DESC'])
// Comma-separated string
->orderBy('name ASC, email DESC, id')
// Array with default direction
->orderBy(['name', 'email'], 'DESC')📚 Documentation
57 comprehensive documentation files (~12,600 lines) covering:
- Getting Started & Core Concepts
- Query Builder & JSON Operations
- Advanced Features (Caching, Pagination, Read/Write Splitting, Batch Processing)
- Error Handling & Helper Functions
- Best Practices & Security
- Complete API Reference
- Real-world Examples & Troubleshooting
🎯 Examples
93 working examples (31 files × 3 dialects):
- All examples tested and passing on MySQL, PostgreSQL, and SQLite
- New examples for all features added in this release
- README files in all example directories
🔧 Technical Details
- ✅ 533 tests passing (+55 new), 2,397 assertions
- ✅ 93/93 examples passing on all dialects
- ✅ PHPStan Level 8 - Zero errors
- ✅ PSR-12 compliant (php-cs-fixer)
- ✅ 100% backward compatible - No breaking changes
- ✅ SOLID principles - KISS, DRY, YAGNI
📈 Performance Improvements
- Memory-efficient generators for CSV/XML loading (handles files larger than RAM)
- Query caching support for 10-1000x speedup on repeated queries
- Optimized connection handling for read/write splitting
🔄 Changed
- Refactored file loading to use PHP generators (dramatically reduced memory usage)
- Improved code architecture (better extensibility with
protectedvisibility) - Enhanced test suite (best practices, no direct connection access)
- Restructured README with detailed feature descriptions
🐛 Fixed
- Cross-dialect compatibility in exception handling examples
- Invalid release date in CHANGELOG for v2.6.1
📦 Installation
composer require tommyknocker/pdo-database-class:^2.7🔗 Links
🙏 Thank You
Thank you to everyone using this library! Your feedback and support drive continuous improvement.
Full Changelog: v2.6.2...v2.7.0
v2.6.2
Release v2.6.2 - Major Architectural Refactoring
Release Date: October 25, 2025
Version: 2.6.2
Type: Minor Release (Backward Compatible)
🎉 What's New
Comprehensive Examples Expansion
- 5 new comprehensive example files demonstrating all library functionality:
05-comparison-helpers.php- LIKE, BETWEEN, IN, NOT operations with practical scenarios06-conditional-helpers.php- CASE statements with complex conditional logic07-boolean-helpers.php- TRUE/FALSE/DEFAULT values and boolean operations08-type-helpers.php- CAST, GREATEST, LEAST type conversion and comparison functions04-subqueries.php- Complete subquery examples (EXISTS, NOT EXISTS, scalar subqueries)
- Enhanced existing examples with real-world scenarios and better educational value
- Complete CONTRIBUTING.md (413 lines) with comprehensive development guidelines
Major Architectural Improvements
This release represents a significant internal refactoring following SOLID principles:
🏗️ Connection Architecture
- Extracted
ConnectionLogger,ConnectionState,DialectRegistry,RetryConfigValidator - Improved separation of concerns and testability
- Better error handling with consistent exception processing
🎯 Strategy Pattern for Exception Handling
- Replaced monolithic
ExceptionFactorywith 7 specialized strategies:ConstraintViolationStrategy(priority 10)AuthenticationStrategy(priority 20)TimeoutStrategy(priority 30)TransactionStrategy(priority 50)ConnectionStrategy(priority 60)ResourceStrategy(priority 70)QueryStrategy(priority 1000 - fallback)
🔧 Dialect Refactoring
- Extracted common functionality into reusable traits:
JsonPathBuilderTrait- eliminates JSON path building duplicationUpsertBuilderTrait- common upsert operations with dialect-specific implementationsFileLoader- CSV/XML loading logic extraction
- Created utility classes:
ParameterManager,ConstraintParser,ErrorCodeRegistry
🧩 Helper System Refactoring
- Converted all helper classes to trait-based organization
- Moved helpers to
src/helpers/traits/directory by functionality - Split
DbErrorinto dialect-specific traits (MysqlErrorTrait,PostgresqlErrorTrait,SqliteErrorTrait)
🔨 QueryBuilder Refactoring
- Split monolithic
QueryBuilderinto focused components with proper interfaces - Moved query interfaces to
src/query/interfaces/directory - Extracted common functionality into reusable traits:
CommonDependenciesTraitExternalReferenceProcessingTraitIdentifierQuotingTraitRawValueResolutionTraitTableManagementTrait
🚀 Technical Improvements
Code Quality
- Eliminated ~1000+ lines of duplicate code through trait extraction
- PHPStan Level 8 compliance - zero errors across entire codebase
- Enhanced type safety with proper PHPDoc annotations and null safety checks
- Improved maintainability through SOLID principles and better organization
Cross-Database Compatibility
- Fixed all examples to work consistently on SQLite, MySQL, and PostgreSQL
- Enhanced examples replaced
Db::raw()calls with helper functions where possible - Better educational value with practical, real-world scenarios
📊 Quality Metrics
- ✅ 478 tests - all passing (2198 assertions)
- ✅ 90 examples - all working across all dialects (30 files × 3 dialects each)
- ✅ PHPStan Level 8 - zero errors
- ✅ 100% backward compatibility - no breaking changes to public API
- ✅ Performance improved through reduced code duplication and better architecture
🔄 Migration Guide
No migration required! This release maintains 100% backward compatibility. All existing code will continue to work unchanged.
🎯 What This Means for You
For Users
- Better performance through optimized architecture
- More comprehensive examples for learning and reference
- Improved reliability through better error handling
- Enhanced documentation with complete development guidelines
For Contributors
- Cleaner codebase following SOLID principles
- Better testability with smaller, focused components
- Easier maintenance through trait-based organization
- Comprehensive guidelines in CONTRIBUTING.md
🏷️ Full Changelog
See CHANGELOG.md for complete details of all changes since v2.6.1.
🔗 Links
- GitHub Release: v2.6.2
- Documentation: README.md
- Examples: examples/
- Contributing: CONTRIBUTING.md
Thank you for using PdoDb! This release represents a significant step forward in code quality and maintainability while preserving the simplicity and power that makes PdoDb great.
Happy coding! 🚀
v2.6.1
Release v2.6.1: Automatic External Reference Detection 🎯
🚀 What's New
This release introduces automatic external reference detection in subqueries, making complex queries much more intuitive to write. No more manual Db::raw() wrapping for external table references!
✨ Key Features
Automatic External Reference Detection
QueryBuilder now automatically detects and converts external table references (table.column) to RawValue objects:
- Works in:
where(),select(),orderBy(),groupBy(),having()methods - Smart detection: Only converts references to tables not in current query's FROM clause
- Alias support: Works with table aliases (e.g.,
u.idwhereuis an alias) - Pattern matching: Detects
table.columnoralias.columnpatterns - Safe processing: Invalid patterns (like
123.invalid) are not converted
New Helper: Db::ref()
Added Db::ref() helper for manual external references (though now mostly unnecessary):
Db::ref('users.id') // Equivalent to Db::raw('users.id')📝 Examples
Before (v2.6.0)
$users = $db->find()
->from('users')
->whereExists(function($query) {
$query->from('orders')
->where('user_id', Db::raw('users.id')) // Manual wrapping required
->where('status', 'completed');
})
->get();After (v2.6.1)
$users = $db->find()
->from('users')
->whereExists(function($query) {
$query->from('orders')
->where('user_id', 'users.id') // ✨ Automatic detection!
->where('status', 'completed');
})
->get();More Examples
SELECT with external references:
$users = $db->find()
->from('users')
->select([
'id',
'name',
'total_orders' => 'COUNT(orders.id)', // Auto-detected
'last_order' => 'MAX(orders.created_at)' // Auto-detected
])
->leftJoin('orders', 'orders.user_id = users.id')
->groupBy('users.id', 'users.name')
->get();ORDER BY with external references:
$users = $db->find()
->from('users')
->select(['users.id', 'users.name', 'total' => 'SUM(orders.amount)'])
->leftJoin('orders', 'orders.user_id = users.id')
->groupBy('users.id', 'users.name')
->orderBy('total', 'DESC') // Auto-detected
->get();With table aliases:
$users = $db->find()
->from('users AS u')
->whereExists(function($query) {
$query->from('orders AS o')
->where('o.user_id', 'u.id') // Auto-detected with aliases
->where('o.status', 'completed');
})
->get();🧪 Test Coverage
- 39 new tests across all dialects (MySQL, PostgreSQL, SQLite)
- 13 tests per dialect covering all scenarios:
whereExistsandwhereNotExistswith external referencesselectexpressions with external referencesorderByandgroupBywith external referenceshavingclauses with external references- Internal references (not converted)
- Aliased table references
- Complex external references
- Edge cases and invalid patterns
🔧 Technical Details
- All tests passing: 429+ tests, 2044+ assertions
- PHPStan Level 8: Zero errors across entire codebase
- All examples passing: 24/24 examples on all database dialects
- Backward compatibility: Fully maintained - existing code continues to work unchanged
- Performance: Minimal overhead - only processes string values matching
table.columnpattern
🎯 Detection Rules
- Pattern:
table.columnoralias.column - Scope: Only converts if the table/alias is not in the current query's FROM clause
- Methods: Works in
where(),select(),orderBy(),groupBy(),having() - Safety: Internal references (tables in current query) are not converted
- Validation: Invalid patterns (like
123.invalid) are not converted
📚 Documentation Updates
- Updated README.md with comprehensive examples
- Added new "Automatic External Reference Detection" section
- Updated subquery examples to demonstrate automatic detection
- Added
Db::ref()to helper functions reference
🔄 Migration Guide
No migration required! This is a fully backward-compatible release. Existing code continues to work unchanged.
If you want to take advantage of the new automatic detection, simply remove Db::raw() wrappers around external table references:
// Old way (still works)
->where('user_id', Db::raw('users.id'))
// New way (automatic detection)
->where('user_id', 'users.id')🎉 Benefits
- Cleaner code: No more
Db::raw()wrappers for simple external references - Better readability: Natural SQL syntax in subqueries
- Reduced errors: Automatic detection prevents common mistakes
- Consistent behavior: Works the same across all database dialects
- Zero breaking changes: Existing code continues to work
Full Changelog: v2.6.0...v2.6.1
Installation: composer require tommyknocker/pdo-database-class:^2.6.1
v2.6.0
🚀 Release v2.6.0: Comprehensive Exception Handling System
Release Date: October 23, 2025
Type: Major Release (Breaking Changes)
🎯 Overview
This major release introduces a comprehensive exception handling system that replaces generic PDOException with specialized exception types, providing better error handling capabilities and improved debugging experience.
⚠️ Breaking Changes
Exception Handling System
loadCsv()andloadXml()now throw specialized exceptions instead of returningfalse- All database operations now throw custom
DatabaseExceptionhierarchy instead ofPDOException
Migration Guide
// Before (v2.5.x)
$result = $db->find()->table('users')->loadCsv('/path/file.csv');
if (!$result) {
// Handle error
}
// After (v2.6.0)
try {
$db->find()->table('users')->loadCsv('/path/file.csv');
} catch (DatabaseException $e) {
// Handle specific exception type
}✨ New Features
🎯 Comprehensive Exception Hierarchy
DatabaseException- Base exception class extendingPDOExceptionConnectionException- Connection-related errors (retryable)QueryException- Query execution errorsConstraintViolationException- Constraint violations with detailed contextTransactionException- Transaction errors (deadlocks, etc.)AuthenticationException- Authentication failuresTimeoutException- Query/connection timeoutsResourceException- Resource exhaustion errors
🏭 ExceptionFactory
- Automatic exception type detection based on error codes and messages
- Cross-dialect support for MySQL, PostgreSQL, and SQLite
- Enhanced error context with driver, query, and retryable status
📊 Enhanced Error Context
catch (ConstraintViolationException $e) {
echo "Constraint: " . $e->getConstraintName();
echo "Table: " . $e->getTableName();
echo "Column: " . $e->getColumnName();
echo "Retryable: " . ($e->isRetryable() ? 'Yes' : 'No');
}🔧 Improved Error Codes
DbErrorclass with standardized error codes for all dialects- Helper methods for retryable error detection
- Human-readable descriptions for error codes
🛠️ Technical Improvements
📝 Documentation & Examples
- Comprehensive error handling examples in
examples/09-exception-handling/ - Enhanced README examples with error handling demonstrations
- Migration guide for breaking changes
- Exception handling best practices
🧪 Testing & Quality
- 429 tests passing across all dialects
- 24 examples working on MySQL, PostgreSQL, and SQLite
- PHPStan level 8 compliance with PHPDoc annotations
- test-examples script integrated into
composer check
🔄 Connection Retry System
- Retry mechanism with exponential backoff
- Comprehensive logging with Monolog integration
- Configurable retry policies for different error types
📈 Statistics
- 18 commits in this release
- 429 tests passing
- 24 examples working across all dialects
- PHPStan level 8 compliance
- Zero external dependencies maintained
🔗 Links
- Full Changelog: v2.5.1...v2.6.0
- Documentation: README.md
- Examples: examples/
Installation:
composer require tommyknocker/pdo-database-class:^2.6.0Upgrade from v2.5.x:
Please review the breaking changes section and update your error handling code accordingly.
v2.5.1
🚀 Release v2.5.1 - UPSERT Improvements & QueryBuilder API Migration
This release brings critical bug fixes for groupBy()/orderBy() with qualified column names, adds Db::inc()/Db::dec() support to UPSERT operations, and migrates all examples to QueryBuilder API for consistency and best practices demonstration.
🎯 Highlights
- ✅ 343 tests, 1544 assertions - all passing
- ✅ 21 examples working on all 3 database dialects (SQLite, MySQL, PostgreSQL)
- ✅ PHPStan Level 8 - zero errors
- ✅ CI testing for all examples added to GitHub Actions
🆕 New Features
Db::inc() / Db::dec() in onDuplicate()
Now you can use convenient helpers for UPSERT increments:
$db->find()->table('user_stats')
->onDuplicate([
'login_count' => Db::inc(1), // Increment old value
'points' => Db::dec(10) // Decrement old value
])
->insert(['user_id' => 1, 'login_count' => 1, 'points' => 100]);Works seamlessly across all dialects:
- MySQL:
counter=counter+ 5 - PostgreSQL: "counter" = "user_stats"."counter" + 5
- SQLite: "counter" = "counter" + 5
CI Testing for Examples
All 21 examples now automatically tested in GitHub Actions on available databases.
🐛 Critical Bug Fixes
1. groupBy() / orderBy() with Qualified Column Names
Bug: Qualified names like u.id were quoted as `u.id` (single identifier) instead of `u`.`id` (table.column).
Impact: Broke on MySQL/PostgreSQL with error: Unknown column 'u.id' in 'group statement'
Example that was broken:
$db->find()
->from('users AS u')
->leftJoin('orders AS o', 'o.user_id = u.id')
->select(['u.name', 'total' => Db::sum('o.amount')])
->groupBy(['u.id', 'u.name']) // ❌ Was broken
->get();Fix: Changed groupBy() and orderBy() to use quoteQualifiedIdentifier().
Test: Added testGroupByWithQualifiedNames() to verify fix across all 3 dialects.
2. Db::inc() / Db::dec() Ignored in onDuplicate()
Bug: Db::inc(5) was ignored, value replaced instead of incremented.
Impact: UPSERT didn't work as expected - counters were reset instead of incremented.
Fix: Added ['__op' => 'inc'] array handling in all dialect's buildUpsertClause().
Test: Added testUpsertWithIncHelper() for all 3 dialects.
3. PostgreSQL UPSERT "Ambiguous Column" Errors
Bug: Db::raw('age + 5') in onDuplicate() caused PostgreSQL error: "column reference 'age' is ambiguous".
Impact: PostgreSQL couldn't distinguish between old table value vs new excluded value.
Fix: Auto-qualify column references with table name.
Test: testUpsertWithRawIncrement() verifies both Db::inc() and Db::raw() work correctly.
4. PostgreSQL lastInsertId() Exception
Bug: Crash when inserting into tables without auto-increment (SERIAL) columns.
Fix: Added try-catch in executeInsert() to gracefully handle missing sequence.
📝 Examples Migration to QueryBuilder API
All 21 examples refactored to use QueryBuilder fluent API instead of raw SQL:
Before (v2.5.0):
$count = $db->rawQueryValue('SELECT COUNT(*) FROM users WHERE is_active = 1');
$db->rawQuery("UPDATE users SET age = age + 1 WHERE id = 5");After (v2.5.1):
$count = $db->find()->from('users')->select([Db::count()])->where('is_active', 1)->getValue();
$db->find()->table('users')->where('id', 5)->update(['age' => Db::inc(1)]);rawQuery() now used ONLY for:
- ✅ CREATE TABLE
- ✅ ALTER TABLE
- ✅ DROP TABLE
- ✅ SET statements (e.g., FOREIGN_KEY_CHECKS)
🔧 API Changes (Backwards Compatible)
Enhanced buildUpsertClause() signature across all dialects:
public function buildUpsertClause(
array $updateColumns,
string $defaultConflictTarget = 'id',
string $tableName = '' // NEW: optional, for PostgreSQL
): string;Breaking changes: NONE - optional parameter with default value maintains full backwards compatibility.
📊 Testing Results
✅ PHPUnit: 343 tests, 1544 assertions (0 failures, 0 errors)
✅ Examples: 61/61 runs passed
- SQLite: 21/21
- MySQL: 20/20
- PostgreSQL: 20/20
✅ PHPStan: Level 8, 0 errors
📦 Files Changed
Source (5 files):
src/query/QueryBuilder.php- qualified identifier handling, executeInsert() fixsrc/dialects/DialectInterface.php- updated interface signaturesrc/dialects/MySQLDialect.php- Db::inc()/Db::dec() supportsrc/dialects/PostgreSQLDialect.php- Db::inc()/Db::dec() + table qualificationsrc/dialects/SqliteDialect.php- Db::inc()/Db::dec() support
Examples (13 files): All migrated to QueryBuilder API
Tests (3 files): Added 9 new tests total
CI (1 file): .github/workflows/tests.yml - added example testing
🔄 Upgrade Path
This release is fully backwards compatible. Simply update:
composer update tommyknocker/pdo-database-classNo code changes required! But you can now:
- Use
Db::inc()/Db::dec()inonDuplicate() - Use qualified column names in
groupBy()/orderBy()withoutDb::raw()
🙏 Contributors
Special thanks to the community for reporting issues and testing across different database dialects!
Full Changelog: v2.5.0...v2.5.1
v2.5.0
✨ Major Feature Release
New Features
Connection Pooling
- Support for initialization without default connection via
new PdoDb() - Better error messages for uninitialized connections
- Perfect for microservices and multi-tenant applications
17 New SQL Helper Functions
- NULL handling:
Db::ifNull(),Db::coalesce(),Db::nullIf() - Math operations:
Db::abs(),Db::round(),Db::mod(),Db::greatest(),Db::least() - String operations:
Db::upper(),Db::lower(),Db::trim(),Db::length(),Db::substring(),Db::replace() - Date/Time extraction:
Db::curDate(),Db::curTime(),Db::date(),Db::time(),Db::year(),Db::month(),Db::day(),Db::hour(),Db::minute(),Db::second()
Complete JSON Operations API
Db::jsonGet(),Db::jsonLength(),Db::jsonKeys(),Db::jsonType()- Unified API across MySQL, PostgreSQL, and SQLite
- Comprehensive edge-case testing
📚 21 Runnable Examples (NEW!)
Complete, tested examples organized by complexity:
- Basic: Connection, CRUD, WHERE conditions, INSERT/UPDATE
- Intermediate: JOINs, aggregations, pagination, transactions
- Advanced: Connection pooling, bulk operations, UPSERT
- JSON: Complete JSON operations guide
- Helpers: String, math, date/time, NULL handling
- Real-World:
- Blog system with posts, comments, tags, and analytics
- User authentication with sessions, RBAC, password hashing
- Advanced search & filters with facets and pagination
- Multi-tenant SaaS with resource tracking and quotas
Comprehensive Test Coverage
- 334 tests, 1499 assertions - ALL PASSING ✅
- 90%+ code coverage with dialect-specific testing
- 68 tests in SharedCoverageTest for dialect-independent code
- 8 new edge-case tests for
Db::concat()bug fixes - Improved test organization with automatic
setUp()cleanup
Utility Scripts
scripts/release.sh- Release automation with validationscripts/test-examples.sh- Example verification
Documentation
- Professional README (1400+ lines)
- Table of contents, error handling, performance tips
- Debugging guide and troubleshooting section
- Complete CHANGELOG from v1.0.3
🐛 Critical Bug Fixes
-
CRITICAL: insertMulti() bulk UPSERT bug: Fixed automatic conflict target determination for PostgreSQL/SQLite ON CONFLICT
buildInsertMultiSql()now correctly uses first column whenidnot present (matchesinsert()behavior)- Enables proper bulk UPSERT operations across all dialects
- Without this fix, bulk inserts with
onDuplicateparameter would fail on PostgreSQL/SQLite
-
CRITICAL: Db::concat() helper bugs (2 major issues fixed):
- Bug #1:
ConcatValuenot initializing parent class → "Typed property not initialized" error- Fixed with
parent::__construct('')and protectivegetValue()override
- Fixed with
- Bug #2: String literals (spaces, special chars) treated as column names
- Enhanced auto-detection and quoting for spaces,
:,|,-, emoji, unicode - Now works:
Db::concat('first_name', ' ', 'last_name')✅
- Enhanced auto-detection and quoting for spaces,
- Added 8 comprehensive edge-case tests covering all scenarios
- Bug #1:
-
Restored RawValue support in rawQuery methods
-
Fixed method calls in lock/unlock/loadData/loadXml
-
MySQL EXPLAIN compatibility (table format preservation)
-
PostgreSQL formatSelectOptions (FOR UPDATE/FOR SHARE)
-
SQLite JSON edge cases
🔧 Improvements
- Enhanced examples: Replaced 30+ raw SQL expressions with
Db::helpers for better readabilityDb::inc()/Db::dec()for balance transfers and countersDb::count(),Db::sum(),Db::case()for aggregationsDb::concat()with automatic string literal handling
- Better test isolation: Added
setUp()method for automatic cleanup- Removed 26+ redundant cleanup statements
- Improved reliability and maintainability
- Optimized QueryBuilder: Refactored with helper methods
- Clearer error messages: Better guidance for common issues
🗑️ Removed (Non-Breaking)
Deprecated helper methods from PdoDb (~130 lines):
inc(),dec(),not()→ UseDb::equivalentsescape()→ Use prepared statementstableExists()→ UseQueryBuilder::tableExists()now()→ UseDb::now()loadData(),loadXml()→ UseQueryBuilder::loadCsv(),QueryBuilder::loadXml()
📝 Technical Details
- Backward compatible: Zero breaking changes
- PHP 8.4+ required (property hooks, union types, match expressions)
- 90%+ test coverage with comprehensive edge-case testing
- All 21 examples tested and verified on PHP 8.4.13
- Full CHANGELOG: See CHANGELOG.md
🚀 Quick Start
composer require tommyknocker/pdo-database-class
# Try examples
cd vendor/tommyknocker/pdo-database-class/examples
cp config.example.php config.php
php 01-basic/01-connection.php
php 06-real-world/02-user-auth.php
php 06-real-world/03-search-filters.php