Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.24% covered (warning)
88.24%
45 / 51
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Expression
88.24% covered (warning)
88.24%
45 / 51
57.14% covered (warning)
57.14%
4 / 7
30.37
0.00% covered (danger)
0.00%
0 / 1
 __construct
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
12.03
 and
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 or
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 andExpr
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 orExpr
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 toSql
84.00% covered (warning)
84.00%
21 / 25
0.00% covered (danger)
0.00%
0 / 1
12.59
 toGeneralizedSql
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Wikimedia\Rdbms;
4
5use InvalidArgumentException;
6use LogicException;
7use Wikimedia\Rdbms\Database\DbQuoter;
8
9/**
10 * A composite leaf representing an expression.
11 *
12 * @since 1.42
13 */
14class Expression implements IExpression {
15    private string $field;
16    private string $op;
17    private $value;
18
19    /**
20     * Store an expression
21     *
22     * @param string $field
23     * @param-taint $field exec_sql
24     * @param string $op One of '>', '<', '!=', '=', '>=', '<=', IExpression::LIKE, IExpression::NOT_LIKE
25     * @param-taint $op exec_sql
26     * @param string|int|float|bool|Blob|null|LikeValue|non-empty-list<string|int|float|bool|Blob> $value
27     * @param-taint $value escapes_sql
28     * @internal Outside of rdbms, Use IReadableDatabase::expr() to create an expression object.
29     */
30    public function __construct( string $field, string $op, $value ) {
31        if ( !in_array( $op, IExpression::ACCEPTABLE_OPERATORS ) ) {
32            throw new InvalidArgumentException( "Operator $op is not supported" );
33        }
34        if (
35            ( is_array( $value ) || $value === null ) &&
36            !in_array( $op, [ '!=', '=' ] )
37        ) {
38            throw new InvalidArgumentException( "Operator $op can't take array or null as value" );
39        }
40
41        if ( is_array( $value ) && in_array( null, $value, true ) ) {
42            throw new InvalidArgumentException( "NULL can't be in the array of values" );
43        }
44
45        if ( in_array( $op, [ IExpression::LIKE, IExpression::NOT_LIKE ] ) && !( $value instanceof LikeValue ) ) {
46            throw new InvalidArgumentException( "Value for 'LIKE' expression must be of LikeValue type" );
47        }
48        if ( !in_array( $op, [ IExpression::LIKE, IExpression::NOT_LIKE ] ) && ( $value instanceof LikeValue ) ) {
49            throw new InvalidArgumentException( "LikeValue may only be used with 'LIKE' expression" );
50        }
51
52        $field = trim( $field );
53        if ( !preg_match( '/^[A-Za-z\d\._]+$/', $field ) ) {
54            throw new InvalidArgumentException( "$field might contain SQL injection" );
55        }
56        $this->field = $field;
57        $this->op = $op;
58        $this->value = $value;
59    }
60
61    /**
62     * @param string $field
63     * @param-taint $field exec_sql
64     * @param string $op One of '>', '<', '!=', '=', '>=', '<=', IExpression::LIKE, IExpression::NOT_LIKE
65     * @param-taint $op exec_sql
66     * @param string|int|float|bool|Blob|null|LikeValue|non-empty-list<string|int|float|bool|Blob> $value
67     * @param-taint $value escapes_sql
68     * @phan-side-effect-free
69     */
70    public function and( string $field, string $op, $value ): AndExpressionGroup {
71        $exprGroup = new AndExpressionGroup( $this );
72        return $exprGroup->and( $field, $op, $value );
73    }
74
75    /**
76     * @param string $field
77     * @param-taint $field exec_sql
78     * @param string $op One of '>', '<', '!=', '=', '>=', '<=', IExpression::LIKE, IExpression::NOT_LIKE
79     * @param-taint $op exec_sql
80     * @param string|int|float|bool|Blob|null|LikeValue|non-empty-list<string|int|float|bool|Blob> $value
81     * @param-taint $value escapes_sql
82     * @phan-side-effect-free
83     */
84    public function or( string $field, string $op, $value ): OrExpressionGroup {
85        $exprGroup = new OrExpressionGroup( $this );
86        return $exprGroup->or( $field, $op, $value );
87    }
88
89    /**
90     * @param IExpression $expr
91     * @return AndExpressionGroup
92     * @phan-side-effect-free
93     */
94    public function andExpr( IExpression $expr ): AndExpressionGroup {
95        $exprGroup = new AndExpressionGroup( $this );
96        return $exprGroup->andExpr( $expr );
97    }
98
99    /**
100     * @param IExpression $expr
101     * @return OrExpressionGroup
102     * @phan-side-effect-free
103     */
104    public function orExpr( IExpression $expr ): OrExpressionGroup {
105        $exprGroup = new OrExpressionGroup( $this );
106        return $exprGroup->orExpr( $expr );
107    }
108
109    /**
110     * @internal to be used by rdbms library only
111     * @return-taint none
112     */
113    public function toSql( DbQuoter $dbQuoter ): string {
114        if ( is_array( $this->value ) ) {
115            if ( !$this->value ) {
116                throw new InvalidArgumentException( "The array of values can't be empty." );
117            }
118            if ( count( $this->value ) === 1 ) {
119                $value = $this->value[ array_key_first( $this->value ) ];
120                if ( $this->op === '=' ) {
121                    return $this->field . ' = ' . $dbQuoter->addQuotes( $value );
122                } elseif ( $this->op === '!=' ) {
123                    return $this->field . ' != ' . $dbQuoter->addQuotes( $value );
124                } else {
125                    throw new LogicException( "Operator $this->op can't take array as value" );
126                }
127            }
128            $list = implode( ',', array_map( static fn ( $value ) => $dbQuoter->addQuotes( $value ), $this->value ) );
129            if ( $this->op === '=' ) {
130                return $this->field . " IN ($list)";
131            } elseif ( $this->op === '!=' ) {
132                return $this->field . " NOT IN ($list)";
133            } else {
134                throw new LogicException( "Operator $this->op can't take array as value" );
135            }
136        }
137        if ( $this->value === null ) {
138            if ( $this->op === '=' ) {
139                return $this->field . " IS NULL";
140            } elseif ( $this->op === '!=' ) {
141                return $this->field . " IS NOT NULL";
142            } else {
143                throw new LogicException( "Operator $this->op can't take null as value" );
144            }
145        }
146        if ( $this->value instanceof LikeValue ) {
147            // implies that `op` is LIKE or NOT_LIKE, checked in constructor
148            return $this->field . ' ' . $this->op . ' ' . $this->value->toSql( $dbQuoter );
149        }
150        return $this->field . ' ' . $this->op . ' ' . $dbQuoter->addQuotes( $this->value );
151    }
152
153    /**
154     * @internal to be used by rdbms library only
155     */
156    public function toGeneralizedSql(): string {
157        return $this->field . ' ' . $this->op . ' ?';
158    }
159}