Passed
Push — master ( 6c3f09...c54108 )
by William
03:43
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 1
Bugs 0 Features 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 1
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_array;
12
use function is_int;
13
use function is_numeric;
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
     * @phpstan-var non-empty-array<non-empty-string,Token::FLAG_KEYWORD_*|int>
84
     */
85
    public static $keywords = [];
86
87
    /**
88
     * List of operators and their flags.
89
     *
90
     * @var array<string, int>
91
     */
92
    public static $operators = [
93
        // Some operators (*, =) may have ambiguous flags, because they depend on
94
        // the context they are being used in.
95
        // For example: 1. SELECT * FROM table; # SQL specific (wildcard)
96
        //                 SELECT 2 * 3;        # arithmetic
97
        //              2. SELECT * FROM table WHERE foo = 'bar';
98
        //                 SET @i = 0;
99
100
        // @see Token::FLAG_OPERATOR_ARITHMETIC
101
        '%' => 1,
102
        '*' => 1,
103
        '+' => 1,
104
        '-' => 1,
105
        '/' => 1,
106
107
        // @see Token::FLAG_OPERATOR_LOGICAL
108
        '!' => 2,
109
        '!=' => 2,
110
        '&&' => 2,
111
        '<' => 2,
112
        '<=' => 2,
113
        '<=>' => 2,
114
        '<>' => 2,
115
        '=' => 2,
116
        '>' => 2,
117
        '>=' => 2,
118
        '||' => 2,
119
120
        // @see Token::FLAG_OPERATOR_BITWISE
121
        '&' => 4,
122
        '<<' => 4,
123
        '>>' => 4,
124
        '^' => 4,
125
        '|' => 4,
126
        '~' => 4,
127
128
        // @see Token::FLAG_OPERATOR_ASSIGNMENT
129
        ':=' => 8,
130
131
        // @see Token::FLAG_OPERATOR_SQL
132
        '(' => 16,
133
        ')' => 16,
134
        '.' => 16,
135
        ',' => 16,
136
        ';' => 16,
137
    ];
138
139
    /**
140
     * The mode of the MySQL server that will be used in lexing, parsing and building the statements.
141
     *
142
     * @internal use the {@see Context::getMode()} method instead.
143
     *
144
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html
145
     * @link https://mariadb.com/kb/en/sql-mode/
146
     *
147
     * @var int
148
     */
149
    public static $mode = self::SQL_MODE_NONE;
150
151
    public const SQL_MODE_NONE = 0;
152
153
    /**
154
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_allow_invalid_dates
155
     * @link https://mariadb.com/kb/en/sql-mode/#allow_invalid_dates
156
     */
157
    public const SQL_MODE_ALLOW_INVALID_DATES = 1;
158
159
    /**
160
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_ansi_quotes
161
     * @link https://mariadb.com/kb/en/sql-mode/#ansi_quotes
162
     */
163
    public const SQL_MODE_ANSI_QUOTES = 2;
164
165
    /** Compatibility mode for Microsoft's SQL server. This is the equivalent of {@see SQL_MODE_ANSI_QUOTES}. */
166
    public const SQL_MODE_COMPAT_MYSQL = 2;
167
168
    /**
169
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_error_for_division_by_zero
170
     * @link https://mariadb.com/kb/en/sql-mode/#error_for_division_by_zero
171
     */
172
    public const SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO = 4;
173
174
    /**
175
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_high_not_precedence
176
     * @link https://mariadb.com/kb/en/sql-mode/#high_not_precedence
177
     */
178
    public const SQL_MODE_HIGH_NOT_PRECEDENCE = 8;
179
180
    /**
181
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_ignore_space
182
     * @link https://mariadb.com/kb/en/sql-mode/#ignore_space
183
     */
184
    public const SQL_MODE_IGNORE_SPACE = 16;
185
186
    /**
187
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_auto_create_user
188
     * @link https://mariadb.com/kb/en/sql-mode/#no_auto_create_user
189
     */
190
    public const SQL_MODE_NO_AUTO_CREATE_USER = 32;
191
192
    /**
193
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_auto_value_on_zero
194
     * @link https://mariadb.com/kb/en/sql-mode/#no_auto_value_on_zero
195
     */
196
    public const SQL_MODE_NO_AUTO_VALUE_ON_ZERO = 64;
