1111use function is_array ;
1212use function json_encode ;
1313use function sprintf ;
14+ use function str_contains ;
1415use function str_replace ;
15- use function str_starts_with ;
1616
1717final 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