Passed
Push — 2.x ( 18bce1...b13a23 )
by Maxim
17:24
created

SQLServerCompiler::compileJsonOrderBy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Driver\SQLServer;
13
14
use Cycle\Database\Driver\Compiler;
15
use Cycle\Database\Driver\Quoter;
16
use Cycle\Database\Driver\SQLServer\Injection\CompileJson;
17
use Cycle\Database\Injection\Fragment;
18
use Cycle\Database\Injection\FragmentInterface;
19
use Cycle\Database\Injection\Parameter;
20
use Cycle\Database\Query\QueryParameters;
21
22
/**
23
 * Microsoft SQL server specific syntax compiler.
24
 */
25
class SQLServerCompiler extends Compiler
26
{
27
    /**
28
     * Column to be used as ROW_NUMBER in fallback selection mechanism, attention! Amount of columns
29
     * in result set will be increaced by 1!
30
     */
31
    public const ROW_NUMBER = '_ROW_NUMBER_';
32
33
    /**
34
     * @psalm-return non-empty-string
35
     */
36
    protected function insertQuery(QueryParameters $params, Quoter $q, array $tokens): string
37
    {
38
        if (empty($tokens['return'])) {
39
            return parent::insertQuery($params, $q, $tokens);
40 450
        }
41
42 450
        $values = [];
43 450
        foreach ($tokens['values'] as $value) {
44
            $values[] = $this->value($params, $q, $value);
45 450
        }
46
47 432
        $output = \implode(',', \array_map(
48
            fn (string|FragmentInterface|null $return) => $return instanceof FragmentInterface
49
                ? $this->fragment($params, $q, $return)
50
                : 'INSERTED.' . $this->quoteIdentifier($return),
0 ignored issues
show
Bug introduced by
It seems like $return can also be of type null; however, parameter $identifier of Cycle\Database\Driver\Compiler::quoteIdentifier() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

50
                : 'INSERTED.' . $this->quoteIdentifier(/** @scrutinizer ignore-type */ $return),
Loading history...
51
            $tokens['return']
52
        ));
53
54
        if ($tokens['columns'] === []) {
55
            return \sprintf(
56 24
                'INSERT INTO %s OUTPUT %s DEFAULT VALUES',
57 24
                $this->name($params, $q, $tokens['table'], true),
58 24
                $output
59 24
            );
60
        }
61
62
        return \sprintf(
63 24
            'INSERT INTO %s (%s) OUTPUT %s VALUES %s',
64 24
            $this->name($params, $q, $tokens['table'], true),
65
            $this->columns($params, $q, $tokens['columns']),
66 24
            $output,
67 24
            \implode(', ', $values)
68 24
        );
69 24
    }
70
71
    /**
72
     * {@inheritdoc}
73
     *
74
     * Attention, limiting and ordering UNIONS will fail in SQL SERVER < 2012.
75
     * For future upgrades: think about using top command.
76
     *
77
     * @link http://stackoverflow.com/questions/603724/how-to-implement-limit-with-microsoft-sql-server
78
     * @link http://stackoverflow.com/questions/971964/limit-10-20-in-sql-server
79
     */
80
    protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens): string
81 450
    {
82
        $limit = $tokens['limit'];
83
        $offset = $tokens['offset'];
84
85
        if (($limit === null && $offset === null) || $tokens['orderBy'] !== []) {
86
            //When no limits are specified we can use normal query syntax
87
            return call_user_func_array([$this, 'baseSelect'], func_get_args());
88 450
        }
89 446
90
        /**
91
         * We are going to use fallback mechanism here in order to properly select limited data from
92
         * database. Avoid usage of LIMIT/OFFSET without proper ORDER BY statement.
93 30
         *
94 6
         * Please see set of alerts raised in SelectQuery builder.
95 6
         */
96
        $tokens['columns'][] = new Fragment(
97 6
            sprintf(
98 6
                'ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS %s',
99 6
                $this->name($params, $q, self::ROW_NUMBER)
100
            )
101
        );
102 6
103
        $tokens['limit'] = null;
104
        $tokens['offset'] = null;
105 24
106
        return sprintf(
107
            "SELECT * FROM (\n%s\n) AS [ORD_FALLBACK] %s",
108 24
            $this->baseSelect($params, $q, $tokens),
109
            $this->limit($params, $q, $limit, $offset, self::ROW_NUMBER)
110 24
        );
111 20
    }
