Skip to content

Commit 4adbee0

Browse files
committed
Add optimized selects
1 parent 3a3e686 commit 4adbee0

File tree

7 files changed

+184
-10
lines changed

7 files changed

+184
-10
lines changed

src/Pagination/PaginateDirective.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
1010
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
1111
use Nuwave\Lighthouse\Schema\Values\FieldValue;
12+
use Nuwave\Lighthouse\Select\SelectHelper;
1213
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
1314
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
1415
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
@@ -103,6 +104,16 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
103104
$this->directiveArgValue('scopes', [])
104105
);
105106

107+
if (config('lighthouse.optimized_selects')) {
108+
$fieldSelection = $resolveInfo->getFieldSelection(4);
109+
110+
if (in_array('data', $fieldSelection) || in_array('edges', $fieldSelection)) {
111+
$fieldSelection = array_keys(in_array($fieldSelection, 'data') ? $fieldSelection['data'] : $fieldSelection['edges']['node']);
112+
$selectColumns = SelectHelper::getSelectColumns($this->definitionNode, $fieldSelection, $this->getModelClass());
113+
$query = $query->select($selectColumns);
114+
}
115+
}
116+
106117
return PaginationArgs
107118
::extractArgs($args, $this->paginationType(), $this->paginateMaxCount())
108119
->applyToBuilder($query);

src/Schema/Directives/AllDirective.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use GraphQL\Type\Definition\ResolveInfo;
66
use Illuminate\Database\Eloquent\Collection;
77
use Nuwave\Lighthouse\Schema\Values\FieldValue;
8+
use Nuwave\Lighthouse\Select\SelectHelper;
89
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
910
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
1011

@@ -35,13 +36,20 @@ public function resolveField(FieldValue $fieldValue): FieldValue
3536
{
3637
return $fieldValue->setResolver(
3738
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): Collection {
38-
return $resolveInfo
39+
$builder = $resolveInfo
3940
->argumentSet
4041
->enhanceBuilder(
4142
$this->getModelClass()::query(),
4243
$this->directiveArgValue('scopes', [])
43-
)
44-
->get();
44+
);
45+
46+
if (config('lighthouse.optimized_selects')) {
47+
$fieldSelection = array_keys($resolveInfo->getFieldSelection(1));
48+
$selectColumns = SelectHelper::getSelectColumns($this->definitionNode, $fieldSelection, $this->getModelClass());
49+
$builder = $builder->select($selectColumns);
50+
}
51+
52+
return $builder->get();
4553
}
4654
);
4755
}

src/Schema/Directives/RelationDirective.php

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
use Nuwave\Lighthouse\Pagination\PaginationType;
1717
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
1818
use Nuwave\Lighthouse\Schema\Values\FieldValue;
19+
use Nuwave\Lighthouse\Select\SelectHelper;
1920
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
2021
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
22+
use Illuminate\Support\Facades\DB;
2123