197
198
    /**
199
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_backslash_escapes
200
     * @link https://mariadb.com/kb/en/sql-mode/#no_backslash_escapes
201
     */
202
    public const SQL_MODE_NO_BACKSLASH_ESCAPES = 128;
203
204
    /**
205
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_dir_in_create
206
     * @link https://mariadb.com/kb/en/sql-mode/#no_dir_in_create
207
     */
208
    public const SQL_MODE_NO_DIR_IN_CREATE = 256;
209
210
    /**
211
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_engine_substitution
212
     * @link https://mariadb.com/kb/en/sql-mode/#no_engine_substitution
213
     */
214
    public const SQL_MODE_NO_ENGINE_SUBSTITUTION = 512;
215
216
    /**
217
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_field_options
218
     * @link https://mariadb.com/kb/en/sql-mode/#no_field_options
219
     */
220
    public const SQL_MODE_NO_FIELD_OPTIONS = 1024;
221
222
    /**
223
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_key_options
224
     * @link https://mariadb.com/kb/en/sql-mode/#no_key_options
225
     */
226
    public const SQL_MODE_NO_KEY_OPTIONS = 2048;
227
228
    /**
229
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_table_options
230
     * @link https://mariadb.com/kb/en/sql-mode/#no_table_options
231
     */
232
    public const SQL_MODE_NO_TABLE_OPTIONS = 4096;
233
234
    /**
235
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_unsigned_subtraction
236
     * @link https://mariadb.com/kb/en/sql-mode/#no_unsigned_subtraction
237
     */
238
    public const SQL_MODE_NO_UNSIGNED_SUBTRACTION = 8192;
239
240
    /**
241
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_zero_date
242
     * @link https://mariadb.com/kb/en/sql-mode/#no_zero_date
243
     */
244
    public const SQL_MODE_NO_ZERO_DATE = 16384;
245
246
    /**
247
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_zero_in_date
248
     * @link https://mariadb.com/kb/en/sql-mode/#no_zero_in_date
249
     */
250
    public const SQL_MODE_NO_ZERO_IN_DATE = 32768;
251
252
    /**
253
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_only_full_group_by
254
     * @link https://mariadb.com/kb/en/sql-mode/#only_full_group_by
255
     */
256
    public const SQL_MODE_ONLY_FULL_GROUP_BY = 65536;
257
258
    /**
259
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_pipes_as_concat
260
     * @link https://mariadb.com/kb/en/sql-mode/#pipes_as_concat
261
     */
262
    public const SQL_MODE_PIPES_AS_CONCAT = 131072;
263
264
    /**
265
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_real_as_float
266
     * @link https://mariadb.com/kb/en/sql-mode/#real_as_float
267
     */
268
    public const SQL_MODE_REAL_AS_FLOAT = 262144;
269
270
    /**
271
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_strict_all_tables
272
     * @link https://mariadb.com/kb/en/sql-mode/#strict_all_tables
273
     */
274
    public const SQL_MODE_STRICT_ALL_TABLES = 524288;
275
276
    /**
277
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_strict_trans_tables
278
     * @link https://mariadb.com/kb/en/sql-mode/#strict_trans_tables
279
     */
280
    public const SQL_MODE_STRICT_TRANS_TABLES = 1048576;
281
282
    /**
283
     * Custom mode.
284
     * The table and column names and any other field that must be escaped will not be.
285
     * Reserved keywords are being escaped regardless this mode is used or not.
286
     */
287
    public const SQL_MODE_NO_ENCLOSING_QUOTES = 1073741824;
288
289
    /**
290
     * Equivalent to {@see SQL_MODE_REAL_AS_FLOAT}, {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES},
291
     * {@see SQL_MODE_IGNORE_SPACE}.
292
     *
293
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_ansi
294
     * @link https://mariadb.com/kb/en/sql-mode/#ansi
295
     */
296
    public const SQL_MODE_ANSI = 393234;
297
298
    /**
299
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
300
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS}.
301
     *
302
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_db2
303
     * @link https://mariadb.com/kb/en/sql-mode/#db2
304
     */
305
    public const SQL_MODE_DB2 = 138258;
