-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Expand file tree
/
Copy pathUniqueCondition.php
More file actions
231 lines (204 loc) · 8.48 KB
/
UniqueCondition.php
File metadata and controls
231 lines (204 loc) · 8.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
<?php
declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\Dbal\DatabaseInterface;
use PhpMyAdmin\SqlParser\Components\CaseExpression;
use PhpMyAdmin\SqlParser\Components\Expression;
use function bin2hex;
use function count;
use function mb_strlen;
use function preg_replace;
use function strcasecmp;
use function trim;
class UniqueCondition
{
private string $whereClause;
private bool $clauseIsUnique = true;
/** @var array<string, string> */
private array $conditionArray = [];
/**
* Function to generate unique condition for specified row.
*
* @param FieldMetadata[] $fieldsMeta meta information about fields
* @param array $row current row
* @param bool $forceUnique generate condition only on pk or unique
* @param string $restrictToTable restrict the unique condition to this table
* @param (Expression|CaseExpression)[] $expressions An array of Expression instances.
* @psalm-param array<string|int|float|null> $row
*/
public function __construct(
array $fieldsMeta,
array $row,
bool $forceUnique = false,
string $restrictToTable = '',
array $expressions = [],
) {
$fieldsCount = count($fieldsMeta);
$primaryKey = '';
$uniqueKey = '';
$nonPrimaryCondition = '';
$preferredCondition = '';
$primaryKeyArray = [];
$uniqueKeyArray = [];
$nonPrimaryConditionArray = [];
foreach ($fieldsMeta as $i => $meta) {
// do not use a column alias in a condition
if ($meta->orgname === '') {
$meta->orgname = $meta->name;
foreach ($expressions as $expression) {
if (
$expression->alias === null || $expression->alias === ''
|| $expression instanceof CaseExpression
|| $expression->column === null || $expression->column === ''
) {
continue;
}
if (strcasecmp($meta->name, $expression->alias) === 0) {
$meta->orgname = $expression->column;
break;
}
}
}
// Do not use a table alias in a condition.
// Test case is:
// select * from galerie x WHERE
//(select count(*) from galerie y where y.datum=x.datum)>1
//
// Also, do not use the original table name if we are dealing with
// a view because this view might be updatable.
// (The isView() verification should not be costly in most cases
// because there is some caching in the function).
if (
$meta->table !== $meta->orgtable
&& ! DatabaseInterface::getInstance()->getTable(Current::$database, $meta->table)->isView()
) {
$meta->table = $meta->orgtable;
}
// If this field is not from the table which the unique clause needs
// to be restricted to.
if ($restrictToTable !== '' && $restrictToTable !== $meta->table) {
continue;
}
// to fix the bug where float fields (primary or not)
// can't be matched because of the imprecision of
// floating comparison, use CONCAT
// (also, the syntax "CONCAT(field) IS NULL"
// that we need on the next "if" will work)
if ($meta->isType(FieldMetadata::TYPE_REAL)) {
$conKey = 'CONCAT(' . Util::backquote($meta->table) . '.'
. Util::backquote($meta->orgname) . ')';
} else {
$conKey = Util::backquote($meta->table) . '.'
. Util::backquote($meta->orgname);
}
$condition = ' ' . $conKey . ' ';
[$conVal, $condition] = $this->getConditionValue(
$row[$i] ?? null,
$meta,
$fieldsCount,
$conKey,
$condition,
);
if ($conVal === null) {
continue;
}
$condition .= $conVal . ' AND';
if ($meta->isPrimaryKey()) {
$primaryKey .= $condition;
$primaryKeyArray[$conKey] = $conVal;
} elseif ($meta->isUniqueKey()) {
$uniqueKey .= $condition;
$uniqueKeyArray[$conKey] = $conVal;
}
$nonPrimaryCondition .= $condition;
$nonPrimaryConditionArray[$conKey] = $conVal;
}
// Correction University of Virginia 19991216:
// prefer primary or unique keys for condition,
// but use conjunction of all values if no primary key
if ($primaryKey !== '') {
$preferredCondition = $primaryKey;
$this->conditionArray = $primaryKeyArray;
} elseif ($uniqueKey !== '') {
$preferredCondition = $uniqueKey;
$this->conditionArray = $uniqueKeyArray;
} elseif (! $forceUnique) {
$preferredCondition = $nonPrimaryCondition;
$this->conditionArray = $nonPrimaryConditionArray;
$this->clauseIsUnique = false;
}
$this->whereClause = trim((string) preg_replace('|\s?AND$|', '', $preferredCondition));
}
public function getWhereClause(): string
{
return $this->whereClause;
}
public function isClauseUnique(): bool
{
return $this->clauseIsUnique;
}
/** @return array<string, string> */
public function getConditionArray(): array
{
return $this->conditionArray;
}
/**
* Build a condition and with a value
*
* @param string|int|float|null $row The row value
* @param FieldMetadata $meta The field metadata
* @param int $fieldsCount A number of fields
* @param string $conditionKey A key used for BINARY fields functions
* @param string $condition The condition
*
* @return array<int,string|null>
* @psalm-return array{string|null, string}
*/
private function getConditionValue(
string|int|float|null $row,
FieldMetadata $meta,
int $fieldsCount,
string $conditionKey,
string $condition,
): array {
if ($row === null) {
return ['IS NULL', $condition];
}
$conditionValue = '';
$isBinaryString = $meta->isType(FieldMetadata::TYPE_STRING) && $meta->isBinary();
// 63 is the binary charset, see: https://dev.mysql.com/doc/internals/en/charsets.html
$isBlobAndIsBinaryCharset = $meta->isType(FieldMetadata::TYPE_BLOB) && $meta->charsetnr === 63;
if ($meta->isNumeric) {
$conditionValue = '= ' . $row;
} elseif ($isBlobAndIsBinaryCharset || ($row != 0 && $isBinaryString)) {
// hexify only if this is a true not empty BLOB or a BINARY
// do not waste memory building a too big condition
$rowLength = mb_strlen((string) $row);
if ($rowLength > 0 && $rowLength < 1000) {
// use a CAST if possible, to avoid problems
// if the field contains wildcard characters % or _
$conditionValue = '= CAST(0x' . bin2hex((string) $row) . ' AS BINARY)';
} elseif ($fieldsCount === 1) {
// when this blob is the only field present
// try settling with length comparison
$condition = ' CHAR_LENGTH(' . $conditionKey . ') ';
$conditionValue = ' = ' . $rowLength;
} else {
// this blob won't be part of the final condition
$conditionValue = null;
}
} elseif ($meta->isMappedTypeGeometry && $row != 0) {
// do not build a too big condition
if (mb_strlen((string) $row) < 5000) {
$condition .= '= CAST(0x' . bin2hex((string) $row) . ' AS BINARY)';
} else {
$condition = '';
}
} elseif ($meta->isMappedTypeBit) {
$conditionValue = "= b'" . Util::printableBitValue((int) $row, $meta->length) . "'";
} else {
$conditionValue = '= ' . DatabaseInterface::getInstance()->quoteString((string) $row);
}
return [$conditionValue, $condition];
}
}