1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Helix\DB; |
4
|
|
|
|
5
|
|
|
use Countable; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Static helper for building SQL. |
9
|
|
|
* |
10
|
|
|
* The methods here are driver agnostic, and do not quote values. |
11
|
|
|
*/ |
12
|
|
|
class SQL { |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* `(... AND ...)` |
16
|
|
|
* |
17
|
|
|
* @param string[] $conditions |
18
|
|
|
* @return string |
19
|
|
|
*/ |
20
|
|
|
public static function all (array $conditions): string { |
21
|
|
|
if (count($conditions) === 1) { |
22
|
|
|
return reset($conditions); |
23
|
|
|
} |
24
|
|
|
return '(' . implode(' AND ', $conditions) . ')'; |
25
|
|
|
} |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* `(... OR ...)` |
29
|
|
|
* |
30
|
|
|
* @param string[] $conditions |
31
|
|
|
* @return string |
32
|
|
|
*/ |
33
|
|
|
public static function any (array $conditions): string { |
34
|
|
|
if (count($conditions) === 1) { |
35
|
|
|
return reset($conditions); |
36
|
|
|
} |
37
|
|
|
return '(' . implode(' OR ', $conditions) . ')'; |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* `AVG($column)` |
42
|
|
|
* |
43
|
|
|
* @param string $column |
44
|
|
|
* @return ExpressionInterface |
45
|
|
|
*/ |
46
|
|
|
public static function avg (string $column): ExpressionInterface { |
47
|
|
|
return new Expression("AVG({$column})"); |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Used to generate a comparison. |
52
|
|
|
* |
53
|
|
|
* `"$a $operator $b"` |
54
|
|
|
* |
55
|
|
|
* If `$a` is an array, the keys and values are used for `$a` and `$b`. |
56
|
|
|
* |
57
|
|
|
* `["$k $operator $v", ...]` |
58
|
|
|
* |
59
|
|
|
* If `$b` is an array, all items are listed within parenthesis. |
60
|
|
|
* |
61
|
|
|
* `"$a $listOperator (...$b)"` |
62
|
|
|
* |
63
|
|
|
* As well as if `$a` is a two-dimensional array. |
64
|
|
|
* |
65
|
|
|
* `["$k $listOperator (...$v)", ...]` |
66
|
|
|
* |
67
|
|
|
* If `$b` is a `{@link Select}, it's used as a subquery. |
68
|
|
|
* |
69
|
|
|
* `"$a $operator $subOperator $b->toSql()"` |
70
|
|
|
* |
71
|
|
|
* As well as if `$a` is an array of {@link Select}. |
72
|
|
|
* |
73
|
|
|
* `["$k $operator $subOperator $v->toSql()", ...]` |
74
|
|
|
* |
75
|
|
|
* @param mixed $a |
76
|
|
|
* @param string $operator |
77
|
|
|
* @param mixed $b |
78
|
|
|
* @param string $subOperator |
79
|
|
|
* @param string $listOperator |
80
|
|
|
* @return string|array |
81
|
|
|
*/ |
82
|
|
|
public static function compare ($a, $operator, $b = null, $subOperator = null, $listOperator = null) { |
83
|
|
|
if (is_array($a)) { |
84
|
|
|
$cmp = []; |
85
|
|
|
foreach ($a as $k => $v) { |
86
|
|
|
$cmp[$k] = static::compare($k, $operator, $v, $subOperator, $listOperator); |
87
|
|
|
} |
88
|
|
|
return $cmp; |
89
|
|
|
} |
90
|
|
|
elseif (is_array($b)) { |
91
|
|
|
return "{$a} {$listOperator} (" . implode(',', $b) . ")"; |
92
|
|
|
} |
93
|
|
|
elseif ($b instanceof Select) { |
94
|
|
|
return "{$a} {$operator} {$subOperator} ({$b->toSql()})"; |
95
|
|
|
} |
96
|
|
|
return "{$a} {$operator} {$b}"; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* `COUNT($column)` |
101
|
|
|
* |
102
|
|
|
* @param string $column |
103
|
|
|
* @return ExpressionInterface |
104
|
|
|
*/ |
105
|
|
|
public static function count (string $column): ExpressionInterface { |
106
|
|
|
return new Expression("COUNT({$column})"); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Returns a `GROUP BY` clause listing all given columns, |
111
|
|
|
* or an empty string if there are no columns. |
112
|
|
|
* |
113
|
|
|
* @param array $columns |
114
|
|
|
* @return string |
115
|
|
|
*/ |
116
|
|
|
public static function group (array $columns): string { |
117
|
|
|
if ($columns) { |
|
|
|
|
118
|
|
|
return ' GROUP BY ' . implode(',', $columns); |
119
|
|
|
} |
120
|
|
|
return ''; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Returns a `HAVING` clause for all conditions, |
125
|
|
|
* or an empty string if there are no conditions. |
126
|
|
|
* |
127
|
|
|
* @param array $conditions |
128
|
|
|
* @return string |
129
|
|
|
*/ |
130
|
|
|
public static function having (array $conditions): string { |
131
|
|
|
if ($conditions) { |
|
|
|
|
132
|
|
|
return ' HAVING ' . static::all($conditions); |
133
|
|
|
} |
134
|
|
|
return ''; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* `$a = $b`, or `$a IN (...$b)`, or `$a = ANY ($b)` |
139
|
|
|
* |
140
|
|
|
* @param string|array $a |
141
|
|
|
* @param string|array|Select $b |
142
|
|
|
* @return string|array |
143
|
|
|
*/ |
144
|
|
|
public static function isEqual ($a, $b = null) { |
145
|
|
|
return static::compare($a, '=', $b, 'ANY', 'IN'); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* `$a > $b`, or `$a > ALL ($b)` |
150
|
|
|
* |
151
|
|
|
* @param string|array $a |
152
|
|
|
* @param string|Select $b |
153
|
|
|
* @return string|array |
154
|
|
|
*/ |
155
|
|
|
public static function isGreater ($a, $b = null) { |
156
|
|
|
return static::compare($a, '>', $b, 'ALL'); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* `$a >= $b`, or `$a >= ALL ($b)` |
161
|
|
|
* |
162
|
|
|
* @param string|array $a |
163
|
|
|
* @param string|Select $b |
164
|
|
|
* @return string|array |
165
|
|
|
*/ |
166
|
|
|
public static function isGreaterOrEqual ($a, $b = null) { |
167
|
|
|
return static::compare($a, '>=', $b, 'ALL'); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* `$a < $b`, or `$a < ALL ($b)` |
172
|
|
|
* |
173
|
|
|
* @param string|array $a |
174
|
|
|
* @param string|Select $b |
175
|
|
|
* @return string|array |
176
|
|
|
*/ |
177
|
|
|
public static function isLess ($a, $b = null) { |
178
|
|
|
return static::compare($a, '<', $b, 'ALL'); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* `$a <= $b`, or `$a <= ALL ($b)` |
183
|
|
|
* |
184
|
|
|
* @param string|array $a |
185
|
|
|
* @param string|Select $b |
186
|
|
|
* @return string|array |
187
|
|
|
*/ |
188
|
|
|
public static function isLessOrEqual ($a, $b = null) { |
189
|
|
|
return static::compare($a, '<=', $b, 'ALL'); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* `$x LIKE $pattern` |
194
|
|
|
* |
195
|
|
|
* @param string|array $x |
196
|
|
|
* @param string $pattern |
197
|
|
|
* @return string|array |
198
|
|
|
*/ |
199
|
|
|
public static function isLike ($x, string $pattern = null) { |
200
|
|
|
return static::compare($x, 'LIKE', $pattern); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* `$a IS NOT UNKNOWN|TRUE|FALSE` |
205
|
|
|
* |
206
|
|
|
* @param string|array $x |
207
|
|
|
* @param null|bool $identity |
208
|
|
|
* @return string|array |
209
|
|
|
*/ |
210
|
|
|
public static function isNot ($x, $identity) { |
211
|
|
|
return static::compare($x, 'IS NOT', ['' => 'UNKNOWN', 1 => 'TRUE', 0 => 'FALSE'][$identity]); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* `$a <> $b`, or `$a NOT IN (...$b)`, or `$a <> ALL ($b)` |
216
|
|
|
* |
217
|
|
|
* @param string|array $a |
218
|
|
|
* @param string|array|Select $b |
219
|
|
|
* @return string|array |
220
|
|
|
*/ |
221
|
|
|
public static function isNotEqual ($a, $b = null) { |
222
|
|
|
return static::compare($a, '<>', $b, 'ALL', 'NOT IN'); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* `$a NOT LIKE $pattern` |
227
|
|
|
* |
228
|
|
|
* @param string|array $x |
229
|
|
|
* @param string $pattern |
230
|
|
|
* @return string|array |
231
|
|
|
*/ |
232
|
|
|
public static function isNotLike ($x, string $pattern = null) { |
233
|
|
|
return static::compare($x, 'NOT LIKE', $pattern); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* `$x IS NOT NULL` |
238
|
|
|
* |
239
|
|
|
* @param string|array $x |
240
|
|
|
* @return string|array |
241
|
|
|
*/ |
242
|
|
|
public static function isNotNull ($x) { |
243
|
|
|
if (is_array($x)) { |
244
|
|
|
return array_map(__METHOD__, $x); |
245
|
|
|
} |
246
|
|
|
return "{$x} IS NOT NULL"; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* `$x NOT REGEXP $pattern` |
251
|
|
|
* |
252
|
|
|
* @param string|array $x |
253
|
|
|
* @param string $pattern |
254
|
|
|
* @return string|array |
255
|
|
|
*/ |
256
|
|
|
public static function isNotRegExp ($x, string $pattern = null) { |
257
|
|
|
return static::compare($x, 'NOT REGEXP', $pattern); |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* `$x REGEXP $pattern` |
262
|
|
|
* |
263
|
|
|
* @param string|array $x |
264
|
|
|
* @param string $pattern |
265
|
|
|
* @return string|array |
266
|
|
|
*/ |
267
|
|
|
public static function isRegExp ($x, string $pattern = null) { |
268
|
|
|
return static::compare($x, 'REGEXP', $pattern); |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* Returns a `LIMIT ... [OFFSET ...]` clause, |
273
|
|
|
* or an empty string if the limit is zero. |
274
|
|
|
* |
275
|
|
|
* @param int $limit |
276
|
|
|
* @param int $offset |
277
|
|
|
* @return string |
278
|
|
|
*/ |
279
|
|
|
public static function limit (int $limit, int $offset = 0): string { |
280
|
|
|
if ($limit) { |
281
|
|
|
$limit = " LIMIT {$limit}"; |
282
|
|
|
if ($offset) { |
283
|
|
|
$limit .= " OFFSET {$offset}"; |
284
|
|
|
} |
285
|
|
|
return $limit; |
286
|
|
|
} |
287
|
|
|
return ''; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* Returns an array of `?` placeholders. |
292
|
|
|
* |
293
|
|
|
* @param int|array|Countable $count |
294
|
|
|
* @return string[] |
295
|
|
|
*/ |
296
|
|
|
public static function marks ($count): array { |
297
|
|
|
if (is_array($count) or $count instanceof Countable) { |
298
|
|
|
$count = count($count); |
299
|
|
|
} |
300
|
|
|
return array_fill(0, $count, '?'); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* `MAX($column)` |
305
|
|
|
* |
306
|
|
|
* @param string $column |
307
|
|
|
* @return ExpressionInterface |
308
|
|
|
*/ |
309
|
|
|
public static function max (string $column): ExpressionInterface { |
310
|
|
|
return new Expression("MAX({$column}"); |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* `MIN($column)` |
315
|
|
|
* |
316
|
|
|
* @param string $column |
317
|
|
|
* @return ExpressionInterface |
318
|
|
|
*/ |
319
|
|
|
public static function min (string $column): ExpressionInterface { |
320
|
|
|
return new Expression("MIN({$column})"); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* `NOT($x)` |
325
|
|
|
* |
326
|
|
|
* @param string|array $x |
327
|
|
|
* @return string|array |
328
|
|
|
*/ |
329
|
|
|
public static function not ($x) { |
330
|
|
|
if (is_array($x)) { |
331
|
|
|
return array_map(__METHOD__, $x); |
332
|
|
|
} |
333
|
|
|
return "NOT({$x})"; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* Returns an `ORDER BY` clause using the given order, |
338
|
|
|
* or an empty string if the order is empty. |
339
|
|
|
* |
340
|
|
|
* @param string $order |
341
|
|
|
* @return string |
342
|
|
|
*/ |
343
|
|
|
public static function order (string $order): string { |
344
|
|
|
if (strlen($order)) { |
345
|
|
|
return ' ORDER BY ' . $order; |
346
|
|
|
} |
347
|
|
|
return ''; |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* Returns a `SELECT` query string. |
352
|
|
|
* |
353
|
|
|
* @param string $table |
354
|
|
|
* @param array $columns String keys are used for aliasing. |
355
|
|
|
* @return string |
356
|
|
|
*/ |
357
|
|
|
public static function select (string $table, array $columns): string { |
358
|
|
|
$names = []; |
359
|
|
|
foreach ($columns as $alias => $name) { |
360
|
|
|
if (is_string($alias) and $alias !== $name) { |
361
|
|
|
$names[] = $name . ' AS ' . $alias; |
362
|
|
|
} |
363
|
|
|
else { |
364
|
|
|
$names[] = $name; |
365
|
|
|
} |
366
|
|
|
} |
367
|
|
|
return 'SELECT ' . implode(', ', $names) . ' FROM ' . $table; |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* Converts an array of columns to `:named` placeholders for prepared queries. |
372
|
|
|
* |
373
|
|
|
* Qualified columns are slotted as `qualifier__column` (two underscores). |
374
|
|
|
* |
375
|
|
|
* @param string[] $columns |
376
|
|
|
* @return string[] `[column => :column]` |
377
|
|
|
*/ |
378
|
|
|
public static function slots (array $columns): array { |
379
|
|
|
$slots = []; |
380
|
|
|
foreach ($columns as $column) { |
381
|
|
|
$slots[(string)$column] = ':' . str_replace('.', '__', $column); |
382
|
|
|
} |
383
|
|
|
return $slots; |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
/** |
387
|
|
|
* Returns a `WHERE` clause for all conditions, |
388
|
|
|
* or an empty string if there are no conditions. |
389
|
|
|
* |
390
|
|
|
* @param array $conditions |
391
|
|
|
* @return string |
392
|
|
|
*/ |
393
|
|
|
public static function where (array $conditions): string { |
394
|
|
|
if ($conditions) { |
|
|
|
|
395
|
|
|
return ' WHERE ' . static::all($conditions); |
396
|
|
|
} |
397
|
|
|
return ''; |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
final private function __construct () { } |
401
|
|
|
} |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.