Skip to content

Commit 0df96ca

Browse files
committed
fix: critical Db::concat() bugs and improve test organization
Fixed 2 critical bugs in Db::concat() helper: - Bug #1: ConcatValue not initializing parent RawValue - Bug #2: String literals not auto-quoted Changes: - src/helpers/ConcatValue.php: Added parent::__construct(''), protective getValue() - src/dialects/DialectAbstract.php: Enhanced concat() string literal detection - tests/SharedCoverageTest.php: Added setUp() for auto-cleanup, 8 new concat tests - examples/: Updated 14 files to use Db helpers over Db::raw() Tests: 334 total (+17), 1499 assertions (+35), all passing ✅ Coverage: 90%+ (improved from 83%)
1 parent d60bb21 commit 0df96ca

18 files changed

+1228
-47
lines changed

CHANGELOG.md

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
- `Db::jsonGet()`, `Db::jsonLength()`, `Db::jsonKeys()`, `Db::jsonType()`
2323
- Unified API across MySQL, PostgreSQL, and SQLite
2424
- Edge-case testing for JSON operations
25-
- **Comprehensive examples directory** (`examples/`) with 18 runnable examples:
25+
- **Comprehensive examples directory** (`examples/`) with 21 runnable examples:
2626
- Basic operations (connection, CRUD, WHERE conditions)
2727
- Intermediate patterns (JOINs, aggregations, pagination, transactions)
2828
- Advanced features (connection pooling, bulk operations, UPSERT)
2929
- JSON operations (complete guide with real-world usage)
3030
- Helper functions (string, math, date/time, NULL handling)
31-
- Real-world applications (blog system with posts, comments, tags)
31+
- Real-world applications:
32+
- Blog system with posts, comments, tags, analytics
33+
- User authentication with sessions, RBAC, password hashing
34+
- Advanced search & filters with facets, sorting, pagination
35+
- Multi-tenant SaaS with resource tracking and quota management
3236
- **Dialect coverage tests** for better test coverage (300 total tests):
3337
- `buildLoadCsvSql()` - CSV loading SQL generation with temp file handling
3438
- `buildLoadXML()` - XML loading SQL generation with temp file handling
@@ -55,6 +59,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5559
- **Improved error messages**: Property hooks now provide clearer guidance for uninitialized connections
5660
- **Updated .gitignore**: Cleaned up and added examples-related ignores, coverage reports
5761
- **README.md improvements**: Removed `ALL_TESTS=1` requirement - tests now run without environment variables
62+
- **Enhanced examples** (14 files updated): Maximized use of `Db::` helpers over `Db::raw()` for better code clarity:
63+
- Replaced 30+ raw SQL expressions with helper functions
64+
- `Db::inc()`/`Db::dec()` for increments/decrements
65+
- `Db::count()`, `Db::sum()`, `Db::avg()`, `Db::coalesce()` for aggregations
66+
- `Db::case()` for conditional logic
67+
- `Db::concat()` with automatic string literal quoting
68+
- **Improved test organization**: Added `setUp()` method in `SharedCoverageTest` for automatic table cleanup before each test
69+
- Removed 26+ redundant cleanup statements
70+
- Better test isolation and reliability
5871

