Skip to content

Commit 830efbe

Browse files
tpetrytaylorotwell
andauthored
[10.x] Add toRawSql, dumpRawSql() and ddRawSql() to Query Builders (#47507)
* [10.x] Add toRawSql, dumpRawSql() and ddRawSql() to Query Builders * styleci * formatting --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 1dd218f commit 830efbe

10 files changed

+248
-0
lines changed

src/Illuminate/Database/Eloquent/Builder.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,11 @@ class Builder implements BuilderContract
9696
'avg',
9797
'count',
9898
'dd',
99+
'ddRawSql',
99100
'doesntExist',
100101
'doesntExistOr',
101102
'dump',
103+
'dumpRawSql',
102104
'exists',
103105
'existsOr',
104106
'explain',
@@ -116,6 +118,7 @@ class Builder implements BuilderContract
116118
'rawValue',
117119
'sum',
118120
'toSql',
121+
'toRawSql',
119122
];
120123

121124
/**

src/Illuminate/Database/Query/Builder.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2634,6 +2634,18 @@ public function toSql()
26342634
return $this->grammar->compileSelect($this);
26352635
}
26362636

2637+
/**
2638+
* Get the raw SQL representation of the query with embedded bindings.
2639+
*
2640+
* @return string
2641+
*/
2642+
public function toRawSql()
2643+
{
2644+
return $this->grammar->substituteBindingsIntoRawSql(
2645+
$this->toSql(), $this->connection->prepareBindings($this->getBindings())
2646+
);
2647+
}
2648+
26372649
/**
26382650
* Execute a query for a single record by ID.
26392651
*
@@ -3897,6 +3909,18 @@ public function dump()
38973909
return $this;
38983910
}
38993911

3912+
/**
3913+
* Dump the raw current SQL with embedded bindings.
3914+
*
3915+
* @return $this
3916+
*/
3917+
public function dumpRawSql()
3918+
{
3919+
dump($this->toRawSql());
3920+
3921+
return $this;
3922+
}
3923+
39003924
/**
39013925
* Die and dump the current SQL and bindings.
39023926
*
@@ -3907,6 +3931,16 @@ public function dd()
39073931
dd($this->toSql(), $this->getBindings());
39083932
}
39093933

3934+
/**
3935+
* Die and dump the current SQL with embedded bindings.
3936+
*
3937+
* @return never
3938+
*/
3939+
public function ddRawSql()
3940+
{
3941+
dd($this->toRawSql());
3942+
}
3943+
39103944
/**
39113945
* Handle dynamic method calls into the method.
39123946
*

src/Illuminate/Database/Query/Grammars/Grammar.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,44 @@ protected function removeLeadingBoolean($value)
13501350
return preg_replace('/and |or /i', '', $value, 1);
13511351
}
13521352

1353+
/**
1354+
* Substitute the given bindings into the given raw SQL query.
1355+
*
1356+
* @param string $sql
1357+
* @param array $bindings
1358+
* @return string
1359+
*/
1360+
public function substituteBindingsIntoRawSql($sql, $bindings)
1361+
{
1362+
$bindings = array_map(fn ($value) => $this->escape($value), $bindings);
1363+
1364+
$query = '';
1365+
1366+
$isStringLiteral = false;
1367+
1368+
for ($i = 0; $i < strlen($sql); $i++) {
1369+
$char = $sql[$i];
1370+
$nextChar = $sql[$i + 1] ?? null;
1371+
1372+
// Single quotes can be escaped as '' according to the SQL standard while
1373+
// MySQL uses \'. Postgres has operators like ?| that must get encoded
1374+
// in PHP like ??|. We should skip over the escaped characters here.
1375+
if (in_array($char.$nextChar, ["\'", "''", '??'])) {
1376+
$query .= $char.$nextChar;
1377+
$i += 1;
1378+
} elseif ($char === "'") { // Starting / leaving string literal...
1379+
$query .= $char;
1380+
$isStringLiteral = ! $isStringLiteral;
1381+
} elseif ($char === '?' && ! $isStringLiteral) { // Substitutable binding...
1382+
$query .= array_shift($bindings) ?? '?';
1383+
} else { // Normal character...
1384+
$query .= $char;
1385+
}
1386+
}
1387+
1388+
return $query;
1389+
}
1390+
13531391
/**
13541392
* Get the grammar specific operators.
13551393
*

src/Illuminate/Database/Query/Grammars/PostgresGrammar.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,4 +698,26 @@ protected function parseJsonPathArrayKeys($attribute)
698698

699699
return [$attribute];
700700
}
701+
702+
/**
703+
* Substitute the given bindings into the given raw SQL query.
704+
*
705+
* @param string $sql
706+
* @param array $bindings
707+
* @return string
708+
*/
709+
public function substituteBindingsIntoRawSql($sql, $bindings)
710+
{
711+
$query = parent::substituteBindingsIntoRawSql($sql, $bindings);
712+
713+
foreach ($this->operators as $operator) {
714+
if (! str_contains($operator, '?')) {
715+
continue;
716+
}
717+
718+
$query = str_replace(str_replace('?', '??', $operator), $operator, $query);
719+
}
720+
721+
return $query;
722+
}
701723
}

tests/Database/DatabaseEloquentBuilderTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2216,6 +2216,17 @@ public function testClone()
22162216
$this->assertSame('select * from "users" where "email" = ?', $clone->toSql());
22172217
}
22182218

