Skip to content

Commit d628a9a

Browse files
fix(query): handle semicolons inside string literals (#131)
The query parser was using query.split(';') to separate statements, which incorrectly split on semicolons inside string literals, causing QueryParseException for valid queries like 'RETURN = "hello;world"'. Replace with _split_query_statements() that tracks quote state and only splits on semicolons that are actual statement terminators. Fixes ActivityWatch/aw-server#145
1 parent 1827ebf commit d628a9a

File tree

2 files changed

+64
-1
lines changed

2 files changed

+64
-1
lines changed

aw_query/query2.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,33 @@ def get_return(namespace):
401401
return namespace["RETURN"]
402402

403403

404+
def _split_query_statements(query: str) -> List[str]:
405+
"""Split query into statements on semicolons, ignoring semicolons inside string literals."""
406+
statements = []
407+
current = ""
408+
in_single_quote = False
409+
in_double_quote = False
410+
prev_char = None
411+
412+
for char in query:
413+
if char == "'" and prev_char != "\\" and not in_double_quote:
414+
in_single_quote = not in_single_quote
415+
current += char
416+
elif char == '"' and prev_char != "\\" and not in_single_quote:
417+
in_double_quote = not in_double_quote
418+
current += char
419+
elif char == ";" and not in_single_quote and not in_double_quote:
420+
statements.append(current)
421+
current = ""
422+
else:
423+
current += char
424+
prev_char = char
425+
426+
if current:
427+
statements.append(current)
428+
return statements
429+
430+
404431
def query(
405432
name: str, query: str, starttime: datetime, endtime: datetime, datastore: Datastore
406433
) -> Any:
@@ -409,7 +436,7 @@ def query(
409436
namespace["STARTTIME"] = starttime.isoformat()
410437
namespace["ENDTIME"] = endtime.isoformat()
411438

412-
query_stmts = query.split(";")
439+
query_stmts = _split_query_statements(query)
413440
for statement in query_stmts:
414441
statement = statement.strip()
415442
if statement:

tests/test_query2.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
QString,
2020
QVariable,
2121
_parse_token,
22+
_split_query_statements,
2223
)
2324

2425
from .utils import param_datastore_objects
@@ -229,6 +230,41 @@ def test_query2_return_value():
229230
result = query(qname, example_query, starttime, endtime, ds)
230231

231232

233+
def test_query2_semicolon_in_string():
234+
"""Test that semicolons inside string literals don't break query parsing (issue #145)."""
235+
ds = mock_ds
236+
qname = "asd"
237+
starttime = iso8601.parse_date("1970-01-01")
238+
endtime = iso8601.parse_date("1970-01-02")
239+
240+
# Semicolon in double-quoted string
241+
example_query = 'RETURN = "hello;world"'
242+
result = query(qname, example_query, starttime, endtime, ds)
243+
assert result == "hello;world"
244+
245+
# Semicolon in single-quoted string
246+
example_query = "RETURN = 'hello;world'"
247+
result = query(qname, example_query, starttime, endtime, ds)
248+
assert result == "hello;world"
249+
250+
# Multiple statements where one value contains a semicolon in a string
251+
example_query = 'a = "foo;bar"; RETURN = a'
252+
result = query(qname, example_query, starttime, endtime, ds)
253+
assert result == "foo;bar"
254+
255+
256+
def test_split_query_statements():
257+
"""Unit test for _split_query_statements."""
258+
# Basic split
259+
assert _split_query_statements("a=1;b=2") == ["a=1", "b=2"]
260+
# Semicolon inside double quotes should NOT split
261+
assert _split_query_statements('a="x;y";b=2') == ['a="x;y"', "b=2"]
262+
# Semicolon inside single quotes should NOT split
263+
assert _split_query_statements("a='x;y';b=2") == ["a='x;y'", "b=2"]
264+
# Trailing semicolon — empty trailing part is omitted
265+
assert _split_query_statements("a=1;") == ["a=1"]
266+
267+
232268
def test_query2_multiline():
233269
ds = mock_ds
234270
qname = "asd"

0 commit comments

Comments
 (0)