5972
### Removed
6073
- **Deprecated helper methods from PdoDb** (~130 lines removed):
@@ -69,6 +82,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6982
- `buildInsertMultiSql()` now correctly uses first column when `id` not present (matches `insert()` behavior)
7083
- Enables proper bulk UPSERT operations across all dialects
7184
- Without this fix, bulk inserts with `onDuplicate` parameter would fail on PostgreSQL/SQLite
85+
- **CRITICAL: Db::concat() helper bugs** (2 major issues fixed):
86+
- **Bug #1**: `ConcatValue` not initializing parent `RawValue` class causing "Typed property not initialized" error
87+
- Added `parent::__construct('')` call in `ConcatValue` constructor
88+
- Added protective `getValue()` override with clear error message to prevent misuse
89+
- **Bug #2**: String literals (spaces, special chars) not auto-quoted, treated as column names
90+
- Enhanced `DialectAbstract::concat()` logic to auto-detect and quote string literals
91+
- Supports spaces, colons, pipes, dashes, emoji, and unicode characters
92+
- Examples: `Db::concat('first_name', ' ', 'last_name')` now works without `Db::raw()`
93+
- Added 8 comprehensive edge-case tests in `SharedCoverageTest`:
94+
- `testConcatWithStringLiterals()` - spaces and simple literals
95+
- `testConcatWithSpecialCharacters()` - colon, pipe, dash
96+
- `testConcatWithNestedHelpers()` - `Db::upper/lower` inside concat
97+
- `testConcatNestedInHelperThrowsException()` - protection from incorrect usage
98+
- `testConcatWithQuotedLiterals()` - already-quoted strings
99+
- `testConcatWithNumericValues()` - number handling
100+
- `testConcatWithEmptyString()` - empty string edge case
101+
- `testConcatWithMixedTypes()` - mixed type concatenation
72102
- Restored `RawValue` union type support in `rawQuery()`, `rawQueryOne()`, `rawQueryValue()` methods
73103
- Corrected method calls in `lock()`, `unlock()`, `loadData()`, `loadXml()` to use `prepare()->execute()` pattern
74104
- SQLite JSON support fixes for edge cases (array indexing, value encoding, numeric sorting)
@@ -78,8 +108,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
78108
- **PostgreSQL formatSelectOptions test**: Fixed to test actual supported features (FOR UPDATE/FOR SHARE)
79109

80110
### Technical Details
81-
- **All tests passing**: 317 tests, 1464 assertions across MySQL, PostgreSQL, and SQLite (3 skipped for live testing)
82-
- **Test coverage**: 83%+ with comprehensive dialect-specific and edge-case testing
111+
- **All tests passing**: 334 tests, 1499 assertions across MySQL, PostgreSQL, and SQLite (3 skipped for live testing)
112+
- **68 tests** in SharedCoverageTest (dialect-independent code)
113+
- **8 new edge-case tests** for `Db::concat()` bug fixes
114+
- Added `setUp()` method for automatic table cleanup before each test
115+
- **Test coverage**: 90%+ with comprehensive dialect-specific and edge-case testing
83116
- **Full backward compatibility maintained**: Zero breaking changes (deprecated methods removal is non-breaking)
84117
- Examples tested and verified on PHP 8.4.13
85118
- **Performance**: Optimized QueryBuilder reduces code duplication and improves maintainability

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ composer require tommyknocker/pdo-database-class
9393
For specific versions:
9494

