Skip to content

Commit

Permalink
[10.x] Add toRawSql, dumpRawSql() and ddRawSql() to Query Builders (#…
Browse files Browse the repository at this point in the history
…47507)

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

* styleci

* formatting

---------

Co-authored-by: Taylor Otwell <taylor@laravel.com>
  • Loading branch information
tpetry and taylorotwell committed Jun 28, 2023
1 parent 1dd218f commit 830efbe
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ class Builder implements BuilderContract
'avg',
'count',
'dd',
'ddRawSql',
'doesntExist',
'doesntExistOr',
'dump',
'dumpRawSql',
'exists',
'existsOr',
'explain',
Expand All @@ -116,6 +118,7 @@ class Builder implements BuilderContract
'rawValue',
'sum',
'toSql',
'toRawSql',
];

/**
Expand Down
34 changes: 34 additions & 0 deletions src/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2634,6 +2634,18 @@ public function toSql()
return $this->grammar->compileSelect($this);
}

/**
* Get the raw SQL representation of the query with embedded bindings.
*
* @return string
*/
public function toRawSql()
{
return $this->grammar->substituteBindingsIntoRawSql(
$this->toSql(), $this->connection->prepareBindings($this->getBindings())
);
}

/**
* Execute a query for a single record by ID.
*
Expand Down Expand Up @@ -3897,6 +3909,18 @@ public function dump()
return $this;
}

/**
* Dump the raw current SQL with embedded bindings.
*
* @return $this
*/
public function dumpRawSql()
{
dump($this->toRawSql());

return $this;
}

/**
* Die and dump the current SQL and bindings.
*
Expand All @@ -3907,6 +3931,16 @@ public function dd()
dd($this->toSql(), $this->getBindings());
}

/**
* Die and dump the current SQL with embedded bindings.
*
* @return never
*/
public function ddRawSql()
{
dd($this->toRawSql());
}

/**
* Handle dynamic method calls into the method.
*
Expand Down
38 changes: 38 additions & 0 deletions src/Illuminate/Database/Query/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,44 @@ protected function removeLeadingBoolean($value)
return preg_replace('/and |or /i', '', $value, 1);
}

/**
* Substitute the given bindings into the given raw SQL query.
*
* @param string $sql
* @param array $bindings
* @return string
*/
public function substituteBindingsIntoRawSql($sql, $bindings)
{
$bindings = array_map(fn ($value) => $this->escape($value), $bindings);

$query = '';

$isStringLiteral = false;

for ($i = 0; $i < strlen($sql); $i++) {
$char = $sql[$i];
$nextChar = $sql[$i + 1] ?? null;

// Single quotes can be escaped as '' according to the SQL standard while
// MySQL uses \'. Postgres has operators like ?| that must get encoded
// in PHP like ??|. We should skip over the escaped characters here.
if (in_array($char.$nextChar, ["\'", "''", '??'])) {
$query .= $char.$nextChar;
$i += 1;
} elseif ($char === "'") { // Starting / leaving string literal...
$query .= $char;
$isStringLiteral = ! $isStringLiteral;
} elseif ($char === '?' && ! $isStringLiteral) { // Substitutable binding...
$query .= array_shift($bindings) ?? '?';
} else { // Normal character...
$query .= $char;
}
}

return $query;
}

/**
* Get the grammar specific operators.
*
Expand Down
22 changes: 22 additions & 0 deletions src/Illuminate/Database/Query/Grammars/PostgresGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -698,4 +698,26 @@ protected function parseJsonPathArrayKeys($attribute)

return [$attribute];
}

/**
* Substitute the given bindings into the given raw SQL query.
*
* @param string $sql
* @param array $bindings
* @return string
*/
public function substituteBindingsIntoRawSql($sql, $bindings)
{
$query = parent::substituteBindingsIntoRawSql($sql, $bindings);

foreach ($this->operators as $operator) {
if (! str_contains($operator, '?')) {
continue;
}

$query = str_replace(str_replace('?', '??', $operator), $operator, $query);
}

return $query;
}
}
11 changes: 11 additions & 0 deletions tests/Database/DatabaseEloquentBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2216,6 +2216,17 @@ public function testClone()
$this->assertSame('select * from "users" where "email" = ?', $clone->toSql());
}

public function testToRawSql()
{
$query = m::mock(BaseBuilder::class);
$query->shouldReceive('toRawSql')
->andReturn('select * from "users" where "email" = \'foo\'');

$builder = new Builder($query);

$this->assertSame('select * from "users" where "email" = \'foo\'', $builder->toRawSql());
}

