Skip to content

Commit

Permalink
fix: add safe suppress to sqlformat error (pinterest#1141)
Browse files Browse the repository at this point in the history
  • Loading branch information
czgu authored and aidenprice committed Jan 3, 2024
1 parent 467c49f commit d837471
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 55 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "querybook",
"version": "3.17.0",
"version": "3.17.1",
"description": "A Big Data Webapp",
"private": true,
"scripts": {
Expand Down
23 changes: 16 additions & 7 deletions querybook/webapp/components/QueryEditor/QueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand All @@ -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]
);
Expand Down
173 changes: 126 additions & 47 deletions querybook/webapp/lib/sql-helper/sql-formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
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) => {
Expand All @@ -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)) {
Expand All @@ -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,
Expand Down

0 comments on commit d837471

Please sign in to comment.