Support _search_COLUMN=text searches, closes #237

distinct-column-values
Simon Willison 2018-05-05 19:33:08 -03:00
rodzic 4d6a568d6c
commit 1259b8ac0b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 17E2DEA2588B7F52
5 zmienionych plików z 78 dodań i 25 usunięć

Wyświetl plik

@ -230,7 +230,7 @@ class BaseView(RenderMixin):
return response_or_template_contexts return response_or_template_contexts
else: else:
data, extra_template_data, templates = response_or_template_contexts data, extra_template_data, templates = response_or_template_contexts
except (sqlite3.OperationalError, InvalidSql, DatasetteError) as e: except (sqlite3.OperationalError, InvalidSql) as e:
raise DatasetteError(str(e), title='Invalid SQL', status=400) raise DatasetteError(str(e), title='Invalid SQL', status=400)
except (sqlite3.OperationalError) as e: except (sqlite3.OperationalError) as e:
raise DatasetteError(str(e)) raise DatasetteError(str(e))
@ -740,16 +740,44 @@ class TableView(RowTableShared):
# _search support: # _search support:
fts_table = info[name]['tables'].get(table, {}).get('fts_table') fts_table = info[name]['tables'].get(table, {}).get('fts_table')
search = special_args.get('_search') search_args = dict(
search_description = None pair for pair in special_args.items()
if search and fts_table: if pair[0].startswith('_search')
where_clauses.append( )
'rowid in (select rowid from [{fts_table}] where [{fts_table}] match :search)'.format( search_descriptions = []
fts_table=fts_table search = ''
if fts_table and search_args:
if '_search' in search_args:
# Simple ?_search=xxx
search = search_args['_search']
where_clauses.append(
'rowid in (select rowid from [{fts_table}] where [{fts_table}] match :search)'.format(
fts_table=fts_table
)
) )
) search_descriptions.append('search matches "{}"'.format(search))
search_description = 'search matches "{}"'.format(search) params['search'] = search
params['search'] = search else:
# More complex: search against specific columns
valid_columns = set(info[name]['tables'][fts_table]['columns'])
for i, (key, search_text) in enumerate(search_args.items()):
search_col = key.split('_search_', 1)[1]
if search_col not in valid_columns:
raise DatasetteError(
'Cannot search by that column',
status=400
)
where_clauses.append(
'rowid in (select rowid from [{fts_table}] where [{search_col}] match :search_{i})'.format(
fts_table=fts_table,
search_col=search_col,
i=i,
)
)
search_descriptions.append(
'search column "{}" matches "{}"'.format(search_col, search_text)
)
params['search_{}'.format(i)] = search_text
table_rows_count = None table_rows_count = None
sortable_columns = set() sortable_columns = set()
@ -955,7 +983,7 @@ class TableView(RowTableShared):
pass pass
# human_description_en combines filters AND search, if provided # human_description_en combines filters AND search, if provided
human_description_en = filters.human_description_en(extra=search_description) human_description_en = filters.human_description_en(extra=search_descriptions)
if sort or sort_desc: if sort or sort_desc:
sorted_by = 'sorted by {}{}'.format( sorted_by = 'sorted by {}{}'.format(

Wyświetl plik

@ -522,7 +522,7 @@ class Filters:
def human_description_en(self, extra=None): def human_description_en(self, extra=None):
bits = [] bits = []
if extra: if extra:
bits.append(extra) bits.extend(extra)
for column, lookup, value in self.selections(): for column, lookup, value in self.selections():
filter = self._filters_by_key.get(lookup, None) filter = self._filters_by_key.get(lookup, None)
if filter: if filter:

Wyświetl plik

@ -146,6 +146,10 @@ The Datasette table view takes a number of special querystring arguments:
`full-text search <https://www.sqlite.org/fts3.html>`_ executes a search `full-text search <https://www.sqlite.org/fts3.html>`_ executes a search
with the provided keywords. with the provided keywords.
``?_search_COLUMN=keywords``
Like ``_search=`` but allows you to specify the column to be searched, as
opposed to searching all columns that have been indexed by FTS.
``?_group_count=COLUMN`` ``?_group_count=COLUMN``
Executes a SQL query that returns a count of the number of rows matching Executes a SQL query that returns a count of the number of rows matching
each unique value in that column, with the most common ordered first. each unique value in that column, with the most common ordered first.

Wyświetl plik

@ -248,16 +248,17 @@ INSERT INTO units VALUES (3, 100000, 75000);
CREATE TABLE searchable ( CREATE TABLE searchable (
pk integer primary key, pk integer primary key,
text1 text, text1 text,
text2 text text2 text,
[name with . and spaces] text
); );
INSERT INTO searchable VALUES (1, 'barry cat', 'john dog'); INSERT INTO searchable VALUES (1, 'barry cat', 'terry dog', 'panther');
INSERT INTO searchable VALUES (2, 'terry cat', 'john weasel'); INSERT INTO searchable VALUES (2, 'terry dog', 'sara weasel', 'puma');
CREATE VIRTUAL TABLE "searchable_fts" CREATE VIRTUAL TABLE "searchable_fts"
USING FTS3 (text1, text2, content="searchable"); USING FTS3 (text1, text2, [name with . and spaces], content="searchable");
INSERT INTO "searchable_fts" (rowid, text1, text2) INSERT INTO "searchable_fts" (rowid, text1, text2, [name with . and spaces])
SELECT rowid, text1, text2 FROM searchable; SELECT rowid, text1, text2, [name with . and spaces] FROM searchable;
CREATE TABLE [select] ( CREATE TABLE [select] (
[group] text, [group] text,

Wyświetl plik

@ -157,7 +157,7 @@ def test_database_page(app_client):
'fts_table': None, 'fts_table': None,
'primary_keys': ['id'] 'primary_keys': ['id']
}, { }, {
'columns': ['pk', 'text1', 'text2'], 'columns': ['pk', 'text1', 'text2', 'name with . and spaces'],
'name': 'searchable', 'name': 'searchable',
'count': 2, 'count': 2,
'foreign_keys': {'incoming': [], 'outgoing': []}, 'foreign_keys': {'incoming': [], 'outgoing': []},
@ -242,7 +242,7 @@ def test_database_page(app_client):
'fts_table': None, 'fts_table': None,
'primary_keys': [], 'primary_keys': [],
}, { }, {
'columns': ['text1', 'text2', 'content'], 'columns': ['text1', 'text2', 'name with . and spaces', 'content'],
'count': 2, 'count': 2,
'foreign_keys': {'incoming': [], 'outgoing': []}, 'foreign_keys': {'incoming': [], 'outgoing': []},
'fts_table': 'searchable_fts', 'fts_table': 'searchable_fts',
@ -251,7 +251,7 @@ def test_database_page(app_client):
'name': 'searchable_fts', 'name': 'searchable_fts',
'primary_keys': [] 'primary_keys': []
}, { }, {
'columns': ['docid', 'c0text1', 'c1text2', 'c2content'], 'columns': ['docid', 'c0text1', 'c1text2', 'c2name with . and spaces', 'c3content'],
'count': 2, 'count': 2,
'foreign_keys': {'incoming': [], 'outgoing': []}, 'foreign_keys': {'incoming': [], 'outgoing': []},
'fts_table': None, 'fts_table': None,
@ -681,12 +681,18 @@ def test_sortable_columns_metadata(app_client):
@pytest.mark.parametrize('path,expected_rows', [ @pytest.mark.parametrize('path,expected_rows', [
('/test_tables/searchable.json?_search=cat', [ ('/test_tables/searchable.json?_search=dog', [
[1, 'barry cat', 'john dog'], [1, 'barry cat', 'terry dog', 'panther'],
[2, 'terry cat', 'john weasel'], [2, 'terry dog', 'sara weasel', 'puma'],
]), ]),
('/test_tables/searchable.json?_search=weasel', [ ('/test_tables/searchable.json?_search=weasel', [
[2, 'terry cat', 'john weasel'], [2, 'terry dog', 'sara weasel', 'puma'],
]),
('/test_tables/searchable.json?_search_text2=dog', [
[1, 'barry cat', 'terry dog', 'panther'],
]),
('/test_tables/searchable.json?_search_name%20with%20.%20and%20spaces=panther', [
[1, 'barry cat', 'terry dog', 'panther'],
]), ]),
]) ])
def test_searchable(app_client, path, expected_rows): def test_searchable(app_client, path, expected_rows):
@ -694,6 +700,20 @@ def test_searchable(app_client, path, expected_rows):
assert expected_rows == response.json['rows'] assert expected_rows == response.json['rows']
def test_searchable_invalid_column(app_client):
response = app_client.get(
'/test_tables/searchable.json?_search_invalid=x',
gather_request=False
)
assert 400 == response.status
assert {
'ok': False,
'error': 'Cannot search by that column',
'status': 400,
'title': None
} == response.json
@pytest.mark.parametrize('path,expected_rows', [ @pytest.mark.parametrize('path,expected_rows', [
('/test_tables/simple_primary_key.json?content=hello', [ ('/test_tables/simple_primary_key.json?content=hello', [
['1', 'hello'], ['1', 'hello'],