306
307
    /**
308
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
309
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS},
310
     * {@see SQL_MODE_NO_AUTO_CREATE_USER}.
311
     *
312
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_maxdb
313
     * @link https://mariadb.com/kb/en/sql-mode/#maxdb
314
     */
315
    public const SQL_MODE_MAXDB = 138290;
316
317
    /**
318
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
319
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS}.
320
     *
321
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_mssql
322
     * @link https://mariadb.com/kb/en/sql-mode/#mssql
323
     */
324
    public const SQL_MODE_MSSQL = 138258;
325
326
    /**
327
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
328
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS},
329
     * {@see SQL_MODE_NO_AUTO_CREATE_USER}.
330
     *
331
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_oracle
332
     * @link https://mariadb.com/kb/en/sql-mode/#oracle
333
     */
334
    public const SQL_MODE_ORACLE = 138290;
335
336
    /**
337
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
338
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS}.
339
     *
340
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_postgresql
341
     * @link https://mariadb.com/kb/en/sql-mode/#postgresql
342
     */
343
    public const SQL_MODE_POSTGRESQL = 138258;
344
345
    /**
346
     * Equivalent to {@see SQL_MODE_STRICT_TRANS_TABLES}, {@see SQL_MODE_STRICT_ALL_TABLES},
347
     * {@see SQL_MODE_NO_ZERO_IN_DATE}, {@see SQL_MODE_NO_ZERO_DATE}, {@see SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO},
348
     * {@see SQL_MODE_NO_AUTO_CREATE_USER}.
349
     *
350
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_traditional
351
     * @link https://mariadb.com/kb/en/sql-mode/#traditional
352
     */
353
    public const SQL_MODE_TRADITIONAL = 1622052;
354
355
    /**
356
     * Checks if the given string is a keyword.
357
     *
358
     * @param bool $isReserved checks if the keyword is reserved
359
     */
360 1290
    public static function isKeyword(string $string, bool $isReserved = false): ?int
361
    {
362 1290
        $upperString = strtoupper($string);
363
364
        if (
365 1290
            ! isset(static::$keywords[$upperString])
366 1290
            || ($isReserved && ! (static::$keywords[$upperString] & Token::FLAG_KEYWORD_RESERVED))
367
        ) {
368 1290
            return null;
369
        }
370
371 1262
        return static::$keywords[$upperString];
372
    }
373
374
    /**
375
     * Checks if the given string is an operator and returns the appropriate flag for the operator.
376
     */
377 1306
    public static function isOperator(string $string): ?int
378
    {
379 1306
        return static::$operators[$string] ?? null;
380
    }
381
382
    /**
383
     * Checks if the given character is a whitespace.
384
     */
385 1314
    public static function isWhitespace(string $string): bool
386
    {
387 1314
        return $string === ' ' || $string === "\r" || $string === "\n" || $string === "\t";
388
    }
389
390
    /**
391
     * Checks if the given string is the beginning of a whitespace.
392
     *
393
     * @return int|null the appropriate flag for the comment type
394
     */
395 1306
    public static function isComment(string $string, bool $end = false): ?int
396
    {
397 1306
        if ($string === '') {
398 2
            return null;
399
        }
400
401
        // If comment is Bash style (#):
402 1306
        if (str_starts_with($string, '#')) {
403 8
            return Token::FLAG_COMMENT_BASH;
404
        }
405
406
        // If comment is a MySQL command
407 1306
        if (str_starts_with($string, '/*!')) {
408
            return Token::FLAG_COMMENT_MYSQL_CMD;
409
        }
410
411
        // If comment is opening C style (/*) or is closing C style (*/), warning, it could conflict
412
        // with wildcard and a real opening C style.
413
        // It would look like the following valid SQL statement: "SELECT */* comment */ FROM...".
414 1306
        if (str_starts_with($string, '/*') || str_starts_with($string, '*/')) {
415 100
            return Token::FLAG_COMMENT_C;
416
        }
417
418
        // If comment is SQL style (--\s?):
419
        if (
420 1306
            str_starts_with($string, '-- ')
421 1306
            || str_starts_with($string, "--\r")
422 1306
            || str_starts_with($string, "--\n")
423 1306
            || str_starts_with($string, "--\t")
424 1306
            || ($string === '--' && $end)
425
        ) {
426 62
            return Token::FLAG_COMMENT_SQL;
427
        }
428
429 1306
        return null;
430
    }
