From d837471b92c4b0864b446eab621f36faa0651432 Mon Sep 17 00:00:00 2001 From: czgu Date: Wed, 1 Feb 2023 16:02:33 -0800 Subject: [PATCH] fix: add safe suppress to sqlformat error (#1141) --- package.json | 2 +- .../components/QueryEditor/QueryEditor.tsx | 23 ++- .../webapp/lib/sql-helper/sql-formatter.ts | 173 +++++++++++++----- 3 files changed, 143 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 788d69e4d..a5a9c7c77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "querybook", - "version": "3.17.0", + "version": "3.17.1", "description": "A Big Data Webapp", "private": true, "scripts": { diff --git a/querybook/webapp/components/QueryEditor/QueryEditor.tsx b/querybook/webapp/components/QueryEditor/QueryEditor.tsx index 30bf994c0..3cb78be90 100644 --- a/querybook/webapp/components/QueryEditor/QueryEditor.tsx +++ b/querybook/webapp/components/QueryEditor/QueryEditor.tsx @@ -9,6 +9,7 @@ import React, { useState, } from 'react'; import { Controlled as ReactCodeMirror } from 'react-codemirror2'; +import toast from 'react-hot-toast'; import { showTooltipFor } from 'components/CodeMirrorTooltip'; import { ICodeMirrorTooltipProps } from 'components/CodeMirrorTooltip/CodeMirrorTooltip'; @@ -232,7 +233,11 @@ export const QueryEditor: React.FC< ); const formatQuery = useCallback( - (options: ISQLFormatOptions = {}) => { + ( + options: ISQLFormatOptions = { + silent: false, + } + ) => { if (editorRef.current) { const indentWithTabs = editorRef.current.getOption('indentWithTabs'); @@ -246,12 +251,16 @@ export const QueryEditor: React.FC< } } - const formattedQuery = format( - editorRef.current.getValue(), - language, - options - ); - editorRef.current?.setValue(formattedQuery); + try { + const formattedQuery = format( + editorRef.current.getValue(), + language, + options + ); + editorRef.current?.setValue(formattedQuery); + } catch (e) { + toast.error('Failed to format query.'); + } }, [language] ); diff --git a/querybook/webapp/lib/sql-helper/sql-formatter.ts b/querybook/webapp/lib/sql-helper/sql-formatter.ts index c98360cc0..11d31515b 100644 --- a/querybook/webapp/lib/sql-helper/sql-formatter.ts +++ b/querybook/webapp/lib/sql-helper/sql-formatter.ts @@ -52,23 +52,26 @@ export interface ISQLFormatOptions { case?: 'lower' | 'upper'; tabWidth?: number; useTabs?: boolean; + + /** + * whether or not to thrown error when encoutering a parsing + * error, defaults to true + */ + silent?: boolean; } -export function format( +interface IProcessedStatement { + statementText: string; + idToTemplateTag: Record; + firstKeyWord: IToken; + originalStatementText: string; +} + +function tokenizeAndFormatQuery( query: string, language: string, - options?: ISQLFormatOptions + options: ISQLFormatOptions ) { - options = { - ...{ - // default options - case: 'upper', - tabWidth: 2, - useTabs: false, - }, - ...options, - }; - const tokens = tokenize(query, { language, includeUnknown: true }); const statements: IToken[][] = []; tokens.reduce((statement, token, index) => { @@ -92,56 +95,102 @@ export function format( return statement; }, [] as IToken[]); + return statements; +} + +function processStatements( + query: string, + language: string, + options: ISQLFormatOptions +) { + const statements = tokenizeAndFormatQuery(query, language, options); const queryLineLength = getQueryLinePosition(query); const newLineBetweenStatement = new Array(statements.length).fill(0); let lastStatementRange = null; - const processedStatements = statements.map((statement, index) => { - // This part of code calculates the number of new lines - // between 2 statements - const firstToken = statement[0]; - const lastToken = statement[statement.length - 1]; - const statementRange = [ - queryLineLength[firstToken.line] + firstToken.start, - queryLineLength[lastToken.line] + lastToken.end, - ]; - if (lastStatementRange) { - const inbetweenString = query.slice( - lastStatementRange[1], - statementRange[0] + const processedStatements: IProcessedStatement[] = statements.map( + (statement, index) => { + // This part of code calculates the number of new lines + // between 2 statements + const firstToken = statement[0]; + const lastToken = statement[statement.length - 1]; + const statementRange = [ + queryLineLength[firstToken.line] + firstToken.start, + queryLineLength[lastToken.line] + lastToken.end, + ]; + if (lastStatementRange) { + const inbetweenString = query.slice( + lastStatementRange[1], + statementRange[0] + ); + const numberOfNewLine = inbetweenString.split('\n').length - 1; + newLineBetweenStatement[index] = Math.max(1, numberOfNewLine); + } + lastStatementRange = statementRange; + + // This part of code formats the query + const firstKeyWord = find( + statement, + (token) => token.type === 'KEYWORD' ); - const numberOfNewLine = inbetweenString.split('\n').length - 1; - newLineBetweenStatement[index] = Math.max(1, numberOfNewLine); + const { statementText, idToTemplateTag } = tokensToText(statement); + + return { + statementText, + idToTemplateTag, + firstKeyWord, + originalStatementText: query.slice( + statementRange[0], + statementRange[1] + 1 + ), + }; } - lastStatementRange = statementRange; + ); + + return { + newLineBetweenStatement, + processedStatements, + }; +} - // This part of code formats the query - const firstKeyWord = find( - statement, - (token) => token.type === 'KEYWORD' - ); - const { statementText, idToTemplateTag } = tokensToText(statement); +function formatEachStatement( + statements: IProcessedStatement[], + language: string, + options: ISQLFormatOptions +) { + const safeSQLFormat = (text: string, unformattedText: string) => { + try { + return sqlFormat(text, { + tabWidth: options.tabWidth, + language: getLanguageForSqlFormatter(language), + useTabs: options.useTabs, + }); + } catch (e) { + if (options.silent) { + return unformattedText; + } else { + throw e; + } + } + }; - return { + const formattedStatements: string[] = statements.map( + ({ + firstKeyWord, statementText, idToTemplateTag, - firstKeyWord, - }; - }); - - const formattedStatements: string[] = processedStatements.map( - ({ firstKeyWord, statementText, idToTemplateTag }) => { + originalStatementText, + }) => { // Use standard formatter to format - let formattedStatement = statementText; + let formattedStatement = originalStatementText; if ( firstKeyWord && allowedStatement.has(firstKeyWord.text.toLocaleLowerCase()) ) { - formattedStatement = sqlFormat(statementText, { - tabWidth: options.tabWidth, - language: getLanguageForSqlFormatter(language), - useTabs: options.useTabs, - }); + formattedStatement = safeSQLFormat( + statementText, + originalStatementText + ); } for (const [id, templateTag] of Object.entries(idToTemplateTag)) { @@ -155,6 +204,36 @@ export function format( } ); + return formattedStatements; +} + +export function format( + query: string, + language: string, + options?: ISQLFormatOptions +) { + options = { + ...{ + // default options + case: 'upper', + tabWidth: 2, + useTabs: false, + silent: true, + }, + ...options, + }; + + const { processedStatements, newLineBetweenStatement } = processStatements( + query, + language, + options + ); + const formattedStatements = formatEachStatement( + processedStatements, + language, + options + ); + return formattedStatements.reduce( (acc, statement, index) => acc + '\n'.repeat(newLineBetweenStatement[index]) + statement,