Skip to content

Commit

Permalink
[10.x] Add toRawSql for query builders
Browse files Browse the repository at this point in the history
  • Loading branch information
tpetry committed Jun 20, 2023
1 parent 31b3d29 commit 9131c6e
Show file tree
Hide file tree
Showing 14 changed files with 218 additions and 25 deletions.
7 changes: 1 addition & 6 deletions .github/workflows/databases.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
name: databases

on:
push:
branches:
- master
- '*.x'
pull_request:
on: [push, pull_request]

jobs:
mysql_57:
Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/facades.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
name: facades

on:
push:
branches:
- master
- '*.x'
on: [push, pull_request]

jobs:
update:
Expand Down
7 changes: 1 addition & 6 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
name: static analysis

on:
push:
branches:
- master
- '*.x'
pull_request:
on: [push, pull_request]

jobs:
types:
Expand Down
9 changes: 1 addition & 8 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
name: tests

on:
push:
branches:
- master
- '*.x'
pull_request:
schedule:
- cron: '0 0 * * *'
on: [push, pull_request]

jobs:
linux_tests:
Expand Down
1 change: 1 addition & 0 deletions src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class Builder implements BuilderContract
'raw',
'rawValue',
'sum',
'toRawSql',
'toSql',
];

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

/**
* Get the raw SQL representation of the query.
*
* @return string
*/
public function toRawSql()
{
$sql = $this->toSql();
$bindings = $this->connection->prepareBindings($this->getBindings());

return $this->grammar->makeRawSql($sql, $bindings);
}

/**
* Execute a query for a single record by ID.
*
Expand Down
35 changes: 35 additions & 0 deletions src/Illuminate/Database/Query/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -1376,4 +1376,39 @@ public function getBitwiseOperators()
{
return $this->bitwiseOperators;
}

/**
* Make raw SQL query.
*
* @param string $sql
* @param array $bindings
* @return string
*/
public function makeRawSql($sql, $bindings)
{
$bindings = array_map(fn ($value) => $this->escape($value), $bindings);
$query = '';

$isEscape = 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 \'.
// PostgreSQL has operators like ?| that have to be encoded in PHP like ??|.
if (in_array($char.$nextChar, ["\'", "''", '??'])) {
$query .= $char.$nextChar;
$i += 1;
} else if ($char === "'") {
$query .= $char;
$isEscape = !$isEscape;
} else if ($char === '?' && !$isEscape) {
$query .= array_shift($bindings) ?? '?';
} else {
$query .= $char;
}
}

return $query;
}
}
14 changes: 14 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,18 @@ protected function parseJsonPathArrayKeys($attribute)

return [$attribute];
}

/**
* Make raw SQL query.
*
* @param string $sql
* @param array $bindings
* @return string
*/
public function makeRawSql($sql, $bindings)
{
$query = parent::makeRawSql($sql, $bindings);

return str_replace('??', '?', $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->makeRawSql(
'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->makeRawSql(
'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('makeRawSql')
->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->makeRawSql(
'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->makeRawSql(
"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 9131c6e

Please sign in to comment.