Repository navigation
Give each value of an array expression parameter its own placeholder - #235
Open
simon-mundy wants to merge 4 commits into
Open
simon-mundy wants to merge 4 commits into
simon-mundy wants to merge 4 commits into
Conversation
An array passed to Expression becomes an Argument\Values, which
processExpression() flattens into one bound value per element, but
getExpressionData() still replaced its `?` with a single `%s`. The
extra values then filled the placeholders of whatever followed in the
same predicate set: where([new Expression('a IN (?)', [[1, 2, 3]]),
'b' => 4]) rendered `a IN (:where1) AND :where2 = :where3`, binding 2
and 3 in place of the column "b" and its value.
Each `?` is now replaced with as many placeholders as its argument
binds: a comma-separated list for Values (NULL when empty) and a single
`%s` otherwise. The surrounding parentheses stay in the expression, as
the documented `id IN (?)` example expects. That example also called
Argument::value() with an array, which is a TypeError, so it now uses
Argument::values().
Signed-off-by: Simon Mundy <simon.mundy@peptolab.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
An array or Argument\Values bound to a `?` expands to a comma-separated
list of placeholders, which is only valid SQL when the `?` sits alone
inside parentheses. `new Expression('a IN ?', [[1, 2, 3]])` rendered
`a IN :p1, :p2, :p3`, so getExpressionData() now throws
InvalidArgumentException::forUnenclosedValuesPlaceholder() unless the
placeholder is directly wrapped by `(` and `)`, whitespace allowed.
Signed-off-by: Simon Mundy <simon.mundy@peptolab.com>
Merging 0.6.x removed Expression from the lint baseline, and the placeholder expansion pushed the class over Mago's cyclomatic complexity limit. The enclosure check moves into placeholderFor(), and the comma-separated list now comes from Values::getSpecification() without its parentheses rather than being rebuilt here. Signed-off-by: Simon Mundy <simon.mundy@peptolab.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
An array passed as an
Expressionparameter is wrapped inArgument\Values. When the statement is built,AbstractSql::flattenExpressionValues()expands that into one bound value per element, butExpression::getExpressionData()replaced the matching?with a single%s. The specification and the value list therefore disagreed in length, and the leftover values were consumed by whatever came next in the same predicate set.WHERE a IN (:where1) AND :where2 = :where3, the columnbis bound as the value2WHERE a IN (:where1, :where2, :where3) AND "b" = :where4getSqlString()WHERE a IN ('1') AND '2' = '3'WHERE a IN ('1', '2', '3') AND "b" = '4'The query is silently wrong rather than failing, and the value
4is never bound.Fix
Expression::getExpressionData()now swaps each?for as many placeholders as its argument will bind:Argument\Values(including a plain array) gives a comma-separated list,%s, %s, %s, orNULLwhen the array is empty. Before this change, an empty array failed invsprintf().%s.The surrounding parentheses are left to the expression. That matches the documented usage (
'id IN (?)'→id IN (?, ?, ?, ?, ?)indocs/book/sql/advanced.md) and the existing'? IN (?)'test. Adding parentheses automatically would turn every existingIN (?)intoIN ((…)).An array placeholder that is not directly wrapped by
(and)(whitespace allowed) now throwsPhpDb\Sql\Exception\InvalidArgumentException::forUnenclosedValuesPlaceholder(). The message names the expression and points toIN (?). Without the check,'a IN ?'with[[1, 2, 3]]would rendera IN :p1, :p2, :p3, which is invalid SQL. The rule is strict:'a IN (?, ?)'with[[1, 2], 3]also throws, because the array's?is followed by a comma rather than). Scalar placeholders are not checked. The check only applies to?placeholders, since named placeholders are never expanded.The named-placeholder fallback check (
:\w*count vs parameter count) is unchanged and still runs against the full rendered specification.Docs
The array example in
advanced.mdcalledArgument::value([1, 2, 3, 4, 5]), which throws aTypeErrorbecausevalue()only takes scalars. It now usesArgument::values(), and a short note explains the expansion, the empty-arrayNULL, and the exception for a placeholder outside its own parentheses.Tests
SelectTest::arrayExpressionParameterDoesNotShiftValuesIntoFollowingPredicates: the reproduction above, with a data provider covering both prepared (named parameters, asserting the SQL and the bound values) and unprepared rendering.ExpressionTest::getExpressionDataGivesEachArrayValueItsOwnPlaceholder: checks the generated specification for a plain array, an empty array, an array between other parameters (? IN (?)), an explicitArgument\Values, and whitespace inside the parentheses (IN ( ? )). None of these throw.ExpressionTest::getExpressionDataRejectsArrayPlaceholderOutsideParentheses: asserts the exception class and message for'a IN ?'(array, empty array, andArgument\Values), an unmatched(or), and'a IN (?, ?)'.InvalidArgumentExceptionTest: the new named constructor is added to the catalogue's provider.Both failed before the fix. With it, the full unit suite passes (2151 tests),
ExpressionandSql\Exception\InvalidArgumentExceptionhave 100% line coverage, andmago format --check,mago lintandmago analyzeare all clean.