Skip to content

Validator JSON types, empty whereIn, pgsql emulate_prepares, string traces - #431

Merged
papac merged 4 commits into
5.xfrom
refactor/code-base
Oct 13, 2026
Merged

papac merged 4 commits into
5.xfrom
refactor/code-base

Conversation

@papac

@papac papac commented Oct 13, 2026

Copy link
Copy Markdown
Member

Summary

Three commits, each independently revertable.

1. Validator: stop crashing on native JSON input types (a072199c)

Bug. A JSON body is decoded with native types, so a field declared nullable|boolean received a real false. The nullable rule passed it to the string-typed Str::isEmpty(), which throws a TypeError under strict_types, so the request died with a 500 before the boolean rule ran. The same assumption broke nullable with ints, floats and arrays, max/min/size with numbers and booleans, and alpha, alphanum, lower, upper, email with any non-string.

Fix.

  • NullableRule: a value is absent only when null, a blank string or an empty array. false, 0, 0.0 are real values and reach the following rules.
  • StringRule: max, min, size share an inputSize() helper following the convention between already documents: native numbers compare by value, strings by length. Numeric strings keep length semantics, so a form phone number declared max:10 behaves as before. Booleans and arrays fail the rule instead of throwing.
  • alpha, alphanum, lower, upper, email fail cleanly on non-strings.

Tests. New database-free tests/Validation/ValidatorTypedInputTest.php (15 tests), written first and failing with 12 TypeErrors. Existing ValidationTest (102 tests) still passes against MySQL.

2. Database: empty whereIn() list and pgsql emulate_prepares (70a1026a)

Both were being re-applied as vendor patches by an application at build time.

  • whereIn('id', []) built id in (), invalid SQL (SQLSTATE 42601) that php bow migrate hits on a project with no migrations yet. It now compiles to id in (null): valid on every driver, matches no row, no bindings. Test on in-memory SQLite checks the SQL, the bindings and the empty result.
  • PostgreSQLAdapter honours a per-connection emulate_prepares key, default false so nothing changes unless opted in. Behind a pooler a server-side prepared statement can outlive a schema change and fail with SQLSTATE 0A000 "cached plan must not change result type"; with emulated prepares PDO quotes the values and nothing is cached on the server. Two connection tests: off by default, on when configured.

3. Log and report exception traces as strings (54c32163)

getTrace() returns frames with their arguments, and serializing those can run code: an argument object whose jsonSerialize() issues a query throws again on an already-aborted transaction, so the logger failed while reporting the original error. LoggerConfiguration and QueueAdapter now log the exception class, file:line and getTraceAsString(). BaseErrorHandler puts the string trace in the non-production JSON response.

Verification

  • tests/Validation: 117 tests pass (102 existing against MySQL, 15 new without a database).
  • tests/Database: 778 tests pass against SQLite, MySQL and PostgreSQL in throwaway containers (Redis excluded, no server).
  • phpcs --standard=psr12 clean on every changed source file.

Not changed

whereNotIn() with an empty array still builds not in (). The right empty-list behaviour there is "exclude nothing", the opposite of whereIn, so it is left for a separate decision.

Companion docs PR: bowphp/docs "Document validator JSON types, empty whereIn and PostgreSQL options".

papac added 4 commits October 13, 2026 09:05
A JSON body is decoded with native types, so a field declared
`nullable|boolean` received a real false and the nullable rule passed it
to the string-typed Str::isEmpty(), throwing a TypeError (HTTP 500) before
the boolean rule ever ran. The same assumption broke nullable with ints,
floats and arrays, max/min/size with numbers and booleans, and alpha,
alphanum, lower, upper and email with any non-string.

- NullableRule: a value is absent only when null, a blank string or an
  empty array; false, 0 and 0.0 are real values and reach the next rules.
- StringRule: max, min and size compare native numbers by value and
  strings by length, following the convention documented by between.
  Numeric strings keep length semantics so form inputs are unchanged.
  Booleans and arrays fail the rule instead of throwing.
- alpha, alphanum, lower, upper and email fail cleanly on non-strings.

Adds a database-free regression test class covering each case and a full
JSON payload.
whereIn() with an empty array built `column in ()`, invalid SQL (SQLSTATE
42601) that `php bow migrate` hit on a project with no migrations yet. An
empty list now compiles to `in (null)`, valid everywhere and matching no
row.

PostgreSQLAdapter honours a per-connection `emulate_prepares` config key
(default false). Behind a pooler a server-side prepared statement can
outlive a schema change and fail with SQLSTATE 0A000 "cached plan must not
change result type"; with emulated prepares PDO quotes the values itself
and nothing is cached on the server.

Both were previously re-applied as vendor patches by an application;
regression tests added for each.
getTrace() returns frames with their arguments, and serializing those can
run code: an argument object whose jsonSerialize() issues a query throws
again when the transaction is already aborted, so the logger itself failed
while reporting the original error.

- LoggerConfiguration and QueueAdapter log the exception class, file:line
  and getTraceAsString() as context instead of the frame array.
- BaseErrorHandler puts getTraceAsString() in the non-production JSON
  response instead of the frame array.
@papac
papac merged commit 76b4b6c into 5.x Oct 13, 2026
0 of 16 checks passed
@papac
papac deleted the refactor/code-base branch October 13, 2026 09:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant