diff --git a/datasette/app.py b/datasette/app.py index 120348a0..1a6200e8 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -19,6 +19,7 @@ from .utils import ( build_where_clauses, compound_pks_from_path, CustomJSONEncoder, + detect_fts_sql, escape_css_string, escape_sqlite_table_name, get_all_foreign_keys, @@ -427,6 +428,22 @@ class TableView(BaseView): where_clauses = [] params = {} + # _search support: + fts_table = None + fts_sql = detect_fts_sql(table) + fts_rows = list(await self.execute(name, fts_sql)) + if fts_rows: + fts_table=fts_rows[0][0] + + search = special_args.get('_search') + if search and fts_table: + where_clauses.append( + 'rowid in (select rowid from {fts_table} where {fts_table} match :search)'.format( + fts_table=fts_table + ) + ) + params['search'] = search + next = special_args.get('_next') offset = '' if next: @@ -504,6 +521,8 @@ class TableView(BaseView): async def extra_template(): return { 'database_hash': hash, + 'supports_search': bool(fts_table), + 'search': search or '', 'use_rowid': use_rowid, 'display_columns': display_columns, 'display_rows': await self.make_display_rows(name, hash, table, rows, display_columns, pks, is_view, use_rowid), diff --git a/datasette/static/app.css b/datasette/static/app.css index 8df4fce4..b05782da 100644 --- a/datasette/static/app.css +++ b/datasette/static/app.css @@ -90,12 +90,13 @@ form.sql textarea { font-family: monospace; font-size: 1.3em; } -form.sql label { +form label { font-weight: bold; display: inline-block; width: 15%; } -form.sql input[type=text] { +form input[type=text], +form input[type=search] { border: 1px solid #ccc; width: 60%; padding: 4px; @@ -103,12 +104,15 @@ form.sql input[type=text] { display: inline-block; font-size: 1.1em; } +form input[type=search] { + width: 40%; +} @media only screen and (max-width: 576px) { form.sql textarea { width: 95%; } } -form.sql input[type=submit] { +form input[type=submit] { color: #fff; background-color: #007bff; border-color: #007bff; @@ -118,7 +122,7 @@ form.sql input[type=submit] { vertical-align: middle; border: 1px solid blue; padding: .275rem .75rem; - font-size: 1rem; - line-height: 1.5; + font-size: 0.9rem; + line-height: 1; border-radius: .25rem; } diff --git a/datasette/templates/table.html b/datasette/templates/table.html index 082d2782..2f57310f 100644 --- a/datasette/templates/table.html +++ b/datasette/templates/table.html @@ -21,6 +21,12 @@

{{ "{:,}".format(table_rows) }} total row{% if table_rows == 1 %}{% else %}s{% endif %} in this table

{% endif %} +{% if supports_search %} +
+

+
+{% endif %} + {% if query.params %}
{{ query.sql }}
params = {{ query.params|tojson(4) }}
diff --git a/datasette/utils.py b/datasette/utils.py index f65e5ad8..b30a8b97 100644 --- a/datasette/utils.py +++ b/datasette/utils.py @@ -235,3 +235,20 @@ def get_all_foreign_keys(conn): }) return table_to_foreign_keys + + +def detect_fts(conn, table, return_sql=False): + "Detect if table has a corresponding FTS virtual table and return it" + rows = conn.execute(detect_fts_sql(table)).fetchall() + if len(rows) == 0: + return None + else: + return rows[0][0] + + +def detect_fts_sql(table): + return r''' + select name from sqlite_master + where rootpage = 0 + and sql like '%VIRTUAL TABLE%USING FTS%content="{}"%'; + '''.format(table) diff --git a/tests/test_utils.py b/tests/test_utils.py index 168b9253..8dd5c76c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,6 +4,7 @@ Tests for various datasette helper functions. from datasette import utils import pytest +import sqlite3 import json @@ -124,3 +125,26 @@ def test_validate_sql_select_bad(bad_sql): ]) def test_validate_sql_select_good(good_sql): utils.validate_sql_select(good_sql) + + +def test_detect_fts(): + sql = ''' + CREATE TABLE "Dumb_Table" ( + "TreeID" INTEGER, + "qSpecies" TEXT + ); + CREATE TABLE "Street_Tree_List" ( + "TreeID" INTEGER, + "qSpecies" TEXT, + "qAddress" TEXT, + "SiteOrder" INTEGER, + "qSiteInfo" TEXT, + "PlantType" TEXT, + "qCaretaker" TEXT + ); + CREATE VIRTUAL TABLE "Street_Tree_List_fts" USING FTS4 ("qAddress", "qCaretaker", "qSpecies", content="Street_Tree_List"); + ''' + conn = sqlite3.connect(':memory:') + conn.executescript(sql) + assert None is utils.detect_fts(conn, 'Dumb_Table') + assert 'Street_Tree_List_fts' == utils.detect_fts(conn, 'Street_Tree_List')