Skip to content

Commit 3c76a8c

Browse files
committed
feat: add JSON modification helpers (jsonSet, jsonRemove, jsonReplace)
- Add Db::jsonSet(), Db::jsonRemove(), Db::jsonReplace() helper methods - Add JsonSetValue, JsonRemoveValue, JsonReplaceValue value classes - Fix formatJsonSet for MariaDB: use parent object creation approach for nested paths without CAST - Fix formatJsonReplace for MariaDB: remove CAST, pass JSON string directly - Improve formatJsonExists for MariaDB: add JSON_TYPE check for more reliable path existence detection - Add shared tests for JSON modification operations - Update documentation and examples for JSON modification - All tests pass on all dialects
1 parent 6e692ad commit 3c76a8c

File tree

17 files changed

+975
-42
lines changed

17 files changed

+975
-42
lines changed

README.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1633,20 +1633,28 @@ $users = $db->find()
16331633
```php
16341634
use tommyknocker\pdodb\helpers\Db;
16351635

1636-
// Update JSON field using QueryBuilder method
1636+
// Update JSON field using Db::jsonSet() helper (creates path if missing)
16371637
$db->find()
16381638
->table('users')
16391639
->where('id', 1)
16401640
->update([
1641-
'meta' => $db->find()->jsonSet('meta', ['city'], 'London')
1641+
'meta' => Db::jsonSet('meta', ['city'], 'London')
16421642
]);
16431643

16441644
// Remove JSON field
16451645
$db->find()
16461646
->table('users')
16471647
->where('id', 1)
16481648
->update([
1649-
'meta' => $db->find()->jsonRemove('meta', ['old_field'])
1649+
'meta' => Db::jsonRemove('meta', ['old_field'])
1650+
]);
1651+
1652+
// Replace JSON value (only if path exists)
1653+
$db->find()
1654+
->table('users')
1655+
->where('id', 1)
1656+
->update([
1657+
'meta' => Db::jsonReplace('meta', ['status'], 'inactive')
16501658
]);
16511659
```
16521660

@@ -3290,6 +3298,11 @@ Db::jsonExtract('meta', ['city']) // Alias for jsonGet
32903298
Db::jsonLength('tags') // Array/object length
32913299
Db::jsonKeys('meta') // Object keys
32923300
Db::jsonType('tags') // Value type
3301+
3302+
// Modify JSON (for UPDATE operations)
3303+
Db::jsonSet('meta', '$.status', 'active') // Set JSON value (creates path if missing)
3304+
Db::jsonRemove('meta', '$.old_field') // Remove JSON path
3305+
Db::jsonReplace('meta', '$.status', 'inactive') // Replace JSON value (only if path exists)
32933306
```
32943307

32953308
### Export Helpers
@@ -3493,8 +3506,8 @@ $constraints = $db->constraints('users');
34933506
| `whereJsonPath(col, path, operator, value, cond)` | Add JSON path condition |
34943507
| `whereJsonContains(col, value, path, cond)` | Add JSON contains condition |
34953508
| `whereJsonExists(col, path, cond)` | Add JSON path existence condition |
3496-
| `jsonSet(col, path, value)` | Set JSON value |
3497-
| `jsonRemove(col, path)` | Remove JSON path |
3509+
| `jsonSet(col, path, value)` | Set JSON value (QueryBuilder method) |
3510+
| `jsonRemove(col, path)` | Remove JSON path (QueryBuilder method) |
34983511
| `orderByJson(col, path, direction)` | Order by JSON path |
34993512

35003513
#### Fetch Modes

documentation/04-json-operations/json-modification.md

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ $db->find()
2727
->table('users')
2828
->where('id', 1)
2929
->update([
30-
'meta' => $db->find()->jsonSet('meta', ['profile', 'address', 'city'], 'Boston')
30+
'meta' => Db::jsonSet('meta', ['profile', 'address', 'city'], 'Boston')
3131
]);
3232
```
3333

