diff --git a/datasette/app.py b/datasette/app.py index 19d708f4..8852b036 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -243,6 +243,8 @@ class DatabaseView(BaseView): template = 'database.html' async def data(self, request, name, hash): + if request.args.get('sql'): + return await self.custom_sql(request, name, hash) tables = [] table_metadata = self.ds.metadata()[name]['tables'] for table_name, table_rows in table_metadata.items(): @@ -265,6 +267,25 @@ class DatabaseView(BaseView): 'database_hash': hash, } + async def custom_sql(self, request, name, hash): + params = request.raw_args + sql = params.pop('sql') + validate_sql_select(sql) + rows = await self.execute(name, sql, params) + columns = [r[0] for r in rows.description] + return { + 'database': name, + 'rows': rows, + 'columns': columns, + 'query': { + 'sql': sql, + 'params': params, + } + }, { + 'database_hash': hash, + 'custom_sql': True, + } + class DatabaseDownload(BaseView): async def view_get(self, request, name, hash, **kwargs): diff --git a/datasette/static/app.css b/datasette/static/app.css index fa8e6fde..b5559447 100644 --- a/datasette/static/app.css +++ b/datasette/static/app.css @@ -73,3 +73,34 @@ th { margin-top: 1em; margin-bottom: 0; } + +form.sql textarea { + border: 1px solid #ccc; + width: 70%; + height: 3em; + padding: 4px; + font-family: monospace; + font-size: 1.3em; +} +@media only screen and (max-width: 576px) { + form.sql textarea { + width: 95%; + } +} +form.sql p { + margin: 0; +} +form.sql input[type=submit] { + color: #fff; + background-color: #007bff; + border-color: #007bff; + font-weight: 400; + cursor: pointer; + text-align: center; + vertical-align: middle; + border: 1px solid blue; + padding: .275rem .75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: .25rem; +} diff --git a/datasette/templates/database.html b/datasette/templates/database.html index adaa8fd9..607d836e 100644 --- a/datasette/templates/database.html +++ b/datasette/templates/database.html @@ -3,7 +3,7 @@ {% block title %}{{ database }}{% endblock %} {% block content %} -
home
+
home{% if query %} / {{ database }}{% endif %}

{{ database }}

@@ -13,6 +13,11 @@

download {{ database }}.db

{% endif %} +
+

+

+
+ {% for table in tables %}

{{ table.name }}

diff --git a/tests/test_app.py b/tests/test_app.py index d6c1e7df..5fab6c32 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -34,6 +34,40 @@ def test_database_page(three_table_app_client): assert response.status == 302 _, response = three_table_app_client.get('/three_tables') assert 'three_tables' in response.text + # Test JSON list of tables + _, response = three_table_app_client.get('/three_tables.json') + data = response.json + assert 'three_tables' == data['database'] + assert [{ + 'columns': ['pk1', 'pk2', 'content'], + 'name': 'compound_primary_key', + 'table_rows': 0 + }, { + 'columns': ['content'], + 'name': 'no_primary_key', + 'table_rows': 0, + }, { + 'columns': ['pk', 'content'], + 'name': 'simple_primary_key', + 'table_rows': 2 + }] == data['tables'] + + +def test_custom_sql(three_table_app_client): + _, response = three_table_app_client.get( + '/three_tables.jsono?sql=select+content+from+simple_primary_key' + ) + data = response.json + assert { + 'sql': 'select content from simple_primary_key', + 'params': {} + } == data['query'] + assert [ + {'content': 'hello'}, + {'content': 'world'} + ] == data['rows'] + assert ['content'] == data['columns'] + assert 'three_tables' == data['database'] def test_table_page(three_table_app_client):