From 8b216880b181ee072ea202d2fa40c8c27d09a027 Mon Sep 17 00:00:00 2001 From: Mike Swierczek <441523+Michael-S@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:04:58 -0400 Subject: [PATCH] [Bug] Fix query splitting with quoted semicolons Signed-off-by: Mike Swierczek <441523+Michael-S@users.noreply.github.com> --- public/utils/utils.test.ts | 34 ++++++++++++++++++++ public/utils/utils.ts | 65 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 public/utils/utils.test.ts diff --git a/public/utils/utils.test.ts b/public/utils/utils.test.ts new file mode 100644 index 00000000..f581d6d3 --- /dev/null +++ b/public/utils/utils.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + getQueries +} from './utils'; + +describe('getQueries', () => { + it('should return a simple query without issues.', () => { + expect(getQueries("select 1")).toEqual(["select 1"]); + }); + it('should split queries by semicolons', () => { + expect(getQueries("select 1;select 2;select 3")) + .toEqual(["select 1", "select 2", "select 3"]); + }); + it('should not choke on opening or closing semicolons', () => { + expect(getQueries(";;;select 1;;")).toEqual(["select 1"]); + }); + it('should not split on quoted semi-colons', () => { + expect(getQueries("select * from x where y = '1;2' or y = '3;4'")).toEqual( + ["select * from x where y = '1;2' or y = '3;4'"]); + }); + it('should not split on quoted escaped semi-colons', () => { + expect(getQueries("select * from x where y = '1\\;2\\;';select * from x where y = '3'")).toEqual( + ["select * from x where y = '1;2;'", "select * from x where y = '3'"]); + }); + it('should not get tripped up by nested quotes and escaped semicolons in quotes, either', () => { + expect(getQueries("select * from x where y = '1\\\\'\\\\';\\;2';select * from x where y = '\\\"3;\\;\\\\'\\\\'4;'")).toEqual( + ["select * from x where y = '1\\'\\';;2'", "select * from x where y = '\"3;;\\'\\'4;'"]); + }); +}); + diff --git a/public/utils/utils.ts b/public/utils/utils.ts index fe8e0c04..b17b47e8 100644 --- a/public/utils/utils.ts +++ b/public/utils/utils.ts @@ -7,14 +7,75 @@ import {ItemIdToExpandedRowMap, QueryMessage, ResponseDetail} from '../components/Main/main'; import {MESSAGE_TAB_LABEL} from "./constants"; +enum QueryParserState { + NORMAL, + ESCAPED, + ESCAPED_IN_SINGLE_QUOTES, + IN_SINGLE_QUOTES, +} + // It returns an array of queries export const getQueries = (queriesString: string): string[] => { if (queriesString == '') { return []; } + const splitQueries: string[] = []; + let index = 0; + let nextString: string = ""; + let queryParserState: QueryParserState = QueryParserState.NORMAL; + while (index < queriesString.length) { + let c = queriesString.charAt(index); + if (c === '\\') { + if (queryParserState == QueryParserState.ESCAPED) { + nextString += c; + queryParserState = QueryParserState.NORMAL; + } else if (queryParserState == QueryParserState.ESCAPED_IN_SINGLE_QUOTES) { + nextString += c; + queryParserState = QueryParserState.IN_SINGLE_QUOTES; + } else if (queryParserState == QueryParserState.IN_SINGLE_QUOTES) { + queryParserState = QueryParserState.ESCAPED_IN_SINGLE_QUOTES; + } else /* queryParserState == QueryParserState.NORMAL */ { + queryParserState = QueryParserState.ESCAPED; + } + } else if (c === ';') { + if (queryParserState == QueryParserState.ESCAPED) { + nextString += c; + queryParserState = QueryParserState.NORMAL; + } else if (queryParserState == QueryParserState.ESCAPED_IN_SINGLE_QUOTES) { + nextString += c; + queryParserState = QueryParserState.IN_SINGLE_QUOTES; + } else if (queryParserState == QueryParserState.IN_SINGLE_QUOTES) { + nextString += c; + } else /* queryParserState == QueryParserState.NORMAL */ { + splitQueries.push(nextString); + nextString = ""; + } + } else if (c === "'") { + nextString += c; + if (queryParserState == QueryParserState.ESCAPED || + queryParserState == QueryParserState.IN_SINGLE_QUOTES) { + queryParserState = QueryParserState.NORMAL; + } else if (queryParserState == QueryParserState.ESCAPED_IN_SINGLE_QUOTES) { + queryParserState = QueryParserState.IN_SINGLE_QUOTES; + } else /* queryParserState == QueryParserState.NORMAL */ { + queryParserState = QueryParserState.IN_SINGLE_QUOTES; + } + } else if (queryParserState == QueryParserState.ESCAPED) { + nextString += c; + queryParserState = QueryParserState.NORMAL; + } else if (queryParserState == QueryParserState.ESCAPED_IN_SINGLE_QUOTES) { + nextString += c; + queryParserState = QueryParserState.IN_SINGLE_QUOTES; + } else /* ((queryParserState == QueryParserState.IN_SINGLE_QUOTES + || queryParserState == QueryParserState.NORMAL) && + (c != '\\' && c != '\'' && c != ';')) */ { + nextString += c; + } + index++; + } + splitQueries.push(nextString); // if it's empty, it gets filtered immediately anyway - return queriesString - .split(';') + return splitQueries .map((query: string) => query.trim().replace(/[\r\n]+/g, ' ')) .filter((query: string) => query != ''); };