3434
## Removing JSON Paths
3535

36-
### jsonRemove()
36+
### Db::jsonRemove()
3737

3838
Remove a path from JSON:
3939

@@ -43,7 +43,7 @@ $db->find()
4343
->table('users')
4444
->where('id', 1)
4545
->update([
46-
'meta' => $db->find()->jsonRemove('meta', ['old_field'])
46+
'meta' => Db::jsonRemove('meta', ['old_field'])
4747
]);
4848
```
4949

@@ -55,10 +55,12 @@ $db->find()
5555
->table('users')
5656
->where('id', 1)
5757
->update([
58-
'tags' => $db->find()->jsonRemove('tags', [1]) // Remove index 1
58+
'tags' => Db::jsonRemove('tags', [1]) // Remove index 1
5959
]);
6060
```
6161

62+
**Note:** In SQLite, removing an array element sets it to `null` to preserve array indices.
63+
6264
## Adding to JSON Arrays
6365

6466
### Append to Array
@@ -107,6 +109,54 @@ $db->find()
107109
]);
108110
```
109111

112+
## Replacing JSON Values
113+
114+
### Db::jsonReplace()
115+
116+
Replace a JSON value at a path (only if path exists, does not create missing paths):
117+
118+
```php
119+
// Replace existing value
120+
$db->find()
121+
->table('users')
122+
->where('id', 1)
123+
->update([
124+
'meta' => Db::jsonReplace('meta', '$.status', 'inactive')
125+
]);
126+
127+
// Try to replace non-existent path (won't create it)
128+
$db->find()
129+
->table('users')
130+
->where('id', 1)
131+
->update([
132+
'meta' => Db::jsonReplace('meta', '$.nonexistent', 'value')
133+
]);
134+
// Path won't be created if it doesn't exist
135+
```
136+
137+
### jsonSet vs jsonReplace
138+
139+
- **`Db::jsonSet()`**: Creates path if missing, always sets the value
140+
- **`Db::jsonReplace()`**: Only replaces if path exists, does not create missing paths
141+
142+
```php
143+
// jsonSet creates path if missing
144+
$db->find()
145+
->table('users')
146+
->where('id', 1)
147+
->update([
148+
'meta' => Db::jsonSet('meta', '$.new_field', 'value') // Creates path
149+
]);
150+
151+
// jsonReplace only replaces if path exists
152+
$db->find()
153+
->table('users')
154+
->where('id', 1)
155+
->update([
156+
'meta' => Db::jsonReplace('meta', '$.another_field', 'value') // Won't create path
157+
]);
158+
```
159+
110160
## Common Patterns
111161

112162
### Update User Preferences
@@ -116,7 +166,7 @@ $db->find()
116166
->table('users')
117167
->where('id', $userId)
118168
->update([
119-
'preferences' => $db->find()->jsonSet('preferences', ['theme'], 'dark'),
169+
'preferences' => Db::jsonSet('preferences', ['theme'], 'dark'),
120170
'updated_at' => Db::now()
121171
]);
122172
```
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
/**
3+
* Example: JSON Modification Helpers
4+
*
5+
* Demonstrates the use of Db::jsonSet(), Db::jsonRemove(), and Db::jsonReplace()
6+
* for modifying JSON data in database columns.
7+
*/
8+
9+
require_once __DIR__ . '/../../vendor/autoload.php';
10+
require_once __DIR__ . '/../helpers.php';
11+
12+
use tommyknocker\pdodb\helpers\Db;
13+
14+
$db = createExampleDb();
15+
$driver = getCurrentDriver($db);
16+
17+
echo "=== JSON Modification Helpers Example (on $driver) ===\n\n";
18+
19+
// Setup
20+
recreateTable($db, 'users', [
21+
'id' => 'INTEGER PRIMARY KEY AUTOINCREMENT',
22+
'name' => 'TEXT',
23+
'meta' => $driver === 'pgsql' ? 'JSONB' : 'TEXT',
24+
'preferences' => $driver === 'pgsql' ? 'JSONB' : 'TEXT'
25+
]);
26+
27+
echo "1. Inserting users with JSON metadata...\n";
28+
$db->find()->table('users')->insertMulti([
29+
[
30+
'name' => 'John Doe',
31+
'meta' => Db::jsonObject([
32+
'status' => 'active',
33+
'score' => 10,
34+
'tags' => ['user', 'premium']
35+
]),
36+
'preferences' => Db::jsonObject([
37+
'theme' => 'light',
38+
'notifications' => true
39+
])
40+
],
41+
[
42+
'name' => 'Jane Smith',
43+
'meta' => Db::jsonObject([
44+
'status' => 'inactive',
45+
'score' => 5,
46+
'tags' => ['user']
47+
]),
48+
'preferences' => Db::jsonObject([
49+
'theme' => 'dark',
50+
'notifications' => false
51+
])
52+
]
53+
]);
54+
55+
echo "2. Using Db::jsonSet() to update JSON values...\n";
56+
// Update status using jsonSet (creates path if missing)
57+
$db->find()
58+
->table('users')
59+
->where('name', 'John Doe')
60+
->update(['meta' => Db::jsonSet('meta', ['status'], 'premium')]);
61+
62+
// Create nested path
63+
$db->find()
64+
->table('users')
65+
->where('name', 'John Doe')
66+
->update(['meta' => Db::jsonSet('meta', ['profile', 'city'], 'New York')]);
67+
68+
// Update array element
69+
$db->find()
70+
->table('users')
71+
->where('name', 'John Doe')
72+
->update(['meta' => Db::jsonSet('meta', ['tags', 0], 'vip')]);
73+
74+
$result = $db->find()
75+
->table('users')
76+
->selectJson('meta', ['status'], 'status')
77+
->selectJson('meta', ['profile', 'city'], 'city')
78+
->selectJson('meta', ['tags', 0], 'first_tag')
79+
->where('name', 'John Doe')
80+
->getOne();
81+
82+
echo " Status: {$result['status']}\n";
83+
echo " City: {$result['city']}\n";
84+
echo " First tag: {$result['first_tag']}\n\n";
85+
86+
echo "3. Using Db::jsonRemove() to remove JSON paths...\n";
87+
// Remove a field
88+
$db->find()
89+
->table('users')
90+
->where('name', 'Jane Smith')
91+
->update(['meta' => Db::jsonRemove('meta', ['score'])]);
92+
93+
// Remove nested path
94+
$db->find()
95+
->table('users')
96+
->where('name', 'John Doe')
97+
->update(['meta' => Db::jsonRemove('meta', ['profile', 'city'])]);
98+
99+
$result = $db->find()
100+
->table('users')
101+
->selectJson('meta', ['score'], 'score')
102+
->selectJson('meta', ['profile', 'city'], 'city')
103+
->where('name', 'Jane Smith')
104+
->getOne();
105+
106+
echo " Score after removal: " . ($result['score'] ?? 'null') . "\n";
107+
echo " City after removal: " . ($result['city'] ?? 'null') . "\n\n";
108+
109+
echo "4. Using Db::jsonReplace() to replace existing values only...\n";
110+
// Replace existing value
111+
$db->find()
112+
->table('users')
113+
->where('name', 'John Doe')
114+
->update(['preferences' => Db::jsonReplace('preferences', ['theme'], 'dark')]);
115+
116+
// Try to replace non-existent path (won't create it)
117+
$db->find()
118+
->table('users')
119+
->where('name', 'Jane Smith')
120+
->update(['preferences' => Db::jsonReplace('preferences', ['language'], 'en')]);
121+
122+
$result1 = $db->find()
123+
->table('users')
124+
->selectJson('preferences', ['theme'], 'theme')
125+
->where('name', 'John Doe')
126+
->getOne();
127+
128+
$result2 = $db->find()
129+
->table('users')
130+
->selectJson('preferences', ['language'], 'language')
131+
->where('name', 'Jane Smith')
132+
->getOne();
133+
134+
echo " Theme replaced: {$result1['theme']}\n";
135+
echo " Language (non-existent): " . ($result2['language'] ?? 'null (not created)') . "\n\n";
136+
137+
echo "5. Comparison: jsonSet vs jsonReplace...\n";
138+
// jsonSet creates path if missing
139+
$db->find()
140+
->table('users')
141+
->where('name', 'Jane Smith')
142+
->update(['preferences' => Db::jsonSet('preferences', ['language'], 'en')]);
143+
144+
$result = $db->find()
145+
->table('users')
146+
->selectJson('preferences', ['language'], 'language')
147+
->where('name', 'Jane Smith')
148+
->getOne();
149+
150+
echo " Language after jsonSet: {$result['language']} (path created)\n\n";
151+
152+
echo "6. Complex JSON operations...\n";
153+
// Set complex nested structure
154+
$complexData = [
155+
'settings' => [
156+
'privacy' => ['public' => false, 'show_email' => false],
157+
'display' => ['compact' => true]
158+
]
159+
];
160+
161+
$db->find()
162+
->table('users')
163+
->where('name', 'John Doe')
164+
->update(['preferences' => Db::jsonSet('preferences', ['settings'], $complexData)]);
165+
166+
$result = $db->find()
167+
->table('users')
168+
->selectJson('preferences', ['settings'], 'settings')
169+
->where('name', 'John Doe')
170+
->getOne();
171+
172+
$settings = json_decode($result['settings'], true);
173+
echo " Settings: " . json_encode($settings, JSON_PRETTY_PRINT) . "\n\n";
174+
175+
echo "=== Example completed successfully! ===\n";
176+

examples/04-json/README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ Querying and filtering JSON data.
3636
- Nested JSON navigation
3737
- WHERE conditions with JSON
3838

39+
### 03-json-modification.php
40+
Modifying JSON data using helper methods.
41+
42+
**Topics covered:**
43+
- Setting JSON values with `Db::jsonSet()`
44+
- Removing JSON paths with `Db::jsonRemove()`
45+
- Replacing JSON values with `Db::jsonReplace()`
46+
- Creating nested paths
47+
- Removing array elements
48+
- Comparison between `jsonSet` and `jsonReplace`
49+
- Complex nested JSON operations
50+
3951
## JSON Helper Functions
4052

4153
PDOdb provides these JSON helpers (from `Db` class):
@@ -55,9 +67,9 @@ PDOdb provides these JSON helpers (from `Db` class):
5567
- `jsonKeys(column, path?)` - Get object keys
5668

5769
### Modification
58-
- `jsonSet(column, path, value)` - Set JSON value
59-
- `jsonInsert(column, path, value)` - Insert JSON value
60-
- `jsonRemove(column, path)` - Remove JSON path
70+
- `Db::jsonSet(column, path, value)` - Set JSON value (creates path if missing)
71+
- `Db::jsonRemove(column, path)` - Remove JSON path
72+
- `Db::jsonReplace(column, path, value)` - Replace JSON value (only if path exists)
6173

6274
## Running Examples
6375

src/dialects/DialectInterface.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,17 @@ public function formatJsonSet(string $col, array|string $path, mixed $value): ar
248248
*/
249249
public function formatJsonRemove(string $col, array|string $path): string;
250250

251+
/**
252+
* Format JSON_REPLACE expression (only replaces if path exists).
253+
*
254+
* @param string $col
255+
* @param array<int, string|int>|string $path
256+
* @param mixed $value
257+
*
258+
* @return array<int|string, mixed> [sql, [param => value]]
259+
*/
260+
public function formatJsonReplace(string $col, array|string $path, mixed $value): array;
261+
251262
/**
252263
* Format JSON_EXISTS expression.
253264
*

0 commit comments

Comments
 (0)