Passed
Push — master ( b29973...763972 )
by Ondřej
02:52
created

StatementExecution::command()   B

Complexity

Conditions 4
Paths 9

Size

Total Lines 24
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.6845
c 0
b 0
f 0
cc 4
eloc 18
nc 9
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
        $typeDict = $this->typeCtl->getTypeDictionary();
44
45
        try {
46
            if ($sqlFragmentPatternOrRelationDefinition instanceof ISqlPatternStatement) {
47
                $this->checkMaxArgs($fragmentsAndParams, 1);
48
                $sqlRelDef = $sqlFragmentPatternOrRelationDefinition;
49
                $serializeArgs = $fragmentsAndParams;
50
                $sql = $sqlRelDef->toSql($typeDict, ...$serializeArgs);
51
            } elseif ($sqlFragmentPatternOrRelationDefinition instanceof IRelationDefinition) {
52
                $this->checkMaxArgs($fragmentsAndParams, 0);
53
                $relDef = $sqlFragmentPatternOrRelationDefinition;
54
                $sql = $relDef->toSql($typeDict);
55
            } else {
56
                $relDef = SqlRelationDefinition::fromFragments(
57
                    $sqlFragmentPatternOrRelationDefinition,
58
                    ...$fragmentsAndParams
59
                );
60
                $sql = $relDef->toSql($typeDict);
61
            }
62
        } catch (InvalidStateException $e) {
63
            throw new \InvalidArgumentException($e->getMessage());
64
        }
65
66
        return $this->rawQuery($sql);
67
    }
68
69
    public function querySingleTuple($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): ITuple
70
    {
71
        $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
72
        if ($rel->count() != 1) {
73
            throw new ResultDimensionException(
74
                "The query should have resulted in exactly one row, but has {$rel->count()} rows."
75
            );
76
        }
77
        return $rel->tuple();
78
    }
79
80
    public function querySingleColumn($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): IColumn
81
    {
82
        $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
83
84
        $colCnt = count($rel->getColumns());
85
        if ($colCnt != 1) {
86
            throw new ResultDimensionException(
87
                "The query should have resulted in exactly one column, but has $colCnt columns."
88
            );
89
        }
90
91
        return $rel->col(0);
92
    }
93
94
    public function querySingleValue($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams)
95
    {
96
        $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
97
98
        $rowCnt = $rel->count();
99
        if ($rowCnt != 1) {
100
            throw new ResultDimensionException(
101
                "The query should have resulted in exactly one row, but has $rowCnt rows."
102
            );
103
        }
104
105
        $colCnt = count($rel->getColumns());
106
        if ($colCnt != 1) {
107
            throw new ResultDimensionException(
108
                "The query should have resulted in exactly one column, but has $colCnt columns."
109
            );
110
        }
111
112
        return $rel->value();
113
    }
114
115
    public function command($sqlFragmentPatternOrCommand, ...$fragmentsAndParams): ICommandResult
116
    {
117
        $typeDict = $this->typeCtl->getTypeDictionary();
118
119
        try {
120
            if ($sqlFragmentPatternOrCommand instanceof ISqlPatternStatement) {
121
                $this->checkMaxArgs($fragmentsAndParams, 1);
122
                $sqlCommand = $sqlFragmentPatternOrCommand;
123
                $serializeArgs = $fragmentsAndParams;
124
                $sql = $sqlCommand->toSql($typeDict, ...$serializeArgs);
125
            } elseif ($sqlFragmentPatternOrCommand instanceof ICommand) {
126
                $this->checkMaxArgs($fragmentsAndParams, 0);
127
                $command = $sqlFragmentPatternOrCommand;
128
                $sql = $command->toSql($typeDict);
129
            } else {
130
                $command = SqlCommand::fromFragments($sqlFragmentPatternOrCommand, ...$fragmentsAndParams);
131
                $sql = $command->toSql($typeDict);
132
            }
133
        } catch (InvalidStateException $e) {
134
            throw new \InvalidArgumentException($e->getMessage());
135
        }
136
137
        return $this->rawCommand($sql);
138
    }
139
140
    private function checkMaxArgs($args, int $maxCount): void
141
    {
142
        if (count($args) > $maxCount) {
143
            throw new \InvalidArgumentException('Too many arguments given.');
144
        }
145
    }
146
147
    public function rawQuery(string $sqlQuery): IQueryResult
148
    {
149
        $result = $this->executeRawStatement($sqlQuery);
150
        if ($result instanceof IQueryResult) {
151
            return $result;
152
        } else {
153
            throw new UsageException(
154
                'The supplied SQL statement was supposed to be a query, but it did not return a result set. ' .
155
                'Did you mean to call command() or rawCommand()?'
156
            );
157
        }
158
    }
159
160
    public function rawCommand(string $sqlCommand): ICommandResult
161
    {
162
        $result = $this->executeRawStatement($sqlCommand);
163
        if ($result instanceof ICommandResult) {
164
            return $result;
165
        } else {
166
            throw new UsageException(
167
                'The supplied SQL statement was supposed to be a command, but it returned a result set. ' .
168
                'Did you mean to call query() or rawQuery()?'
169
            );
170
        }
171
    }
172
173
    public function executeStatement($sqlStatement): IResult
174
    {
175
        if (is_string($sqlStatement)) {
176
            $rawStatement = $sqlStatement;
177
        } elseif ($sqlStatement instanceof ISqlPatternStatement) {
178
            $typeDict = $this->typeCtl->getTypeDictionary();
179
            $rawStatement = $sqlStatement->toSql($typeDict);
180
        } else {
181
            throw new \InvalidArgumentException(
182
                '$sqlStatement must either by a string or ' . ISqlPatternStatement::class
183
            );
184
        }
185
186
        return $this->executeRawStatement($rawStatement);
187
    }
188
189
    private function executeRawStatement(string $sqlStatement): IResult
190
    {
191
        $connHandler = $this->connCtl->requireConnection();
192
193
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
194
            usleep(1);
195
        }
196
197
        // pg_send_query_params(), as opposed to pg_send_query(), prevents $stmt from containing multiple statements
198
        $sent = pg_send_query_params($connHandler, $sqlStatement, []);
199
        if (!$sent) {
200
            // TODO: consider trapping errors to get more detailed error message
201
            throw new ConnectionException('Error sending the query to the database.');
202
        }
203
204
        $resultHandler = pg_get_result($connHandler);
205
        if ($resultHandler === false) {
206
            throw new ConnectionException('No results received from the database.');
207
        }
208
        /* For erroneous queries, one must call pg_get_result() once again to update the structures at the client side.
209
         * Even worse, a loop might actually be needed according to
210
         * http://www.postgresql.org/message-id/flat/[email protected]#[email protected],
211
         * which does not sound logical, though, and hopefully was just meant as a generic way to processing results of
212
         * multiple statements sent in a single pg_send_query() call. Anyway, looping seems to be the safest solution.
213
         */
214
        while (pg_get_result($connHandler) !== false) {
215
            trigger_error('The database gave an unexpected result set.', E_USER_NOTICE);
216
        }
217
218
        return $this->processResult($connHandler, $resultHandler, $sqlStatement);
219
    }
220
221
    public function runScript(string $sqlScript): array
222
    {
223
        $connHandler = $this->connCtl->requireConnection();
224
225
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
226
            usleep(1);
227
        }
228
229
        $sent = pg_send_query($connHandler, $sqlScript);
230
        if (!$sent) {
231
            throw new ConnectionException('Error sending the query to the database.');
232
        }
233
234
        $resHandlers = [];
235
        while (($res = pg_get_result($connHandler)) !== false) {
236
            /* NOTE: Cannot process the result right away - the remaining results must all be read, or they would, in
237
             * case of error, block the connection from accepting further queries.
238
             */
239
            $resHandlers[] = $res;
240
        }
241
        $results = [];
242
        foreach ($resHandlers as $resHandler) {
243
            $results[] = $this->processResult($connHandler, $resHandler, $sqlScript);
244
        }
245
        return $results;
246
    }
247
248
    /**
249
     * @param resource $connHandler
250
     * @param resource $resHandler
251
     * @param string $query
252
     * @return IResult
253
     * @throws StatementException upon an SQL statement error
254
     */
255
    private function processResult($connHandler, $resHandler, string $query): IResult
256
    {
257
        $notice = $this->getLastResultNotice();
258
        $stat = pg_result_status($resHandler);
259
        switch ($stat) {
260
            case PGSQL_COMMAND_OK:
261
                return new CommandResult($resHandler, $notice);
262
            case PGSQL_TUPLES_OK:
263
                return new QueryResult($resHandler, $this->typeCtl, $notice);
264
            case PGSQL_COPY_IN:
265
                return new CopyInResult($connHandler, $resHandler, $notice);
266
            case PGSQL_COPY_OUT:
267
                return new CopyOutResult($connHandler, $resHandler, $notice);
268
269
            case PGSQL_EMPTY_QUERY:
270
            case PGSQL_BAD_RESPONSE:
271
            case PGSQL_NONFATAL_ERROR:
272
                // non-fatal errors are supposedly not possible to be received by the PHP client library, but anyway...
273
            case PGSQL_FATAL_ERROR:
274
                throw $this->stmtExFactory->createException($resHandler, $query, Ivory::getStatementExceptionFactory());
275
276
            default:
277
                throw new \UnexpectedValueException("Unexpected PostgreSQL statement result status: $stat", $stat);
278
        }
279
    }
280
281
    private function getLastResultNotice(): ?string
282
    {
283
        $resNotice = pg_last_notice($this->connCtl->requireConnection());
284
        $connNotice = $this->connCtl->getLastNotice();
285
        if ($resNotice !== $connNotice) {
286
            $this->connCtl->setLastNotice($resNotice);
287
            return $resNotice;
288
        } else {
289
            return null;
290
        }
291
    }
292
293
    public function getStatementExceptionFactory(): StatementExceptionFactory
294
    {
295
        return $this->stmtExFactory;
296
    }
297
}
298