Passed
Push — master ( 27ca4d...8d618a )
by Ondřej
02:45 queued 10s
created

StatementExecution::rawCommand()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
namespace Ivory\Connection;
4
5
use Ivory\Exception\InvalidStateException;
6
use Ivory\Exception\ResultDimensionException;
7
use Ivory\Exception\StatementException;
8
use Ivory\Exception\ConnectionException;
9
use Ivory\Exception\StatementExceptionFactory;
10
use Ivory\Exception\UsageException;
11
use Ivory\Ivory;
12
use Ivory\Query\ICommand;
13
use Ivory\Query\IRelationDefinition;
14
use Ivory\Query\ISqlPatternStatement;
15
use Ivory\Query\SqlCommand;
16
use Ivory\Query\SqlRelationDefinition;
17
use Ivory\Relation\IColumn;
18
use Ivory\Relation\ITuple;
19
use Ivory\Result\AsyncCommandResult;
20
use Ivory\Result\AsyncQueryResult;
21
use Ivory\Result\AsyncResult;
22
use Ivory\Result\CommandResult;
23
use Ivory\Result\CopyInResult;
24
use Ivory\Result\CopyOutResult;
25
use Ivory\Result\IAsyncCommandResult;
26
use Ivory\Result\IAsyncQueryResult;
27
use Ivory\Result\IAsyncResult;
28
use Ivory\Result\ICommandResult;
29
use Ivory\Result\IQueryResult;
30
use Ivory\Result\IResult;
31
use Ivory\Result\QueryResult;
32
33
class StatementExecution implements IStatementExecution
34
{
35
    private $connCtl;
36
    private $typeCtl;
37
    private $stmtExFactory;
38
39
    public function __construct(ConnectionControl $connCtl, ITypeControl $typeCtl)
40
    {
41
        $this->connCtl = $connCtl;
42
        $this->typeCtl = $typeCtl;
43
        $this->stmtExFactory = Ivory::getCoreFactory()->createStatementExceptionFactory($this);
44
    }
45
46
    public function query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): IQueryResult