431
432
    /**
433
     * Checks if the given string is a boolean value.
434
     * This actually check only for `TRUE` and `FALSE` because `1` or `0` are
435
     * actually numbers and are parsed by specific methods.
436
     */
437 1290
    public static function isBool(string $string): bool
438
    {
439 1290
        $upperString = strtoupper($string);
440
441 1290
        return $upperString === 'TRUE' || $upperString === 'FALSE';
442
    }
443
444
    /**
445
     * Checks if the given character can be a part of a number.
446
     */
447 2
    public static function isNumber(string $string): bool
448
    {
449 2
        return in_array($string, ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-', '+', 'e', 'E'], true);
450
    }
451
452
    /**
453
     * Checks if the given character is the beginning of a symbol. A symbol
454
     * can be either a variable or a field name.
455
     *
456
     * @return int|null the appropriate flag for the symbol type
457
     */
458 1290
    public static function isSymbol(string $string): ?int
459
    {
460 1290
        if ($string === '') {
461 2
            return null;
462
        }
463
464 1290
        if (str_starts_with($string, '@')) {
465 114
            return Token::FLAG_SYMBOL_VARIABLE;
466
        }
467
468 1288
        if (str_starts_with($string, '`')) {
469 330
            return Token::FLAG_SYMBOL_BACKTICK;
470
        }
471
472 1288
        if (str_starts_with($string, ':') || str_starts_with($string, '?')) {
473 6
            return Token::FLAG_SYMBOL_PARAMETER;
474
        }
475
476 1288
        return null;
477
    }
478
479
    /**
480
     * Checks if the given character is the beginning of a string.
481
     *
482
     * @param string $string string to be checked
483
     *
484
     * @return int|null the appropriate flag for the string type
485
     */
486 1290
    public static function isString(string $string): ?int
487
    {
488 1290
        if ($string === '') {
489 2
            return null;
490
        }
491
492 1290
        if (str_starts_with($string, '\'')) {
493 300
            return Token::FLAG_STRING_SINGLE_QUOTES;
494
        }
495
496 1290
        if (str_starts_with($string, '"')) {
497 142
            return Token::FLAG_STRING_DOUBLE_QUOTES;
498
        }
499
500 1290
        return null;
501
    }
502
503
    /**
504
     * Checks if the given character can be a separator for two lexeme.
505
     *
506
     * @param string $string string to be checked
507
     */
508 1290
    public static function isSeparator(string $string): bool
509
    {
510
        // NOTES:   Only non-alphanumeric ASCII characters may be separators.
511
        //          `~` is the last printable ASCII character.
512 1290
        return $string <= '~'
513 1290
            && $string !== '_'
514 1290
            && $string !== '$'
515 1290
            && ($string < '0' || $string > '9')
516 1290
            && ($string < 'a' || $string > 'z')
517 1290
            && ($string < 'A' || $string > 'Z');
518
    }
519
520
    /**
521
     * Loads the specified context.
522
     *
523
     * Contexts may be used by accessing the context directly.
524
     *
525
     * @param string $context name of the context or full class name that defines the context
526
     *
527
     * @return bool true if the context was loaded, false otherwise
528
     */
529 1656
    public static function load(string $context = ''): bool
530
    {
531 1656
        if (empty($context)) {
532 1656
            $context = self::$defaultContext;
533
        }
534
535 1656
        if ($context[0] !== '\\') {
536
            // Short context name (must be formatted into class name).
537 46
            $context = self::$contextPrefix . $context;
538
        }
539
540 1656
        if (! class_exists($context)) {
541 12
            return false;
542
        }
543
544 1656
        self::$loadedContext = $context;
545 1656
        self::$keywords = $context::$keywords;
0 ignored issues
show
Bug introduced by
The property keywords does not exist on string.
Loading history...
546
547 1656
        return true;
548
    }
549
550
    /**
551
     * Loads the context with the closest version to the one specified.
552
     *
553
     * The closest context is found by replacing last digits with zero until one
554
     * is loaded successfully.
555
     *
556
     * @see Context::load()
557
     *
558
     * @param string $context name of the context or full class name that defines the context
559
     *
560
     * @return string|null The loaded context. `null` if no context was loaded.
561
     */
562 14
    public static function loadClosest(string $context = ''): ?string
563
    {
564 14
        $length = strlen($context);
565 14
        for ($i = $length; $i > 0;) {
566
            /* Trying to load the new context */
567 14
            if (static::load($context)) {
568 12
                return $context;
569
            }
570
571
            /* Replace last two non zero digits by zeroes */
572
            do {
573 10
                $i -= 2;
574 10
                $part = substr($context, $i, 2);
575
                /* No more numeric parts to strip */
576 10
                if (! is_numeric($part)) {
577 6
                    break 2;
578
                }
579 8
            } while (intval($part) === 0 && $i > 0);
580
581 8
            $context = substr($context, 0, $i) . '00' . substr($context, $i + 2);
582
        }
583
584
        /* Fallback to loading at least matching engine */
585 6
        if (str_starts_with($context, 'MariaDb')) {
586 2
            return static::loadClosest('MariaDb100300');
587
        }
588
589 4
        if (str_starts_with($context, 'MySql')) {
590 2
            return static::loadClosest('MySql50700');
591
        }
592
593 2
        return null;
594
    }
595
596
    /**
597
     * Gets the SQL mode.
598
     */
599 72
    public static function getMode(): int
600
    {
601 72
        return static::$mode;
602
    }
603
604
    /**
605
     * Sets the SQL mode.
606
     *
607
     * @param int|string $mode
608
     *
609
     * @return void
610
     */
611 826
    public static function setMode($mode = self::SQL_MODE_NONE)
612
    {
613 826
        if (is_int($mode)) {
614 760
            static::$mode = $mode;
615
616 760
            return;
617
        }
618
619 66
        static::$mode = self::SQL_MODE_NONE;
620 66
        if ($mode === '') {
621 4
            return;
622
        }
623
624 64
        $modes = explode(',', $mode);
625 64
        foreach ($modes as $sqlMode) {
626 64
            static::$mode |= self::getModeFromString($sqlMode);
627
        }
628
    }
629
630
    /**
631
     * @psalm-suppress MixedReturnStatement, MixedInferredReturnType Is caused by the LSB of the constants
632
     */
633 64
    private static function getModeFromString(string $mode): int
634
    {
635
        // phpcs:disable SlevomatCodingStandard.Classes.DisallowLateStaticBindingForConstants.DisallowedLateStaticBindingForConstant
636
        switch ($mode) {
637 64
            case 'ALLOW_INVALID_DATES':
638 2
                return static::SQL_MODE_ALLOW_INVALID_DATES;
639
640 62
            case 'ANSI_QUOTES':
641 4
                return static::SQL_MODE_ANSI_QUOTES;
642
643 60
            case 'COMPAT_MYSQL':
644 2
                return static::SQL_MODE_COMPAT_MYSQL;
645
646 58
            case 'ERROR_FOR_DIVISION_BY_ZERO':
647 2
                return static::SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO;
648
649 56
            case 'HIGH_NOT_PRECEDENCE':
650 2
                return static::SQL_MODE_HIGH_NOT_PRECEDENCE;
651
652 54
            case 'IGNORE_SPACE':
653 4
                return static::SQL_MODE_IGNORE_SPACE;
654
655 52
            case 'NO_AUTO_CREATE_USER':
656 2
                return static::SQL_MODE_NO_AUTO_CREATE_USER;
657
658 50
            case 'NO_AUTO_VALUE_ON_ZERO':
659 2
                return static::SQL_MODE_NO_AUTO_VALUE_ON_ZERO;
660
661 48
            case 'NO_BACKSLASH_ESCAPES':
662 2
                return static::SQL_MODE_NO_BACKSLASH_ESCAPES;
663
664 46
            case 'NO_DIR_IN_CREATE':
665 2
                return static::SQL_MODE_NO_DIR_IN_CREATE;
666
667 44
            case 'NO_ENGINE_SUBSTITUTION':
668 2
                return static::SQL_MODE_NO_ENGINE_SUBSTITUTION;
669
670 42
            case 'NO_FIELD_OPTIONS':
671 2
                return static::SQL_MODE_NO_FIELD_OPTIONS;
672
673 40
            case 'NO_KEY_OPTIONS':
674 2
                return static::SQL_MODE_NO_KEY_OPTIONS;
675
676 38
            case 'NO_TABLE_OPTIONS':
677 2
                return static::SQL_MODE_NO_TABLE_OPTIONS;
678
679 36
            case 'NO_UNSIGNED_SUBTRACTION':
680 2
                return static::SQL_MODE_NO_UNSIGNED_SUBTRACTION;
681
682 34
            case 'NO_ZERO_DATE':
683 2
                return static::SQL_MODE_NO_ZERO_DATE;
684
685 32
            case 'NO_ZERO_IN_DATE':
686 2
                return static::SQL_MODE_NO_ZERO_IN_DATE;
687
688 30
            case 'ONLY_FULL_GROUP_BY':
689 2
                return static::SQL_MODE_ONLY_FULL_GROUP_BY;
690
691 28
            case 'PIPES_AS_CONCAT':
692 2
                return static::SQL_MODE_PIPES_AS_CONCAT;
693
694 26
            case 'REAL_AS_FLOAT':
695 4
                return static::SQL_MODE_REAL_AS_FLOAT;
696
697 24
            case 'STRICT_ALL_TABLES':
698 2
                return static::SQL_MODE_STRICT_ALL_TABLES;
699
700 22
            case 'STRICT_TRANS_TABLES':
701 2
                return static::SQL_MODE_STRICT_TRANS_TABLES;
702
703 20
            case 'NO_ENCLOSING_QUOTES':
704 2
                return static::SQL_MODE_NO_ENCLOSING_QUOTES;
705
706 18
            case 'ANSI':
707 2
                return static::SQL_MODE_ANSI;
708
709 16
            case 'DB2':
710 2
                return static::SQL_MODE_DB2;
711
712 14
            case 'MAXDB':
713 2
                return static::SQL_MODE_MAXDB;
714
715 12
            case 'MSSQL':
716 2
                return static::SQL_MODE_MSSQL;
717
718 10
            case 'ORACLE':
719 2
                return static::SQL_MODE_ORACLE;
720
721 8
            case 'POSTGRESQL':
722 2
                return static::SQL_MODE_POSTGRESQL;
723
724 6
            case 'TRADITIONAL':
725 4
                return static::SQL_MODE_TRADITIONAL;
726
727
            default:
728 2
                return self::SQL_MODE_NONE;
729
        }
730
        // phpcs:enable
731
    }
732
733
    /**
734
     * Escapes the symbol by adding surrounding backticks.
735
     *
736
     * @param string[]|string $str   the string to be escaped
737
     * @param string          $quote quote to be used when escaping
738
     *
739
     * @return string|string[]
740
     */
741 134
    public static function escape($str, string $quote = '`')
742
    {
743 134
        if (is_array($str)) {
744 30
            foreach ($str as $key => $value) {
745 30
                $str[$key] = static::escape($value);
746
            }
747
748 30
            return $str;
749
        }
750
751 134
        if ((static::$mode & self::SQL_MODE_NO_ENCLOSING_QUOTES) && (! static::isKeyword($str, true))) {
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...
752 2
            return $str;
753
        }
754
755 134
        if (static::$mode & self::SQL_MODE_ANSI_QUOTES) {
756 2
            $quote = '"';
757
        }
758
759 134
        return $quote . str_replace($quote, $quote . $quote, $str) . $quote;
760
    }
761
762
    /**
763
     * Returns char used to quote identifiers based on currently set SQL Mode (ie. standard or ANSI_QUOTES)
764
     *
765
     * @return string either " (double quote, ansi_quotes mode) or ` (backtick, standard mode)
766
     */
767
    public static function getIdentifierQuote(): string
768
    {
769
        return self::hasMode(self::SQL_MODE_ANSI_QUOTES) ? '"' : '`';
770
    }
771
772
    /**
773
     * Function verifies that given SQL Mode constant is currently set
774
     *
775
     * @param int $flag for example {@see Context::SQL_MODE_ANSI_QUOTES}
776
     *
777
     * @return bool false on empty param, true/false on given constant/int value
778
     */
779 4
    public static function hasMode(?int $flag = null): bool
780
    {
781 4
        if (empty($flag)) {
782
            return false;
783
        }
784
785 4
        return (self::$mode & $flag) === $flag;
786
    }
787
}
788