Skip to content

Commit

Permalink
Merge pull request #122 from socialblue/develop
Browse files Browse the repository at this point in the history
feature: add support for database expressions in query bindings
  • Loading branch information
mbroersen committed Oct 6, 2022
2 parents b6792d2 + e4e40d1 commit 97016b5
Show file tree
Hide file tree
Showing 12 changed files with 163 additions and 89 deletions.
2 changes: 1 addition & 1 deletion public/js/app.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/mix-manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"/js/app.js": "/js/app.js?id=bcdcff62c4c28fced9bfca2bc43ab58c",
"/js/app.js": "/js/app.js?id=7504eb3b44db8f2afcb55463993a7e23",
"/css/app.css": "/css/app.css?id=bc537e586d6c5f6f44082b50222fdafd"
}
6 changes: 3 additions & 3 deletions resources/assets/js/modules/query/components/query-block.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default {
type: Object,
},
sessionId: {
sessionKey: {
type: String,
}
Expand All @@ -101,15 +101,15 @@ export default {
methods: {
showExplainDialog() {
this.$router.push({name: 'session-query-explain', params: {
sessionKey: this.sessionId,
sessionKey: this.sessionKey,
time: this.query.time,
timeKey: this.query.timeKey,
}});
},
showExecuteDialog() {
this.$router.push({name: 'session-query-execute', params: {
sessionKey: this.sessionId,
sessionKey: this.sessionKey,
time: this.query.time,
timeKey: this.query.timeKey,
sql: this.query.sql
Expand Down
4 changes: 2 additions & 2 deletions resources/assets/js/modules/session/components/session.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
export default {
props: {
id: {
sessionKey: {
type: Number,
default() {
return 0;
Expand Down Expand Up @@ -78,7 +78,7 @@
methods: {
openSession() {
this.$router.push({name: 'session', params: {sessionKey: this.id}});
this.$router.push({name: 'session', params: {sessionKey: this.sessionKey}});
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion resources/assets/js/modules/session/views/session.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<div class="column" v-for="query in dataList[key]" >
<query-block
:query="query"
:session-id="sessionKey"
:session-key="sessionKey"
>
</query-block>
</div>
Expand Down
79 changes: 3 additions & 76 deletions src/DataListener/QueryListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\Cache;
use Socialblue\LaravelQueryAdviser\Helper\QueryBuilderHelper;
use Socialblue\LaravelQueryAdviser\DataListener\Services\SessionFormatter;

class QueryListener
{
Expand All @@ -24,89 +24,16 @@ public static function listen(QueryExecuted $query)
if (str_contains($url, '/query-adviser')) {
return;
}
$time = time();
$referer = request()->headers->get('referer');

$time = time();
$data = self::getFromCache($time, $sessionKey);

$possibleTraces = self::formatPossibleTraces(self::getPossibleTraces());

self::putToCache(
self::formatData($query, $data, $time, $possibleTraces, $url, $referer),
(new SessionFormatter())->format($time, $data, $query),
$sessionKey
);
}

protected static function getPossibleTraces(): array
{
$traces = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 25);
krsort($traces);

$possibleTraces = array_filter(
$traces,
static function ($trace) {
return isset($trace['file']) &&
isset($trace['object']) &&
strpos($trace['file'], base_path('vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php')) !== false;
}
);

$currentPossibleTrace = current($possibleTraces);

if (! empty($currentPossibleTrace)) {
$calledBy = $traces[key($possibleTraces) + 1];
$currentPossibleTrace['file'] = $calledBy['file'];
$currentPossibleTrace['line'] = $calledBy['line'];
$currentPossibleTrace['function'] = $calledBy['function'];
return [$currentPossibleTrace];
}

return $possibleTraces;
}

/**
* @param $possibleTraces
* @return mixed
*/
protected static function formatPossibleTraces($possibleTraces)
{
array_walk($possibleTraces, static function (&$trace) {
if (method_exists($trace['object'], 'getModel')) {
$a = $trace['object']->getModel();
if (is_string($a)) {
$trace['model'] = $a;
} else {
$trace['model'] = get_class($a);
}
}
unset($trace['object']);
unset($trace['args']);
});
return $possibleTraces;
}

/**
* @param $possibleTraces
* @param string|null $referer
*/
protected static function formatData(QueryExecuted $query, array $data, int $time, $possibleTraces, string $url, $referer = ''): array
{
$key = count($data[$time]);

$data[$time][$key] = [
'time' => $time,
'timeKey' => $key,
'backtrace' => $possibleTraces,
'sql' => QueryBuilderHelper::combineQueryAndBindings($query->sql, $query->bindings),
'rawSql' => $query->sql,
'bindings' => $query->bindings,
'queryTime' => $query->time,
'url' => empty($url) ? '/' : $url,
'referer' => empty($referer) ? '/' : $referer,
];
return $data;
}

/**
* @param $sessionKey
*/
Expand Down
31 changes: 31 additions & 0 deletions src/DataListener/Services/BindingsMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Socialblue\LaravelQueryAdviser\DataListener\Services;

use Illuminate\Database\Query\Expression;
use Illuminate\Support\Facades\DB;

class BindingsMapper
{
const EXPRESSION = "{EXPRESSION:} ";

public function toCache(array $bindings): array
{
foreach ($bindings as &$binding) {
if ($binding instanceof Expression) {
$binding = self::EXPRESSION . $binding->getValue();
}
}
return $bindings;
}

public function fromCache(array $bindings): array
{
foreach ($bindings as &$binding) {
if (str_starts_with($binding, self::EXPRESSION)) {
$binding = DB::raw(substr($binding, 14));
}
}
return $bindings;
}
}
30 changes: 30 additions & 0 deletions src/DataListener/Services/SessionFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Socialblue\LaravelQueryAdviser\DataListener\Services;

use Socialblue\LaravelQueryAdviser\Helper\QueryBuilderHelper;

class SessionFormatter
{
public function format($time, $data, $query): array
{
$referer = request()->headers->get('referer');
$url = url()->current();
$possibleTraces = (new TraceMapper())->get();
$key = count($data[$time]);

$data[$time][$key] = [
'time' => $time,
'timeKey' => $key,
'backtrace' => $possibleTraces,
'sql' => QueryBuilderHelper::combineQueryAndBindings($query->sql, $query->bindings),
'rawSql' => $query->sql,
'bindings' => (new BindingsMapper())->toCache($query->bindings),
'queryTime' => $query->time,
'url' => empty($url) ? '/' : $url,
'referer' => empty($referer) ? '/' : $referer,
];

return $data;
}
}
57 changes: 57 additions & 0 deletions src/DataListener/Services/TraceMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace Socialblue\LaravelQueryAdviser\DataListener\Services;

class TraceMapper
{
public function get()
{
return $this->format(
$this->possibleTraces()
);
}

private function possibleTraces(): array
{
$traces = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 25);
krsort($traces);

$possibleTraces = array_filter(
$traces,
static function ($trace) {
return isset($trace['file']) &&
isset($trace['object']) &&
strpos($trace['file'], base_path('vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php')) !== false;
}
);

$currentPossibleTrace = current($possibleTraces);

if (! empty($currentPossibleTrace)) {
$calledBy = $traces[key($possibleTraces) + 1];
$currentPossibleTrace['file'] = $calledBy['file'];
$currentPossibleTrace['line'] = $calledBy['line'];
$currentPossibleTrace['function'] = $calledBy['function'];
return [$currentPossibleTrace];
}

return $possibleTraces;
}

private function format(array $possibleTraces): array
{
array_walk($possibleTraces, static function (&$trace) {
if (method_exists($trace['object'], 'getModel')) {
$a = $trace['object']->getModel();
if (is_string($a)) {
$trace['model'] = $a;
} else {
$trace['model'] = get_class($a);
}
}
unset($trace['object']);
unset($trace['args']);
});
return $possibleTraces;
}
}
5 changes: 3 additions & 2 deletions src/Http/Controllers/QueryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Socialblue\LaravelQueryAdviser\DataListener\Services\BindingsMapper;
use Socialblue\LaravelQueryAdviser\Helper\QueryBuilderHelper;

/**
Expand All @@ -23,7 +24,7 @@ public function exec(string $sessionKey, string $time, string $timeKey, Request

if (isset($data[$time][$timeKey])) {
$query = $data[$time][$timeKey];
return DB::connection()->select($query['rawSql'], $query['bindings']);
return DB::connection()->select($query['rawSql'], (new BindingsMapper())->fromCache($query['bindings']));
}

return [];
Expand All @@ -37,7 +38,7 @@ public function explain(string $sessionKey, string $time, string $timeKey, Reque
$data = Cache::get($sessionKey);
if (isset($data[$time][$timeKey])) {
$query = $data[$time][$timeKey];
return QueryBuilderHelper::analyze($query['rawSql'], $query['bindings']);
return QueryBuilderHelper::analyze($query['rawSql'], (new BindingsMapper())->fromCache($query['bindings']));
}

return [
Expand Down
16 changes: 14 additions & 2 deletions tests/Feature/QueryControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public function can_explain_query()
*/
public function can_execute_query_of_session()
{
DB::statement("CREATE TABLE user (id INTEGER, name varchar);");
DB::insert("INSERT INTO user VALUES(?,?);", [1, 'test']);
DB::statement('CREATE TABLE user (id INTEGER, name varchar);');
DB::insert('INSERT INTO user VALUES(?,?);', [1, 'test']);
$sessionKey = $this->get('/query-adviser/api/session/start')->json('session_id');

// reset current url for test
Expand All @@ -52,4 +52,16 @@ public function can_execute_query_of_session()
$this->assertEquals([['id' => '1', 'name' => 'test']], $data);
}

/**
* @test
*/
public function returns_empty_array_when_no_traces_in_session()
{
$sessionKey = $this->get('/query-adviser/api/session/start')->json('session_id');
$sessionData = $this->get("/query-adviser/api/session/$sessionKey/")->json();

$this->assertSame([], $sessionData);
}


}
18 changes: 17 additions & 1 deletion tests/Unit/QueryListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Socialblue\LaravelQueryAdviser\DataListener\QueryListener;
use Socialblue\LaravelQueryAdviser\DataListener\Services\BindingsMapper;
use Socialblue\LaravelQueryAdviser\Http\Controllers\SessionController;
use Socialblue\LaravelQueryAdviser\Tests\TestCase;

class QueryListenerTest extends TestCase {
class QueryListenerTest extends TestCase
{

/** @test */
public function query_listener_stores_all_data_keys_in_cache()
Expand Down Expand Up @@ -38,7 +40,21 @@ public function query_listener_stores_all_data_keys_in_cache()
$this->assertArrayHasKey('url', $data);

$this->assertEquals('select * from user where id = \'1\'', $data['sql']);
}

/**
* @test
*/
public function binding_mapper_should_be_able_to_process_expressions()
{
$data = [1, '1', DB::raw("test")];

$toCache = (new BindingsMapper())->toCache($data);

$this->assertSame([1, '1', '{EXPRESSION:} test'], $toCache);

$fromCache = (new BindingsMapper())->fromCache($toCache);
$this->assertEquals($data, $fromCache);
}

}

0 comments on commit 97016b5

Please sign in to comment.