forked from staabm/phpstan-dba
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathQuerySimulation.php
194 lines (160 loc) · 6.87 KB
/
QuerySimulation.php
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
<?php
declare(strict_types=1);
namespace staabm\PHPStanDba\QueryReflection;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Accessory\AccessoryType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use staabm\PHPStanDba\DbaException;
use staabm\PHPStanDba\UnresolvableQueryMixedTypeException;
use staabm\PHPStanDba\UnresolvableQueryStringTypeException;
/**
* @internal
*/
final class QuerySimulation
{
private const DATE_FORMAT = 'Y-m-d';
/**
* @throws \staabm\PHPStanDba\UnresolvableQueryException
*/
public static function simulateParamValueType(Type $paramType, bool $preparedParam): ?string
{
if ($paramType instanceof ConstantScalarType) {
return (string) $paramType->getValue();
}
if ($paramType instanceof ArrayType) {
return self::simulateParamValueType($paramType->getItemType(), $preparedParam);
}
$integerType = new IntegerType();
if ($integerType->isSuperTypeOf($paramType)->yes()) {
return '1';
}
$booleanType = new BooleanType();
if ($booleanType->isSuperTypeOf($paramType)->yes()) {
return '1';
}
if ($paramType->isNumericString()->yes()) {
return '1';
}
$floatType = new FloatType();
if ($floatType->isSuperTypeOf($paramType)->yes()) {
return '1.0';
}
if ($paramType instanceof UnionType) {
foreach ($paramType->getTypes() as $type) {
// pick one representative value out of the union
$simulated = self::simulateParamValueType($type, $preparedParam);
if (null !== $simulated) {
return $simulated;
}
}
return null;
}
// TODO the dateformat should be taken from bound-parameter-types, see https://github.com/staabm/phpstan-dba/pull/342
if ($paramType instanceof ObjectType && $paramType->isInstanceOf(\DateTimeInterface::class)->yes()) {
return date(self::DATE_FORMAT, 0);
}
$stringType = new StringType();
$isStringableObjectType = $paramType instanceof ObjectType
&& !$paramType->toString() instanceof ErrorType;
if (
$stringType->isSuperTypeOf($paramType)->yes()
|| $isStringableObjectType
) {
// in a prepared context, regular strings are fine
if (true === $preparedParam) {
// returns a string in date-format, so in case the simulated value is used against a date/datetime column
// we won't run into a sql error.
// XX in case we would have a proper sql parser, we could feed schema-type-dependent default values in case of strings.
return date(self::DATE_FORMAT, 0);
}
// plain string types can contain anything.. we cannot reason about it
if (QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) {
throw new UnresolvableQueryStringTypeException('Cannot resolve query with variable type: '.$paramType->describe(VerbosityLevel::precise()));
}
return null;
}
if ($paramType instanceof IntersectionType) {
foreach ($paramType->getTypes() as $type) {
if ($type instanceof AccessoryType) {
continue;
}
$simulated = self::simulateParamValueType($type, $preparedParam);
if (null !== $simulated) {
return $simulated;
}
}
}
// all types which we can't simulate and render a query unresolvable at analysis time
if ($paramType instanceof MixedType || $paramType instanceof IntersectionType) {
if (QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) {
throw new UnresolvableQueryMixedTypeException('Cannot simulate parameter value for type: '.$paramType->describe(VerbosityLevel::precise()));
}
return null;
}
throw new DbaException(sprintf('Unexpected expression type %s of class %s', $paramType->describe(VerbosityLevel::precise()), \get_class($paramType)));
}
public static function simulate(string $queryString): ?string
{
$queryString = self::stripTrailers(self::stripComments($queryString));
if (null === $queryString) {
return null;
}
// make sure we don't unnecessarily transfer data, as we are only interested in the statement is succeeding
if ('SELECT' === QueryReflection::getQueryType($queryString)) {
$queryString .= ' LIMIT 0';
}
return $queryString;
}
public static function stripTrailers(string $queryString): ?string
{
// XXX someday we will use a proper SQL parser
$queryString = rtrim($queryString);
// strip trailling delimiting semicolon
$queryString = rtrim($queryString, ';');
// strip trailling FOR UPDATE/FOR SHARE
$queryString = preg_replace('/(.*)FOR (UPDATE|SHARE)\s*(SKIP\s+LOCKED|NOWAIT)?$/i', '$1', $queryString);
if (null === $queryString) {
throw new ShouldNotHappenException('Could not strip trailling FOR UPDATE/SHARE from query');
}
// strip trailling OFFSET
$queryString = preg_replace('/(.*)OFFSET\s+["\']?\d+["\']?\s*$/i', '$1', $queryString);
if (null === $queryString) {
throw new ShouldNotHappenException('Could not strip trailing OFFSET from query');
}
return preg_replace('/\s*LIMIT\s+["\']?\d+["\']?\s*(,\s*["\']?\d*["\']?)?\s*$/i', '', $queryString);
}
/**
* @see https://larrysteinle.com/2011/02/09/use-regular-expressions-to-clean-sql-statements/
* @see https://github.com/decemberster/sql-strip-comments/blob/3bef3558211a6f6191d2ad0ceb8577eda39dd303/index.js
*/
public static function stripComments(string $query): string
{
return trim(preg_replace_callback(
'/("(""|[^"])*")|(\'(\'\'|[^\'])*\')|(--[^\n\r]*)|(\/\*[\w\W]*?(?=\*\/)\*\/)/m',
static function (array $matches): string {
$match = $matches[0];
$matchLength = \strlen($match);
if (
('"' === $match[0] && '"' === $match[$matchLength - 1])
|| ('\'' === $match[0] && '\'' === $match[$matchLength - 1])
) {
return $match;
}
return '';
},
$query
) ?? $query);
}
}