Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.x] Add toRawSql, dumpRawSql() and ddRawSql() to Query Builders #1

Open
wants to merge 3 commits into
base: 10.x
Choose a base branch
from

Conversation

tpetry
Copy link
Owner

@tpetry tpetry commented Jun 20, 2023

For quite some time, many developers have requested to get SQL queries with merged bindings from the query builder (laravel#38027, laravel#39053, laravel#39551, laravel#45705, laravel#45189).

They want to have something like this:

User::where('email', 'foo@example.com')->ddRawSql();
// "SELECT * FROM users WHERE email = 'foo@example.com'"

Instead of:

User::where('email', 'foo@example.com')->dd();
// "SELECT * FROM users WHERE email = ?"
// [
//  0 => "foo@example.com"
// ]

Prior Implementations

All prior implementations had the same issues that prevented them from being merged:

  1. The bindings had been inserted into the query as strings without any further processing. Each of those generated queries was vulnerable to SQL injection attacks.
  2. They replaced all question marks with values that would break on raw calls, e.g. ->whereRaw("description = 'foo?'")

Improved Implementation

These new ways of generating SQL queries with embedded bindings are available:

$sql = User::where('email', 'foo@example.com')
  ->toRawSql();// $sql = "SELECT * FROM users WHERE email = 'foo@example.com'"

User::where('email', 'foo@example.com')
  ->dumpRawSql(); // "SELECT * FROM users WHERE email = 'foo@example.com'"

User::where('email', 'foo@example.com')
  ->ddRawSql(); // "SELECT * FROM users WHERE email = 'foo@example.com'"

$sql = DB::connection()->getQueryGrammar()->makeRawSql(
  'SELECT * FROM users WHERE email = ?',
  'foo@example.com',
); // $sql = "SELECT * FROM users WHERE email = 'foo@example.com'"

1. SQL Injections

I've built an extension for the database layer that can escape any values for safe embedding into SQL queries (laravel#46558) that is already merged into Laravel 10.x. Based on that code, any binding is escaped before being injected into the SQL query:

User::where('name', "Robert'; drop table users; --")->dd();
// "SELECT * FROM users WHERE email = 'Robert\'; drop table users; --'"

2. Ambiguous Question Marks

Simple search-and-replace operations are not enough to reliably generate a raw SQL statement. With raw expressions anyone can embed more question marks into a SQL query that are clearly no placeholders:

User::whereRaw("abc = 'Hello World?'")->where('name', 'Robert')->dd();
// "SELECT * FROM users WHERE abc = 'Hello WorldRobert' name = ?"

But this can also be solved relatively easily. The generated SQL string with placeholders is parsed by a very simple LL(1) parser (just 20 lines) to watch for string escape sequences and only replace question not being escaped in string literals:

  • The occurrence of ' starts a string literal -> no question marks will be replaced
  • The occurrence of '' and \' marks escaped quotes -> they are copied
  • The occurrence of ' ends a string literal -> question marks will be replaced again.

That way the generated raw SQL string have no problem with question marks in string literals:

User::whereRaw("abc = 'Hello World?'")->where('name', 'Robert')->dd();
// "SELECT * FROM users WHERE abc = 'Hello World?' name = 'Robert'"

3. Executability of Raw SQL Queries

A generated query by these new functions should be able to be copied and pasted into any query tool and execute without problems. This is guaranteed for any database except PostgreSQL. Because PostgreSQL has special operators involving a question that needs to be doubled because of some PDO behaviour:

User::where('json', '?', 'abc')->dd(); // json object contains key "abc"
// "SELECT * FROM users WHERE json ?? 'abc'"

The query can not be executed as Laravel (correctly!) doubles the question mark (double ones are exempt from replacement 😉). To also make these special queries copy-able any (1) PostgreSQL operator containing a question mark that is (2) included in Laravel's operator information is decoded again:

User::where('json', '?', 'abc')->dd(); // json object contains key "abc"
// "SELECT * FROM users WHERE json ? 'abc'"

Final

This implementation solves any known problems of generating raw SQL string known until today (including mine added for PostgreSQL).

I am open to a different naming for these new methods.

@tpetry tpetry changed the title [10.x] Add toRawSql for query builders [10.x] Add toRawSql, dumpRawSql() and ddRawSql() to Query Builders Jun 20, 2023
tpetry pushed a commit that referenced this pull request Jan 30, 2024
…laravel#49474)

* test: validateJson should return false when value is null

Fails with Laravel Framework 10.38.2 in PHP < 8.3, introduced in laravel#49413

* fix: validateJson should return false when value is null

Return false when $value is null.

Avoid TypeError: json_validate(): Argument #1 ($json) must be of type string, null given, when using symfony/polyfill-php83 in PHP < 8.3.

Avoid deprecation warning: json_validate(): Passing null to parameter #1 ($json) of type string is deprecated, when using PHP 8.3.

---------

Co-authored-by: Rogelio Jacinto <rogelio@elabmexico.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants