0024: Explain runs EXPLAIN through its own PHP

On this page

Status: accepted

Context

The Dumps screen shows the queries a Laravel app runs. Explain should say why one is slow: the plan from the database, and what to change. The query arrives from the app with its values, and those values can come from anyone who reaches the site, including visitors to a shared site. Until now the values were pasted into the SQL for display. Sending that text back to the database, through mysql -e or psql -c, would let a crafted value add a second statement and run it as root. Short Leash had no database driver of its own; it uses each database's command-line clients.

Decision

  • Capture the query in parts. Laravel's query listener records the SQL with its ? placeholders, the values as JSON (bytes as base64), and where it ran: driver, database, host, port and socket, never the password. These stay in the daemon's memory, the values as the JSON text that arrived, and aren't sent to the UI. A query whose SQL or values pass 64 KB, like a bulk insert, is sent without them: its plan says little, and its values would take memory. The Dumps screen offers Explain only on queries that already pass the checks below that don't need the database.
  • Ask with EXPLAIN only. Short Leash never runs EXPLAIN ANALYZE, which runs the query. Reads run in a read-only session with a 5-second limit.
  • One statement, values apart. Before asking, Short Leash refuses anything but a single select, with, insert, update, delete or replace: any ; before the end refuses it. A short PHP script, run with Short Leash's own PHP (which has PDO for MySQL, PostgreSQL and SQLite), sends the plan request:
    • MySQL: multiple statements are turned off, and PDO fills in each value with its type, as Laravel binds it: ints as numbers, strings escaped for the connection.
    • PostgreSQL: the server prepares the statement itself, which allows only one.
    • SQLite: the file is opened read-only.
  • Only Short Leash's own databases. The query's connection must be one of Short Leash's MySQL or PostgreSQL instances, running and found by its port, or by its socket when the app uses one (any socket Short Leash didn't name is refused, never matched by port), or a local SQLite file (a relative path is found in the site's folder, as Laravel finds it). Other servers are refused: Short Leash doesn't know their passwords and shouldn't use them. The database name must be plain (letters, digits, _, -, $), since it goes into a connection string.
  • Findings are rules, not guesses. Short Leash reads each database's plan format and reports:
    • full scans of big tables
    • columns wrapped in a function, with a range to compare instead of date(col) = day
    • patterns that start with %
    • sorts and temporary tables
    • the index that would help, as a Laravel migration line

Consequences

  • No new Rust dependencies: the database drivers are PDO's, inside the PHP Short Leash already ships.
  • Explain needs the PHP the site uses (or the default PHP), and takes about as long as starting PHP plus the EXPLAIN.
  • Queries from servers Short Leash doesn't run, and from in-memory SQLite, can't be explained.
  • MySQL can still evaluate constant subqueries while planning; that's the developer's own query, which already ran once.
  • Queries that held a ; inside a string can't be explained. That's rare in SQL an app sends with placeholders.