Passed
Pull Request — master (#520)
by
unknown
02:50
created

Context::isOperator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser;
6
7
use function class_exists;
8
use function explode;
9
use function in_array;
10
use function intval;
11
use function is_int;
12
use function is_numeric;
13
use function preg_match;
14
use function str_replace;
15
use function str_starts_with;
16
use function strlen;
17
use function strtoupper;
18
use function substr;
19
20
/**
21
 * Defines a context class that is later extended to define other contexts.
22
 *
23
 * A context is a collection of keywords, operators and functions used for parsing.
24
 *
25
 * Holds the configuration of the context that is currently used.
26
 */
27
abstract class Context
28
{
29
    /**
30
     * The maximum length of a keyword.
31
     */
32
    public const KEYWORD_MAX_LENGTH = 30;
33
34
    /**
35
     * The maximum length of a label.
36
     *
37
     * Ref: https://dev.mysql.com/doc/refman/5.7/en/statement-labels.html
38
     */
39
    public const LABEL_MAX_LENGTH = 16;
40
41
    /**
42
     * The maximum length of an operator.
43
     */
44
    public const OPERATOR_MAX_LENGTH = 4;
45
46
    /**
47
     * The name of the default content.
48
     *
49
     * @var string
50
     */
51
    public static $defaultContext = '\\PhpMyAdmin\\SqlParser\\Contexts\\ContextMySql50700';
52
53
    /**
54
     * The name of the loaded context.
55
     *
56
     * @var string
57
     */
58
    public static $loadedContext = '\\PhpMyAdmin\\SqlParser\\Contexts\\ContextMySql50700';
59
60
    /**
61
     * The prefix concatenated to the context name when an incomplete class name
62
     * is specified.
63
     *
64
     * @var string
65
     */
66
    public static $contextPrefix = '\\PhpMyAdmin\\SqlParser\\Contexts\\Context';
67
68
    /**
69
     * List of keywords.
70
     *
71
     * Because, PHP's associative arrays are basically hash tables, it is more
72
     * efficient to store keywords as keys instead of values.
73
     *
74
     * The value associated to each keyword represents its flags.
75
     *
76
     * @see Token::FLAG_KEYWORD_RESERVED Token::FLAG_KEYWORD_COMPOSED
77
     *      Token::FLAG_KEYWORD_DATA_TYPE Token::FLAG_KEYWORD_KEY
78
     *      Token::FLAG_KEYWORD_FUNCTION
79
     *
80
     * Elements are sorted by flags, length and keyword.
81
     *
82
     * @var array<string,int>
83
     * @psalm-var non-empty-array<string,Token::FLAG_KEYWORD_*|int>
84
     * @phpstan-var non-empty-array<non-empty-string,Token::FLAG_KEYWORD_*|int>
85
     */
86
    public static $keywords = [];
87
88
    /**
89
     * List of operators and their flags.
90
     *
91
     * @var array<string, int>
92
     */
93
    public static $operators = [
94
        // Some operators (*, =) may have ambiguous flags, because they depend on
95
        // the context they are being used in.
96
        // For example: 1. SELECT * FROM table; # SQL specific (wildcard)
97
        //                 SELECT 2 * 3;        # arithmetic
98
        //              2. SELECT * FROM table WHERE foo = 'bar';
99
        //                 SET @i = 0;
100
101
        // @see Token::FLAG_OPERATOR_ARITHMETIC
102
        '%' => 1,
103
        '*' => 1,
104
        '+' => 1,
105
        '-' => 1,
106
        '/' => 1,
107
108
        // @see Token::FLAG_OPERATOR_LOGICAL
109
        '!' => 2,
110
        '!=' => 2,
111
        '&&' => 2,
112
        '<' => 2,
113
        '<=' => 2,
114
        '<=>' => 2,
115
        '<>' => 2,
116
        '=' => 2,
117
        '>' => 2,
118
        '>=' => 2,
119
        '||' => 2,
120
121
        // @see Token::FLAG_OPERATOR_BITWISE
122
        '&' => 4,
123
        '<<' => 4,
124
        '>>' => 4,
125
        '^' => 4,
126
        '|' => 4,
127
        '~' => 4,
128
129
        // @see Token::FLAG_OPERATOR_ASSIGNMENT
130
        ':=' => 8,
131
132
        // @see Token::FLAG_OPERATOR_SQL
133
        '(' => 16,
134
        ')' => 16,
135
        '.' => 16,
136
        ',' => 16,
137
        ';' => 16,
138
    ];
139
140
    /**
141
     * The mode of the MySQL server that will be used in lexing, parsing and building the statements.
142
     *
143
     * @internal use the {@see Context::getMode()} method instead.
144
     *
145
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html
146
     * @link https://mariadb.com/kb/en/sql-mode/
147
     *
148
     * @var int
149
     */
150
    public static $mode = self::SQL_MODE_NONE;
151
152
    public const SQL_MODE_NONE = 0;
153
154
    /**
155
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_allow_invalid_dates
156
     * @link https://mariadb.com/kb/en/sql-mode/#allow_invalid_dates
157
     */
158
    public const SQL_MODE_ALLOW_INVALID_DATES = 1;
159
160
    /**
161
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_ansi_quotes
162
     * @link https://mariadb.com/kb/en/sql-mode/#ansi_quotes
163
     */
164
    public const SQL_MODE_ANSI_QUOTES = 2;
165
166
    /** Compatibility mode for Microsoft's SQL server. This is the equivalent of {@see SQL_MODE_ANSI_QUOTES}. */
167
    public const SQL_MODE_COMPAT_MYSQL = 2;
168
169
    /**
170
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_error_for_division_by_zero
171
     * @link https://mariadb.com/kb/en/sql-mode/#error_for_division_by_zero
172
     */
173
    public const SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO = 4;
174
175
    /**
176
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_high_not_precedence
177
     * @link https://mariadb.com/kb/en/sql-mode/#high_not_precedence
178
     */
179
    public const SQL_MODE_HIGH_NOT_PRECEDENCE = 8;
180
181
    /**
182
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_ignore_space
183
     * @link https://mariadb.com/kb/en/sql-mode/#ignore_space
184
     */
185
    public const SQL_MODE_IGNORE_SPACE = 16;
186
187
    /**
188
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_auto_create_user
189
     * @link https://mariadb.com/kb/en/sql-mode/#no_auto_create_user
190
     */
191
    public const SQL_MODE_NO_AUTO_CREATE_USER = 32;
192
193
    /**
194
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_auto_value_on_zero
195
     * @link https://mariadb.com/kb/en/sql-mode/#no_auto_value_on_zero
196
     */
197
    public const SQL_MODE_NO_AUTO_VALUE_ON_ZERO = 64;
198
199
    /**
200
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_backslash_escapes
201
     * @link https://mariadb.com/kb/en/sql-mode/#no_backslash_escapes
202
     */
203
    public const SQL_MODE_NO_BACKSLASH_ESCAPES = 128;
204
205
    /**
206
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_dir_in_create
207
     * @link https://mariadb.com/kb/en/sql-mode/#no_dir_in_create
208
     */
209
    public const SQL_MODE_NO_DIR_IN_CREATE = 256;
210
211
    /**
212
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_engine_substitution
213
     * @link https://mariadb.com/kb/en/sql-mode/#no_engine_substitution
214
     */
215
    public const SQL_MODE_NO_ENGINE_SUBSTITUTION = 512;
216
217
    /**
218
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_field_options
219
     * @link https://mariadb.com/kb/en/sql-mode/#no_field_options
220
     */
221
    public const SQL_MODE_NO_FIELD_OPTIONS = 1024;
222
223
    /**
224
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_key_options
225
     * @link https://mariadb.com/kb/en/sql-mode/#no_key_options
226
     */
227
    public const SQL_MODE_NO_KEY_OPTIONS = 2048;
228
229
    /**
230
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_table_options
231
     * @link https://mariadb.com/kb/en/sql-mode/#no_table_options
232
     */
233
    public const SQL_MODE_NO_TABLE_OPTIONS = 4096;
234
235
    /**
236
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_unsigned_subtraction
237
     * @link https://mariadb.com/kb/en/sql-mode/#no_unsigned_subtraction
238
     */
239
    public const SQL_MODE_NO_UNSIGNED_SUBTRACTION = 8192;
240
241
    /**
242
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_zero_date
243
     * @link https://mariadb.com/kb/en/sql-mode/#no_zero_date
244
     */
245
    public const SQL_MODE_NO_ZERO_DATE = 16384;
246
247
    /**
248
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_zero_in_date
249
     * @link https://mariadb.com/kb/en/sql-mode/#no_zero_in_date
250
     */
251
    public const SQL_MODE_NO_ZERO_IN_DATE = 32768;
252
253
    /**
254
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_only_full_group_by
255
     * @link https://mariadb.com/kb/en/sql-mode/#only_full_group_by
256
     */
257
    public const SQL_MODE_ONLY_FULL_GROUP_BY = 65536;
258
259
    /**
260
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_pipes_as_concat
261
     * @link https://mariadb.com/kb/en/sql-mode/#pipes_as_concat
262
     */
263
    public const SQL_MODE_PIPES_AS_CONCAT = 131072;
264
265
    /**
266
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_real_as_float
267
     * @link https://mariadb.com/kb/en/sql-mode/#real_as_float
268
     */
269
    public const SQL_MODE_REAL_AS_FLOAT = 262144;
270
271
    /**
272
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_strict_all_tables
273
     * @link https://mariadb.com/kb/en/sql-mode/#strict_all_tables
274
     */
275
    public const SQL_MODE_STRICT_ALL_TABLES = 524288;
276
277
    /**
278
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_strict_trans_tables
279
     * @link https://mariadb.com/kb/en/sql-mode/#strict_trans_tables
280
     */
281
    public const SQL_MODE_STRICT_TRANS_TABLES = 1048576;
282
283
    /**
284
     * Custom mode.
285
     * The table and column names and any other field that must be escaped will not be.
286
     * Reserved keywords are being escaped regardless this mode is used or not.
287
     */
288
    public const SQL_MODE_NO_ENCLOSING_QUOTES = 1073741824;
289
290
    /**
291
     * Equivalent to {@see SQL_MODE_REAL_AS_FLOAT}, {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES},
292
     * {@see SQL_MODE_IGNORE_SPACE}.
293
     *
294
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_ansi
295
     * @link https://mariadb.com/kb/en/sql-mode/#ansi
296
     */
297
    public const SQL_MODE_ANSI = 393234;
298
299
    /**
300
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
301
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS}.
302
     *
303
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_db2
304
     * @link https://mariadb.com/kb/en/sql-mode/#db2
305
     */
306
    public const SQL_MODE_DB2 = 138258;
307
308
    /**
309
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
310
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS},
311
     * {@see SQL_MODE_NO_AUTO_CREATE_USER}.
312
     *
313
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_maxdb
314
     * @link https://mariadb.com/kb/en/sql-mode/#maxdb
315
     */
316
    public const SQL_MODE_MAXDB = 138290;
317
318
    /**
319
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
320
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS}.
321
     *
322
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_mssql
323
     * @link https://mariadb.com/kb/en/sql-mode/#mssql
324
     */
325
    public const SQL_MODE_MSSQL = 138258;
326
327
    /**
328
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
329
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS},
330
     * {@see SQL_MODE_NO_AUTO_CREATE_USER}.
331
     *
332
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_oracle
333
     * @link https://mariadb.com/kb/en/sql-mode/#oracle
334
     */
335
    public const SQL_MODE_ORACLE = 138290;
336
337
    /**
338
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
339
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS}.
340
     *
341
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_postgresql
342
     * @link https://mariadb.com/kb/en/sql-mode/#postgresql
343
     */
344
    public const SQL_MODE_POSTGRESQL = 138258;
345
346
    /**
347
     * Equivalent to {@see SQL_MODE_STRICT_TRANS_TABLES}, {@see SQL_MODE_STRICT_ALL_TABLES},
348
     * {@see SQL_MODE_NO_ZERO_IN_DATE}, {@see SQL_MODE_NO_ZERO_DATE}, {@see SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO},
349
     * {@see SQL_MODE_NO_AUTO_CREATE_USER}.
350
     *
351
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_traditional
352
     * @link https://mariadb.com/kb/en/sql-mode/#traditional
353
     */
354
    public const SQL_MODE_TRADITIONAL = 1622052;
355
356
    /**
357
     * Checks if the given string is a keyword.
358
     *
359
     * @param bool $isReserved checks if the keyword is reserved
360
     */
361 1420
    public static function isKeyword(string $string, bool $isReserved = false): int|null
362
    {
363 1420
        $upperString = strtoupper($string);
364
365
        if (
366 1420
            ! isset(static::$keywords[$upperString])
367 1420
            || ($isReserved && ! (static::$keywords[$upperString] & Token::FLAG_KEYWORD_RESERVED))
368
        ) {
369 1420
            return null;
370
        }
371
372 1390
        return static::$keywords[$upperString];
373
    }
374
375
    /**
376
     * Checks if the given string is an operator and returns the appropriate flag for the operator.
377
     */
378 1436
    public static function isOperator(string $string): int|null
379
    {
380 1436
        return static::$operators[$string] ?? null;
381
    }
382
383
    /**
384
     * Checks if the given character is a whitespace.
385
     */
386 1444
    public static function isWhitespace(string $string): bool
387
    {
388 1444
        return $string === ' ' || $string === "\r" || $string === "\n" || $string === "\t";
389
    }
390
391
    /**
392
     * Checks if the given string is the beginning of a whitespace.
393
     *
394
     * @return int|null the appropriate flag for the comment type
395
     */
396 1436
    public static function isComment(string $string, bool $end = false): int|null
397
    {
398 1436
        if ($string === '') {
399 2
            return null;
400
        }
401
402
        // If comment is Bash style (#):
403 1436
        if (str_starts_with($string, '#')) {
404 8
            return Token::FLAG_COMMENT_BASH;
405
        }
406
407
        // If comment is a MySQL command
408 1436
        if (str_starts_with($string, '/*!')) {
409
            return Token::FLAG_COMMENT_MYSQL_CMD;
410
        }
411
412
        // If comment is opening C style (/*) or is closing C style (*/), warning, it could conflict
413
        // with wildcard and a real opening C style.
414
        // It would look like the following valid SQL statement: "SELECT */* comment */ FROM...".
415 1436
        if (str_starts_with($string, '/*') || str_starts_with($string, '*/')) {
416 102
            return Token::FLAG_COMMENT_C;
417
        }
418
419
        // If comment is SQL style (--\s?):
420
        if (
421 1436
            str_starts_with($string, '-- ')
422 1436
            || str_starts_with($string, "--\r")
423 1436
            || str_starts_with($string, "--\n")
424 1436
            || str_starts_with($string, "--\t")
425 1436
            || ($string === '--' && $end)
426
        ) {
427 72
            return Token::FLAG_COMMENT_SQL;
428
        }
429
430 1436
        return null;
431
    }
432
433
    /**
434
     * Checks if the given string is a boolean value.
435
     * This actually check only for `TRUE` and `FALSE` because `1` or `0` are
436
     * actually numbers and are parsed by specific methods.
437
     */
438 1420
    public static function isBool(string $string): bool
439
    {
440 1420
        $upperString = strtoupper($string);
441
442 1420
        return $upperString === 'TRUE' || $upperString === 'FALSE';
443
    }
444
445
    /**
446
     * Checks if the given character can be a part of a number.
447
     */
448 2
    public static function isNumber(string $string): bool
449
    {
450 2
        return in_array($string, ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-', '+', 'e', 'E'], true);
451
    }
452
453
    /**
454
     * Checks if the given character is the beginning of a symbol. A symbol
455
     * can be either a variable or a field name.
456
     *
457
     * @return int|null the appropriate flag for the symbol type
458
     */
459 1420
    public static function isSymbol(string $string): int|null
460
    {
461 1420
        if ($string === '') {
462 2
            return null;
463
        }
464
465 1420
        if (str_starts_with($string, '@')) {
466 124
            return Token::FLAG_SYMBOL_VARIABLE;
467
        }
468
469 1418
        if (str_starts_with($string, '`')) {
470 368
            return Token::FLAG_SYMBOL_BACKTICK;
471
        }
472
473 1418
        if (str_starts_with($string, ':') || str_starts_with($string, '?')) {
474 18
            return Token::FLAG_SYMBOL_PARAMETER;
475
        }
476
477 1418
        return null;
478
    }
479
480
    /**
481
     * Checks if the given character is the beginning of a string.
482
     *
483
     * @param string $string string to be checked
484
     *
485
     * @return int|null the appropriate flag for the string type
486
     */
487 1420
    public static function isString(string $string): int|null
488
    {
489 1420
        if ($string === '') {
490 2
            return null;
491
        }
492
493 1420
        if (str_starts_with($string, '\'')) {
494 334
            return Token::FLAG_STRING_SINGLE_QUOTES;
495
        }
496
497 1420
        if (str_starts_with($string, '"')) {
498 160
            return Token::FLAG_STRING_DOUBLE_QUOTES;
499
        }
500
501 1420
        return null;
502
    }
503
504
    /**
505
     * Checks if the given character can be a separator for two lexeme.
506
     *
507
     * @param string $string string to be checked
508
     */
509 1420
    public static function isSeparator(string $string): bool
510
    {
511
        // NOTES:   Only non-alphanumeric ASCII characters may be separators.
512
        //          `~` is the last printable ASCII character.
513 1420
        return $string <= '~'
514 1420
            && $string !== '_'
515 1420
            && $string !== '$'
516 1420
            && ($string < '0' || $string > '9')
517 1420
            && ($string < 'a' || $string > 'z')
518 1420
            && ($string < 'A' || $string > 'Z');
519
    }
520
521
    /**
522
     * Loads the specified context.
523
     *
524
     * Contexts may be used by accessing the context directly.
525
     *
526
     * @param string $context name of the context or full class name that defines the context
527
     *
528
     * @return bool true if the context was loaded, false otherwise
529
     */
530 1762
    public static function load(string $context = ''): bool
531
    {
532 1762
        if (empty($context)) {
533 1762
            $context = self::$defaultContext;
534
        }
535
536 1762
        if ($context[0] !== '\\') {
537
            // Short context name (must be formatted into class name).
538 46
            $context = self::$contextPrefix . $context;
539
        }
540
541 1762
        if (! class_exists($context)) {
542 12
            return false;
543
        }
544
545 1762
        self::$loadedContext = $context;
546 1762
        self::$keywords = $context::$keywords;
0 ignored issues
show
Bug introduced by
The property keywords does not exist on string.
Loading history...
547
548 1762
        return true;
549
    }
550
551
    /**
552
     * Loads the context with the closest version to the one specified.
553
     *
554
     * The closest context is found by replacing last digits with zero until one
555
     * is loaded successfully.
556
     *
557
     * @see Context::load()
558
     *
559
     * @param string $context name of the context or full class name that defines the context
560
     *
561
     * @return string|null The loaded context. `null` if no context was loaded.
562
     */
563 14
    public static function loadClosest(string $context = ''): string|null
564
    {
565 14
        $length = strlen($context);
566 14
        for ($i = $length; $i > 0;) {
567
            /* Trying to load the new context */
568 14
            if (static::load($context)) {
569 12
                return $context;
570
            }
571
572
            /* Replace last two non zero digits by zeroes */
573
            do {
574 10
                $i -= 2;
575 10
                $part = substr($context, $i, 2);
576
                /* No more numeric parts to strip */
577 10
                if (! is_numeric($part)) {
578 6
                    break 2;
579
                }
580 8
            } while (intval($part) === 0 && $i > 0);
581
582 8
            $context = substr($context, 0, $i) . '00' . substr($context, $i + 2);
583
        }
584
585
        /* Fallback to loading at least matching engine */
586 6
        if (str_starts_with($context, 'MariaDb')) {
587 2
            return static::loadClosest('MariaDb100300');
588
        }
589
590 4
        if (str_starts_with($context, 'MySql')) {
591 2
            return static::loadClosest('MySql50700');
592
        }
593
594 2
        return null;
595
    }
596
597
    /**
598
     * Gets the SQL mode.
599
     */
600 72
    public static function getMode(): int
601
    {
602 72
        return static::$mode;
603
    }
604
605
    /**
606
     * Sets the SQL mode.
607
     *
608
     * @param int|string $mode
609
     */
610 942
    public static function setMode($mode = self::SQL_MODE_NONE): void
611
    {
612 942
        if (is_int($mode)) {
613 876
            static::$mode = $mode;
614
615 876
            return;
616
        }
617
618 66
        static::$mode = self::SQL_MODE_NONE;
619 66
        if ($mode === '') {
620 4
            return;
621
        }
622
623 64
        $modes = explode(',', $mode);
624 64
        foreach ($modes as $sqlMode) {
625 64
            static::$mode |= self::getModeFromString($sqlMode);
626
        }
627
    }
628
629
    /**
630
     * @psalm-suppress MixedReturnStatement, MixedInferredReturnType Is caused by the LSB of the constants
631
     */
632 64
    private static function getModeFromString(string $mode): int
633
    {
634 64
        return match ($mode) {
635 64
            'ALLOW_INVALID_DATES' => self::SQL_MODE_ALLOW_INVALID_DATES,
636 64
            'ANSI_QUOTES' => self::SQL_MODE_ANSI_QUOTES,
637 64
            'COMPAT_MYSQL' => self::SQL_MODE_COMPAT_MYSQL,
638 64
            'ERROR_FOR_DIVISION_BY_ZERO' => self::SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO,
639 64
            'HIGH_NOT_PRECEDENCE' => self::SQL_MODE_HIGH_NOT_PRECEDENCE,
640 64
            'IGNORE_SPACE' => self::SQL_MODE_IGNORE_SPACE,
641 64
            'NO_AUTO_CREATE_USER' => self::SQL_MODE_NO_AUTO_CREATE_USER,
642 64
            'NO_AUTO_VALUE_ON_ZERO' => self::SQL_MODE_NO_AUTO_VALUE_ON_ZERO,
643 64
            'NO_BACKSLASH_ESCAPES' => self::SQL_MODE_NO_BACKSLASH_ESCAPES,
644 64
            'NO_DIR_IN_CREATE' => self::SQL_MODE_NO_DIR_IN_CREATE,
645 64
            'NO_ENGINE_SUBSTITUTION' => self::SQL_MODE_NO_ENGINE_SUBSTITUTION,
646 64
            'NO_FIELD_OPTIONS' => self::SQL_MODE_NO_FIELD_OPTIONS,
647 64
            'NO_KEY_OPTIONS' => self::SQL_MODE_NO_KEY_OPTIONS,
648 64
            'NO_TABLE_OPTIONS' => self::SQL_MODE_NO_TABLE_OPTIONS,
649 64
            'NO_UNSIGNED_SUBTRACTION' => self::SQL_MODE_NO_UNSIGNED_SUBTRACTION,
650 64
            'NO_ZERO_DATE' => self::SQL_MODE_NO_ZERO_DATE,
651 64
            'NO_ZERO_IN_DATE' => self::SQL_MODE_NO_ZERO_IN_DATE,
652 64
            'ONLY_FULL_GROUP_BY' => self::SQL_MODE_ONLY_FULL_GROUP_BY,
653 64
            'PIPES_AS_CONCAT' => self::SQL_MODE_PIPES_AS_CONCAT,
654 64
            'REAL_AS_FLOAT' => self::SQL_MODE_REAL_AS_FLOAT,
655 64
            'STRICT_ALL_TABLES' => self::SQL_MODE_STRICT_ALL_TABLES,
656 64
            'STRICT_TRANS_TABLES' => self::SQL_MODE_STRICT_TRANS_TABLES,
657 64
            'NO_ENCLOSING_QUOTES' => self::SQL_MODE_NO_ENCLOSING_QUOTES,
658 64
            'ANSI' => self::SQL_MODE_ANSI,
659 64
            'DB2' => self::SQL_MODE_DB2,
660 64
            'MAXDB' => self::SQL_MODE_MAXDB,
661 64
            'MSSQL' => self::SQL_MODE_MSSQL,
662 64
            'ORACLE' => self::SQL_MODE_ORACLE,
663 64
            'POSTGRESQL' => self::SQL_MODE_POSTGRESQL,
664 64
            'TRADITIONAL' => self::SQL_MODE_TRADITIONAL,
665 64
            default => self::SQL_MODE_NONE,
666 64
        };
667
    }
668
669
    /**
670
     * Escapes the symbol by adding surrounding backticks.
671
     *
672
     * @param string $str   the string to be escaped
673
     * @param string $quote quote to be used when escaping
674
     */
675 142
    public static function escape(string $str, string $quote = '`'): string
676
    {
677
        if (
678 142
            (static::$mode & self::SQL_MODE_NO_ENCLOSING_QUOTES) && ! (
679 142
                static::isKeyword($str, true) || self::doesIdentifierRequireQuoting($str)
0 ignored issues
show
Bug Best Practice introduced by
The expression static::isKeyword($str, true) of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
680
            )
681
        ) {
682 2
            return $str;
683
        }
684
685 142
        if (static::$mode & self::SQL_MODE_ANSI_QUOTES) {
686 2
            $quote = '"';
687
        }
688
689 142
        return $quote . str_replace($quote, $quote . $quote, $str) . $quote;
690
    }
691
692
    /**
693
     * Escapes the symbol by adding surrounding backticks.
694
     *
695
     * @param string[] $strings the string to be escaped
696
     *
697
     * @return string[]
698
     */
699 32
    public static function escapeAll(array $strings): array
700
    {
701 32
        foreach ($strings as $key => $value) {
702 32
            $strings[$key] = static::escape($value);
703
        }
704
705 32
        return $strings;
706
    }
707
708
    /**
709
     * Function verifies that given SQL Mode constant is currently set
710
     *
711
     * @param int $flag for example {@see Context::SQL_MODE_ANSI_QUOTES}
712
     *
713
     * @return bool false on empty param, true/false on given constant/int value
714
     */
715 4
    public static function hasMode(int|null $flag = null): bool
716
    {
717 4
        if (empty($flag)) {
718
            return false;
719
        }
720
721 4
        return (self::$mode & $flag) === $flag;
722
    }
723
724 2
    private static function doesIdentifierRequireQuoting(string $identifier): bool
725
    {
726 2
        return preg_match('/^[$]|^\d+$|[^0-9a-zA-Z$_\x80-\xffff]/', $identifier) === 1;
727
    }
728
}
729