Passed
Push — main ( ccb13a...a45c19 )
by Thierry
01:38
created

Grammar::nextQueryPos()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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