Completed
Push — master ( e6b195...a64aa1 )
by Ondřej
03:03
created

StatementExecution::queryOneTuple()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 2
1
<?php
2
namespace Ivory\Connection;
3
4
use Ivory\Exception\ResultException;
5
use Ivory\Exception\StatementException;
6
use Ivory\Exception\ConnectionException;
7
use Ivory\Exception\StatementExceptionFactory;
8
use Ivory\Ivory;
9
use Ivory\Query\ICommandRecipe;
10
use Ivory\Query\IRelationRecipe;
11
use Ivory\Query\ISqlPatternRecipe;
12
use Ivory\Query\SqlCommandRecipe;
13
use Ivory\Query\SqlRelationRecipe;
14
use Ivory\Relation\ITuple;
15
use Ivory\Result\CommandResult;
16
use Ivory\Result\CopyInResult;
17
use Ivory\Result\CopyOutResult;
18
use Ivory\Result\ICommandResult;
19
use Ivory\Result\IQueryResult;
20
use Ivory\Result\IResult;
21
use Ivory\Result\QueryResult;
22
23
class StatementExecution implements IStatementExecution
24
{
25
    private $connCtl;
26
    private $typeCtl;
27
    private $stmtExFactory;
28
29
    public function __construct(ConnectionControl $connCtl, ITypeControl $typeCtl)
30
    {
31
        $this->connCtl = $connCtl;
32
        $this->typeCtl = $typeCtl;
33
        $this->stmtExFactory = new StatementExceptionFactory();
34
    }
35
36
    public function query($sqlFragmentPatternOrRecipe, ...$fragmentsAndParams): IQueryResult
37
    {
38
        if ($sqlFragmentPatternOrRecipe instanceof IRelationRecipe) {
39
            $recipe = $this->setupRecipe($sqlFragmentPatternOrRecipe, ...$fragmentsAndParams);
40
        } else {
41
            $recipe = SqlRelationRecipe::fromFragments($sqlFragmentPatternOrRecipe, ...$fragmentsAndParams);
42
        }
43
44
        $sql = $recipe->toSql($this->typeCtl->getTypeDictionary());
45
        return $this->rawQuery($sql);
46
    }
47
48
    public function querySingleTuple($sqlFragmentPatternOrRecipe, ...$fragmentsAndParams): ITuple
49
    {
50
        $rel = $this->query($sqlFragmentPatternOrRecipe, ...$fragmentsAndParams);
51
        if ($rel->count() != 1) {
52
            throw new ResultException(
53
                "The query should have resulted in exactly one row, but has {$rel->count()} rows."
54
            );
55
        }
56
        return $rel->tuple();
57
    }
58
59
    public function querySingleValue($sqlFragmentPatternOrRecipe, ...$fragmentsAndParams)
60
    {
61
        $rel = $this->query($sqlFragmentPatternOrRecipe, ...$fragmentsAndParams);
62
        if ($rel->count() != 1) {
63
            throw new ResultException(
64
                "The query should have resulted in exactly one row, but has {$rel->count()} rows."
65
            );
66
        }
67
        $colCnt = count($rel->getColumns());
68
        if ($colCnt != 1) {
69
            throw new ResultException(
70
                "The query should have resulted in exactly one column, but has $colCnt columns."
71
            );
72
        }
73
        return $rel->value();
74
    }
75
76
    public function command($sqlFragmentPatternOrRecipe, ...$fragmentsAndParams): ICommandResult
77
    {
78
        if ($sqlFragmentPatternOrRecipe instanceof ICommandRecipe) {
79
            $recipe = $this->setupRecipe($sqlFragmentPatternOrRecipe, ...$fragmentsAndParams);
80
        } else {
81
            $recipe = SqlCommandRecipe::fromFragments($sqlFragmentPatternOrRecipe, ...$fragmentsAndParams);
82
        }
83
84
        $sql = $recipe->toSql($this->typeCtl->getTypeDictionary());
85
        return $this->rawCommand($sql);
86
    }
87
88
    private function setupRecipe($recipe, ...$args)
89
    {
90
        if ($args) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $args of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
91
            if ($recipe instanceof ISqlPatternRecipe) {
92
                if (count($args) > 1) {
93
                    throw new \InvalidArgumentException('Too many arguments given.');
94
                }
95
                $namedParamsMap = $args[0];
96
                $recipe->setParams($namedParamsMap);
97
            } else {
98
                throw new \InvalidArgumentException('Too many arguments given.');
99
            }
100
        }
101
102
        return $recipe;
103
    }
104
105
    public function rawQuery(string $sqlQuery): IQueryResult
106
    {
107
        $result = $this->executeRawStatement($sqlQuery, $resultHandler);
108
        if ($result instanceof IQueryResult) {
109
            return $result;
110
        } else {
111
            trigger_error(
112
                'The supplied SQL statement was supposed to be a query, but it did not return a result set. ' .
113
                'Returning an empty relation. Consider calling command() or rawCommand() instead. ' .
114
                "SQL statement: $sqlQuery",
115
                E_USER_WARNING
116
            );
117
            return new QueryResult($resultHandler, $this->typeCtl->getTypeDictionary(), $result->getLastNotice());
118
        }
119
    }
120
121
    public function rawCommand(string $sqlCommand): ICommandResult
