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