Skip to content

Commit f43e6c7

Browse files
committed
fix nesting & add more features
1 parent 5c8f59d commit f43e6c7

File tree

4 files changed

+251
-46
lines changed

4 files changed

+251
-46
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ $collection->updateOne(
8686
| **Evaluation** | `$regex`, `$mod` |
8787
| **Array** | `$all`, `$size`, `$elemMatch` |
8888

89+
Projection supports dot-notation in both inclusion and exclusion (e.g. `['profile.stats.score' => 0]`).
90+
8991
### Aggregation Framework
9092
| Feature | Details |
9193
| --- | --- |
@@ -95,7 +97,7 @@ $collection->updateOne(
9597
### Update Operators
9698
| Category | Operators |
9799
| --- | --- |
98-
| **Field** | `$set`, `$setOnInsert`, `$inc`, `$mul`, `$unset`, `$rename`, `$min`, `$max`, `$currentDate` |
100+
| **Field** | `$set`, `$setOnInsert`, `$inc`, `$mul`, `$unset`, `$rename`, `$min`, `$max`, `$currentDate` (incl. dot-notation) |
99101
| **Array** | `$push` (inc. `$each`), `$pull`, `$addToSet`, `$pop` |
100102
| **Bitwise** | `$bit` (`and`, `or`, `xor`) |
101103

src/Query/ProjectionBuilder.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use function explode;
1010
use function implode;
1111
use function sprintf;
12+
use function str_contains;
13+
use function str_replace;
1214
use function str_starts_with;
1315
use function strlen;
1416
use function substr;
@@ -73,7 +75,11 @@ public function buildProjectionColumn(array $projection, string $column = 'data'
7375
if (!empty($exclude)) {
7476
$result = $column;
7577
foreach ($exclude as $field) {
76-
$result = sprintf('%s - %s', $result, $this->pdo->quote($field));
78+
if (str_contains($field, '.')) {
79+
$result = sprintf('(%s) #- %s::text[]', $result, $this->pathLiteral($field));
80+
} else {
81+
$result = sprintf('(%s) - %s::text', $result, $this->pdo->quote($field));
82+
}
7783
}
7884

7985
return $result;
@@ -126,4 +132,11 @@ private function buildNestedProjectionForTopKey(string $topKey, array $allInclus
126132

127133
return sprintf('jsonb_build_object(%s)', implode(', ', $fields));
128134
}
135+
136+
private function pathLiteral(string $field): string
137+
{
138+
$path = '{' . str_replace('.', ',', $field) . '}';
139+
140+
return $this->pdo->quote($path);
141+
}
129142
}

src/Query/UpdateBuilder.php

Lines changed: 126 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
use function is_array;
1212
use function json_encode;
1313
use function sprintf;
14+
use function str_contains;
1415
use function str_replace;
15-
use function str_starts_with;
1616

1717
final class UpdateBuilder
1818
{
19+
private const DATA_PLACEHOLDER = '__DATA__';
20+
1921
public function __construct(
2022
private readonly PDO $pdo,
2123
) {
@@ -46,64 +48,84 @@ public function buildUpdateExpression(array $update): array
4648
$updateParts = [];
4749

4850
if ($setData) {
49-
$updateParts[] = sprintf('data || %s', $this->pdo->quote(json_encode($setData)));
51+
foreach ($setData as $field => $value) {
52+
$baseExpression = $this->baseExpression($field, self::DATA_PLACEHOLDER);
53+
$updateParts[] = sprintf(
54+
'jsonb_set(%s, %s, %s::jsonb, true)',
55+
$baseExpression,
56+
$this->pathLiteral($field),
57+
$this->pdo->quote(json_encode($value)),
58+
);
59+
}
5060
}
5161

5262
if ($incData) {
5363
foreach ($incData as $field => $value) {
64+
$baseExpression = $this->baseExpression($field, self::DATA_PLACEHOLDER);
5465
$updateParts[] = sprintf(
55-
'jsonb_set(data, %s, (COALESCE(data->>%s, \'0\')::numeric + %s)::text::jsonb)',
56-
$this->pdo->quote('{' . $field . '}'),
57-
$this->pdo->quote($field),
66+
'jsonb_set(%s, %s, (COALESCE(%s, \'0\')::numeric + %s)::text::jsonb, true)',
67+
$baseExpression,
68+
$this->pathLiteral($field),
69+
$this->jsonbExtractText($field, $baseExpression),
5870
(float)$value,
5971
);
6072
}
6173
}
6274

6375
if ($mulData) {
6476
foreach ($mulData as $field => $value) {
77+
$baseExpression = $this->baseExpression($field, self::DATA_PLACEHOLDER);
6578
$updateParts[] = sprintf(
66-
'jsonb_set(data, %s, (COALESCE(data->>%s, \'0\')::numeric * %s)::text::jsonb)',
67-
$this->pdo->quote('{' . $field . '}'),
68-
$this->pdo->quote($field),
79+
'jsonb_set(%s, %s, (COALESCE(%s, \'0\')::numeric * %s)::text::jsonb, true)',
80+
$baseExpression,
81+
$this->pathLiteral($field),
82+
$this->jsonbExtractText($field, $baseExpression),
6983
(float)$value,
7084
);
7185
}
7286
}
7387
if ($unsetData) {
7488
foreach ($unsetData as $field => $value) {
75-
$updateParts[] = sprintf('data - %s', $this->pdo->quote($field));
89+
$updateParts[] = sprintf('%s #- %s', self::DATA_PLACEHOLDER, $this->pathLiteral($field));
7690
}
7791
}
7892

7993
if ($renameData) {
8094
foreach ($renameData as $oldField => $newField) {
95+
$baseExpression = $this->baseExpression(
96+
$newField,
97+
sprintf('%s #- %s', self::DATA_PLACEHOLDER, $this->pathLiteral($oldField)),
98+
);
8199
$updateParts[] = sprintf(
82-
'jsonb_set(data - %s, %s, data->%s)',
83-
$this->pdo->quote($oldField),
84-
$this->pdo->quote('{' . $newField . '}'),
85-
$this->pdo->quote($oldField),
100+
'jsonb_set(%s, %s, %s, true)',
101+
$baseExpression,
102+
$this->pathLiteral($newField),
103+
$this->jsonbExtract($oldField, self::DATA_PLACEHOLDER),
86104
);
87105
}
88106
}
89107

90108
if ($minData) {
91109
foreach ($minData as $field => $value) {
110+
$baseExpression = $this->baseExpression($field, self::DATA_PLACEHOLDER);
92111
$updateParts[] = sprintf(
93-
'jsonb_set(data, %s, LEAST(data->%s, %s::jsonb))',
94-
$this->pdo->quote('{' . $field . '}'),
95-
$this->pdo->quote($field),
112+
'jsonb_set(%s, %s, LEAST(%s, %s::jsonb), true)',
113+
$baseExpression,
114+
$this->pathLiteral($field),
115+
$this->jsonbExtract($field, $baseExpression),
96116
$this->pdo->quote(json_encode($value)),
97117
);
98118
}
99119
}
100120

101121
if ($maxData) {
102122
foreach ($maxData as $field => $value) {
123+
$baseExpression = $this->baseExpression($field, self::DATA_PLACEHOLDER);
103124
$updateParts[] = sprintf(
104-
'jsonb_set(data, %s, GREATEST(data->%s, %s::jsonb))',
105-
$this->pdo->quote('{' . $field . '}'),
106-
$this->pdo->quote($field),
125+
'jsonb_set(%s, %s, GREATEST(%s, %s::jsonb), true)',
126+
$baseExpression,
127+
$this->pathLiteral($field),
128+
$this->jsonbExtract($field, $baseExpression),
107129
$this->pdo->quote(json_encode($value)),
108130
);
109131
}
@@ -117,50 +139,59 @@ public function buildUpdateExpression(array $update): array
117139
$pushValue = json_encode([$value]);
118140
}
119141

142+
$baseExpression = $this->baseExpression($field, self::DATA_PLACEHOLDER);
120143
$updateParts[] = sprintf(
121-
'jsonb_set(data, %s, COALESCE(data->%s, \'[]\'::jsonb) || %s)',
122-
$this->pdo->quote('{' . $field . '}'),
123-
$this->pdo->quote($field),
144+
'jsonb_set(%s, %s, COALESCE(%s, \'[]\'::jsonb) || %s, true)',
145+
$baseExpression,
146+
$this->pathLiteral($field),
147+
$this->jsonbExtract($field, $baseExpression),
124148
$this->pdo->quote($pushValue),
125149
);
126150
}
127151
}
128152

129153
if ($pullData) {
130154
foreach ($pullData as $field => $value) {
155+
$baseExpression = $this->baseExpression($field, self::DATA_PLACEHOLDER);
131156
$updateParts[] = sprintf(
132-
'jsonb_set(data, %s, COALESCE((SELECT jsonb_agg(x) FROM jsonb_array_elements(data->%s) x WHERE x != %s), \'[]\'::jsonb))',
133-
$this->pdo->quote('{' . $field . '}'),
134-
$this->pdo->quote($field),
157+
'jsonb_set(%s, %s, COALESCE((SELECT jsonb_agg(x) FROM jsonb_array_elements(%s) x WHERE x != %s), \'[]\'::jsonb), true)',
158+
$baseExpression,
159+
$this->pathLiteral($field),
160+
$this->jsonbExtract($field, $baseExpression),
135161
$this->pdo->quote(json_encode($value)),
136162
);
137163
}
138164
}
139165

140166
if ($addToSetData) {
141167
foreach ($addToSetData as $field => $value) {
168+
$baseExpression = $this->baseExpression($field, self::DATA_PLACEHOLDER);
142169
$updateParts[] = sprintf(
143-
'jsonb_set(data, %1$s, COALESCE(data->%2$s, \'[]\'::jsonb) || (CASE WHEN (data->%2$s) @> %3$s THEN \'[]\'::jsonb ELSE %3$s::jsonb END))',
144-
$this->pdo->quote('{' . $field . '}'),
145-
$this->pdo->quote($field),
170+
'jsonb_set(%1$s, %2$s, COALESCE(%3$s, \'[]\'::jsonb) || (CASE WHEN (%3$s) @> %4$s THEN \'[]\'::jsonb ELSE %4$s::jsonb END), true)',
171+
$baseExpression,
172+
$this->pathLiteral($field),
173+
$this->jsonbExtract($field, $baseExpression),
146174
$this->pdo->quote(json_encode([$value])),
147175
);
148176
}
149177
}
150178

151179
if ($popData) {
152180
foreach ($popData as $field => $value) {
181+
$baseExpression = $this->baseExpression($field, self::DATA_PLACEHOLDER);
153182
if ($value === 1) {
154183
$updateParts[] = sprintf(
155-
'jsonb_set(data, %1$s, (data->%2$s) - (jsonb_array_length(data->%2$s) - 1))',
156-
$this->pdo->quote('{' . $field . '}'),
157-
$this->pdo->quote($field),
184+
'jsonb_set(%1$s, %2$s, (%3$s) - (jsonb_array_length(%3$s) - 1), true)',
185+
$baseExpression,
186+
$this->pathLiteral($field),
187+
$this->jsonbExtract($field, $baseExpression),
158188
);
159189
} elseif ($value === -1) {
160190
$updateParts[] = sprintf(
161-
'jsonb_set(data, %1$s, (data->%2$s) - 0)',
162-
$this->pdo->quote('{' . $field . '}'),
163-
$this->pdo->quote($field),
191+
'jsonb_set(%1$s, %2$s, (%3$s) - 0, true)',
192+
$baseExpression,
193+
$this->pathLiteral($field),
194+
$this->jsonbExtract($field, $baseExpression),
164195
);
165196
}
166197
}
@@ -176,10 +207,12 @@ public function buildUpdateExpression(array $update): array
176207
default => throw new RuntimeException(sprintf('Bitwise operator "%s" is not supported', $op)),
177208
};
178209

210+
$baseExpression = $this->baseExpression($field, self::DATA_PLACEHOLDER);
179211
$updateParts[] = sprintf(
180-
'jsonb_set(data, %s, (COALESCE(data->>%s, \'0\')::bigint %s %d)::text::jsonb)',
181-
$this->pdo->quote('{' . $field . '}'),
182-
$this->pdo->quote($field),
212+
'jsonb_set(%s, %s, (COALESCE(%s, \'0\')::bigint %s %d)::text::jsonb, true)',
213+
$baseExpression,
214+
$this->pathLiteral($field),
215+
$this->jsonbExtractText($field, $baseExpression),
183216
$sqlOp,
184217
(int)$value,
185218
);
@@ -189,9 +222,11 @@ public function buildUpdateExpression(array $update): array
189222

190223
if ($currentDateData) {
191224
foreach ($currentDateData as $field => $value) {
225+
$baseExpression = $this->baseExpression($field, self::DATA_PLACEHOLDER);
192226
$updateParts[] = sprintf(
193-
'jsonb_set(data, %s, to_jsonb(CURRENT_TIMESTAMP))',
194-
$this->pdo->quote('{' . $field . '}'),
227+
'jsonb_set(%s, %s, to_jsonb(CURRENT_TIMESTAMP), true)',
228+
$baseExpression,
229+
$this->pathLiteral($field),
195230
);
196231
}
197232
}
@@ -202,11 +237,7 @@ public function buildUpdateExpression(array $update): array
202237

203238
$dataExpression = 'data';
204239
foreach ($updateParts as $part) {
205-
if (str_starts_with($part, 'data ')) {
206-
$dataExpression = str_replace('data ', $dataExpression . ' ', $part);
207-
} else {
208-
$dataExpression = str_replace('data,', $dataExpression . ',', $part);
209-
}
240+
$dataExpression = str_replace(self::DATA_PLACEHOLDER, $dataExpression, $part);
210241
}
211242

212243
return [
@@ -227,4 +258,55 @@ public function buildUpsertDocument(array $filter, array|null $setData, array|nu
227258
{
228259
return array_merge($filter, $setData ?? [], $setOnInsertData ?? []);
229260
}
261+
262+
private function pathLiteral(string $field): string
263+
{
264+
$path = '{' . str_replace('.', ',', $field) . '}';
265+
266+
return $this->pdo->quote($path) . '::text[]';
267+
}
268+
269+
private function jsonbExtract(string $field, string $baseExpression): string
270+
{
271+
if (str_contains($field, '.')) {
272+
return sprintf('(%s)#>%s', $baseExpression, $this->pathLiteral($field));
273+
}
274+
275+
return sprintf('(%s)->%s', $baseExpression, $this->pdo->quote($field));
276+
}
277+
278+
private function jsonbExtractText(string $field, string $baseExpression): string
279+
{
280+
if (str_contains($field, '.')) {
281+
return sprintf('(%s)#>>%s', $baseExpression, $this->pathLiteral($field));
282+
}
283+
284+
return sprintf('(%s)->>%s', $baseExpression, $this->pdo->quote($field));
285+
}
286+
287+
private function baseExpression(string $field, string $baseExpression): string
288+
{
289+
if (!str_contains($field, '.')) {
290+
return $baseExpression;
291+
}
292+
293+
$parts = explode('.', $field);
294+
array_pop($parts);
295+
296+
$expression = $baseExpression;
297+
$path = [];
298+
foreach ($parts as $part) {
299+
$path[] = $part;
300+
$pathLiteral = $this->pathLiteral(implode('.', $path));
301+
$expression = sprintf(
302+
'jsonb_set(%s, %s, COALESCE((%s)#>%s, \'{}\'::jsonb), true)',
303+
$expression,
304+
$pathLiteral,
305+
$expression,
306+
$pathLiteral,
307+
);
308+
}
309+
310+
return $expression;
311+
}
230312
}

0 commit comments

Comments
 (0)