122
    {
123
        $result = $this->executeRawStatement($sqlCommand, $resultHandler);
124
        if ($result instanceof ICommandResult) {
125
            return $result;
126
        } else {
127
            trigger_error(
128
                'The supplied SQL statement was supposed to be a command, but it returned a result set. ' .
129
                'Returning an empty result. Consider calling query() or rawQuery() instead. ' .
130
                "SQL statement: $sqlCommand",
131
                E_USER_WARNING
132
            );
133
            return new CommandResult($resultHandler, $result->getLastNotice());
134
        }
135
    }
136
137
    private function executeRawStatement(string $sqlStatement, &$resultHandler = null): IResult
138
    {
139
        $connHandler = $this->connCtl->requireConnection();
140
141
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
142
            usleep(1);
143
        }
144
145
        // pg_send_query_params(), as opposed to pg_send_query(), prevents $stmt from containing multiple statements
146
        $sent = pg_send_query_params($connHandler, $sqlStatement, []);
147
        if (!$sent) {
148
            // TODO: consider trapping errors to get more detailed error message
149
            throw new ConnectionException('Error sending the query to the database.');
150
        }
151
152
        $resultHandler = pg_get_result($connHandler);
153
        if ($resultHandler === false) {
154
            throw new ConnectionException('No results received from the database.');
155
        }
156
        /* For erroneous queries, one must call pg_get_result() once again to update the structures at the client side.
157
         * Even worse, a loop might actually be needed according to
158
         * http://www.postgresql.org/message-id/flat/[email protected]#[email protected],
159
         * which does not sound logical, though, and hopefully was just meant as a generic way to processing results of
160
         * multiple statements sent in a single pg_send_query() call. Anyway, looping seems to be the safest solution.
161
         */
162
        while (pg_get_result($connHandler) !== false) {
163
            trigger_error('The database gave an unexpected result set.', E_USER_NOTICE);
164
        }
165
166
        return $this->processResult($connHandler, $resultHandler, $sqlStatement);
167
    }
168
169
    public function rawMultiStatement($sqlStatements)
170
    {
171
        if (!is_array($sqlStatements) && !$sqlStatements instanceof \Traversable) {
172
            throw new \InvalidArgumentException('$sqlStatements is neither array nor \Traversable object');
173
        }
174
175
        $results = [];
176
        foreach ($sqlStatements as $stmtKey => $stmt) {
177
            $results[$stmtKey] = $this->executeRawStatement($stmt);
178
        }
179
        return $results;
180
    }
181
182
    public function runScript(string $sqlScript)
183
    {
184
        $connHandler = $this->connCtl->requireConnection();
185
186
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
187
            usleep(1);
188
        }
189
190
        $sent = pg_send_query($connHandler, $sqlScript);
191
        if (!$sent) {
192
            throw new ConnectionException('Error sending the query to the database.');
193
        }
194
195
        $resHandlers = [];
196
        while (($res = pg_get_result($connHandler)) !== false) {
197
            /* NOTE: Cannot process the result right away - the remaining results must all be read, or they would, in
198
             * case of error, block the connection from accepting further queries.
199
             */
200
            $resHandlers[] = $res;
201
        }
202
        $results = [];
203
        foreach ($resHandlers as $resHandler) {
204
            $results[] = $this->processResult($connHandler, $resHandler, $sqlScript);
205
        }
206
        return $results;
207
    }
208
209
    /**
210
     * @param resource $connHandler
211
     * @param resource $resHandler
212
     * @param string $query
213
     * @return IResult
214
     * @throws StatementException upon an SQL statement error
215
     */
216
    private function processResult($connHandler, $resHandler, string $query): IResult
217
    {
218
        $notice = $this->getLastResultNotice();
219
        $stat = pg_result_status($resHandler);
220
        switch ($stat) {
221
            case PGSQL_COMMAND_OK:
222
                return new CommandResult($resHandler, $notice);
223
            case PGSQL_TUPLES_OK:
224
                $typeDict = $this->typeCtl->getTypeDictionary();
225
                return new QueryResult($resHandler, $typeDict, $notice);
226
            case PGSQL_COPY_IN:
227
                return new CopyInResult($connHandler, $resHandler, $notice);
228
            case PGSQL_COPY_OUT:
229
                return new CopyOutResult($connHandler, $resHandler, $notice);
230
231
            case PGSQL_EMPTY_QUERY:
232
            case PGSQL_BAD_RESPONSE:
233
            case PGSQL_NONFATAL_ERROR:
234
                // non-fatal errors are supposedly not possible to be received by the PHP client library, but anyway...
235
            case PGSQL_FATAL_ERROR:
236
                throw $this->stmtExFactory->createException($resHandler, $query, Ivory::getStatementExceptionFactory());
237
238
            default:
239
                throw new \UnexpectedValueException("Unexpected PostgreSQL statement result status: $stat", $stat);
240
        }
241
    }
242
243
    private function getLastResultNotice()
244
    {
245
        $resNotice = pg_last_notice($this->connCtl->requireConnection());
246
        $connNotice = $this->connCtl->getLastNotice();
247
        if ($resNotice !== $connNotice) {
248
            $this->connCtl->setLastNotice($resNotice);
249
            return $resNotice;
250
        } else {
251
            return null;
252
        }
253
    }
254
255
    public function getStatementExceptionFactory(): StatementExceptionFactory
256
    {
257
        return $this->stmtExFactory;
258
    }
259
}
260