Passed
Push — master ( b2bdac...3a7f22 )
by Ondřej
03:22
created

StatementExecution::runScript()   B

Complexity

Conditions 5
Paths 10

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 26
rs 8.439
cc 5
eloc 14
nc 10
nop 1
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\Exception\UsageException;
9
use Ivory\Ivory;
10
use Ivory\Query\ICommand;
11
use Ivory\Query\IRelationDefinition;
12
use Ivory\Query\ISqlPatternDefinition;
13
use Ivory\Query\SqlCommand;
14
use Ivory\Query\SqlRelationDefinition;
15
use Ivory\Relation\ITuple;
16
use Ivory\Result\CommandResult;
17
use Ivory\Result\CopyInResult;
18
use Ivory\Result\CopyOutResult;
19
use Ivory\Result\ICommandResult;
20
use Ivory\Result\IQueryResult;
21
use Ivory\Result\IResult;
22
use Ivory\Result\QueryResult;
23
24
class StatementExecution implements IStatementExecution
25
{
26
    private $connCtl;
27
    private $typeCtl;
28
    private $stmtExFactory;
29
30
    public function __construct(ConnectionControl $connCtl, ITypeControl $typeCtl)
31
    {
32
        $this->connCtl = $connCtl;
33
        $this->typeCtl = $typeCtl;
34
        $this->stmtExFactory = new StatementExceptionFactory();
35
    }
36
37
    public function query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): IQueryResult
38
    {
39
        if ($sqlFragmentPatternOrRelationDefinition instanceof IRelationDefinition) {
40
            $relDef = $this->setupDefinition($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
41
        } else {
42
            $relDef = SqlRelationDefinition::fromFragments(
43
                $sqlFragmentPatternOrRelationDefinition,
44
                ...$fragmentsAndParams
45
            );
46
        }
47
48
        $sql = $relDef->toSql($this->typeCtl->getTypeDictionary());
49
        return $this->rawQuery($sql);
50
    }
51
52
    public function querySingleTuple($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): ITuple
53
    {
54
        $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
55
        if ($rel->count() != 1) {
56
            throw new ResultException(
57
                "The query should have resulted in exactly one row, but has {$rel->count()} rows."
58
            );
59
        }
60
        return $rel->tuple();
61
    }
62
63
    public function querySingleValue($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams)
64
    {
65
        $rel = $this->query($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
66
        if ($rel->count() != 1) {
67
            throw new ResultException(
68
                "The query should have resulted in exactly one row, but has {$rel->count()} rows."
69
            );
70
        }
71
        $colCnt = count($rel->getColumns());
72
        if ($colCnt != 1) {
73
            throw new ResultException(
74
                "The query should have resulted in exactly one column, but has $colCnt columns."
75
            );
76
        }
77
        return $rel->value();
78
    }
79
80
    public function command($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams): ICommandResult
81
    {
82
        if ($sqlFragmentPatternOrRelationDefinition instanceof ICommand) {
83
            $command = $this->setupDefinition($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
84
        } else {
85
            $command = SqlCommand::fromFragments($sqlFragmentPatternOrRelationDefinition, ...$fragmentsAndParams);
86
        }
87
88
        $sql = $command->toSql($this->typeCtl->getTypeDictionary());
89
        return $this->rawCommand($sql);
90
    }
91
92
    private function setupDefinition($definition, ...$args)
93
    {
94
        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...
95
            if ($definition instanceof ISqlPatternDefinition) {
96
                if (count($args) > 1) {
97
                    throw new \InvalidArgumentException('Too many arguments given.');
98
                }
99
                $namedParamsMap = $args[0];
100
                $definition->setParams($namedParamsMap);
101
            } else {
102
                throw new \InvalidArgumentException('Too many arguments given.');
103
            }
104
        }
105
106
        return $definition;
107
    }
108
109
    public function rawQuery(string $sqlQuery): IQueryResult
110
    {
111
        $result = $this->executeRawStatement($sqlQuery, $resultHandler);
112
        if ($result instanceof IQueryResult) {
113
            return $result;
114
        } else {
115
            throw new UsageException(
116
                'The supplied SQL statement was supposed to be a query, but it did not return a result set. ' .
117
                'Did you mean to call command() or rawCommand()?'
118
            );
119
        }
120
    }
121
122
    public function rawCommand(string $sqlCommand): ICommandResult
123
    {
124
        $result = $this->executeRawStatement($sqlCommand, $resultHandler);
125
        if ($result instanceof ICommandResult) {
126
            return $result;
127
        } else {
128
            throw new UsageException(
129
                'The supplied SQL statement was supposed to be a command, but it returned a result set. ' .
130
                'Did you mean to call query() or rawQuery()?'
131
            );
132
        }
133
    }
134
135
    private function executeRawStatement(string $sqlStatement, &$resultHandler = null): IResult
136
    {
137
        $connHandler = $this->connCtl->requireConnection();
138
139
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
140
            usleep(1);
141
        }
142
143
        // pg_send_query_params(), as opposed to pg_send_query(), prevents $stmt from containing multiple statements
144
        $sent = pg_send_query_params($connHandler, $sqlStatement, []);
145
        if (!$sent) {
146
            // TODO: consider trapping errors to get more detailed error message
147
            throw new ConnectionException('Error sending the query to the database.');
148
        }
149
150
        $resultHandler = pg_get_result($connHandler);
151
        if ($resultHandler === false) {
152
            throw new ConnectionException('No results received from the database.');
153
        }
154
        /* For erroneous queries, one must call pg_get_result() once again to update the structures at the client side.
155
         * Even worse, a loop might actually be needed according to
156
         * http://www.postgresql.org/message-id/flat/[email protected]#[email protected],
157
         * which does not sound logical, though, and hopefully was just meant as a generic way to processing results of
158
         * multiple statements sent in a single pg_send_query() call. Anyway, looping seems to be the safest solution.
159
         */
160
        while (pg_get_result($connHandler) !== false) {
161
            trigger_error('The database gave an unexpected result set.', E_USER_NOTICE);
162
        }
163
164
        return $this->processResult($connHandler, $resultHandler, $sqlStatement);
165
    }
166
167
    public function rawMultiStatement($sqlStatements)
168
    {
169
        if (!is_array($sqlStatements) && !$sqlStatements instanceof \Traversable) {
170
            throw new \InvalidArgumentException('$sqlStatements is neither array nor \Traversable object');
171
        }
172
173
        $results = [];
174
        foreach ($sqlStatements as $stmtKey => $stmt) {
175
            $results[$stmtKey] = $this->executeRawStatement($stmt);
176
        }
177
        return $results;
178
    }
179
180
    public function runScript(string $sqlScript)
181
    {
182
        $connHandler = $this->connCtl->requireConnection();
183
184
        while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen
185
            usleep(1);
186
        }
187
188
        $sent = pg_send_query($connHandler, $sqlScript);
189
        if (!$sent) {
190
            throw new ConnectionException('Error sending the query to the database.');
191
        }
192
193
        $resHandlers = [];
194
        while (($res = pg_get_result($connHandler)) !== false) {
195
            /* NOTE: Cannot process the result right away - the remaining results must all be read, or they would, in
196
             * case of error, block the connection from accepting further queries.
197
             */
198
            $resHandlers[] = $res;
199
        }
200
        $results = [];
201
        foreach ($resHandlers as $resHandler) {
202
            $results[] = $this->processResult($connHandler, $resHandler, $sqlScript);
203
        }
204
        return $results;
205
    }
