Skip to content

Commit

Permalink
Add optimized selects
Browse files Browse the repository at this point in the history
  • Loading branch information
DDynamic committed Jan 10, 2021
1 parent 3a3e686 commit 4adbee0
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 10 deletions.
11 changes: 11 additions & 0 deletions src/Pagination/PaginateDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Select\SelectHelper;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
Expand Down Expand Up @@ -103,6 +104,16 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
$this->directiveArgValue('scopes', [])
);

if (config('lighthouse.optimized_selects')) {
$fieldSelection = $resolveInfo->getFieldSelection(4);

if (in_array('data', $fieldSelection) || in_array('edges', $fieldSelection)) {
$fieldSelection = array_keys(in_array($fieldSelection, 'data') ? $fieldSelection['data'] : $fieldSelection['edges']['node']);
$selectColumns = SelectHelper::getSelectColumns($this->definitionNode, $fieldSelection, $this->getModelClass());
$query = $query->select($selectColumns);
}
}

return PaginationArgs
::extractArgs($args, $this->paginationType(), $this->paginateMaxCount())
->applyToBuilder($query);
Expand Down
14 changes: 11 additions & 3 deletions src/Schema/Directives/AllDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Database\Eloquent\Collection;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Select\SelectHelper;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;

Expand Down Expand Up @@ -35,13 +36,20 @@ public function resolveField(FieldValue $fieldValue): FieldValue
{
return $fieldValue->setResolver(
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): Collection {
return $resolveInfo
$builder = $resolveInfo
->argumentSet
->enhanceBuilder(
$this->getModelClass()::query(),
$this->directiveArgValue('scopes', [])
)
->get();
);

if (config('lighthouse.optimized_selects')) {
$fieldSelection = array_keys($resolveInfo->getFieldSelection(1));
$selectColumns = SelectHelper::getSelectColumns($this->definitionNode, $fieldSelection, $this->getModelClass());
$builder = $builder->select($selectColumns);
}

return $builder->get();
}
);
}
Expand Down
27 changes: 21 additions & 6 deletions src/Schema/Directives/RelationDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
use Nuwave\Lighthouse\Pagination\PaginationType;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Select\SelectHelper;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Illuminate\Support\Facades\DB;