112 20
113 20
    /**
114
     * {@inheritdoc}
115 4
     *
116 4
     * @param string $rowNumber Row used in a fallback sorting mechanism, ONLY when no ORDER BY
117
     *                          specified.
118
     *
119 24
     * @link http://stackoverflow.com/questions/2135418/equivalent-of-limit-and-offset-for-sql-server
120
     */
121
    protected function limit(
122
        QueryParameters $params,
123
        Quoter $q,
124
        int $limit = null,
125 450
        int $offset = null,
126
        string $rowNumber = null
127
    ): string {
128 450
        if ($limit === null && $offset === null) {
129 450
            return '';
130 450
        }
131
132 450
        //Modern SQLServer are easier to work with
133 70
        if ($rowNumber === null) {
134
            $statement = 'OFFSET ? ROWS ';
135
            $params->push(new Parameter((int)$offset));
136 450
137 450
            if ($limit !== null) {
138 450
                $statement .= 'FETCH FIRST ? ROWS ONLY';
139 450
                $params->push(new Parameter($limit));
140 450
            }
141 450
142 450
            return trim($statement);
143 450
        }
144 450
145 450
        $statement = "WHERE {$this->name($params, $q, $rowNumber)} ";
146 450
147 450
        //0 = row_number(1)
148 450
        ++$offset;
149
150
        if ($limit !== null) {
151
            $statement .= 'BETWEEN ? AND ?';
152
            $params->push(new Parameter((int)$offset));
153
            $params->push(new Parameter($offset + $limit - 1));
154
        } else {
155
            $statement .= '>= ?';
156
            $params->push(new Parameter((int)$offset));
157
        }
158
159
        return $statement;
160
    }
161
162
    protected function compileJsonOrderBy(string $path): FragmentInterface
163
    {
164
        return new CompileJson($path);
165
    }
166
167
    /**
168
     * @inheritDoc
169
     */
170
    private function baseSelect(QueryParameters $params, Quoter $q, array $tokens): string
171
    {
172
        // This statement(s) parts should be processed first to define set of table and column aliases
173
        $tables = [];
174
        foreach ($tokens['from'] as $table) {
175
            $tables[] = $this->name($params, $q, $table, true);
176
        }
177
        foreach ($tokens['join'] as $join) {
178
            $this->nameWithAlias(new QueryParameters(), $q, $join['outer'], $join['alias'], true);
179
        }
180
181
        return sprintf(
182
            "SELECT%s %s\nFROM %s%s%s%s%s%s%s%s%s",
183
            $this->optional(' ', $this->distinct($params, $q, $tokens['distinct'])),
184
            $this->columns($params, $q, $tokens['columns']),
185
            implode(', ', $tables),
186
            $this->optional(' ', $tokens['forUpdate'] ? 'WITH (UPDLOCK,ROWLOCK)' : '', ' '),
187
            $this->optional(' ', $this->joins($params, $q, $tokens['join']), ' '),
188
            $this->optional("\nWHERE", $this->where($params, $q, $tokens['where'])),
189
            $this->optional("\nGROUP BY", $this->groupBy($params, $q, $tokens['groupBy']), ' '),
190
            $this->optional("\nHAVING", $this->where($params, $q, $tokens['having'])),
191
            $this->optional("\n", $this->unions($params, $q, $tokens['union'])),
192
            $this->optional("\nORDER BY", $this->orderBy($params, $q, $tokens['orderBy'])),
193
            $this->optional("\n", $this->limit($params, $q, $tokens['limit'], $tokens['offset']))
194
        );
195
    }
196
}
197