diff --git a/aw_query/query2.py b/aw_query/query2.py index 431dcab..87f8edb 100644 --- a/aw_query/query2.py +++ b/aw_query/query2.py @@ -401,6 +401,33 @@ def get_return(namespace): return namespace["RETURN"] +def _split_query_statements(query: str) -> List[str]: + """Split query into statements on semicolons, ignoring semicolons inside string literals.""" + statements = [] + current = "" + in_single_quote = False + in_double_quote = False + prev_char = None + + for char in query: + if char == "'" and prev_char != "\\" and not in_double_quote: + in_single_quote = not in_single_quote + current += char + elif char == '"' and prev_char != "\\" and not in_single_quote: + in_double_quote = not in_double_quote + current += char + elif char == ";" and not in_single_quote and not in_double_quote: + statements.append(current) + current = "" + else: + current += char + prev_char = char + + if current: + statements.append(current) + return statements + + def query( name: str, query: str, starttime: datetime, endtime: datetime, datastore: Datastore ) -> Any: @@ -409,7 +436,7 @@ def query( namespace["STARTTIME"] = starttime.isoformat() namespace["ENDTIME"] = endtime.isoformat() - query_stmts = query.split(";") + query_stmts = _split_query_statements(query) for statement in query_stmts: statement = statement.strip() if statement: diff --git a/tests/test_query2.py b/tests/test_query2.py index 56154b4..db413e2 100644 --- a/tests/test_query2.py +++ b/tests/test_query2.py @@ -19,6 +19,7 @@ QString, QVariable, _parse_token, + _split_query_statements, ) from .utils import param_datastore_objects @@ -229,6 +230,41 @@ def test_query2_return_value(): result = query(qname, example_query, starttime, endtime, ds) +def test_query2_semicolon_in_string(): + """Test that semicolons inside string literals don't break query parsing (issue #145).""" + ds = mock_ds + qname = "asd" + starttime = iso8601.parse_date("1970-01-01") + endtime = iso8601.parse_date("1970-01-02") + + # Semicolon in double-quoted string + example_query = 'RETURN = "hello;world"' + result = query(qname, example_query, starttime, endtime, ds) + assert result == "hello;world" + + # Semicolon in single-quoted string + example_query = "RETURN = 'hello;world'" + result = query(qname, example_query, starttime, endtime, ds) + assert result == "hello;world" + + # Multiple statements where one value contains a semicolon in a string + example_query = 'a = "foo;bar"; RETURN = a' + result = query(qname, example_query, starttime, endtime, ds) + assert result == "foo;bar" + + +def test_split_query_statements(): + """Unit test for _split_query_statements.""" + # Basic split + assert _split_query_statements("a=1;b=2") == ["a=1", "b=2"] + # Semicolon inside double quotes should NOT split + assert _split_query_statements('a="x;y";b=2') == ['a="x;y"', "b=2"] + # Semicolon inside single quotes should NOT split + assert _split_query_statements("a='x;y';b=2") == ["a='x;y'", "b=2"] + # Trailing semicolon — empty trailing part is omitted + assert _split_query_statements("a=1;") == ["a=1"] + + def test_query2_multiline(): ds = mock_ds qname = "asd"