From 83f6799a96f48b5acef4911c0273973f15efdf05 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 10 Jul 2021 11:30:48 -0700 Subject: [PATCH] searchmode: raw table metadata property, closes #1389 --- datasette/views/table.py | 8 +++++++- docs/full_text_search.rst | 29 ++++++++++++++++++---------- tests/test_api.py | 40 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/datasette/views/table.py b/datasette/views/table.py index 81d4d721..1bda7496 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -495,7 +495,13 @@ class TableView(RowTableShared): if pair[0].startswith("_search") and pair[0] != "_searchmode" ) search = "" - search_mode_raw = special_args.get("_searchmode") == "raw" + search_mode_raw = table_metadata.get("searchmode") == "raw" + # Or set it from the querystring + qs_searchmode = special_args.get("_searchmode") + if qs_searchmode == "escaped": + search_mode_raw = False + if qs_searchmode == "raw": + search_mode_raw = True if fts_table and search_args: if "_search" in search_args: # Simple ?_search=xxx diff --git a/docs/full_text_search.rst b/docs/full_text_search.rst index b414ff37..f549296f 100644 --- a/docs/full_text_search.rst +++ b/docs/full_text_search.rst @@ -36,7 +36,11 @@ Advanced SQLite search queries SQLite full-text search includes support for `a variety of advanced queries `__, including ``AND``, ``OR``, ``NOT`` and ``NEAR``. -By default Datasette disables these features to ensure they do not cause any confusion for users who are not aware of them. You can disable this escaping and use the advanced queries by adding ``?_searchmode=raw`` to the table page query string. +By default Datasette disables these features to ensure they do not cause errors or confusion for users who are not aware of them. You can disable this escaping and use the advanced queries by adding ``&_searchmode=raw`` to the table page query string. + +If you want to enable these operators by default for a specific table, you can do so by adding ``"searchmode": "raw"`` to the metadata configuration for that table, see :ref:`full_text_search_table_or_view`. + +If that option has been specified in the table metadata but you want to over-ride it and return to the default behavior you can append ``&_searchmode=escaped`` to the query string. .. _full_text_search_table_or_view: @@ -53,19 +57,24 @@ https://latest.datasette.io/fixtures/searchable_view?_fts_table=searchable_fts&_ The ``fts_table`` metadata property can be used to specify an associated FTS table. If the primary key column in your table which was used to populate the FTS table is something other than ``rowid``, you can specify the column to use with the ``fts_pk`` property. -Here is an example which enables full-text search for a ``display_ads`` view which is defined against the ``ads`` table and hence needs to run FTS against the ``ads_fts`` table, using the ``id`` as the primary key:: +The ``"searchmode": "raw"`` property can be used to default the table to accepting SQLite advanced search operators, as described in :ref:`full_text_search_advanced_queries`. + +Here is an example which enables full-text search (with SQLite advanced search operators) for a ``display_ads`` view which is defined against the ``ads`` table and hence needs to run FTS against the ``ads_fts`` table, using the ``id`` as the primary key: + +.. code-block:: json { - "databases": { - "russian-ads": { - "tables": { - "display_ads": { - "fts_table": "ads_fts", - "fts_pk": "id" + "databases": { + "russian-ads": { + "tables": { + "display_ads": { + "fts_table": "ads_fts", + "fts_pk": "id", + "search_mode": "raw" + } + } } - } } - } } .. _full_text_search_custom_sql: diff --git a/tests/test_api.py b/tests/test_api.py index 2d891aae..cb3c255d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1078,6 +1078,46 @@ def test_searchable(app_client, path, expected_rows): assert expected_rows == response.json["rows"] +_SEARCHMODE_RAW_RESULTS = [ + [1, "barry cat", "terry dog", "panther"], + [2, "terry dog", "sara weasel", "puma"], +] + + +@pytest.mark.parametrize( + "table_metadata,querystring,expected_rows", + [ + ( + {}, + "_search=te*+AND+do*", + [], + ), + ( + {"searchmode": "raw"}, + "_search=te*+AND+do*", + _SEARCHMODE_RAW_RESULTS, + ), + ( + {}, + "_search=te*+AND+do*&_searchmode=raw", + _SEARCHMODE_RAW_RESULTS, + ), + # Can be over-ridden with _searchmode=escaped + ( + {"searchmode": "raw"}, + "_search=te*+AND+do*&_searchmode=escaped", + [], + ), + ], +) +def test_searchmode(table_metadata, querystring, expected_rows): + with make_app_client( + metadata={"databases": {"fixtures": {"tables": {"searchable": table_metadata}}}} + ) as client: + response = client.get("/fixtures/searchable.json?" + querystring) + assert expected_rows == response.json["rows"] + + @pytest.mark.parametrize( "path,expected_rows", [