Repository navigation
Query: validate tuples, quote untyped values, require a condition - #15
Merged
Merged
Conversation
A value starting with '!' used to turn the comparison into != and lose
its first character. Values are now always compared as they are; the
comparator is only taken from the third element ('NOT', '!=', ...).
Query::setLegacyNegationPrefix(true) switches the old behaviour back on
for a project that still relies on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqZUi2UiBJP6rZoCSRcdpE
Values were substituted one after another over the whole query, through fixed marker strings. Text inside one value could therefore be taken for the marker of another. All markers are now swapped for their values in one strtr() pass that never rescans substituted text, and markers carry a random part per query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DqZUi2UiBJP6rZoCSRcdpE
…on' into fix/query-strict-values
The query builder is strict about what it is given. - Tuples are validated. A WHERE tuple is [ value, type, comparator ], a SET tuple [ value, type, nullOnEmpty ]: a list with 1 to 3 elements, a Query::PARAM_* type, and a comparator out of =, !=, NOT, <, >, <=, >=, LIKE, IN (case-insensitive). Anything else throws InvalidParameter, where an unknown comparator used to be read as equality. A list of values is only accepted for IN and holds scalars and nulls. Values bound on a written query get the same checks on shape, type and lists. - Untyped values are always written as quoted strings, integers included. PARAM_NUMBER still writes a bare number. Non-finite numbers throw, PARAM_NULL writes NULL and an unknown type throws. - A null condition no longer shifts the values after it: `IS NULL` has no placeholder and no longer collects a value. The builders check that they wrote as many placeholders as they collected values. - update() and delete() throw without a condition. Compatibility: a query that binds an untyped integer where SQL needs a bare number (LIMIT ?) has to bind it as PARAM_NUMBER. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DqZUi2UiBJP6rZoCSRcdpE
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.
Builds on #9 and #10 (this branch contains both) and should be merged after them. Until they are merged the diff below also shows their changes; the commit of this PR is
ba8f0b9.What changes
Tuples are validated. The builders read an array as a tuple:
[ value, type, comparator ]in WHERE and[ value, type, nullOnEmpty ]in SET.0..2). An associative array or a longer list throwsInvalidParameter.Query::PARAM_*constants.=,!=,NOT,<,>,<=,>=,LIKE,IN(case-insensitive, as before). An unknown comparator such as<>orNOT LIKEused to be read as equality and now throws.IN(explicit, or implied by passing a list as the value) and holds scalars and nulls.INwith a single value, a list with another comparator, a nested list and a list as a SET value throw.bindValue,bindValues) get the same checks on shape, type and lists when the query is parsed. Their third element keeps being read loosely as the "null on empty" flag.PARAM_NULLwritesNULL(and throws for another value); a type the class does not know throws instead of writing nothing.Untyped values are quoted. A value without a type (
PARAM_UNKNOWN) is always written as a quoted string, integers included:id = '5'.PARAM_NUMBERstill writes a bare number.INF/NANthrow for every type, floats are written with a decimal point in every locale, and the coordinates of aPointhave to be numeric.Null conditions no longer shift later values.
'col' => nullis written ascol IS NULL, which has no placeholder, but it still collected a value: the next condition got that value and every value after it moved up one place, unless the null condition was the last one. It no longer collects a value, so a null condition can stand anywhere. The builders also check that they wrote as many placeholders as they collected values and throwInvalidParameterotherwise (for example for a?in a column name).update() and delete() require a condition. With an empty
$wherethey throwInvalidParameterinstead of addressing every row.select()is unchanged.Compatibility
PARAM_NUMBER. The common case isLIMIT ?/OFFSET ?:->bindValue (1, $limit)becomes->bindValue (1, $limit, Query::PARAM_NUMBER). Comparisons, assignments, arithmetic andINlists read a quoted number as a number for numeric columns. Against a string column the value is now compared as a string. AnENUMcolumn compared with an untyped integer is compared by value instead of by index.Query::update ($table, $set, array ())andQuery::delete ($table, array ())throw. A statement for every row is written as a query:new Query ('DELETE FROM ...').new Query ($sql)plusbindValue) is not counted, because it may hold a literal question mark after its last placeholder.INis written in upper case whatever the case of the comparator.Not touched: quoting of identifiers (column keys, selected columns,
ORDER BY,LIMITofselect()), a literal question mark in written SQL,!= NULLfor a null value with another comparator than=, andIN ()for an empty list.Tests
tests/DbQueryStrictTest.php(new, runs on theTestDatabasestub). Two tests intests/DbQueryInjectionTest.phppinned the old behaviour and were updated:testUnknownParamIntIsNotQuoted(nowtestUnknownParamIntIsQuoted) andtestDeleteNoWhere(nowtestDeleteNoWhereThrows). The expected strings intests/DbQueryTest.php(groupdatabase, not run in CI) follow the quoting.🤖 Generated with Claude Code
https://claude.ai/code/session_01DqZUi2UiBJP6rZoCSRcdpE