-
-
Notifications
You must be signed in to change notification settings - Fork 17
/
QuerySimulation.php
127 lines (102 loc) · 3.91 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
<?php
declare(strict_types=1);
namespace staabm\PHPStanDba\QueryReflection;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use staabm\PHPStanDba\DbaException;
use staabm\PHPStanDba\UnresolvableQueryException;
/**
* @internal
*/
final class QuerySimulation
{
/**
* @throws 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;
}
$stringType = new StringType();
if ($stringType->isSuperTypeOf($paramType)->yes()) {
// in a prepared context, regular strings are fine
if (true === $preparedParam) {
return '1';
}
// plain string types can contain anything.. we cannot reason about it
return null;
}
// 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 UnresolvableQueryException('Cannot simulate parameter value for type: '.$paramType->describe(VerbosityLevel::precise()));
}
return null;
}
throw new DbaException(sprintf('Unexpected expression type %s', \get_class($paramType)));
}
public static function simulate(string $queryString): ?string
{
$queryString = self::stripTraillingLimit($queryString);
if (null === $queryString) {
return null;
}
$queryString .= ' LIMIT 0';
return $queryString;
}
private static function stripTraillingLimit(string $queryString): ?string
{
// XXX someday we will use a proper SQL parser,
$queryString = rtrim($queryString, ';');
// strip trailling FOR UPDATE/FOR SHARE
$queryString = preg_replace('/(.*)FOR (UPDATE|SHARE)\s*$/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);
}
}