Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optimized selects #1626

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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!]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
columns: [String!]
columns: [String!]!

This directive has no use without this argument.

) 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are trying to optimize performance here: Those should all be const.


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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, but if I need an accessor that is not in the table columns, we will get a "Column not found" SQL error.

Suggested change
return array_unique($selectColumns);
$allColumns = Schema::getColumnListing($model->getTable());
return array_intersect($allColumns, 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