abstract class RelationDirective extends BaseDirective implements FieldResolver
{
Expand All @@ -26,8 +28,7 @@ public function resolveField(FieldValue $value): FieldValue
$value->setResolver(
function (Model $parent, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) {
$relationName = $this->directiveArgValue('relation', $this->nodeName());

$decorateBuilder = $this->makeBuilderDecorator($resolveInfo);
$decorateBuilder = $this->makeBuilderDecorator($resolveInfo, $parent, $relationName);
$paginationArgs = $this->paginationArgs($args);

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

protected function makeBuilderDecorator(ResolveInfo $resolveInfo): Closure
{
return function ($builder) use ($resolveInfo) {
$resolveInfo
protected function makeBuilderDecorator(ResolveInfo $resolveInfo, Model $parent, string $relationName): Closure
{
return function ($builder) use ($resolveInfo, $parent, $relationName) {
$builderDecorator = $resolveInfo
->argumentSet
->enhanceBuilder(
$builder,
$this->directiveArgValue('scopes', [])
);

if (config('lighthouse.optimized_selects')) {
$fieldSelection = array_keys($resolveInfo->getFieldSelection(1));
$selectColumns = SelectHelper::getSelectColumns($this->definitionNode, $fieldSelection, get_class($builderDecorator->getRelated()));
$foreignKeyName = $parent->{$relationName}()->getForeignKeyName();

if (! in_array($foreignKeyName, $selectColumns)) {
array_push($selectColumns, $foreignKeyName);
}

$builderDecorator->select($selectColumns);

// at some point, the builder is "infected" with a "with" clause causing it to select the relation or something, idk
}
};
}

Expand Down
23 changes: 23 additions & 0 deletions src/Select/SelectDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Nuwave\Lighthouse\Select;

use Nuwave\Lighthouse\Schema\Directives\BaseDirective;

class SelectDirective extends BaseDirective
{
public static function definition(): string
{
return /** @lang GraphQL */ <<<'GRAPHQL'
"""
Specify the SQL column dependencies of this field.
"""
directive @select(
"""
SQL columns names to pass to the Eloquent query builder
"""
columns: [String!]
) on FIELD_DEFINITION
GRAPHQL;
}
}
99 changes: 99 additions & 0 deletions src/Select/SelectHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace Nuwave\Lighthouse\Select;

use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use Illuminate\Database\Eloquent\Model;
use Nuwave\Lighthouse\Schema\AST\ASTBuilder;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;

class SelectHelper
{
/**
* Given a field definition node, resolve info, and a model name, return the SQL columns that should be selected.
* Accounts for relationships and the rename and select directives.
*
* @param mixed[] $fieldSelection
* @return string[]
*/
public static function getSelectColumns(Node $definitionNode, array $fieldSelection, string $modelName): array
{
$returnTypeName = ASTHelper::getUnderlyingTypeName($definitionNode);

/** @var \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST */
$documentAST = app(ASTBuilder::class)->documentAST();

$type = $documentAST->types[$returnTypeName];
// error_log(print_r($type, true));

if ($type instanceof UnionTypeDefinitionNode) {
$type = $documentAST->types[ASTHelper::getUnderlyingTypeName($type->types[0])];
}


/** @var iterable<\GraphQL\Language\AST\FieldDefinitionNode> $fieldDefinitions */
$fieldDefinitions = $type->fields;

$model = new $modelName;

$selectColumns = [];

foreach ($fieldSelection as $field) {
$fieldDefinition = ASTHelper::firstByName($fieldDefinitions, $field);

if ($fieldDefinition) {
$directivesRequiringLocalKey = ['hasOne', 'hasMany', 'count'];
$directivesRequiringForeignKey = ['belongsTo', 'belongsToMany', 'morphTo'];
$directivesRequiringKeys = array_merge($directivesRequiringLocalKey, $directivesRequiringForeignKey);

foreach ($directivesRequiringKeys as $directiveType) {
if (ASTHelper::hasDirective($fieldDefinition, $directiveType)) {
$directive = ASTHelper::directiveDefinition($fieldDefinition, $directiveType);

if (in_array($directiveType, $directivesRequiringLocalKey)) {
$relationName = ASTHelper::directiveArgValue($directive, 'relation', $field);

if (method_exists($model, $relationName)) {
array_push($selectColumns, $model->{$relationName}()->getLocalKeyName());
}
}

if (in_array($directiveType, $directivesRequiringForeignKey)) {
$relationName = ASTHelper::directiveArgValue($directive, 'relation', $field);

if (method_exists($model, $relationName)) {
array_push($selectColumns, $model->{$relationName}()->getForeignKeyName());
}
}

continue 2;
}
}

if (ASTHelper::hasDirective($fieldDefinition, 'select')) {
// append selected columns in select directive to seletion
$directive = ASTHelper::directiveDefinition($fieldDefinition, 'select');

if ($directive) {
$selectFields = ASTHelper::directiveArgValue($directive, 'columns') ?? [];
$selectColumns = array_merge($selectColumns, $selectFields);
}
} elseif (ASTHelper::hasDirective($fieldDefinition, 'rename')) {
// append renamed attribute to selection
$directive = ASTHelper::directiveDefinition($fieldDefinition, 'rename');

if ($directive) {
$renamedAttribute = ASTHelper::directiveArgValue($directive, 'attribute');
array_push($selectColumns, $renamedAttribute);
}
} else {
// fallback to selecting the field name
array_push($selectColumns, $field);
}
}
}

return array_unique($selectColumns);
}
}
13 changes: 13 additions & 0 deletions src/lighthouse.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,19 @@

'batchload_relations' => true,

/*
|--------------------------------------------------------------------------
| Optimized Selects
|--------------------------------------------------------------------------
|
| If set to true, Eloquent will only select the columns neccessary to resolve
| a query. You must use the select directive to resolve advanced field dependencies
| on other columns.
|
*/

'optimized_selects' => true,

/*
|--------------------------------------------------------------------------
| GraphQL Subscriptions
Expand Down
7 changes: 6 additions & 1 deletion tests/DBTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Tests;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;

abstract class DBTestCase extends TestCase
{
Expand Down Expand Up @@ -39,11 +40,15 @@ protected function getEnvironmentSetUp($app): void
{
parent::getEnvironmentSetUp($app);

/*Event::listen('Illuminate\Database\Events\QueryExecuted', function ($query) {
error_log($query->sql . ' - ' . serialize($query->bindings));
});*/

$app['config']->set('database.default', 'mysql');
$app['config']->set('database.connections.mysql', [
'driver' => 'mysql',
'database' => env('LIGHTHOUSE_TEST_DB_DATABASE', 'test'),
'host' => env('LIGHTHOUSE_TEST_DB_HOST', 'mysql'),
'host' => env('LIGHTHOUSE_TEST_DB_HOST', 'localhost'),
'username' => env('LIGHTHOUSE_TEST_DB_USERNAME', 'root'),
'password' => env('LIGHTHOUSE_TEST_DB_PASSWORD', ''),
]);
Expand Down

0 comments on commit 4adbee0

Please sign in to comment.