-
-
Notifications
You must be signed in to change notification settings - Fork 438
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
Support optimized select for top-level query #2235
base: master
Are you sure you want to change the base?
Conversation
3f9cc91
to
61ed5cd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work on this, the tests look comprehensive and the overall direction looks right.
tests/Integration/WhereConditions/WhereConditionsDirectiveTest.php
Outdated
Show resolved
Hide resolved
foreach ($bindings as $type => $binding) { | ||
$builder = $builder->addBinding($binding, $type); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean it only needs to re-set the bindings['select'] but not the whole bindings?
That sounds reasonable then. Saves unnecessary work and is more explicit about what is happening and why.
src/lighthouse.php
Outdated
| | ||
*/ | ||
|
||
'optimized_selects' => true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should consider starting this setting with false
and wait for reports of forgotten edge cases to come in.
Co-authored-by: Benedikt Franke <benedikt@franke.tech>
Is there anything that needs to change for this pull request to be merged? |
if (($hasData = Arr::has($fieldSelection, 'data')) || Arr::has($fieldSelection, 'edges')) { | ||
$data = $hasData | ||
? $fieldSelection['data'] | ||
: $fieldSelection['edges']['node']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can derive which field to check from $this->directiveArgValue('type')
, no need to check both.
/** @var string|string[] $keyName */ | ||
$keyName = $model->getKeyName(); | ||
if (is_string($keyName)) { | ||
$keyName = [$keyName]; | ||
} | ||
|
||
foreach ($keyName as $name) { | ||
$query->orderBy($name); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be unrelated to optimizing the select.
} | ||
$fieldTypeName = ASTHelper::getUnderlyingTypeName($fieldDefinition); | ||
|
||
return preg_replace('/(Connection|SimplePaginator|Paginator)$/', '', $fieldTypeName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can look at the argument of @paginate(type: ?)
to know exactly which one to remove. That way, a model called ProductSimple
of type PAGINATOR
will not wrongly be seen as Product
.
/** @var string|string[] $keyName */ | ||
$keyName = $model->getKeyName(); | ||
if (is_string($keyName)) { | ||
$keyName = [$keyName]; | ||
} | ||
|
||
foreach ($keyName as $name) { | ||
$query->orderBy($name); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, this seems like an unnecessary addition that is orthogonal to optimizing select.
|
||
/** @var string|string[] $keyName */ | ||
$keyName = $model->getKeyName(); | ||
if (is_string($keyName)) { | ||
$keyName = [$keyName]; | ||
} | ||
|
||
foreach ($keyName as $name) { | ||
$builder->orderBy($name); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary
$relationName = ASTHelper::directiveArgValue($directive, 'relation', $field); | ||
if (method_exists($model, $relationName)) { | ||
$relation = $model->{$relationName}(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling methods equivalent to the name of a field just because they exist is potentially dangerous.
type MyModel {
launchTheNukes: Boolean! @method(name: "checkIfUserIsAllowedToLaunchTheNukesButNotActuallyLaunchThem")
}
Consider
lighthouse/src/Execution/Arguments/ArgPartitioner.php
Lines 171 to 200 in 46a57e3
/** | |
* Does a method on the model return a relation of the given class? | |
*/ | |
public static function methodReturnsRelation( | |
\ReflectionClass $modelReflection, | |
string $name, | |
string $relationClass | |
): bool { | |
if (! $modelReflection->hasMethod($name)) { | |
return false; | |
} | |
$relationMethodCandidate = $modelReflection->getMethod($name); | |
$returnType = $relationMethodCandidate->getReturnType(); | |
if (! $returnType instanceof \ReflectionNamedType) { | |
return false; | |
} | |
if ($returnType->isBuiltin()) { | |
return false; | |
} | |
if (! class_exists($returnType->getName())) { | |
throw new DefinitionException('Class ' . $returnType->getName() . ' does not exist, did you forget to import the Eloquent relation class?'); | |
} | |
return is_a($returnType->getName(), $relationClass, true); | |
} | |
} |
$renamedAttribute = ASTHelper::directiveArgValue($directive, 'attribute'); | ||
$selectColumns[] = $renamedAttribute; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no guarantee that the renamed attribute is actually a column, it could just as well be a virtual property.
$relation = null !== $directive | ||
? $model->{ASTHelper::directiveArgValue($directive, 'name')}() | ||
: $model->{$field}(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, this is dangerously calling methods of the model. Due to the possibility of there being custom resolver directives, we don't know at all if they would be called during field resolution.
Even with @method(name: ?)
, there are still multiple ways this could go wrong:
- the field might have required arguments that would be passed to the method when actually resolved
- there might be middleware that prevents the current user from actually resolving the field
There are probably more, but once again this proves my point:
I don't see how we could reliably determine if a field is a column without explicit configuration.
It is not that I don't want to add this feature, I just need to make sure it is done in a way that does not cause random runtime crashes, security breaches, unintended side effects or a multitude of other problems. We really need to limit the magic to cases where we can be absolutely sure nothing can go wrong, otherwise we can only depend on explicit configuration.
// fallback to selecting the field name | ||
$selectColumns[] = $field; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This just goes wrong for the very simple case of there being a custom getter for the field - or any custom directive that we can not possibly know about. Consider the following:
type MyModel {
foo: Int @someCustomDirectiveThatWeDoNotKnowAbout
bar: ID @field(resolver: "SomeCustomClass@andAMethodWeCanNotPossiblyLookInto")
}
I don't think any amount of magic can help us here, any approach of trying to determine columns without explicit configuration is just fundamentally flawed.
|-------------------------------------------------------------------------- | ||
| | ||
| If set to true, Eloquent will only select the columns necessary to resolve a query. | ||
| Use the @select directive to specify column dependencies of compound fields. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to the problems I outlined in SelectHelper
, I think we need to change the approach to this entirely. I believe we need to require that every field in a model that needs to select any columns has an @select
configuration. I know that this is cumbersome, but perhaps we can allow iterative adoption by marking some models as being optimizable and only applying the optimization to those.
type Foo @select {
id: ID @select(columns: ["id"])
}
@select
on the model type signifies it can be considered for optimization.
Thank you for your continued efforts on this. Unfortunately this pull request needs significant rework for the reasons I outlined in my review. |
You can add the following entry to
|
Thanks @spawnia, could we add a @storipress tag as well — we'll get back to this one sometime next month as we're working on some other features right now. We expect this to be done EO March |
Sure, that sounds fine to me. |
I made some updates from #1626.
Tests passed directives
@all
@find
@paginate
The query builder and scout builder are not optimized.
Tests passed relation types
Only the root query will be optimized. The nested queries are not optimized.
Changes
Only query the selection fields.
Breaking changes
@select
@orderBy
https://dba.stackexchange.com/a/6053
Note
This PR was only tested in our own project, and there may be some edge cases that are not considered.