-
-
Notifications
You must be signed in to change notification settings - Fork 581
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 support for EXPLAIN options to GraphiQL #1618
Conversation
This is very cool! One significant concern that I have currently is that if we allow |
@@ -1,12 +1,11 @@ | |||
import React from 'react'; | |||
import GraphiQL from 'graphiql'; | |||
import { getOperationAST, parse } from 'graphql'; | |||
import {buildClientSchema, getIntrospectionQuery, getOperationAST, GraphQLObjectType, isType, parse} from 'graphql'; |
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.
I do not understand why prettier is not formatting this file in the way I'd expect. It doesn't seem to be in the prettierignore
file, and it doesn't seem to be excluded from the prettier glob
And yet, CI passes. Very strange. I will have to investigate at some point.
What about |
Other than leaking through things like sequences ( |
To avoid running the mutation twice when ANALYZE is enabled
@benjie I added the savepoint. Could you do another pass when you get a chance? |
explainPromise = pgClient[$$pgClientOrigQuery] | ||
// Create a savepoint before running the EXPLAIN, so we can roll back to it to avoid running mutations | ||
// twice when ANALYZE is enabled. | ||
.call(this, 'savepoint postgraphile_explain') | ||
.then( | ||
// Savepoint created - we are in a transaction, which means this is a mutation. We need to roll back | ||
// to the savepoint after running the EXPLAIN. | ||
() => pgClient[$$pgClientOrigQuery].call(this, explain, values) | ||
.then((data: any) => pgClient[$$pgClientOrigQuery] | ||
.call(this, 'rollback to savepoint postgraphile_explain') | ||
.then(() => data)), | ||
// Failed to create savepoint - we are not in a transaction, which means this is a query. No need to | ||
// rollback the EXPLAIN. | ||
() => pgClient[$$pgClientOrigQuery].call(this, explain, values) | ||
) | ||
.then((data: any) => data.rows) | ||
// swallow errors during explain | ||
.catch(() => null); |
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 isn't safe because our queries can occur in parallel, and thus the rollback might be issued out of order (or there may even be concurrent rollbacks for the same savepoint!)
Instead you must ensure that they are all added in the same tick so that they get issued in order without a chance for other (parallel) queries to be inserted in between:
// IMPORTANT: the following 3 lines MUST BE CALLED SYNCHRONOUSLY.
pgClient[$$pgClientOrigQuery].query('savepoint postgraphile_explain');
const explainResultPromise = pgClient[$$pgClientOrigQuery].query(explain, values);
pgClient[$$pgClientOrigQuery].query('rollback savepoint postgraphile_explain');
// Now use `explainResultPromise` however you want.
…from interleaving
@benjie this is ready for another pass! |
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.
Excellent work - looking good! Ideally you should force a transaction for queries that involve EXPLAIN
so we don't get the errors from SAVEPOINT
/ ROLLBACK
(which I'm almost certain I'll start hearing about in a few months time...); but if you can't figure out how to do that I'm happy to do that as part of the final cleanup before merge 👍
haveActiveSubscription: false, | ||
socketStatus: POSTGRAPHILE_CONFIG.websockets === 'none' ? null : 'pending', | ||
}; | ||
|
||
parseFromStorage(key) { | ||
const value = this._storage.get(key); | ||
return value ? JSON.parse(value) : undefined; |
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 should probably be wrapped with a try/catch
@@ -375,6 +400,7 @@ class PostGraphiQL extends React.PureComponent { | |||
...(this.state.explain && POSTGRAPHILE_CONFIG.allowExplain | |||
? { 'X-PostGraphile-Explain': 'on' } | |||
: null), | |||
'X-PostGraphile-Explain-Options': buildExplainOptionsHeader(this.state.explainOptions) |
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.
I wonder if we should just replace the { 'X-PostGraphile-Explain': 'on' }
header with
{ 'X-PostGraphile-Explain': `on; ${buildExplainOptionsHeader(this.state.explainOptions)}` }
?
explain: allowExplain && req.headers['x-postgraphile-explain'] === 'on', | ||
explainOptions: parseExplainOptions(req), |
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.
Let's merge explain/explainOptions together. We don't need to support any legacy here since PostGraphile and PostGraphiQL are deeply integrated and tied to the exact same versions, so unified changes are fine - no need for backwards compatibility.
explain: allowExplain && req.headers['x-postgraphile-explain'] === 'on', | |
explainOptions: parseExplainOptions(req), | |
explain: allowExplain && parseExplainOptions(req), |
@@ -1175,6 +1209,7 @@ function addCORSHeaders(res: PostGraphileResponse): void { | |||
'Content-Length', | |||
// For our 'Explain' feature | |||
'X-PostGraphile-Explain', | |||
'X-PostGraphile-Explain-Options', |
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.
Thinking about it, I'm very keen that we don't introduce another header because it means people may have to rewrite their nginx, CORS and similar config.
@benjie I addressed everything you commented except for forcing the transaction. Couldn't figure out a nice way to do that, so it's all yours. |
Thanks! Just a heads up that it's going to be a little while before I get around to re-reviewing this, I'm focussed on V5 work currently. I'm excited though! |
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.
So sorry, this slipped off of my TODO list (the notification got removed from my GitHub notifications somehow), I only just noticed it again 😓
I don't have permission to push to your master
branch so I've applied a number of minor fixes and tweaks to the explain
branch: https://github.com/graphile/crystal/tree/explain
However, the main reason that this can't be merged currently is that it looks ripe for SQL injection attacks. Inserting user-submitted content (sent from a header) directly into an SQL query's raw text is a recipe for disaster. We need server-side validation of the values, and I'd rather see an exhaustive "if options.analyze then sql.push('analyze')" for each option that makes it extremely explicit exactly what text is being pushed into the explain query.
// Build the EXPLAIN command | ||
let explainCommand = 'explain'; | ||
const explainOptionClauses = Object.entries(pgClient._explainOptions ?? {}).map( | ||
([option, value]) => `${option} ${value}`, |
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.
_explainOptions
isn't safe to use like this, this opens PostGraphile up to SQL injection attacks.
Hi @benjie. I will not continue working on this PR. I'm happy to hand it off to you if you want. Otherwise, I'll be closing it and deleting my fork. |
Hi @angelyan; no worries and sorry I dropped the ball here on the review time, I really don't know how the notification disappeared from my inbox. I'll be adding something like this to PostGraphile V5 and your code now exists on the |
No worries. Thank you! |
Description
EXPLAIN
options (analyze, verbose, costs, settings, buffers, wal, timing, summary, format) from the toolbar.Screenshots
"Options" and "Format" menu items are added next to "Explain".

The "Options" menu allows toggling of all boolean options. Note: An asterisk appears to the left of "TIMING" as it is enabled but it requires "ANALYZE" to be enabled as well.

The "Format" menu allows changing between all output formats (text, json, yaml, xml).

Fixes #1235.
Performance impact
unknown
Security impact
unknown
Checklist
yarn lint:fix
passes.yarn test
passes.RELEASE_NOTES.md
file (if one exists).