206
207
    /**
208
     * @param resource $connHandler
209
     * @param resource $resHandler
210
     * @param string $query
211
     * @return IResult
212
     * @throws StatementException upon an SQL statement error
213
     */
214
    private function processResult($connHandler, $resHandler, string $query): IResult
215
    {
216
        $notice = $this->getLastResultNotice();
217
        $stat = pg_result_status($resHandler);
218
        switch ($stat) {
219
            case PGSQL_COMMAND_OK:
220
                return new CommandResult($resHandler, $notice);
221
            case PGSQL_TUPLES_OK:
222
                return new QueryResult($resHandler, $this->typeCtl, $notice);
223
            case PGSQL_COPY_IN:
224
                return new CopyInResult($connHandler, $resHandler, $notice);
225
            case PGSQL_COPY_OUT:
226
                return new CopyOutResult($connHandler, $resHandler, $notice);
227
228
            case PGSQL_EMPTY_QUERY:
229
            case PGSQL_BAD_RESPONSE:
230
            case PGSQL_NONFATAL_ERROR:
231
                // non-fatal errors are supposedly not possible to be received by the PHP client library, but anyway...
232
            case PGSQL_FATAL_ERROR:
233
                throw $this->stmtExFactory->createException($resHandler, $query, Ivory::getStatementExceptionFactory());
234
235
            default:
236
                throw new \UnexpectedValueException("Unexpected PostgreSQL statement result status: $stat", $stat);
237
        }
238
    }
239
240
    private function getLastResultNotice()
241
    {
242
        $resNotice = pg_last_notice($this->connCtl->requireConnection());
243
        $connNotice = $this->connCtl->getLastNotice();
244
        if ($resNotice !== $connNotice) {
245
            $this->connCtl->setLastNotice($resNotice);
246
            return $resNotice;
247
        } else {
248
            return null;
249
        }
250
    }
251
252
    public function getStatementExceptionFactory(): StatementExceptionFactory
253
    {
254
        return $this->stmtExFactory;
255
    }
256
}
257