Skip to content

Commit 11fd698

Browse files
committed
feat: add Db::inc()/Db::dec() support to onDuplicate() + migrate all examples to QueryBuilder API
MAJOR ENHANCEMENT: Complete migration to QueryBuilder API + UPSERT improvements Features Added: 1. Db::inc() and Db::dec() now work in onDuplicate() for all dialects 2. PostgreSQL UPSERT with Db::raw() now properly handles column references 3. All examples migrated from rawQuery to QueryBuilder API 4. CI now tests all examples on all available dialects API Changes (backwards compatible): - buildUpsertClause() signature updated across all dialects: public function buildUpsertClause( array $updateColumns, string $defaultConflictTarget = 'id', string $tableName = '' // NEW: for PostgreSQL ambiguous column resolution ): string Examples Refactored (13 files): ✅ Replaced rawQuery SELECT → find()->select()->where()->get() ✅ Replaced rawQuery INSERT → find()->insert() ✅ Replaced rawQuery UPDATE → find()->update() ✅ Replaced rawQuery DELETE → find()->delete() ✅ Replaced rawQueryValue COUNT → find()->select([Db::count()])->getValue() ✅ Kept rawQuery ONLY for DDL: CREATE TABLE, ALTER TABLE, DROP TABLE Dialect Fixes: MySQL: - Added Db::inc()/Db::dec() handling in buildUpsertClause() - Generates: `age` = `age` + 5 PostgreSQL: - Added Db::inc()/Db::dec() handling with table qualification - Generates: "age" = "user_stats"."age" + 5 - Fixed Db::raw() to auto-qualify column references - Resolves "ambiguous column" errors in UPSERT SQLite: - Added Db::inc()/Db::dec() handling in buildUpsertClause() - Generates: "age" = "age" + 5 QueryBuilder Improvements: - executeInsert() now catches PDOException for tables without auto-increment - Passes table name to buildUpsertClause() for PostgreSQL support Files Changed: Examples (13): - 01-basic/04-insert-update.php (concat via Db::raw) - 02-intermediate/03-pagination.php (COUNT via QueryBuilder) - 03-advanced/02-bulk-operations.php (DELETE via QueryBuilder) - 03-advanced/03-upsert.php (fixed PRIMARY KEY, Db::inc() usage) - 04-json/01-json-basics.php (JSONB for PostgreSQL) - 04-json/02-json-queries.php (JSONB for PostgreSQL) - 05-helpers/01-string-helpers.php (string concat via Db::raw) - 05-helpers/02-math-helpers.php (UPDATE via QueryBuilder) - 05-helpers/04-null-helpers.php (INSERT/SELECT via QueryBuilder) - 06-real-world/01-blog-system.php (JSONB, SELECT COUNT via QueryBuilder) - 06-real-world/02-user-auth.php (hasPermission via QueryBuilder) - 06-real-world/03-search-filters.php (all SELECTs via QueryBuilder) - 06-real-world/04-multi-tenant.php (SELECT COUNT via QueryBuilder) Source (5): - src/dialects/DialectInterface.php - src/dialects/MySQLDialect.php - src/dialects/PostgreSQLDialect.php - src/dialects/SqliteDialect.php - src/query/QueryBuilder.php Tests (3): - tests/PdoDbMySQLTest.php (added testUpsertWithIncHelper) - tests/PdoDbPostgreSQLTest.php (added testUpsertWithIncHelper) - tests/PdoDbSqliteTest.php (added testUpsertWithIncHelper) Scripts (1): - scripts/test-examples.sh (fixed PostgreSQL port detection) CI (1): - .github/workflows/tests.yml (added example testing for all dialects) Testing Results: ✅ All 343 PHPUnit tests pass (1544 assertions) ✅ All 21 examples work on SQLite (21/21) ✅ All 20 examples work on MySQL (20/20) ✅ All 20 examples work on PostgreSQL (20/20) ✅ PHPStan level 8 passes with no errors ✅ No warnings or errors in any example Breaking Changes: NONE (API is backwards compatible)
1 parent 48563c6 commit 11fd698

23 files changed

+329
-87
lines changed

.github/workflows/tests.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,25 @@ jobs:
5252
DB_USER="testuser" \
5353
DB_PASS="testpass" \
5454
vendor/bin/phpunit tests/PdoDbMySQLTest.php --coverage-clover coverage-mysql.xml
55+
- name: Test MySQL Examples
56+
run: |
57+
cp examples/config.example.php examples/config.mysql.php
58+
cat > examples/config.mysql.php << 'EOF'
59+
<?php
60+
return [
61+
'driver' => 'mysql',
62+
'host' => '127.0.0.1',
63+
'port' => 3306,
64+
'username' => 'testuser',
65+
'password' => 'testpass',
66+
'dbname' => 'testdb',
67+
'charset' => 'utf8mb4',
68+
'options' => [
69+
PDO::MYSQL_ATTR_LOCAL_INFILE => true
70+
]
71+
];
72+
EOF
73+
./scripts/test-examples.sh
5574
- name: Upload MySQL coverage to Codecov
5675
uses: codecov/codecov-action@v4
5776
with:
@@ -95,6 +114,20 @@ jobs:
95114
DB_USER="testuser" \
96115
DB_PASS="testpass" \
97116
vendor/bin/phpunit tests/PdoDbPostgreSQLTest.php --coverage-clover coverage-postgres.xml
117+
- name: Test PostgreSQL Examples
118+
run: |
119+
cat > examples/config.pgsql.php << 'EOF'
120+
<?php
121+
return [
122+
'driver' => 'pgsql',
123+
'host' => 'localhost',
124+
'port' => 5433,
125+
'username' => 'testuser',
126+
'password' => 'testpass',
127+
'dbname' => 'testdb'
128+
];
129+
EOF
130+
./scripts/test-examples.sh
98131
- name: Upload PostgreSQL coverage to Codecov
99132
uses: codecov/codecov-action@v4
100133
with:
@@ -117,6 +150,8 @@ jobs:
117150
- run: |
118151
DB_DSN="sqlite::memory:" \
119152
vendor/bin/phpunit tests/PdoDbSqliteTest.php --coverage-clover coverage-sqlite.xml
153+
- name: Test SQLite Examples
154+
run: ./scripts/test-examples.sh
120155
- name: Upload SQLite coverage to Codecov
121156
uses: codecov/codecov-action@v4
122157
with:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
->where('name', 'page_views')
109109
->update([
110110
'value' => Db::inc(50),
111-
'name' => Db::raw('name || "_total"'),
111+
'name' => Db::raw(getCurrentDriver($db) === 'pgsql' ? "name || '_total'" : "CONCAT(name, '_total')"),
112112
'updated_at' => Db::now()
113113
]);
114114

examples/02-intermediate/03-pagination.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060

6161
// Example 3: Calculate total pages
6262
echo "3. Pagination metadata...\n";
63-
$totalUsers = $db->rawQueryValue('SELECT COUNT(*) FROM users');
63+
$totalUsers = $db->find()->from('users')->select([Db::count()])->getValue();
6464
$totalPages = ceil($totalUsers / $perPage);
6565

6666
echo " Total users: $totalUsers\n";
@@ -79,7 +79,7 @@ function getPaginatedResults($db, $page, $perPage = 10) {
7979
->offset($offset)
8080
->get();
8181

82-
$total = $db->rawQueryValue('SELECT COUNT(*) FROM users');
82+
$total = $db->find()->from('users')->select([Db::count()])->getValue();
8383

8484
return [
8585
'data' => $results,

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
echo "✗ Inserted 100 rows in {$elapsed}ms (SLOW)\n\n";
4242

4343
// Clear for comparison
44-
$db->rawQuery('DELETE FROM users');
44+
$db->find()->table('users')->where(Db::raw('1=1'))->delete();
4545

4646
// Example 2: Bulk insert (fast)
4747
echo "2. Bulk insert with insertMulti (RECOMMENDED)...\n";
@@ -63,7 +63,7 @@
6363

6464
// Example 3: Bulk insert in batches
6565
echo "3. Bulk insert in batches (for very large datasets)...\n";
66-
$db->rawQuery('DELETE FROM users');
66+
$db->find()->table('users')->where(Db::raw('1=1'))->delete();
6767

6868
$totalUsers = 1000;
6969
$batchSize = 100;
@@ -88,15 +88,15 @@
8888
}
8989

9090
$elapsed = round((microtime(true) - $start) * 1000, 2);
91-
$count = $db->rawQueryValue('SELECT COUNT(*) FROM users');
91+
$count = $db->find()->from('users')->select([Db::count()])->getValue();
9292

9393
echo "✓ Inserted $count rows in $batches batches ({$elapsed}ms)\n";
9494
echo " Batch size: $batchSize\n";
9595
echo " Average per batch: " . round($elapsed / $batches, 2) . "ms\n\n";
9696

9797
// Example 4: Bulk insert with transactions
9898
echo "4. Bulk insert with transaction (even faster)...\n";
99-
$db->rawQuery('DELETE FROM users');
99+
$db->find()->table('users')->where(Db::raw('1=1'))->delete();
100100

101101
$users = [];
102102
for ($i = 1; $i <= 500; $i++) {
@@ -139,7 +139,7 @@
139139
->where('age', 25, '<')
140140
->delete();
141141

142-
$remaining = $db->rawQueryValue('SELECT COUNT(*) FROM users');
142+
$remaining = $db->find()->from('users')->select([Db::count()])->getValue();
143143
echo "✓ Deleted $deleted rows, $remaining remaining\n\n";
144144

145145
// Example 7: Truncate (fastest way to clear table)
@@ -148,7 +148,7 @@
148148
$db->find()->table('users')->truncate();
149149
$elapsed = round((microtime(true) - $start) * 1000, 2);
150150

151-
$count = $db->rawQueryValue('SELECT COUNT(*) FROM users');
151+
$count = $db->find()->from('users')->select([Db::count()])->getValue();
152152
echo "✓ Table truncated in {$elapsed}ms, $count rows remaining\n";
153153

154154
echo "\nBulk operations example completed!\n";

examples/03-advanced/03-upsert.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
// Setup
2020
recreateTable($db, 'user_stats', [
21-
'user_id' => 'INTEGER PRIMARY KEY',
21+
'user_id' => 'INTEGER PRIMARY KEY', // Not AUTOINCREMENT - user_id is set manually
2222
'login_count' => 'INTEGER DEFAULT 0',
2323
'last_login' => 'DATETIME',
2424
'total_points' => 'INTEGER DEFAULT 0'
@@ -85,7 +85,7 @@
8585
]);
8686
}
8787

88-
$count = $db->rawQueryValue('SELECT COUNT(*) FROM user_stats');
88+
$count = $db->find()->from('user_stats')->select([Db::count()])->getValue();
8989
echo " ✓ Total users in stats table: $count\n\n";
9090

9191
// Example 5: Simulate daily login tracking
@@ -123,8 +123,8 @@
123123
echo "7. UPSERT with bonus points for frequent users...\n";
124124
$db->find()->table('user_stats')
125125
->onDuplicate([
126-
'login_count' => Db::raw('login_count + 1'),
127-
'total_points' => Db::raw('CASE WHEN login_count >= 3 THEN total_points + 50 ELSE total_points + 10 END')
126+
'login_count' => Db::inc(1),
127+
'total_points' => Db::raw('CASE WHEN user_stats.login_count >= 3 THEN user_stats.total_points + 50 ELSE user_stats.total_points + 10 END')
128128
])
129129
->insert([
130130
'user_id' => 1,

examples/04-json/01-json-basics.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616

1717
echo "=== JSON Basics Example (on $driver) ===\n\n";
1818

19-
// Create table
19+
// Create table
20+
$driver = getCurrentDriver($db);
2021
recreateTable($db, 'users', [
2122
'id' => 'INTEGER PRIMARY KEY AUTOINCREMENT',
2223
'name' => 'TEXT',
23-
'settings' => 'TEXT',
24-
'tags' => 'TEXT'
24+
'settings' => $driver === 'pgsql' ? 'JSONB' : 'TEXT',
25+
'tags' => $driver === 'pgsql' ? 'JSONB' : 'TEXT'
2526
]);
2627

2728
echo "✓ Table created\n\n";

examples/04-json/02-json-queries.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717
echo "=== JSON Queries Example (on $driver) ===\n\n";
1818

1919
// Setup
20+
$driver = getCurrentDriver($db);
2021
recreateTable($db, 'products', [
2122
'id' => 'INTEGER PRIMARY KEY AUTOINCREMENT',
2223
'name' => 'TEXT',
23-
'specs' => 'TEXT',
24-
'tags' => 'TEXT'
24+
'specs' => $driver === 'pgsql' ? 'JSONB' : 'TEXT',
25+
'tags' => $driver === 'pgsql' ? 'JSONB' : 'TEXT'
2526
]);
2627

2728
echo "1. Inserting products with nested JSON...\n";

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
->select([
129129
'first_name',
130130
'last_name',
131-
'full_name_upper' => Db::raw('UPPER(first_name || " " || last_name)'),
131+
'full_name_upper' => Db::raw(getCurrentDriver($db) === 'pgsql' ? "UPPER(first_name || ' ' || last_name)" : "UPPER(CONCAT(first_name, ' ', last_name))"),
132132
'email_lower' => Db::lower('email')
133133
])
134134
->limit(2)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@
8686
// Example 4: GREATEST - Maximum of multiple values
8787
echo "4. GREATEST - Maximum of multiple columns...\n";
8888
$db->rawQuery("ALTER TABLE measurements ADD COLUMN alt_reading INTEGER DEFAULT 0");
89-
$db->rawQuery("UPDATE measurements SET alt_reading = reading + 10 WHERE id <= 2");
90-
$db->rawQuery("UPDATE measurements SET alt_reading = reading - 5 WHERE id > 2");
89+
$db->find()->table('measurements')->where('id', 2, '<=')->update(['alt_reading' => Db::raw('reading + 10')]);
90+
$db->find()->table('measurements')->where('id', 2, '>')->update(['alt_reading' => Db::raw('reading - 5')]);
9191

9292
$results = $db->find()
9393
->from('measurements')

examples/05-helpers/04-null-helpers.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888

8989
// Example 5: NULLIF - Return NULL if equal
9090
echo "5. NULLIF - Convert empty strings to NULL...\n";
91-
$db->rawQuery("INSERT INTO users (name, email, phone) VALUES ('Eve', '', '555-0005')");
91+
$db->find()->table('users')->insert(['name' => 'Eve', 'email' => '', 'phone' => '555-0005']);
9292

9393
$users = $db->find()
9494
->from('users')
@@ -139,15 +139,16 @@
139139

140140
// Example 8: NULL statistics
141141
echo "8. NULL statistics report...\n";
142-
$stats = $db->rawQuery("
143-
SELECT
144-
SUM(CASE WHEN email IS NULL THEN 1 ELSE 0 END) as missing_email,
145-
SUM(CASE WHEN phone IS NULL THEN 1 ELSE 0 END) as missing_phone,
146-
SUM(CASE WHEN address IS NULL THEN 1 ELSE 0 END) as missing_address,
147-
SUM(CASE WHEN bio IS NULL THEN 1 ELSE 0 END) as missing_bio,
148-
COUNT(*) as total
149-
FROM users
150-
")[0];
142+
$stats = $db->find()
143+
->from('users')
144+
->select([
145+
'missing_email' => Db::sum(Db::case(['email IS NULL' => '1'], '0')),
146+
'missing_phone' => Db::sum(Db::case(['phone IS NULL' => '1'], '0')),
147+
'missing_address' => Db::sum(Db::case(['address IS NULL' => '1'], '0')),
148+
'missing_bio' => Db::sum(Db::case(['bio IS NULL' => '1'], '0')),
149+
'total' => Db::count()
150+
])
151+
->getOne();
151152

152153
echo " Data completeness:\n";
153154
echo " • Total users: {$stats['total']}\n";

0 commit comments

Comments
 (0)