kopia lustrzana https://github.com/simonw/datasette
Support _search_COLUMN=text searches, closes #237
rodzic
4d6a568d6c
commit
1259b8ac0b
|
@ -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(
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
Ładowanie…
Reference in New Issue