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 support for EXPLAIN options to GraphiQL #1618

Closed
wants to merge 11 commits into from
Closed

Add support for EXPLAIN options to GraphiQL #1618

wants to merge 11 commits into from

Conversation

angelyan
Copy link

Description

  • Allows setting all available EXPLAIN options (analyze, verbose, costs, settings, buffers, wal, timing, summary, format) from the toolbar.
  • Adds buttons to copy the SQL query and plan to the clipboard.

Screenshots

"Options" and "Format" menu items are added next to "Explain".
Screenshot 2022-04-14 at 17-53-14 PostGraphile GraphiQL

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.
Screenshot 2022-04-14 at 17-53-40 PostGraphile GraphiQL

The "Format" menu allows changing between all output formats (text, json, yaml, xml).
Screenshot 2022-04-14 at 17-55-35 PostGraphile GraphiQL

Fixes #1235.

Performance impact

unknown

Security impact

unknown

Checklist

  • My code matches the project's code style and yarn lint:fix passes.
  • I've added tests for the new feature, and yarn test passes.
  • I have detailed the new feature in the relevant documentation.
  • I have added this feature to 'Pending' in the RELEASE_NOTES.md file (if one exists).
  • If this is a breaking change I've explained why.

@benjie
Copy link
Member

benjie commented Apr 15, 2022

This is very cool! One significant concern that I have currently is that if we allow explain analyze then for mutations the operation will run twice (once for the explain analyze and once again for the mutation itself) - and this is the main reason that I shyed away from adding that option. I'd like this to be addressed before anything that allows explain analyze to be enabled, but I don't know how best to do this. We could just disable it for mutation operations on the server side, but I'm not sure if this is sufficiently obvious to the user?

@@ -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';
Copy link
Member

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

https://github.com/graphile/postgraphile/blob/55bff41460b113481c8161ef8f178f5af0a17df3/package.json#L48

And yet, CI passes. Very strange. I will have to investigate at some point.

@angelyan
Copy link
Author

angelyan commented Apr 15, 2022

One significant concern that I have currently is that if we allow explain analyze then for mutations the operation will run twice

What about running the EXPLAIN in a transaction and then rolling it back creating a savepoint before the EXPLAIN and then roll back to it?

@benjie
Copy link
Member

benjie commented Apr 15, 2022

Other than leaking through things like sequences (serial type, for example) I can’t think of a particularly good reason not to do that. We’d have to know if we’re in a transaction or not: if we are we should always savepoint, and if not then it’s not a mutation so no need to worry. Good idea 👍

To avoid running the mutation twice when ANALYZE is enabled
@angelyan angelyan requested a review from benjie April 16, 2022 18:16
@angelyan
Copy link
Author

@benjie I added the savepoint. Could you do another pass when you get a chance?

Comment on lines 553 to 570
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);
Copy link
Member

@benjie benjie Apr 20, 2022

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.

@angelyan angelyan requested a review from benjie April 20, 2022 12:39
@angelyan
Copy link
Author

@benjie this is ready for another pass!

Copy link
Member

@benjie benjie left a 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;
Copy link
Member

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

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)}` }

?

Comment on lines 185 to 186
explain: allowExplain && req.headers['x-postgraphile-explain'] === 'on',
explainOptions: parseExplainOptions(req),
Copy link
Member

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.

Suggested change
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',
Copy link
Member

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.

@angelyan
Copy link
Author

@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.

@angelyan angelyan requested a review from benjie May 13, 2022 03:22
@benjie
Copy link
Member

benjie commented May 16, 2022

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!

@benjie benjie mentioned this pull request Oct 4, 2023
3 tasks
Copy link
Member

@benjie benjie left a 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}`,
Copy link
Member

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.

@angelyan
Copy link
Author

angelyan commented Feb 8, 2024

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.

@benjie
Copy link
Member

benjie commented Feb 8, 2024

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 explain branch so you can feel free to close and delete the fork. All the best!

@angelyan
Copy link
Author

angelyan commented Feb 8, 2024

No worries. Thank you!

@angelyan angelyan closed this Feb 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

GraphiQL / Explain Improvements
2 participants