2224
abstract class RelationDirective extends BaseDirective implements FieldResolver
2325
{
@@ -26,8 +28,7 @@ public function resolveField(FieldValue $value): FieldValue
2628
$value->setResolver(
2729
function (Model $parent, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) {
2830
$relationName = $this->directiveArgValue('relation', $this->nodeName());
29-
30-
$decorateBuilder = $this->makeBuilderDecorator($resolveInfo);
31+
$decorateBuilder = $this->makeBuilderDecorator($resolveInfo, $parent, $relationName);
3132
$paginationArgs = $this->paginationArgs($args);
3233

3334
if (config('lighthouse.batchload_relations')) {
@@ -70,15 +71,29 @@ function (Model $parent, array $args, GraphQLContext $context, ResolveInfo $reso
7071
return $value;
7172
}
7273

73-
protected function makeBuilderDecorator(ResolveInfo $resolveInfo): Closure
74-
{
75-
return function ($builder) use ($resolveInfo) {
76-
$resolveInfo
74+
protected function makeBuilderDecorator(ResolveInfo $resolveInfo, Model $parent, string $relationName): Closure
75+
{
76+
return function ($builder) use ($resolveInfo, $parent, $relationName) {
77+
$builderDecorator = $resolveInfo
7778
->argumentSet
7879
->enhanceBuilder(
7980
$builder,
8081
$this->directiveArgValue('scopes', [])
8182
);
83+
84+
if (config('lighthouse.optimized_selects')) {
85+
$fieldSelection = array_keys($resolveInfo->getFieldSelection(1));
86+
$selectColumns = SelectHelper::getSelectColumns($this->definitionNode, $fieldSelection, get_class($builderDecorator->getRelated()));
87+
$foreignKeyName = $parent->{$relationName}()->getForeignKeyName();
88+
89+
if (! in_array($foreignKeyName, $selectColumns)) {
90+
array_push($selectColumns, $foreignKeyName);
91+
}
92+
93+
$builderDecorator->select($selectColumns);
94+
95+
// at some point, the builder is "infected" with a "with" clause causing it to select the relation or something, idk
96+
}
8297
};
8398
}
8499

src/Select/SelectDirective.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Nuwave\Lighthouse\Select;
4+
5+
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
6+
7+
class SelectDirective extends BaseDirective
8+
{
9+
public static function definition(): string
10+
{
11+
return /** @lang GraphQL */ <<<'GRAPHQL'
12+
"""
13+
Specify the SQL column dependencies of this field.
14+
"""
15+
directive @select(
16+
"""
17+
SQL columns names to pass to the Eloquent query builder
18+
"""
19+
columns: [String!]
20+
) on FIELD_DEFINITION
21+
GRAPHQL;
22+
}
23+
}

src/Select/SelectHelper.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
namespace Nuwave\Lighthouse\Select;
4+
5+
use GraphQL\Language\AST\Node;
6+
use GraphQL\Language\AST\UnionTypeDefinitionNode;
7+
use Illuminate\Database\Eloquent\Model;
8+
use Nuwave\Lighthouse\Schema\AST\ASTBuilder;
9+
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
10+
11+
class SelectHelper
12+
{
13+
/**
14+
* Given a field definition node, resolve info, and a model name, return the SQL columns that should be selected.
15+
* Accounts for relationships and the rename and select directives.
16+
*
17+
* @param mixed[] $fieldSelection
18+
* @return string[]
19+
*/
20+
public static function getSelectColumns(Node $definitionNode, array $fieldSelection, string $modelName): array
21+
{
22+
$returnTypeName = ASTHelper::getUnderlyingTypeName($definitionNode);
23+
24+
/** @var \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST */
25+
$documentAST = app(ASTBuilder::class)->documentAST();
26+
27+
$type = $documentAST->types[$returnTypeName];
28+
// error_log(print_r($type, true));
29+
30+
if ($type instanceof UnionTypeDefinitionNode) {
31+
$type = $documentAST->types[ASTHelper::getUnderlyingTypeName($type->types[0])];
32+
}
33+
34+
35+
/** @var iterable<\GraphQL\Language\AST\FieldDefinitionNode> $fieldDefinitions */
36+
$fieldDefinitions = $type->fields;
37+
38+
$model = new $modelName;
39+
40+
$selectColumns = [];
41+
42+
foreach ($fieldSelection as $field) {
43+
$fieldDefinition = ASTHelper::firstByName($fieldDefinitions, $field);
44+
45+
if ($fieldDefinition) {
46+
$directivesRequiringLocalKey = ['hasOne', 'hasMany', 'count'];
47+
$directivesRequiringForeignKey = ['belongsTo', 'belongsToMany', 'morphTo'];
48+
$directivesRequiringKeys = array_merge($directivesRequiringLocalKey, $directivesRequiringForeignKey);
49+
50+
foreach ($directivesRequiringKeys as $directiveType) {
51+
if (ASTHelper::hasDirective($fieldDefinition, $directiveType)) {
52+
$directive = ASTHelper::directiveDefinition($fieldDefinition, $directiveType);
53+
54+
if (in_array($directiveType, $directivesRequiringLocalKey)) {
55+
$relationName = ASTHelper::directiveArgValue($directive, 'relation', $field);
56+
57+
if (method_exists($model, $relationName)) {
58+
array_push($selectColumns, $model->{$relationName}()->getLocalKeyName());
59+
}
60+
}
61+
62+
if (in_array($directiveType, $directivesRequiringForeignKey)) {
63+
$relationName = ASTHelper::directiveArgValue($directive, 'relation', $field);
64+
65+
if (method_exists($model, $relationName)) {
66+
array_push($selectColumns, $model->{$relationName}()->getForeignKeyName());
67+
}
68+
}
69+
70+
continue 2;
71+
}
72+
}
73+
74+
if (ASTHelper::hasDirective($fieldDefinition, 'select')) {
75+
// append selected columns in select directive to seletion
76+
$directive = ASTHelper::directiveDefinition($fieldDefinition, 'select');
77+
78+
if ($directive) {
79+
$selectFields = ASTHelper::directiveArgValue($directive, 'columns') ?? [];
80+
$selectColumns = array_merge($selectColumns, $selectFields);
81+
}
82+
} elseif (ASTHelper::hasDirective($fieldDefinition, 'rename')) {
83+
// append renamed attribute to selection
84+
$directive = ASTHelper::directiveDefinition($fieldDefinition, 'rename');
85+
86+
if ($directive) {
87+
$renamedAttribute = ASTHelper::directiveArgValue($directive, 'attribute');
88+
array_push($selectColumns, $renamedAttribute);
89+
}
90+
} else {
91+
// fallback to selecting the field name
92+
array_push($selectColumns, $field);
93+
}
94+
}
95+
}
96+
97+
return array_unique($selectColumns);
98+
}
99+
}

src/lighthouse.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,19 @@
295295

296296
'batchload_relations' => true,
297297

298+
/*
299+
|--------------------------------------------------------------------------
300+
| Optimized Selects
301+
|--------------------------------------------------------------------------
302+
|
303+
| If set to true, Eloquent will only select the columns neccessary to resolve
304+
| a query. You must use the select directive to resolve advanced field dependencies
305+
| on other columns.
306+
|
307+
*/
308+
309+
'optimized_selects' => true,
310+
298311
/*
299312
|--------------------------------------------------------------------------
300313
| GraphQL Subscriptions

tests/DBTestCase.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Tests;
44

55
use Illuminate\Support\Facades\DB;
6+
use Illuminate\Support\Facades\Event;
67

78
abstract class DBTestCase extends TestCase
89
{
@@ -39,11 +40,15 @@ protected function getEnvironmentSetUp($app): void
3940
{
4041
parent::getEnvironmentSetUp($app);
4142

43+
/*Event::listen('Illuminate\Database\Events\QueryExecuted', function ($query) {
44+
error_log($query->sql . ' - ' . serialize($query->bindings));
45+
});*/
46+
4247
$app['config']->set('database.default', 'mysql');
4348
$app['config']->set('database.connections.mysql', [
4449
'driver' => 'mysql',
4550
'database' => env('LIGHTHOUSE_TEST_DB_DATABASE', 'test'),
46-
'host' => env('LIGHTHOUSE_TEST_DB_HOST', 'mysql'),
51+
'host' => env('LIGHTHOUSE_TEST_DB_HOST', 'localhost'),
4752
'username' => env('LIGHTHOUSE_TEST_DB_USERNAME', 'root'),
4853
'password' => env('LIGHTHOUSE_TEST_DB_PASSWORD', ''),
4954
]);

0 commit comments

Comments
 (0)