47
    {
48
        $sql = $this->extractRawQuery($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
49
        return $this->rawQuery($sql);
50
    }
51
52
    public function queryAsync($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): IAsyncQueryResult
53
    {
54
        $sql = $this->extractRawQuery($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
55
        $this->sendRawStatement($sql);
56
        return new AsyncQueryResult(
57
            function () use ($sql) {
58
                return $this->fetchStatementResult($sql);
59
            }
60
        );
61
    }
62
63
    private function extractRawQuery($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): string
64
    {
65
        $typeDict = $this->typeCtl->getTypeDictionary();
66
67
        try {
68
            if ($sqlFragmentPatternOrRelationDefinition instanceof ISqlPatternStatement) {
69
                $this->checkMaxArgs($fragmentsAndParams, 1);
70
                $sqlRelDef = $sqlFragmentPatternOrRelationDefinition;
71
                $serializeArgs = $fragmentsAndParams;
72
                return $sqlRelDef->toSql($typeDict, ...$serializeArgs);
0 ignored issues
show
Bug introduced by
$serializeArgs is expanded, but the parameter $namedParameterValues of Ivory\Query\ISqlPatternStatement::toSql() does not expect variable arguments. ( Ignorable by Annotation )

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

72
                return $sqlRelDef->toSql($typeDict, /** @scrutinizer ignore-type */ ...$serializeArgs);
Loading history...
73
            } elseif ($sqlFragmentPatternOrRelationDefinition instanceof IRelationDefinition) {
74
                $this->checkMaxArgs($fragmentsAndParams, 0);
75
                $relDef = $sqlFragmentPatternOrRelationDefinition;
76
                return $relDef->toSql($typeDict);
77
            } else {
78
                $relDef = SqlRelationDefinition::fromFragments(
79
                    $sqlFragmentPatternOrRelationDefinition,
80
                    ...$fragmentsAndParams
81
                );
82
                return $relDef->toSql($typeDict);
83
            }
84
        } catch (InvalidStateException $e) {
85
            throw new \InvalidArgumentException($e->getMessage());
86
        }
87
    }
88
89
    public function querySingleTuple($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): ?ITuple
90
    {
91
        $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
92
        switch ($rel->count()) {
93
            case 1:
94
                return $rel->tuple();
95
            case 0:
96
                return null;
97
            default:
98
                throw new ResultDimensionException(
99
                    "The query should have resulted in at most one row, but has {$rel->count()} rows."
100
                );
101
        }
102
    }
103
104
    public function querySingleColumn($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): IColumn
105
    {
106
        $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
107
108
        $colCnt = count($rel->getColumns());
109
        if ($colCnt != 1) {
110
            throw new ResultDimensionException(
111
                "The query should have resulted in exactly one column, but has $colCnt columns."
112
            );
113
        }
114
115
        return $rel->col(0);
116
    }
117
118
    public function querySingleValue($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams)
119
    {
120
        $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
121
122
        $colCnt = count($rel->getColumns());
123
        if ($colCnt != 1) {
124
            throw new ResultDimensionException(
125
                "The query should have resulted in exactly one column, but has $colCnt columns."
126
            );
127
        }
128
129
        switch ($rel->count()) {
130
            case 1:
131
                return $rel->value();
132
            case 0:
133
                return null;
134
            default:
135
                throw new ResultDimensionException(
136
                    "The query should have resulted in at most one row, but has {$rel->count()} rows."
137
                );
138
        }
139
    }
140
141
    public function command($sqlFragmentPatternOrCommand, ...$fragmentsAndParams): ICommandResult
142
    {
143
        $sql = $this->extractRawCommand($sqlFragmentPatternOrCommand, ...$fragmentsAndParams);
144
        return $this->rawCommand($sql);
145
    }
146
147
    public function commandAsync($sqlFragmentPatternOrCommand, ...$fragmentsAndParams): IAsyncCommandResult
148
    {
149
        $sql = $this->extractRawCommand($sqlFragmentPatternOrCommand, ...$fragmentsAndParams);
150
        $this->sendRawStatement($sql);
151
        return new AsyncCommandResult(
152
            function () use ($sql) {
153
                return $this->fetchStatementResult($sql);
154
            }
155
        );
156
    }
157
158
    private function extractRawCommand($sqlFragmentPatternOrCommand, ...$fragmentsAndParams): string
159
    {
160
        $typeDict = $this->typeCtl->getTypeDictionary();
161
162
        try {
163
            if ($sqlFragmentPatternOrCommand instanceof ISqlPatternStatement) {
164
                $this->checkMaxArgs($fragmentsAndParams, 1);
165
                $sqlCommand = $sqlFragmentPatternOrCommand;
166
                $serializeArgs = $fragmentsAndParams;
167
                return $sqlCommand->toSql($typeDict, ...$serializeArgs);
0 ignored issues
show
Bug introduced by
$serializeArgs is expanded, but the parameter $namedParameterValues of Ivory\Query\ISqlPatternStatement::toSql() does not expect variable arguments. ( Ignorable by Annotation )

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

167
                return $sqlCommand->toSql($typeDict, /** @scrutinizer ignore-type */ ...$serializeArgs);
Loading history...
168
            } elseif ($sqlFragmentPatternOrCommand instanceof ICommand) {
169
                $this->checkMaxArgs($fragmentsAndParams, 0);
170
                $command = $sqlFragmentPatternOrCommand;
171
                return $command->toSql($typeDict);
172
            } else {
173
                $command = SqlCommand::fromFragments($sqlFragmentPatternOrCommand, ...$fragmentsAndParams);
174
                return $command->toSql($typeDict);
175
            }
176
        } catch (InvalidStateException $e) {
177
            throw new \InvalidArgumentException($e->getMessage());
178
        }
179
    }
180
181
    private function checkMaxArgs($args, int $maxCount): void
182
    {
183
        if (count($args) > $maxCount) {
184
            throw new \InvalidArgumentException('Too many arguments given.');
185
        }
186
    }
187
188
    public function rawQuery(string $sqlQuery): IQueryResult
189
    {
190
        $result = $this->executeRawStatement($sqlQuery);
191
        if ($result instanceof IQueryResult) {
192
            return $result;
193
        } else {
194
            throw new UsageException(
195
                'The supplied SQL statement was supposed to be a query, but it did not return a result set. ' .
196
                'Did you mean to call command() or rawCommand()?'
197
            );
198
        }
199
    }
200
201
    public function rawCommand(string $sqlCommand): ICommandResult
202
    {
203
        $result = $this->executeRawStatement($sqlCommand);
204
        if ($result instanceof ICommandResult) {
205
            return $result;
206
        } else {
207
            throw new UsageException(
208
                'The supplied SQL statement was supposed to be a command, but it returned a result set. ' .
209
                'Did you mean to call query() or rawQuery()?'
210
            );
211
        }
212
    }
213
214
    public function executeStatement($sqlStatement): IResult
215
    {
216
        $rawStatement = $this->extractRawStatement($sqlStatement);
217
        return $this->executeRawStatement($rawStatement);
218
    }
219
220
    public function executeStatementAsync($sqlStatement): IAsyncResult
221
    {
222
        $rawStatement = $this->extractRawStatement($sqlStatement);
223
        $this->sendRawStatement($rawStatement);
224
        return new AsyncResult(
225
            function () use ($rawStatement) {
226
                return $this->fetchStatementResult($rawStatement);
227
            }
228
        );
229
    }
230
231
    private function extractRawStatement($sqlStatement): string
232
    {
233
        if (is_string($sqlStatement)) {
234
            return $sqlStatement;
235
        } elseif ($sqlStatement instanceof ISqlPatternStatement) {
236
            $typeDict = $this->typeCtl->getTypeDictionary();
237
            return $sqlStatement->toSql($typeDict);
238
        } else {
239
            throw new \InvalidArgumentException(
240
                '$sqlStatement must either by a string or ' . ISqlPatternStatement::class
241
            );
242
        }
243
    }
244
245
    private function executeRawStatement(string $sqlStatement): IResult
246
    {
247
        $this->sendRawStatement($sqlStatement);
248
        return $this->fetchStatementResult($sqlStatement);
249
    }
250
251
    private function sendRawStatement(string $sqlStatement): void
252
    {
253
        $connHandler = $this->connCtl->requireConnection();
254
255
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
256
            usleep(1);
257
        }
258
259
        // pg_send_query_params(), as opposed to pg_send_query(), prevents $stmt from containing multiple statements
260
        $sent = pg_send_query_params($connHandler, $sqlStatement, []);
261
        if (!$sent) {
262
            // TODO: consider trapping errors to get more detailed error message
263
            throw new ConnectionException('Error sending the query to the database.');
264
        }
265
    }
266
267
    private function fetchStatementResult(string $sqlStatement): IResult
268
    {
269
        $connHandler = $this->connCtl->requireConnection();
270
271
        $resultHandler = pg_get_result($connHandler);
272
        if ($resultHandler === false) {
273
            throw new ConnectionException('No results received from the database.');
274
        }
275
        /**
276
         * For erroneous queries, one must call pg_get_result() once again to update the structures at the client side.
277
         * Even worse, a loop might actually be needed according to
278
         * {@link http://www.postgresql.org/message-id/flat/[email protected]#[email protected]},
279
         * which does not sound logical, though, and hopefully was just meant as a generic way to processing results of
280
         * multiple statements sent in a single pg_send_query() call. Anyway, looping seems to be the safest solution.
281
         */
282
        while (pg_get_result($connHandler) !== false) {
283
            trigger_error('The database gave an unexpected result set.', E_USER_NOTICE);
284
        }
285
286
        return $this->processResult($connHandler, $resultHandler, $sqlStatement);
287
    }
288
289
    public function runScript(string $sqlScript): array
290
    {
291
        $connHandler = $this->connCtl->requireConnection();
292
293
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
294
            usleep(1);
295
        }
296
297
        $sent = pg_send_query($connHandler, $sqlScript);
298
        if (!$sent) {
299
            throw new ConnectionException('Error sending the query to the database.');
300
        }
301
302
        $resHandlers = [];
303
        while (($res = pg_get_result($connHandler)) !== false) {
304
            /* NOTE: Cannot process the result right away - the remaining results must all be read, or they would, in
305
             * case of error, block the connection from accepting further queries.
306
             */
307
            $resHandlers[] = $res;
308
        }
309
        $results = [];
310
        foreach ($resHandlers as $resHandler) {
311
            $results[] = $this->processResult($connHandler, $resHandler, $sqlScript);
312
        }
313
        return $results;
314
    }
315
316
    /**
317
     * @param resource $connHandler
318
     * @param resource $resHandler
319
     * @param string $query
320
     * @return IResult
321
     * @throws StatementException upon an SQL statement error
322
     */
323
    private function processResult($connHandler, $resHandler, string $query): IResult
324
    {
325
        $notice = $this->getLastResultNotice();
326
        $stat = pg_result_status($resHandler);
327
        switch ($stat) {
328
            case PGSQL_COMMAND_OK:
329
                return new CommandResult($resHandler, $notice);
330
            case PGSQL_TUPLES_OK:
331
                return new QueryResult($resHandler, $this->typeCtl, $notice);
332
            case PGSQL_COPY_IN:
333
                return new CopyInResult($connHandler, $resHandler, $notice);
334
            case PGSQL_COPY_OUT:
335
                return new CopyOutResult($connHandler, $resHandler, $notice);
336
337
            case PGSQL_EMPTY_QUERY:
338
            case PGSQL_BAD_RESPONSE:
339
            case PGSQL_NONFATAL_ERROR:
340
                // non-fatal errors are supposedly not possible to be received by the PHP client library, but anyway...
341
            case PGSQL_FATAL_ERROR:
342
                throw $this->stmtExFactory->createException($resHandler, $query, Ivory::getStatementExceptionFactory());
343
344
            default:
345
                throw new \UnexpectedValueException("Unexpected PostgreSQL statement result status: $stat", $stat);
346
        }
347
    }
348
349
    private function getLastResultNotice(): ?string
350
    {
351
        $resNotice = pg_last_notice($this->connCtl->requireConnection());
352
        $connNotice = $this->connCtl->getLastNotice();
353
        if ($resNotice !== $connNotice) {
354
            $this->connCtl->setLastNotice($resNotice);
355
            return $resNotice;
356
        } else {
357
            return null;
358
        }
359
    }
360
361
    public function getStatementExceptionFactory(): StatementExceptionFactory
362
    {
363
        return $this->stmtExFactory;
364
    }
365
}
366