9595
```bash
96+
# Latest 2.x version
97+
composer require tommyknocker/pdo-database-class:^2.0
98+
9699
# Latest 1.x version
97100
composer require tommyknocker/pdo-database-class:^1.0
98101

examples/01-basic/04-insert-update.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
->table('counters')
108108
->where('name', 'page_views')
109109
->update([
110-
'value' => Db::raw('value + 50'),
110+
'value' => Db::inc(50),
111111
'name' => Db::raw('name || "_total"'),
112112
'updated_at' => Db::now()
113113
]);

examples/02-intermediate/01-joins.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@
8383
->leftJoin('reviews AS r', 'r.user_id = u.id')
8484
->select([
8585
'u.name',
86-
'order_count' => Db::raw('COUNT(DISTINCT o.id)'),
87-
'review_count' => Db::raw('COUNT(DISTINCT r.id)')
86+
'order_count' => Db::count('DISTINCT o.id'),
87+
'review_count' => Db::count('DISTINCT r.id')
8888
])
8989
->groupBy('u.id')
9090
->get();
@@ -102,10 +102,10 @@
102102
->leftJoin('orders AS o', 'o.user_id = u.id')
103103
->select([
104104
'u.name',
105-
'total_spent' => Db::raw('COALESCE(SUM(o.amount), 0)')
105+
'total_spent' => Db::coalesce(Db::sum('o.amount'), '0')
106106
])
107107
->groupBy('u.id')
108-
->orderBy(Db::raw('total_spent'), 'DESC')
108+
->orderBy('total_spent', 'DESC')
109109
->get();
110110

111111
echo " User spending:\n";
@@ -122,12 +122,12 @@
122122
->select([
123123
'u.name',
124124
'u.city',
125-
'order_count' => Db::raw('COUNT(*)'),
126-
'total' => Db::raw('SUM(o.amount)')
125+
'order_count' => Db::count(),
126+
'total' => Db::sum('o.amount')
127127
])
128128
->where('u.city', 'NYC')
129129
->groupBy('u.id')
130-
->having(Db::raw('SUM(o.amount)'), 500, '>')
130+
->having(Db::sum('o.amount'), 500, '>')
131131
->get();
132132

133133
echo " High-value NYC customers:\n";

examples/02-intermediate/04-transactions.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@
3535
$db->find()
3636
->table('accounts')
3737
->where('name', 'Alice')
38-
->update(['balance' => Db::raw('balance - 200')]);
38+
->update(['balance' => Db::dec(200)]);
3939

4040
// Add to Bob
4141
$db->find()
4242
->table('accounts')
4343
->where('name', 'Bob')
44-
->update(['balance' => Db::raw('balance + 200')]);
44+
->update(['balance' => Db::inc(200)]);
4545

4646
// Record transaction
4747
$db->find()->table('transactions')->insert([
@@ -82,7 +82,7 @@
8282
$db->find()
8383
->table('accounts')
8484
->where('name', 'Bob')
85-
->update(['balance' => Db::raw('balance - 2000')]);
85+
->update(['balance' => Db::dec(2000)]);
8686

8787
$db->commit();
8888

@@ -110,13 +110,13 @@
110110
$db->find()
111111
->table('accounts')
112112
->where('name', $transfer['from'])
113-
->update(['balance' => Db::raw("balance - {$transfer['amount']}")]);
113+
->update(['balance' => Db::dec($transfer['amount'])]);
114114

115115
// Add
116116
$db->find()
117117
->table('accounts')
118118
->where('name', $transfer['to'])
119-
->update(['balance' => Db::raw("balance + {$transfer['amount']}")]);
119+
->update(['balance' => Db::inc($transfer['amount'])]);
120120

121121
// Log
122122
$fromId = $db->find()->from('accounts')->where('name', $transfer['from'])->getValue('id');

examples/03-advanced/01-connection-pooling.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
->from('events')
114114
->select([
115115
'user_id',
116-
'event_count' => Db::raw('COUNT(*)')
116+
'event_count' => Db::count()
117117
])
118118
->groupBy('user_id')
119119
->get();

examples/03-advanced/02-bulk-operations.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
$affected = $db->find()
120120
->table('users')
121121
->where('age', 50, '>=')
122-
->update(['age' => Db::raw('age - 10')]);
122+
->update(['age' => Db::dec(10)]);
123123

124124
$elapsed = round((microtime(true) - $start) * 1000, 2);
125125
echo "✓ Updated $affected rows in {$elapsed}ms\n\n";

examples/05-helpers/01-string-helpers.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
->from('users')
3232
->select([
3333
'id',
34-
'full_name' => Db::concat(Db::upper('first_name'), Db::raw("' '"), Db::upper('last_name'))
34+
'full_name' => Db::concat(Db::upper('first_name'), ' ', Db::upper('last_name'))
3535
])
3636
->get();
3737

examples/05-helpers/02-math-helpers.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
->select([
7272
'name',
7373
'reading',
74-
'remainder' => Db::mod('reading', Db::raw('2'))
74+
'remainder' => Db::mod('reading', '2')
7575
])
7676
->get();
7777

examples/05-helpers/03-date-helpers.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
->from('events')
128128
->select([
129129
'month' => Db::month('event_date'),
130-
'event_count' => Db::raw('COUNT(*)')
130+
'event_count' => Db::count()
131131
])
132132
->groupBy(Db::month('event_date'))
133133
->orderBy(Db::month('event_date'))

0 commit comments

Comments
 (0)