Skip to content

Commit 8ae77a6

Browse files
committed
table.search(quote=True) parameter, refs #296
1 parent f0fd192 commit 8ae77a6

File tree

3 files changed

+54
-11
lines changed

3 files changed

+54
-11
lines changed

docs/python-api.rst

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,8 +1843,15 @@ The ``.has_counts_triggers`` property shows if a table has been configured with
18431843
18441844
.. _python_api_fts:
18451845
1846-
Enabling full-text search
1847-
=========================
1846+
Full-text search
1847+
================
1848+
1849+
SQLite includes bundled extensions that implement `powerful full-text search <https://www.sqlite.org/fts5.html>`__.
1850+
1851+
.. _python_api_fts_enable:
1852+
1853+
Enabling full-text search for a table
1854+
-------------------------------------
18481855
18491856
You can enable full-text search on a table using ``.enable_fts(columns)``:
18501857
@@ -1947,6 +1954,9 @@ The ``.search()`` method also accepts the following optional parameters:
19471954
``offset`` integer
19481955
Offset to use along side the limit parameter.
19491956
1957+
``quote`` bool
1958+
Apply :ref:`FTS quoting rules <python_api_quote_fts>` to the search query, disabling advanced query syntax in a way that avoids surprising errors.
1959+
19501960
To return just the title and published columns for three matches for ``"dog"`` ordered by ``published`` with the most recent first, use the following:
19511961
19521962
.. code-block:: python

sqlite_utils/db.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1924,7 +1924,13 @@ def optimize(self) -> "Table":
19241924
)
19251925
return self
19261926

1927-
def search_sql(self, columns=None, order_by=None, limit=None, offset=None) -> str:
1927+
def search_sql(
1928+
self,
1929+
columns: Optional[Iterable[str]] = None,
1930+
order_by: Optional[str] = None,
1931+
limit: Optional[int] = None,
1932+
offset: Optional[int] = None,
1933+
) -> str:
19281934
"Return SQL string that can be used to execute searches against this table."
19291935
# Pick names for table and rank column that don't clash
19301936
original = "original_" if self.name == "original" else "original"
@@ -1986,19 +1992,21 @@ def search(
19861992
self,
19871993
q: str,
19881994
order_by: Optional[str] = None,
1989-
columns: Optional[List[str]] = None,
1995+
columns: Optional[Iterable[str]] = None,
19901996
limit: Optional[int] = None,
19911997
offset: Optional[int] = None,
1998+
quote: bool = False,
19921999
) -> Generator[dict, None, None]:
19932000
"""
19942001
Execute a search against this table using SQLite full-text search, returning a sequence of
19952002
dictionaries for each row.
19962003
1997-
- ``q`` - words to search for
2004+
- ``q`` - terms to search for
19982005
- ``order_by`` - defaults to order by rank, or specify a column here.
19992006
- ``columns`` - list of columns to return, defaults to all columns.
20002007
- ``limit`` - optional integer limit for returned rows.
20012008
- ``offset`` - optional integer SQL offset.
2009+
- ``quote`` - apply quoting to disable any special characters in the search query
20022010
20032011
See :ref:`python_api_fts_search`.
20042012
"""
@@ -2009,7 +2017,7 @@ def search(
20092017
limit=limit,
20102018
offset=offset,
20112019
),
2012-
{"query": q},
2020+
{"query": self.db.quote_fts(q) if quote else q},
20132021
)
20142022
columns = [c[0] for c in cursor.description]
20152023
for row in cursor:

tests/test_fts.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -503,13 +503,38 @@ def test_search_sql(kwargs, fts, expected):
503503
assert sql == expected
504504

505505

506-
def test_quote_fts_query(fresh_db):
507-
506+
@pytest.mark.parametrize(
507+
"input,expected",
508+
(
509+
("dog", '"dog"'),
510+
("cat,", '"cat,"'),
511+
("cat's", '"cat\'s"'),
512+
("dog.", '"dog."'),
513+
("cat dog", '"cat" "dog"'),
514+
# If a phrase is already double quoted, leave it so
515+
('"cat dog"', '"cat dog"'),
516+
('"cat dog" fish', '"cat dog" "fish"'),
517+
# Sensibly handle unbalanced double quotes
518+
('cat"', '"cat"'),
519+
('"cat dog" "fish', '"cat dog" "fish"'),
520+
),
521+
)
522+
def test_quote_fts_query(fresh_db, input, expected):
508523
table = fresh_db["searchable"]
509524
table.insert_all(search_records)
510525
table.enable_fts(["text", "country"])
526+
quoted = fresh_db.quote_fts(input)
527+
assert quoted == expected
528+
# Executing query does not crash.
529+
list(table.search(quoted))
530+
511531

532+
def test_search_quote(fresh_db):
533+
table = fresh_db["searchable"]
534+
table.insert_all(search_records)
535+
table.enable_fts(["text", "country"])
512536
query = "cat's"
513-
result = fresh_db.quote_fts(query)
514-
# Executing query does not crash.
515-
list(table.search(result))
537+
with pytest.raises(sqlite3.OperationalError):
538+
list(table.search(query))
539+
# No exception with quote=True
540+
list(table.search(query, quote=True))

0 commit comments

Comments
 (0)