Completed
Push — master ( 6001b7...42953d )
by Ondřej
08:39
created

StatementExecution::checkDefinitionParams()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 4
nop 2
1
<?php
2
declare(strict_types=1);
3
4
namespace Ivory\Connection;
5
6
use Ivory\Exception\InvalidStateException;
7
use Ivory\Exception\ResultDimensionException;
8
use Ivory\Exception\StatementException;
9
use Ivory\Exception\ConnectionException;
10
use Ivory\Exception\StatementExceptionFactory;
11
use Ivory\Exception\UsageException;
12
use Ivory\Ivory;
13
use Ivory\Query\ICommand;
14
use Ivory\Query\IRelationDefinition;
15
use Ivory\Query\ISqlPatternStatement;
16
use Ivory\Query\SqlCommand;
17
use Ivory\Query\SqlRelationDefinition;
18
use Ivory\Relation\IColumn;
19
use Ivory\Relation\ITuple;
20
use Ivory\Result\CommandResult;
21
use Ivory\Result\CopyInResult;
22
use Ivory\Result\CopyOutResult;
23
use Ivory\Result\ICommandResult;
24
use Ivory\Result\IQueryResult;
25
use Ivory\Result\IResult;
26
use Ivory\Result\QueryResult;
27
28
class StatementExecution implements IStatementExecution
29
{
30
    private $connCtl;
31
    private $typeCtl;
32
    private $stmtExFactory;
33
34
    public function __construct(ConnectionControl $connCtl, ITypeControl $typeCtl)
35
    {
36
        $this->connCtl = $connCtl;
37
        $this->typeCtl = $typeCtl;
38
        $this->stmtExFactory = Ivory::getCoreFactory()->createStatementExceptionFactory($this);
39
    }
40
41
    public function query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): IQueryResult
42
    {
43
        if ($sqlFragmentPatternOrRelationDefinition instanceof IRelationDefinition) {
44
            $relDef = $sqlFragmentPatternOrRelationDefinition;
45
            $serializeArgs = $fragmentsAndParams;
46
            $this->checkDefinitionParams($relDef, $serializeArgs);
47
        } else {
48
            $relDef = SqlRelationDefinition::fromFragments(
49
                $sqlFragmentPatternOrRelationDefinition,
50
                ...$fragmentsAndParams
51
            );
52
            $serializeArgs = [];
53
        }
54
55
        try {
56
            $typeDict = $this->typeCtl->getTypeDictionary();
57
            $sql = $relDef->toSql($typeDict, ...$serializeArgs);
0 ignored issues
show
Unused Code introduced by
The call to IRelationDefinition::toSql() has too many arguments starting with $serializeArgs.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
58
        } catch (InvalidStateException $e) {
59
            throw new \InvalidArgumentException($e->getMessage());
60
        }
61
62
        return $this->rawQuery($sql);
63
    }
64
65
    public function querySingleTuple($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): ITuple
66
    {
67
        $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
68
        if ($rel->count() != 1) {
69
            throw new ResultDimensionException(
70
                "The query should have resulted in exactly one row, but has {$rel->count()} rows."
71
            );
72
        }
73
        return $rel->tuple();
74
    }
75
76
    public function querySingleColumn($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): IColumn
77
    {
78
        $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
79
80
        $colCnt = count($rel->getColumns());
81
        if ($colCnt != 1) {
82
            throw new ResultDimensionException(
83
                "The query should have resulted in exactly one column, but has $colCnt columns."
84
            );
85
        }
86
87
        return $rel->col(0);
88
    }
89
90
    public function querySingleValue($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams)
91
    {
92
        $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
93
94
        $rowCnt = $rel->count();
95
        if ($rowCnt != 1) {
96
            throw new ResultDimensionException(
97
                "The query should have resulted in exactly one row, but has $rowCnt rows."
98
            );
99
        }
100
101
        $colCnt = count($rel->getColumns());
102
        if ($colCnt != 1) {
103
            throw new ResultDimensionException(
104
                "The query should have resulted in exactly one column, but has $colCnt columns."
105
            );
106
        }
107
108
        return $rel->value();
109
    }
110
111
    public function command($sqlFragmentPatternOrCommand, ...$fragmentsAndParams): ICommandResult
112
    {
113
        if ($sqlFragmentPatternOrCommand instanceof ICommand) {
114
            $command = $sqlFragmentPatternOrCommand;
115
            $serializeArgs = $fragmentsAndParams;
116
            $this->checkDefinitionParams($command, $serializeArgs);
117
        } else {
118
            $command = SqlCommand::fromFragments($sqlFragmentPatternOrCommand, ...$fragmentsAndParams);
119
            $serializeArgs = [];
120
        }
121
122
        try {
123
            $typeDict = $this->typeCtl->getTypeDictionary();
124
            $sql = $command->toSql($typeDict, ...$serializeArgs);
0 ignored issues
show
Unused Code introduced by
The call to ICommand::toSql() has too many arguments starting with $serializeArgs.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
125
        } catch (InvalidStateException $e) {
126
            throw new \InvalidArgumentException($e->getMessage());
127
        }
128
129
        return $this->rawCommand($sql);
130
    }
131
132
    private function checkDefinitionParams($definition, $params)
133
    {
134
        if ($definition instanceof ISqlPatternStatement) {
135
            if (count($params) > 1) {
136
                throw new \InvalidArgumentException('Too many arguments given.');
137
            }
138
        } else {
139
            if (count($params) > 0) {
140
                throw new \InvalidArgumentException('Too many arguments given.');
141
            }
142
        }
143
    }
144
145
    public function rawQuery(string $sqlQuery): IQueryResult
146
    {
147
        $result = $this->executeRawStatement($sqlQuery);
148
        if ($result instanceof IQueryResult) {
149
            return $result;
150
        } else {
151
            throw new UsageException(
152
                'The supplied SQL statement was supposed to be a query, but it did not return a result set. ' .
153
                'Did you mean to call command() or rawCommand()?'
154
            );
155
        }
156
    }
157
158
    public function rawCommand(string $sqlCommand): ICommandResult
159
    {
160
        $result = $this->executeRawStatement($sqlCommand);
161
        if ($result instanceof ICommandResult) {
162
            return $result;
163
        } else {
164
            throw new UsageException(
165
                'The supplied SQL statement was supposed to be a command, but it returned a result set. ' .
166
                'Did you mean to call query() or rawQuery()?'
167
            );
168
        }
169
    }
170
171
    public function executeStatement($sqlStatement): IResult
172
    {
173
        if (is_string($sqlStatement)) {
174
            $rawStatement = $sqlStatement;
175
        } elseif ($sqlStatement instanceof ISqlPatternStatement) {
176
            $typeDict = $this->typeCtl->getTypeDictionary();
177
            $rawStatement = $sqlStatement->toSql($typeDict);
178
        } else {
179
            throw new \InvalidArgumentException(
180
                '$sqlStatement must either by a string or ' . ISqlPatternStatement::class
181
            );
182
        }
183
184
        return $this->executeRawStatement($rawStatement);
185
    }
186
187
    private function executeRawStatement(string $sqlStatement): IResult
188
    {
189
        $connHandler = $this->connCtl->requireConnection();
190
191
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
192
            usleep(1);
193
        }
194
195
        // pg_send_query_params(), as opposed to pg_send_query(), prevents $stmt from containing multiple statements
196
        $sent = pg_send_query_params($connHandler, $sqlStatement, []);
197
        if (!$sent) {
198
            // TODO: consider trapping errors to get more detailed error message
199
            throw new ConnectionException('Error sending the query to the database.');
200
        }
201
202
        $resultHandler = pg_get_result($connHandler);
203
        if ($resultHandler === false) {
204
            throw new ConnectionException('No results received from the database.');
205
        }
206
        /* For erroneous queries, one must call pg_get_result() once again to update the structures at the client side.
207
         * Even worse, a loop might actually be needed according to
208
         * http://www.postgresql.org/message-id/flat/[email protected]#[email protected],
209
         * which does not sound logical, though, and hopefully was just meant as a generic way to processing results of
210
         * multiple statements sent in a single pg_send_query() call. Anyway, looping seems to be the safest solution.
211
         */
212
        while (pg_get_result($connHandler) !== false) {
213
            trigger_error('The database gave an unexpected result set.', E_USER_NOTICE);
214
        }
215
216
        return $this->processResult($connHandler, $resultHandler, $sqlStatement);
217
    }
218
219
    public function runScript(string $sqlScript): array
220
    {
221
        $connHandler = $this->connCtl->requireConnection();
222
223
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
224
            usleep(1);
225
        }
226
227
        $sent = pg_send_query($connHandler, $sqlScript);
228
        if (!$sent) {
229
            throw new ConnectionException('Error sending the query to the database.');
230
        }
231
232
        $resHandlers = [];
233
        while (($res = pg_get_result($connHandler)) !== false) {
234
            /* NOTE: Cannot process the result right away - the remaining results must all be read, or they would, in
235
             * case of error, block the connection from accepting further queries.
236
             */
237
            $resHandlers[] = $res;
238
        }
239
        $results = [];
240
        foreach ($resHandlers as $resHandler) {
241
            $results[] = $this->processResult($connHandler, $resHandler, $sqlScript);
242
        }
243
        return $results;
244
    }
245
246
    /**
247
     * @param resource $connHandler
248
     * @param resource $resHandler
249
     * @param string $query
250
     * @return IResult
251
     * @throws StatementException upon an SQL statement error
252
     */
253
    private function processResult($connHandler, $resHandler, string $query): IResult
254
    {
255
        $notice = $this->getLastResultNotice();
256
        $stat = pg_result_status($resHandler);
257
        switch ($stat) {
258
            case PGSQL_COMMAND_OK:
259
                return new CommandResult($resHandler, $notice);
260
            case PGSQL_TUPLES_OK:
261
                return new QueryResult($resHandler, $this->typeCtl, $notice);
262
            case PGSQL_COPY_IN:
263
                return new CopyInResult($connHandler, $resHandler, $notice);
264
            case PGSQL_COPY_OUT:
265
                return new CopyOutResult($connHandler, $resHandler, $notice);
266
267
            case PGSQL_EMPTY_QUERY:
268
            case PGSQL_BAD_RESPONSE:
269
            case PGSQL_NONFATAL_ERROR:
270
                // non-fatal errors are supposedly not possible to be received by the PHP client library, but anyway...
271
            case PGSQL_FATAL_ERROR:
272
                throw $this->stmtExFactory->createException($resHandler, $query, Ivory::getStatementExceptionFactory());
273
274
            default:
275
                throw new \UnexpectedValueException("Unexpected PostgreSQL statement result status: $stat", $stat);
276
        }
277
    }
278
279
    private function getLastResultNotice(): ?string
280
    {
281
        $resNotice = pg_last_notice($this->connCtl->requireConnection());
282
        $connNotice = $this->connCtl->getLastNotice();
283
        if ($resNotice !== $connNotice) {
284
            $this->connCtl->setLastNotice($resNotice);
285
            return $resNotice;
286
        } else {
287
            return null;
288
        }
289
    }
290
291
    public function getStatementExceptionFactory(): StatementExceptionFactory
292
    {
293
        return $this->stmtExFactory;
294
    }
295
}
296