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