protected function mockConnectionForModel($model, $database)
{
$grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar';
Expand Down
31 changes: 31 additions & 0 deletions tests/Database/DatabaseMySqlQueryGrammarTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Illuminate\Tests\Database;

use Illuminate\Database\Connection;
use Illuminate\Database\Query\Grammars\MySqlGrammar;
use Mockery as m;
use PHPUnit\Framework\TestCase;

class DatabaseMySqlQueryGrammarTest extends TestCase
{
protected function tearDown(): void
{
m::close();
}

public function testToRawSql()
{
$connection = m::mock(Connection::class);
$connection->shouldReceive('escape')->with('foo', false)->andReturn("'foo'");
$grammar = new MySqlGrammar;
$grammar->setConnection($connection);

$query = $grammar->substituteBindingsIntoRawSql(
'select * from "users" where \'Hello\\\'World?\' IS NOT NULL AND "email" = ?',
['foo'],
);

$this->assertSame('select * from "users" where \'Hello\\\'World?\' IS NOT NULL AND "email" = \'foo\'', $query);
}
}
31 changes: 31 additions & 0 deletions tests/Database/DatabasePostgresQueryGrammarTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Illuminate\Tests\Database;

use Illuminate\Database\Connection;
use Illuminate\Database\Query\Grammars\PostgresGrammar;
use Mockery as m;
use PHPUnit\Framework\TestCase;

class DatabasePostgresQueryGrammarTest extends TestCase
{
protected function tearDown(): void
{
m::close();
}

public function testToRawSql()
{
$connection = m::mock(Connection::class);
$connection->shouldReceive('escape')->with('foo', false)->andReturn("'foo'");
$grammar = new PostgresGrammar;
$grammar->setConnection($connection);

$query = $grammar->substituteBindingsIntoRawSql(
'select * from "users" where \'{}\' ?? \'Hello\\\'\\\'World?\' AND "email" = ?',
['foo'],
);

$this->assertSame('select * from "users" where \'{}\' ? \'Hello\\\'\\\'World?\' AND "email" = \'foo\'', $query);
}
}
16 changes: 16 additions & 0 deletions tests/Database/DatabaseQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5663,6 +5663,22 @@ public function testCloneWithoutBindings()
$this->assertEquals([], $clone->getBindings());
}

public function testToRawSql()
{
$connection = m::mock(ConnectionInterface::class);
$connection->shouldReceive('prepareBindings')
->with(['foo'])
->andReturn(['foo']);
$grammar = m::mock(Grammar::class)->makePartial();
$grammar->shouldReceive('substituteBindingsIntoRawSql')
->with('select * from "users" where "email" = ?', ['foo'])
->andReturn('select * from "users" where "email" = \'foo\'');
$builder = new Builder($connection, $grammar, m::mock(Processor::class));
$builder->select('*')->from('users')->where('email', 'foo');

$this->assertSame('select * from "users" where "email" = \'foo\'', $builder->toRawSql());
}

protected function getConnection()
{
$connection = m::mock(ConnectionInterface::class);
Expand Down
31 changes: 31 additions & 0 deletions tests/Database/DatabaseSQLiteQueryGrammarTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Illuminate\Tests\Database;

use Illuminate\Database\Connection;
use Illuminate\Database\Query\Grammars\SQLiteGrammar;
use Mockery as m;
use PHPUnit\Framework\TestCase;

class DatabaseSQLiteQueryGrammarTest extends TestCase
{
protected function tearDown(): void
{
m::close();
}

public function testToRawSql()
{
$connection = m::mock(Connection::class);
$connection->shouldReceive('escape')->with('foo', false)->andReturn("'foo'");
$grammar = new SQLiteGrammar;
$grammar->setConnection($connection);

$query = $grammar->substituteBindingsIntoRawSql(
'select * from "users" where \'Hello\'\'World?\' IS NOT NULL AND "email" = ?',
['foo'],
);

$this->assertSame('select * from "users" where \'Hello\'\'World?\' IS NOT NULL AND "email" = \'foo\'', $query);
}
}
31 changes: 31 additions & 0 deletions tests/Database/DatabaseSqlServerQueryGrammarTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Illuminate\Tests\Database;

use Illuminate\Database\Connection;
use Illuminate\Database\Query\Grammars\SqlServerGrammar;
use Mockery as m;
use PHPUnit\Framework\TestCase;

class DatabaseSqlServerQueryGrammarTest extends TestCase
{
protected function tearDown(): void
{
m::close();
}

public function testToRawSql()
{
$connection = m::mock(Connection::class);
$connection->shouldReceive('escape')->with('foo', false)->andReturn("'foo'");
$grammar = new SqlServerGrammar;
$grammar->setConnection($connection);

$query = $grammar->substituteBindingsIntoRawSql(
"select * from [users] where 'Hello''World?' IS NOT NULL AND [email] = ?",
['foo'],
);

$this->assertSame("select * from [users] where 'Hello''World?' IS NOT NULL AND [email] = 'foo'", $query);
}
}

0 comments on commit 830efbe

Please sign in to comment.