A SQL engine executes a query in two strictly ordered stages: parse (the raw text is tokenized into a fixed grammar, producing an Abstract Syntax Tree — the code plane) and bind/execute (values are substituted into the already-built plan — the data plane). Injection is possible only when untrusted input is concatenated into the text before parsing, so the parser tokenizes the attacker's own SQL keywords as if the developer wrote them.
-- vulnerable: data plane leaks into code plane before parsing
sql = "SELECT * FROM users WHERE name='" + input + "'"
-- input = ' OR '1'='1'-- → parser sees an extra OR clause
-- parameterized: code plane is compiled first, input bound after
stmt = prepare("SELECT * FROM users WHERE name = ?")
stmt.bind(1, input) -- input never re-enters the tokenizer
With a prepared statement the placeholder ? is compiled into the plan as a single opaque value slot. Whatever string is bound there — quotes, OR, --, semicolons — is compared byte-for-byte against the column; it can never add AST nodes, because the parser has already finished running and will never run again on that value.
- Vulnerable mode — the payload's characters are spliced into the SQL text and re-tokenized, so metacharacters (
', OR, UNION, --) become real syntax and the AST grows extra branches, matching rows the query never intended to select.
- Parameterized mode — the same payload travels the pipeline as one indivisible data block that never touches the tokenizer; it is compared as a literal string, so the query returns at most the one legitimately matching row.
- AST nodes parsed — counts syntax-tree nodes created by the parser; a rising count in vulnerable mode is the attacker's clause being compiled as code.
This is exactly why OWASP lists parameterized queries / prepared statements as the primary defense against SQL injection (CWE-89): the fix is structural — separating the code and data planes — not just filtering characters.