Grammar::parseQueries()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 14
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 21
rs 9.4888
1
<?php
2
3
namespace Lagdo\DbAdmin\Driver\Db;
4
5
use Lagdo\DbAdmin\Driver\Entity\TableSelectEntity;
6
use Lagdo\DbAdmin\Driver\Entity\ForeignKeyEntity;
7
use Lagdo\DbAdmin\Driver\Entity\QueryEntity;
8
use Lagdo\DbAdmin\Driver\DriverInterface;
9
use Lagdo\DbAdmin\Driver\Utils\Utils;
10
11
use function preg_match;
12
use function preg_quote;
13
use function substr;
14
use function strlen;
15
use function is_string;
16
use function trim;
17
use function rtrim;
18
use function intval;
19
use function implode;
20
use function array_map;
21
22
abstract class Grammar implements GrammarInterface
23
{
24
    use GrammarTrait;
25
26
    /**
27
     * @var DriverInterface
28
     */
29
    protected $driver;
30
31
    /**
32
     * @var Utils
33
     */
34
    protected $utils;
35
36
    /**
37
     * The constructor
38
     *
39
     * @param DriverInterface $driver
40
     * @param Utils $utils
41
     */
42
    public function __construct(DriverInterface $driver, Utils $utils)
43
    {
44
        $this->driver = $driver;
45
        $this->utils = $utils;
46
    }
47
48
    /**
49
     * @inheritDoc
50
     */
51
    public function escapeTableName(string $idf)
52
    {
53
        return $this->escapeId($idf);
54
    }
55
56
    /**
57
     * @inheritDoc
58
     */
59
    public function getLimitClause(string $query, string $where, int $limit, int $offset = 0)
60
    {
61
        $sql = " $query$where";
62
        if ($limit > 0) {
63
            $sql .= " LIMIT $limit";
64
            if ($offset > 0) {
65
                $sql .= " OFFSET $offset";
66
            }
67
        }
68
        return $sql;
69
    }
70
71
    /**
72
     * @param ForeignKeyEntity $foreignKey
73
     *
74
     * @return array
75
     */
76
    private function fkFields(ForeignKeyEntity $foreignKey)
77
    {
78
        $escape = function ($idf) { return $this->escapeId($idf); };
79
        return [
80
            implode(', ', array_map($escape, $foreignKey->source)),
81
            implode(', ', array_map($escape, $foreignKey->target)),
82
        ];
83
    }
84
85
    /**
86
     * @param ForeignKeyEntity $foreignKey
87
     *
88
     * @return string
89
     */
90
    private function fkTablePrefix(ForeignKeyEntity $foreignKey)
91
    {
92
        $prefix = '';
93
        if ($foreignKey->database !== '' && $foreignKey->database !== $this->driver->database()) {
94
            $prefix .= $this->escapeId($foreignKey->database) . '.';
95
        }
96
        if ($foreignKey->schema !== '' && $foreignKey->schema !== $this->driver->schema()) {
97
            $prefix .= $this->escapeId($foreignKey->schema) . '.';
98
        }
99
        return $prefix;
100
    }
101
102
    /**
103
     * @inheritDoc
104
     */
105
    public function formatForeignKey(ForeignKeyEntity $foreignKey)
106
    {
107
        [$sources, $targets] = $this->fkFields($foreignKey);
108
        $onActions = $this->driver->actions();
109
        $query = " FOREIGN KEY ($sources) REFERENCES " . $this->fkTablePrefix($foreignKey) .
110
            $this->escapeTableName($foreignKey->table) . " ($targets)";
111
        if (preg_match("~^($onActions)\$~", $foreignKey->onDelete)) {
112
            $query .= " ON DELETE {$foreignKey->onDelete}";
113
        }
114
        if (preg_match("~^($onActions)\$~", $foreignKey->onUpdate)) {
115
            $query .= " ON UPDATE {$foreignKey->onUpdate}";
116
        }
117
118
        return $query;
119
    }
120
121
    /**
122
     * @inheritDoc
123
     */
124
    public function buildSelectQuery(TableSelectEntity $select)
125
    {
126
        $query = \implode(', ', $select->fields) . ' FROM ' . $this->escapeTableName($select->table);
127
        $limit = +$select->limit;
128
        $offset = $select->page ? $limit * $select->page : 0;
129
130
        return 'SELECT' . $this->getLimitClause($query, $select->clauses, $limit, $offset);
131
    }
132
133
    /**
134
     * @inheritDoc
135
     */
136
    public function getDefaultValueClause($field)
137
    {
138
        $default = $field->default;
139
        return ($default === null ? '' : ' DEFAULT ' .
140
            (preg_match('~char|binary|text|enum|set~', $field->type) ||
141
            preg_match('~^(?![a-z])~i', $default) ? $this->driver->quote($default) : $default));
142
    }
143
144
    /**
145
     * @inheritDoc
146
     */
147
    public function convertFields(array $columns, array $fields, array $select = [])
148
    {
149
        $clause = '';
150
        foreach ($columns as $key => $val) {
151
            if (!empty($select) && !in_array($this->escapeId($key), $select)) {
152
                continue;
153
            }
154
            $as = $this->convertField($fields[$key]);
155
            if ($as) {
156
                $clause .= ', $as AS ' . $this->escapeId($key);
157
            }
158
        }
159
        return $clause;
160
    }
161
162
    /**
163
     * @inheritDoc
164
     */
165
    public function getRowCountQuery(string $table, array $where, bool $isGroup, array $groups)
166
    {
167
        $query = ' FROM ' . $this->escapeTableName($table);
168
        if (!empty($where)) {
169
            $query .= ' WHERE ' . implode(' AND ', $where);
170
        }
171
        return ($isGroup && ($this->driver->jush() == 'sql' || count($groups) == 1) ?
172
            'SELECT COUNT(DISTINCT ' . implode(', ', $groups) . ")$query" :
173
            'SELECT COUNT(*)' . ($isGroup ? " FROM (SELECT 1$query GROUP BY " .
174
            implode(', ', $groups) . ') x' : $query)
175
        );
176
    }
177
178
    /**
179
     * @param QueryEntity $queryEntity
180
     *
181
     * @return bool
182
     */
183
    private function setDelimiter(QueryEntity $queryEntity)
184
    {
185
        $space = "(?:\\s|/\\*[\s\S]*?\\*/|(?:#|-- )[^\n]*\n?|--\r?\n)";
186
        if ($queryEntity->offset !== 0 ||
187
            !preg_match("~^$space*+DELIMITER\\s+(\\S+)~i", $queryEntity->queries, $match)) {
188
            return false;
189
        }
190
        $queryEntity->delimiter = $match[1];
191
        $queryEntity->queries = substr($queryEntity->queries, strlen($match[0]));
192
        return true;
193
    }
194
195
    /**
196
     * Return the regular expression for queries
197
     *
198
     * @return string
199
     */
200
    abstract protected function queryRegex();
201
    // Original code from Adminer
202
    // {
203
    //     $parse = '[\'"' .
204
    //         ($this->driver->jush() == "sql" ? '`#' :
205
    //         ($this->driver->jush() == "sqlite" ? '`[' :
206
    //         ($this->driver->jush() == "mssql" ? '[' : ''))) . ']|/\*|-- |$' .
207
    //         ($this->driver->jush() == "pgsql" ? '|\$[^$]*\$' : '');
208
    //     return "\\s*|$parse";
209
    // }
210
211
    /**
212
     * @param QueryEntity $queryEntity
213
     * @param string $found
214
     * @param array $match
215
     *
216
     * @return bool
217
     */
218
    private function notQuery(QueryEntity $queryEntity, string $found, array &$match)
219
    {
220
        return preg_match('(' . ($found == '/*' ? '\*/' : ($found == '[' ? ']' :
221
            (preg_match('~^-- |^#~', $found) ? "\n" : preg_quote($found) . "|\\\\."))) . '|$)s',
222
            $queryEntity->queries, $match, PREG_OFFSET_CAPTURE, $queryEntity->offset) > 0;
223
    }
224
225
    /**
226
     * @param QueryEntity $queryEntity
227
     * @param string $found
228
     *
229
     * @return void
230
     */
231
    private function skipComments(QueryEntity $queryEntity, string $found)
232
    {
233
        // Find matching quote or comment end
234
        $match = [];
235
        while ($this->notQuery($queryEntity, $found, $match)) {
236
            //! Respect sql_mode NO_BACKSLASH_ESCAPES
237
            $s = $match[0][0];
238
            $queryEntity->offset = $match[0][1] + strlen($s);
239
            if ($s[0] != "\\") {
240
                break;
241
            }
242
        }
243
    }
244
245
    /**
246
     * @param QueryEntity $queryEntity
247
     *
248
     * @return int
249
     */
250
    private function nextQueryPos(QueryEntity $queryEntity)
251
    {
252
        // TODO: Move this to driver implementations
253
        $parse = $this->queryRegex();
254
        $delimiter = preg_quote($queryEntity->delimiter);
255
        // Should always match
256
        preg_match("($delimiter$parse)", $queryEntity->queries, $match,
257
            PREG_OFFSET_CAPTURE, $queryEntity->offset);
258
        [$found, $pos] = $match[0];
259
        if (!is_string($found) && $queryEntity->queries == '') {
260
            return -1;
261
        }
262
        $queryEntity->offset = $pos + strlen($found);
263
        if (empty($found) || rtrim($found) == $queryEntity->delimiter) {
264
            return intval($pos);
265
        }
266
        // Find matching quote or comment end
267
        $this->skipComments($queryEntity, $found);
268
        return 0;
269
    }
270
271
    /**
272
     * @inheritDoc
273
     */
274
    public function parseQueries(QueryEntity $queryEntity)
275
    {
276
        $queryEntity->queries = trim($queryEntity->queries);
277
        while ($queryEntity->queries !== '') {
278
            if ($this->setDelimiter($queryEntity)) {
279
                continue;
280
            }
281
            $pos = $this->nextQueryPos($queryEntity);
282
            if ($pos < 0) {
283
                return false;
284
            }
285
            if ($pos === 0) {
286
                continue;
287
            }
288
            // End of a query
289
            $queryEntity->query = substr($queryEntity->queries, 0, $pos);
290
            $queryEntity->queries = substr($queryEntity->queries, $queryEntity->offset);
291
            $queryEntity->offset = 0;
292
            return true;
293
        }
294
        return false;
295
    }
296
}
297