2219+
public function testToRawSql()
2220+
{
2221+
$query = m::mock(BaseBuilder::class);
2222+
$query->shouldReceive('toRawSql')
2223+
->andReturn('select * from "users" where "email" = \'foo\'');
2224+
2225+
$builder = new Builder($query);
2226+
2227+
$this->assertSame('select * from "users" where "email" = \'foo\'', $builder->toRawSql());
2228+
}
2229+
22192230
protected function mockConnectionForModel($model, $database)
22202231
{
22212232
$grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Database;
4+
5+
use Illuminate\Database\Connection;
6+
use Illuminate\Database\Query\Grammars\MySqlGrammar;
7+
use Mockery as m;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class DatabaseMySqlQueryGrammarTest extends TestCase
11+
{
12+
protected function tearDown(): void
13+
{
14+
m::close();
15+
}
16+
17+
public function testToRawSql()
18+
{
19+
$connection = m::mock(Connection::class);
20+
$connection->shouldReceive('escape')->with('foo', false)->andReturn("'foo'");
21+
$grammar = new MySqlGrammar;
22+
$grammar->setConnection($connection);
23+
24+
$query = $grammar->substituteBindingsIntoRawSql(
25+
'select * from "users" where \'Hello\\\'World?\' IS NOT NULL AND "email" = ?',
26+
['foo'],
27+
);
28+
29+
$this->assertSame('select * from "users" where \'Hello\\\'World?\' IS NOT NULL AND "email" = \'foo\'', $query);
30+
}
31+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Database;
4+
5+
use Illuminate\Database\Connection;
6+
use Illuminate\Database\Query\Grammars\PostgresGrammar;
7+
use Mockery as m;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class DatabasePostgresQueryGrammarTest extends TestCase
11+
{
12+
protected function tearDown(): void
13+
{
14+
m::close();
15+
}
16+
17+
public function testToRawSql()
18+
{
19+
$connection = m::mock(Connection::class);
20+
$connection->shouldReceive('escape')->with('foo', false)->andReturn("'foo'");
21+
$grammar = new PostgresGrammar;
22+
$grammar->setConnection($connection);
23+
24+
$query = $grammar->substituteBindingsIntoRawSql(
25+
'select * from "users" where \'{}\' ?? \'Hello\\\'\\\'World?\' AND "email" = ?',
26+
['foo'],
27+
);
28+
29+
$this->assertSame('select * from "users" where \'{}\' ? \'Hello\\\'\\\'World?\' AND "email" = \'foo\'', $query);
30+
}
31+
}

tests/Database/DatabaseQueryBuilderTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5663,6 +5663,22 @@ public function testCloneWithoutBindings()
56635663
$this->assertEquals([], $clone->getBindings());
56645664
}
56655665

5666+
public function testToRawSql()
5667+
{
5668+
$connection = m::mock(ConnectionInterface::class);
5669+
$connection->shouldReceive('prepareBindings')
5670+
->with(['foo'])
5671+
->andReturn(['foo']);
5672+
$grammar = m::mock(Grammar::class)->makePartial();
5673+
$grammar->shouldReceive('substituteBindingsIntoRawSql')
5674+
->with('select * from "users" where "email" = ?', ['foo'])
5675+
->andReturn('select * from "users" where "email" = \'foo\'');
5676+
$builder = new Builder($connection, $grammar, m::mock(Processor::class));
5677+
$builder->select('*')->from('users')->where('email', 'foo');
5678+
5679+
$this->assertSame('select * from "users" where "email" = \'foo\'', $builder->toRawSql());
5680+
}
5681+
56665682
protected function getConnection()
56675683
{
56685684
$connection = m::mock(ConnectionInterface::class);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Database;
4+
5+
use Illuminate\Database\Connection;
6+
use Illuminate\Database\Query\Grammars\SQLiteGrammar;
7+
use Mockery as m;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class DatabaseSQLiteQueryGrammarTest extends TestCase
11+
{
12+
protected function tearDown(): void
13+
{
14+
m::close();
15+
}
16+
17+
public function testToRawSql()
18+
{
19+
$connection = m::mock(Connection::class);
20+
$connection->shouldReceive('escape')->with('foo', false)->andReturn("'foo'");
21+
$grammar = new SQLiteGrammar;
22+
$grammar->setConnection($connection);
23+
24+
$query = $grammar->substituteBindingsIntoRawSql(
25+
'select * from "users" where \'Hello\'\'World?\' IS NOT NULL AND "email" = ?',
26+
['foo'],
27+
);
28+
29+
$this->assertSame('select * from "users" where \'Hello\'\'World?\' IS NOT NULL AND "email" = \'foo\'', $query);
30+
}
31+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Database;
4+
5+
use Illuminate\Database\Connection;
6+
use Illuminate\Database\Query\Grammars\SqlServerGrammar;
7+
use Mockery as m;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class DatabaseSqlServerQueryGrammarTest extends TestCase
11+
{
12+
protected function tearDown(): void
13+
{
14+
m::close();
15+
}
16+
17+
public function testToRawSql()
18+
{
19+
$connection = m::mock(Connection::class);
20+
$connection->shouldReceive('escape')->with('foo', false)->andReturn("'foo'");
21+
$grammar = new SqlServerGrammar;
22+
$grammar->setConnection($connection);
23+
24+
$query = $grammar->substituteBindingsIntoRawSql(
25+
"select * from [users] where 'Hello''World?' IS NOT NULL AND [email] = ?",
26+
['foo'],
27+
);
28+
29+
$this->assertSame("select * from [users] where 'Hello''World?' IS NOT NULL AND [email] = 'foo'", $query);
30+
}
31+
}

0 commit comments

Comments
 (0)