Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.18% covered (success)
87.18%
102 / 117
83.33% covered (success)
83.33%
20 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractSql
87.18% covered (success)
87.18%
102 / 117
83.33% covered (success)
83.33%
20 / 24
97.52
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isMysql
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isPgsql
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSqlsrv
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSqlite
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 db
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDb
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setIdQuoteType
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setPlaceholder
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDbType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPlaceholder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIdQuoteType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOpenQuote
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCloseQuote
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParameterCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 incrementParameterCount
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 decrementParameterCount
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isParameter
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
5
 getParameter
85.71% covered (success)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
13.49
 quoteId
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
9
 quote
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
27.44
 init
79.17% covered (success)
79.17%
19 / 24
0.00% covered (danger)
0.00%
0 / 1
10.90
 initQuoteType
76.92% covered (success)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
5.31
 isSupportedFunction
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2/**
3 * Pop PHP Framework (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
8 * @license    https://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Db\Sql;
15
16use Pop\Db\Adapter;
17
18/**
19 * Abstract SQL class
20 *
21 * @category   Pop
22 * @package    Pop\Db
23 * @author     Nick Sagona, III <dev@noladev.com>
24 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
25 * @license    https://www.popphp.org/license     New BSD License
26 * @version    6.6.5
27 */
28abstract class AbstractSql
29{
30
31    /**
32     * Constants for database types
33     */
34    const MYSQL  = 'MYSQL';
35    const PGSQL  = 'PGSQL';
36    const SQLITE = 'SQLITE';
37    const SQLSRV = 'SQLSRV';
38
39    /**
40     * Constants for id quote types
41     */
42    const BACKTICK     = 'BACKTICK';
43    const BRACKET      = 'BRACKET';
44    const DOUBLE_QUOTE = 'DOUBLE_QUOTE';
45    const NO_QUOTE     = 'NO_QUOTE';
46
47    /**
48     * Database object
49     * @var ?Adapter\AbstractAdapter
50     */
51    protected ?Adapter\AbstractAdapter $db = null;
52
53    /**
54     * Database type
55     * @var ?string
56     */
57    protected ?string $dbType = null;
58
59    /**
60     * SQL placeholder
61     * @var ?string
62     */
63    protected ?string $placeholder = null;
64
65    /**
66     * ID quote type
67     * @var string
68     */
69    protected string $idQuoteType = 'NO_QUOTE';
70
71    /**
72     * ID open quote
73     * @var ?string
74     */
75    protected ?string $openQuote = null;
76
77    /**
78     * ID close quote
79     * @var ?string
80     */
81    protected ?string $closeQuote = null;
82
83    /**
84     * Parameter count
85     * @var int
86     */
87    protected int $parameterCount = 0;
88
89    /**
90     * Supported standard SQL aggregate functions
91     * @var array
92     */
93    protected static array $aggregateFunctions = [
94        'AVG', 'COUNT', 'MAX', 'MIN', 'SUM'
95    ];
96
97    /**
98     * Supported standard SQL math functions
99     * @var array
100     */
101    protected static array $mathFunctions = [
102        'ABS', 'RAND', 'SQRT', 'POW', 'POWER', 'EXP', 'LN', 'LOG', 'LOG10', 'GREATEST', 'LEAST',
103        'DIV', 'MOD', 'ROUND', 'TRUNC', 'CEIL', 'CEILING', 'FLOOR', 'COS', 'ACOS', 'ACOSH', 'SIN',
104        'SINH', 'ASIN', 'ASINH', 'TAN', 'TANH', 'ATANH', 'ATAN2',
105    ];
106
107    /**
108     * Supported standard SQL string functions
109     * @var array
110     */
111    protected static array $stringFunctions = [
112        'CONCAT', 'FORMAT', 'INSTR', 'LCASE', 'LEFT', 'LENGTH', 'LOCATE', 'LOWER', 'LPAD',
113        'LTRIM', 'POSITION', 'QUOTE', 'REGEXP', 'REPEAT', 'REPLACE', 'REVERSE', 'RIGHT', 'RPAD',
114        'RTRIM', 'SPACE', 'STRCMP', 'SUBSTRING', 'SUBSTR', 'TRIM', 'UCASE', 'UPPER'
115    ];
116
117    /**
118     * Supported standard SQL date-time functions
119     * @var array
120     */
121    protected static array $dateTimeFunctions = [
122        'CURRENT_DATE', 'CURRENT_TIMESTAMP', 'CURRENT_TIME', 'CURDATE', 'CURTIME', 'DATE', 'DATETIME',
123        'DAY', 'EXTRACT', 'GETDATE', 'HOUR', 'LOCALTIME', 'LOCALTIMESTAMP', 'MINUTE', 'MONTH',
124        'NOW', 'SECOND', 'TIME', 'TIMEDIFF', 'TIMESTAMP', 'UNIX_TIMESTAMP', 'YEAR',
125    ];
126
127    /**
128     * Constructor
129     *
130     * Instantiate the SQL object
131     *
132     * @param  Adapter\AbstractAdapter $db
133     */
134    public function __construct(Adapter\AbstractAdapter $db)
135    {
136        $this->db = $db;
137        $this->init(strtolower(get_class($db)));
138    }
139
140    /**
141     * Determine if the DB type is MySQL
142     *
143     * @return bool
144     */
145    public function isMysql(): bool
146    {
147        return ($this->dbType == self::MYSQL);
148    }
149
150    /**
151     * Determine if the DB type is PostgreSQL
152     *
153     * @return bool
154     */
155    public function isPgsql(): bool
156    {
157        return ($this->dbType == self::PGSQL);
158    }
159
160    /**
161     * Determine if the DB type is SQL Server
162     *
163     * @return bool
164     */
165    public function isSqlsrv(): bool
166    {
167        return ($this->dbType == self::SQLSRV);
168    }
169
170    /**
171     * Determine if the DB type is SQLite
172     *
173     * @return bool
174     */
175    public function isSqlite(): bool
176    {
177        return ($this->dbType == self::SQLITE);
178    }
179
180    /**
181     * Get the current database adapter object (alias method)
182     *
183     * @return ?Adapter\AbstractAdapter
184     */
185    public function db(): ?Adapter\AbstractAdapter
186    {
187        return $this->db;
188    }
189
190    /**
191     * Get the current database adapter object
192     *
193     * @return ?Adapter\AbstractAdapter
194     */
195    public function getDb(): ?Adapter\AbstractAdapter
196    {
197        return $this->db;
198    }
199
200    /**
201     * Set the quote ID type
202     *
203     * @param  string $type
204     * @return AbstractSql
205     */
206    public function setIdQuoteType(string $type = self::NO_QUOTE): AbstractSql
207    {
208        if (defined('Pop\Db\Sql::' . $type)) {
209            $this->idQuoteType = $type;
210            $this->initQuoteType();
211        }
212        return $this;
213    }
214
215    /**
216     * Set the placeholder
217     *
218     * @param  string $placeholder
219     * @return AbstractSql
220     */
221    public function setPlaceholder(string $placeholder): AbstractSql
222    {
223        $this->placeholder = $placeholder;
224        return $this;
225    }
226
227    /**
228     * Get the current database type
229     *
230     * @return ?string
231     */
232    public function getDbType(): ?string
233    {
234        return $this->dbType;
235    }
236
237    /**
238     * Get the SQL placeholder
239     *
240     * @return ?string
241     */
242    public function getPlaceholder(): ?string
243    {
244        return $this->placeholder;
245    }
246
247    /**
248     * Get the quote ID type
249     *
250     * @return string
251     */
252    public function getIdQuoteType(): string
253    {
254        return $this->idQuoteType;
255    }
256
257    /**
258     * Get open quote
259     *
260     * @return ?string
261     */
262    public function getOpenQuote(): ?string
263    {
264        return $this->openQuote;
265    }
266
267    /**
268     * Get close quote
269     *
270     * @return ?string
271     */
272    public function getCloseQuote(): ?string
273    {
274        return $this->closeQuote;
275    }
276
277    /**
278     * Get parameter count
279     *
280     * @return int
281     */
282    public function getParameterCount(): int
283    {
284        return $this->parameterCount;
285    }
286
287    /**
288     * Increment parameter count
289     *
290     * @return AbstractSql
291     */
292    public function incrementParameterCount(): AbstractSql
293    {
294        $this->parameterCount++;
295        return $this;
296    }
297
298    /**
299     * Decrement parameter count
300     *
301     * @return AbstractSql
302     */
303    public function decrementParameterCount(): AbstractSql
304    {
305        $this->parameterCount--;
306        return $this;
307    }
308
309    /**
310     * Check if value is parameter placeholder
311     *
312     * @param  mixed   $value
313     * @param  ?string $column
314     * @return bool
315     */
316    public function isParameter(mixed $value, ?string $column = null): bool
317    {
318        return ((!empty($value) && ($column !== null) && ((':' . $column) == $value)) ||
319                ((preg_match('/^\$\d*\d$/', (string)$value) == 1)) ||
320                (($value == '?')));
321    }
322
323    /**
324     * Get parameter placeholder value
325     *
326     * @param  mixed   $value
327     * @param  ?string $column
328     * @return string
329     */
330    public function getParameter(mixed $value, ?string $column = null): string
331    {
332        $detectedDbType = null;
333        $parameter      = $value;
334
335        // SQLITE
336        if (($column !== null) && ((':' . $column) == $value)) {
337            $detectedDbType = self::SQLITE;
338        // PGSQL
339        } else if (preg_match('/^\$\d*\d$/', $value) == 1) {
340            $detectedDbType = self::PGSQL;
341        // MYSQL/SQLSRV
342        } else if ($value == '?') {
343            $detectedDbType = self::MYSQL;
344        }
345
346        $this->incrementParameterCount();
347
348        // If the parameter is given in a different format than what the db expects, translate it
349        $realDbType = $this->dbType;
350        if ($this->placeholder == ':') {
351            // Either native SQLITE or PDO, in which case also use :param syntax
352            $realDbType = self::SQLITE;
353        }
354
355        if (($detectedDbType !== null) && ($realDbType != $detectedDbType)) {
356            switch ($realDbType) {
357                case self::MYSQL:
358                case self::SQLSRV:
359                    $parameter = '?';
360                    break;
361                case self::PGSQL:
362                    $parameter = '$' . $this->parameterCount;
363                    break;
364                case self::SQLITE:
365                    if ($column !== null) {
366                        $parameter = ':' . $column;
367                    }
368                    break;
369            }
370        }
371
372        return $parameter;
373    }
374
375    /**
376     * Quote the identifier
377     *
378     * @param  string $identifier
379     * @return string
380     */
381    public function quoteId(string $identifier): string
382    {
383        $quotedId = null;
384
385        if (str_contains($identifier, '.')) {
386            $identifierAry = explode('.', $identifier);
387            foreach ($identifierAry as $key => $val) {
388                $identifierAry[$key] = ($val != '*') ? $this->openQuote . $val . $this->closeQuote : $val;
389            }
390            $quotedId = implode('.', $identifierAry);
391        } else if (($identifier != '*') &&
392            ((preg_match('/^\$\d*\d$/', $identifier) == 0) && !is_int($identifier) &&
393                !is_float($identifier) && (preg_match('/^\d*$/', $identifier) == 0))) {
394            $quotedId = $this->openQuote . $identifier . $this->closeQuote;
395        } else {
396            $quotedId = $identifier;
397        }
398
399        return $quotedId;
400    }
401
402    /**
403     * Quote the value (if it is not a numeric value)
404     *
405     * @param  ?string $value
406     * @param  bool    $force
407     * @return float|int|string
408     */
409    public function quote(?string $value = null, bool $force = false): float|int|string
410    {
411        if ($force) {
412            if (($value == '') ||
413                ((preg_match('/^\$\d*\d$/', $value) == 0) &&
414                    !is_int($value) && !is_float($value) && (preg_match('/^\d*$/', $value) == 0))) {
415                $value = "'" . $this->db->escape($value) . "'";
416            }
417        } else {
418            if (($value == '') ||
419                (($value != '?') &&
420                    (!empty($this->openQuote) && !empty($this->closeQuote) &&
421                        !(str_starts_with($value, $this->openQuote) && str_ends_with($value, $this->closeQuote))) &&
422                    (!str_starts_with($value, ':')) && (preg_match('/^\$\d*\d$/', $value) == 0) &&
423                    !is_int($value) && !is_float($value) && (preg_match('/^\d*$/', $value) == 0))) {
424                $value = "'" . $this->db->escape($value) . "'";
425            }
426        }
427
428        return $value;
429    }
430
431    /**
432     * Initialize SQL object
433     *
434     * @param  string $adapter
435     * @return void
436     */
437    protected function init(string $adapter): void
438    {
439        if (stripos($adapter, 'pdo') !== false) {
440            $adapter           = $this->db->getType();
441            $this->placeholder = ':';
442        }
443
444        if (stripos($adapter, 'mysql') !== false) {
445            $this->dbType      = self::MYSQL;
446            $this->idQuoteType = self::BACKTICK;
447            if ($this->placeholder === null) {
448                $this->placeholder = '?';
449            }
450        } else if (stripos($adapter, 'pgsql') !== false) {
451            $this->dbType      = self::PGSQL;
452            $this->idQuoteType = self::DOUBLE_QUOTE;
453            if ($this->placeholder === null) {
454                $this->placeholder = '$';
455            }
456        } else if (stripos($adapter, 'sqlite') !== false) {
457            $this->dbType      = self::SQLITE;
458            $this->idQuoteType = self::DOUBLE_QUOTE;
459            if ($this->placeholder === null) {
460                $this->placeholder = ':';
461            }
462        } else if (stripos($adapter, 'sqlsrv') !== false) {
463            $this->dbType      = self::SQLSRV;
464            $this->idQuoteType = self::BRACKET;
465            if ($this->placeholder === null) {
466                $this->placeholder = '?';
467            }
468        }
469
470        $this->initQuoteType();
471    }
472
473    /**
474     * Initialize quite type
475     *
476     * @return void
477     */
478    protected function initQuoteType(): void
479    {
480        switch ($this->idQuoteType) {
481            case (self::BACKTICK):
482                $this->openQuote   = '`';
483                $this->closeQuote  = '`';
484                break;
485            case (self::DOUBLE_QUOTE):
486                $this->openQuote   = '"';
487                $this->closeQuote  = '"';
488                break;
489            case (self::BRACKET):
490                $this->openQuote   = '[';
491                $this->closeQuote  = ']';
492                break;
493            case (self::NO_QUOTE):
494                $this->openQuote   = null;
495                $this->closeQuote  = null;
496                break;
497        }
498    }
499
500    /**
501     * Check if value contains a standard SQL supported function
502     *
503     * @param  string $value
504     * @return bool
505     */
506    public static function isSupportedFunction(string $value): bool
507    {
508        if (str_contains($value, '(')) {
509            $value = trim(substr($value, 0, strpos($value, '(')));
510        }
511        $value = strtoupper($value);
512
513        return (in_array($value, static::$aggregateFunctions) ||
514            in_array($value, static::$mathFunctions) ||
515            in_array($value, static::$stringFunctions) ||
516            in_array($value, static::$dateTimeFunctions));
517    }
518
519}