|
1
|
|
|
<?php |
|
2
|
|
|
namespace Ivory\Connection; |
|
3
|
|
|
|
|
4
|
|
|
use Ivory\Exception\StatementException; |
|
5
|
|
|
use Ivory\Exception\ConnectionException; |
|
6
|
|
|
use Ivory\Exception\StatementExceptionFactory; |
|
7
|
|
|
use Ivory\Ivory; |
|
8
|
|
|
use Ivory\Query\SqlCommandRecipe; |
|
9
|
|
|
use Ivory\Query\SqlRecipe; |
|
10
|
|
|
use Ivory\Query\SqlRelationRecipe; |
|
11
|
|
|
use Ivory\Result\CommandResult; |
|
12
|
|
|
use Ivory\Result\CopyInResult; |
|
13
|
|
|
use Ivory\Result\CopyOutResult; |
|
14
|
|
|
use Ivory\Result\IResult; |
|
15
|
|
|
use Ivory\Result\QueryResult; |
|
16
|
|
|
|
|
17
|
|
|
class StatementExecution implements IStatementExecution |
|
18
|
|
|
{ |
|
19
|
|
|
private $connCtl; |
|
20
|
|
|
private $typeCtl; |
|
21
|
|
|
private $stmtExFactory; |
|
22
|
|
|
|
|
23
|
|
|
public function __construct(ConnectionControl $connCtl, ITypeControl $typeCtl) |
|
24
|
|
|
{ |
|
25
|
|
|
$this->connCtl = $connCtl; |
|
26
|
|
|
$this->typeCtl = $typeCtl; |
|
27
|
|
|
$this->stmtExFactory = new StatementExceptionFactory(); |
|
28
|
|
|
} |
|
29
|
|
|
|
|
30
|
|
View Code Duplication |
public function query($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap) |
|
|
|
|
|
|
31
|
|
|
{ |
|
32
|
|
|
if ($sqlFragmentPatternOrRecipe instanceof SqlRecipe) { |
|
33
|
|
|
$recipe = $sqlFragmentPatternOrRecipe; |
|
34
|
|
|
if ($fragmentsAndPositionalParamsAndNamedParamsMap) { |
|
|
|
|
|
|
35
|
|
|
if (count($fragmentsAndPositionalParamsAndNamedParamsMap) > 1) { |
|
36
|
|
|
throw new \InvalidArgumentException('Too many arguments given.'); |
|
37
|
|
|
} |
|
38
|
|
|
$namedParamsMap = $fragmentsAndPositionalParamsAndNamedParamsMap[0]; |
|
39
|
|
|
$recipe->setParams($namedParamsMap); |
|
40
|
|
|
} |
|
41
|
|
|
} |
|
42
|
|
|
else { |
|
43
|
|
|
$recipe = SqlRelationRecipe::fromFragments($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap); |
|
44
|
|
|
} |
|
45
|
|
|
|
|
46
|
|
|
$sql = $recipe->toSql($this->typeCtl->getTypeDictionary()); |
|
47
|
|
|
return $this->rawQuery($sql); |
|
48
|
|
|
} |
|
49
|
|
|
|
|
50
|
|
View Code Duplication |
public function command($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap) |
|
|
|
|
|
|
51
|
|
|
{ |
|
52
|
|
|
if ($sqlFragmentPatternOrRecipe instanceof SqlRecipe) { |
|
53
|
|
|
$recipe = $sqlFragmentPatternOrRecipe; |
|
54
|
|
|
if ($fragmentsAndPositionalParamsAndNamedParamsMap) { |
|
|
|
|
|
|
55
|
|
|
if (count($fragmentsAndPositionalParamsAndNamedParamsMap) > 1) { |
|
56
|
|
|
throw new \InvalidArgumentException('Too many arguments given.'); |
|
57
|
|
|
} |
|
58
|
|
|
$namedParamsMap = $fragmentsAndPositionalParamsAndNamedParamsMap[0]; |
|
59
|
|
|
$recipe->setParams($namedParamsMap); |
|
60
|
|
|
} |
|
61
|
|
|
} |
|
62
|
|
|
else { |
|
63
|
|
|
$recipe = SqlCommandRecipe::fromFragments($sqlFragmentPatternOrRecipe, ...$fragmentsAndPositionalParamsAndNamedParamsMap); |
|
64
|
|
|
} |
|
65
|
|
|
|
|
66
|
|
|
$sql = $recipe->toSql($this->typeCtl->getTypeDictionary()); |
|
67
|
|
|
return $this->rawQuery($sql); // FIXME: call rawCommand() instead once it is defined |
|
68
|
|
|
} |
|
69
|
|
|
|
|
70
|
|
|
public function rawQuery(string $sqlStatement) |
|
71
|
|
|
{ |
|
72
|
|
|
$connHandler = $this->connCtl->requireConnection(); |
|
73
|
|
|
|
|
74
|
|
|
while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen |
|
75
|
|
|
usleep(1); |
|
76
|
|
|
} |
|
77
|
|
|
|
|
78
|
|
|
// pg_send_query_params(), as opposed to pg_send_query(), prevents $stmt from containing multiple statements |
|
79
|
|
|
$sent = pg_send_query_params($connHandler, $sqlStatement, []); // TODO: consider trapping errors to get more detailed error message |
|
80
|
|
|
if (!$sent) { |
|
81
|
|
|
throw new ConnectionException('Error sending the query to the database.'); |
|
82
|
|
|
} |
|
83
|
|
|
|
|
84
|
|
|
$res = pg_get_result($connHandler); |
|
85
|
|
|
if ($res === false) { |
|
86
|
|
|
throw new ConnectionException('No results received from the database.'); |
|
87
|
|
|
} |
|
88
|
|
|
/* For erroneous queries, one must call pg_get_result() once again to update the structures at the client side. |
|
89
|
|
|
* Even worse, a loop might actually be needed according to |
|
90
|
|
|
* http://www.postgresql.org/message-id/flat/[email protected]#[email protected], |
|
91
|
|
|
* which does not sound logical, though, and hopefully was just meant as a generic way to processing results of |
|
92
|
|
|
* multiple statements sent in a single pg_send_query() call. Anyway, looping seems to be the safest solution. |
|
93
|
|
|
*/ |
|
94
|
|
|
while (pg_get_result($connHandler) !== false) { |
|
95
|
|
|
trigger_error('The database gave an unexpected result set.', E_USER_NOTICE); |
|
96
|
|
|
} |
|
97
|
|
|
|
|
98
|
|
|
$result = $this->processResult($connHandler, $res, $sqlStatement); |
|
99
|
|
|
|
|
100
|
|
|
// TODO: emit warning if ICommandResult is returned for rawQuery(), or if IQueryResult is returned for command() |
|
101
|
|
|
|
|
102
|
|
|
return $result; |
|
103
|
|
|
} |
|
104
|
|
|
|
|
105
|
|
|
public function rawMultiQuery($sqlStatements) |
|
106
|
|
|
{ |
|
107
|
|
|
if (!is_array($sqlStatements) && !$sqlStatements instanceof \Traversable) { |
|
108
|
|
|
throw new \InvalidArgumentException('$sqlStatements is neither array nor \Traversable object'); |
|
109
|
|
|
} |
|
110
|
|
|
|
|
111
|
|
|
$results = []; |
|
112
|
|
|
foreach ($sqlStatements as $stmtKey => $stmt) { |
|
113
|
|
|
$results[$stmtKey] = $this->rawQuery($stmt); |
|
114
|
|
|
} |
|
115
|
|
|
return $results; |
|
116
|
|
|
} |
|
117
|
|
|
|
|
118
|
|
|
public function runScript($sqlScript) |
|
119
|
|
|
{ |
|
120
|
|
|
$connHandler = $this->connCtl->requireConnection(); |
|
121
|
|
|
|
|
122
|
|
|
while (pg_connection_busy($connHandler)) { // just to make things safe, it shall not ever happen |
|
123
|
|
|
usleep(1); |
|
124
|
|
|
} |
|
125
|
|
|
|
|
126
|
|
|
$sent = pg_send_query($connHandler, $sqlScript); |
|
127
|
|
|
if (!$sent) { |
|
128
|
|
|
throw new ConnectionException('Error sending the query to the database.'); |
|
129
|
|
|
} |
|
130
|
|
|
|
|
131
|
|
|
$resHandlers = []; |
|
132
|
|
|
while (($res = pg_get_result($connHandler)) !== false) { |
|
133
|
|
|
$resHandlers[] = $res; // NOTE: cannot process the result right away - the remaining results must all be read, or they would, in case of error, block the connection from accepting further queries |
|
134
|
|
|
} |
|
135
|
|
|
$results = []; |
|
136
|
|
|
foreach ($resHandlers as $resHandler) { |
|
137
|
|
|
$results[] = $this->processResult($connHandler, $resHandler, $sqlScript); |
|
138
|
|
|
} |
|
139
|
|
|
return $results; |
|
140
|
|
|
} |
|
141
|
|
|
|
|
142
|
|
|
/** |
|
143
|
|
|
* @param resource $connHandler |
|
144
|
|
|
* @param resource $resHandler |
|
145
|
|
|
* @param string $query |
|
146
|
|
|
* @return IResult |
|
147
|
|
|
* @throws StatementException upon an SQL statement error |
|
148
|
|
|
*/ |
|
149
|
|
|
private function processResult($connHandler, $resHandler, $query) |
|
150
|
|
|
{ |
|
151
|
|
|
$notice = $this->getLastResultNotice(); |
|
152
|
|
|
$stat = pg_result_status($resHandler); |
|
153
|
|
|
switch ($stat) { |
|
154
|
|
|
case PGSQL_COMMAND_OK: |
|
155
|
|
|
return new CommandResult($resHandler, $notice); |
|
156
|
|
|
case PGSQL_TUPLES_OK: |
|
157
|
|
|
$typeDict = $this->typeCtl->getTypeDictionary(); |
|
158
|
|
|
return new QueryResult($resHandler, $typeDict, $notice); |
|
159
|
|
|
case PGSQL_COPY_IN: |
|
160
|
|
|
return new CopyInResult($connHandler, $resHandler, $notice); |
|
161
|
|
|
case PGSQL_COPY_OUT: |
|
162
|
|
|
return new CopyOutResult($connHandler, $resHandler, $notice); |
|
163
|
|
|
|
|
164
|
|
|
case PGSQL_EMPTY_QUERY: |
|
165
|
|
|
case PGSQL_BAD_RESPONSE: |
|
166
|
|
|
case PGSQL_NONFATAL_ERROR: |
|
167
|
|
|
// non-fatal errors are supposedly not possible to be received by the PHP client library, but anyway... |
|
168
|
|
|
case PGSQL_FATAL_ERROR: |
|
169
|
|
|
throw $this->stmtExFactory->createException($resHandler, $query, Ivory::getStatementExceptionFactory()); |
|
170
|
|
|
|
|
171
|
|
|
default: |
|
172
|
|
|
throw new \UnexpectedValueException("Unexpected PostgreSQL statement result status: $stat", $stat); |
|
173
|
|
|
} |
|
174
|
|
|
} |
|
175
|
|
|
|
|
176
|
|
|
private function getLastResultNotice() |
|
177
|
|
|
{ |
|
178
|
|
|
$resNotice = pg_last_notice($this->connCtl->requireConnection()); |
|
179
|
|
|
$connNotice = $this->connCtl->getLastNotice(); |
|
180
|
|
|
if ($resNotice !== $connNotice) { |
|
181
|
|
|
$this->connCtl->setLastNotice($resNotice); |
|
182
|
|
|
return $resNotice; |
|
183
|
|
|
} |
|
184
|
|
|
else { |
|
185
|
|
|
return null; |
|
186
|
|
|
} |
|
187
|
|
|
} |
|
188
|
|
|
|
|
189
|
|
|
public function getStatementExceptionFactory() |
|
190
|
|
|
{ |
|
191
|
|
|
return $this->stmtExFactory; |
|
192
|
|
|
} |
|
193
|
|
|
} |
|
194
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.