diff --git a/datasette/utils/__init__.py b/datasette/utils/__init__.py index 0df0a871..f3f4e2bd 100644 --- a/datasette/utils/__init__.py +++ b/datasette/utils/__init__.py @@ -1142,7 +1142,7 @@ def format_bytes(bytes): return f"{current:.1f} {unit}" -_escape_fts_re = re.compile(r'\s+|(".*?")') +_escape_fts_re = re.compile(r'\s+|(".*?")', re.DOTALL) def escape_fts(query): diff --git a/tests/test_table_api.py b/tests/test_table_api.py index 58a5ddc3..38f1cfa3 100644 --- a/tests/test_table_api.py +++ b/tests/test_table_api.py @@ -562,6 +562,16 @@ async def test_searchable(ds_client, path, expected_rows): assert expected_rows == response.json()["rows"] +@pytest.mark.asyncio +async def test_search_quote_followed_by_newline(ds_client): + # A quote followed by a newline used to produce an invalid FTS query + response = await ds_client.get( + "/fixtures/searchable.json?_shape=arrays&_search=e%22%0A%21" + ) + assert response.status_code == 200 + assert response.json()["rows"] == [] + + _SEARCHMODE_RAW_RESULTS = [ [1, "barry cat", "terry dog", "panther"], [2, "terry dog", "sara weasel", "puma"], diff --git a/tests/test_utils.py b/tests/test_utils.py index 68d4a504..ae536f09 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -605,6 +605,11 @@ def test_format_bytes(bytes, expected): # Sensibly handle unbalanced double quotes ('cat"', '"cat"'), ('"cat dog" "fish', '"cat dog" "fish"'), + # Quoted phrases and unbalanced quotes can contain newlines + ('"cat\ndog"', '"cat\ndog"'), + ('"cat\ndog" fish', '"cat\ndog" "fish"'), + ('cat "dog\nfish', '"cat" "dog\nfish"'), + ('cat"\n!', '"cat" "\n!"'), ], ) def test_escape_fts(query, expected):