Canned query support + database/query template refactor

Named canned queries can now be defined in metadata.json like this:

    {
        "databases": {
            "timezones": {
                "queries": {
                    "timezone_for_point": "select tzid from timezones ..."
                }
            }
        }
    }

These will be shown in a new "Queries" section beneath "Views" on the database page.

As part of this, I refactored the logic for the database index page. It used
to combine the functionality for listing available tables and the
functionality for executing custom SQL queries in a single template and view.
I have split that template out into database.html and query.html and reworked
the view to more clearly separate the custom SQL executing code.

Refs #20
sanic-07
Simon Willison 2017-12-05 08:17:02 -08:00
rodzic 0cfd7ce59d
commit a743cdeafc
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 17E2DEA2588B7F52
5 zmienionych plików z 165 dodań i 100 usunięć

Wyświetl plik

@ -50,6 +50,8 @@ class RenderMixin(HTTPMethodView):
class BaseView(RenderMixin):
re_named_parameter = re.compile(':([a-zA-Z0-9_]+)')
def __init__(self, datasette):
self.ds = datasette
self.files = datasette.files
@ -258,6 +260,46 @@ class BaseView(RenderMixin):
)
return r
async def custom_sql(self, request, name, hash, sql, editable=True, canned_query=None):
params = request.raw_args
if 'sql' in params:
params.pop('sql')
# Extract any :named parameters
named_parameters = self.re_named_parameter.findall(sql)
named_parameter_values = {
named_parameter: params.get(named_parameter) or ''
for named_parameter in named_parameters
}
# Set to blank string if missing from params
for named_parameter in named_parameters:
if named_parameter not in params:
params[named_parameter] = ''
extra_args = {}
if params.get('_sql_time_limit_ms'):
extra_args['custom_time_limit'] = int(params['_sql_time_limit_ms'])
rows, truncated, description = await self.execute(
name, sql, params, truncate=True, **extra_args
)
columns = [r[0] for r in description]
return {
'database': name,
'rows': rows,
'truncated': truncated,
'columns': columns,
'query': {
'sql': sql,
'params': params,
}
}, {
'database_hash': hash,
'custom_sql': True,
'named_parameter_values': named_parameter_values,
'editable': editable,
'canned_query': canned_query,
}, ('query-{}.html'.format(to_css_class(name)), 'query.html')
class IndexView(RenderMixin):
def __init__(self, datasette):
@ -315,12 +357,13 @@ async def favicon(request):
class DatabaseView(BaseView):
re_named_parameter = re.compile(':([a-zA-Z0-9_]+)')
async def data(self, request, name, hash):
if request.args.get('sql'):
return await self.custom_sql(request, name, hash)
sql = request.raw_args.pop('sql')
validate_sql_select(sql)
return await self.custom_sql(request, name, hash, sql)
info = self.ds.inspect()[name]
metadata = self.ds.metadata.get('databases', {}).get(name, {})
tables = list(info['tables'].values())
tables.sort(key=lambda t: (t['hidden'], t['name']))
return {
@ -328,48 +371,14 @@ class DatabaseView(BaseView):
'tables': tables,
'hidden_count': len([t for t in tables if t['hidden']]),
'views': info['views'],
'queries': [{
'name': query_name,
'sql': query_sql,
} for query_name, query_sql in (metadata.get('queries') or {}).items()],
}, {
'database_hash': hash,
'show_hidden': request.args.get('_show_hidden'),
}, ('database-{}.html'.format(to_css_class(name)), 'database.html')
async def custom_sql(self, request, name, hash):
params = request.raw_args
sql = params.pop('sql')
validate_sql_select(sql)
# Extract any :named parameters
named_parameters = self.re_named_parameter.findall(sql)
named_parameter_values = {
named_parameter: params.get(named_parameter) or ''
for named_parameter in named_parameters
}
# Set to blank string if missing from params
for named_parameter in named_parameters:
if named_parameter not in params:
params[named_parameter] = ''
extra_args = {}
if params.get('_sql_time_limit_ms'):
extra_args['custom_time_limit'] = int(params['_sql_time_limit_ms'])
rows, truncated, description = await self.execute(
name, sql, params, truncate=True, **extra_args
)
columns = [r[0] for r in description]
return {
'database': name,
'rows': rows,
'truncated': truncated,
'columns': columns,
'query': {
'sql': sql,
'params': params,
}
}, {
'database_hash': hash,
'custom_sql': True,
'named_parameter_values': named_parameter_values,
'editable': True,
}, ('database-{}.html'.format(to_css_class(name)), 'database.html')
@ -466,6 +475,9 @@ class RowTableShared(BaseView):
class TableView(RowTableShared):
async def data(self, request, name, hash, table):
table = urllib.parse.unquote_plus(table)
canned_query = self.ds.get_canned_query(name, table)
if canned_query is not None:
return await self.custom_sql(request, name, hash, canned_query['sql'], editable=False, canned_query=table)
pks = await self.pks_for_table(name, table)
is_view = bool(list(await self.execute(name, "SELECT count(*) from sqlite_master WHERE type = 'view' and name=:n", {
'n': table,
@ -792,6 +804,20 @@ class Datasette:
self.template_dir = template_dir
self.static_mounts = static_mounts or []
def get_canned_query(self, database_name, query_name):
query = self.metadata.get(
'databases', {}
).get(
database_name, {}
).get(
'queries', {}
).get(query_name)
if query:
return {
'name': query_name,
'sql': query,
}
def asset_urls(self, key):
for url_or_dict in (self.metadata.get(key) or []):
if isinstance(url_or_dict, dict):

Wyświetl plik

@ -0,0 +1,7 @@
<script src="/-/static/codemirror-5.31.0.js"></script>
<link rel="stylesheet" href="/-/static/codemirror-5.31.0-min.css" />
<script src="/-/static/codemirror-5.31.0-sql.min.js"></script>
<style>
.CodeMirror { height: auto; min-height: 70px; width: 80%; border: 1px solid #ddd; }
.CodeMirror-scroll { max-height: 200px; }
</style>

Wyświetl plik

@ -0,0 +1,13 @@
<script>
var editor = CodeMirror.fromTextArea(document.getElementsByName("sql")[0], {
lineNumbers: true,
mode: "text/x-sql",
lineWrapping: true,
});
editor.setOption("extraKeys", {
"Shift-Enter": function() {
document.getElementsByClassName("sql")[0].submit();
},
Tab: false
});
</script>

Wyświetl plik

@ -3,63 +3,22 @@
{% block title %}{{ database }}{% endblock %}
{% block extra_head %}
{% if columns %}
<style>
@media only screen and (max-width: 576px) {
{% for column in columns %}
td:nth-of-type({{ loop.index }}):before { content: "{{ column|escape_css_string }}"; }
{% endfor %}
}
</style>
{% endif %}
<script src="/-/static/codemirror-5.31.0.js"></script>
<link rel="stylesheet" href="/-/static/codemirror-5.31.0-min.css" />
<script src="/-/static/codemirror-5.31.0-sql.min.js"></script>
<style>
.CodeMirror { height: auto; min-height: 70px; width: 80%; border: 1px solid #ddd; }
.CodeMirror-scroll { max-height: 200px; }
</style>
{% include "_codemirror.html" %}
{% endblock %}
{% block body_class %}db db-{{ database|to_css_class }}{% endblock %}
{% block content %}
<div class="hd"><a href="/">home</a>{% if query %} / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a>{% endif %}</div>
<div class="hd"><a href="/">home</a></div>
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_hash[:6] }}">{{ database }}</h1>
<form class="sql" action="/{{ database }}-{{ database_hash }}" method="get">
<h3>Custom SQL query{% if rows %} returning {% if truncated %}more than {% endif %}{{ "{:,}".format(rows|length) }} row{% if rows|length == 1 %}{% else %}s{% endif %}{% endif %}</h3>
<p><textarea name="sql">{% if query and query.sql %}{{ query.sql }}{% else %}select * from {{ tables[0].name|escape_table_name }}{% endif %}</textarea></p>
{% if named_parameter_values %}
<h3>Query parameters</h3>
{% for name, value in named_parameter_values.items() %}
<p><label for="qp{{ loop.counter }}">{{ name }}</label> <input type="text" id="qp{{ loop.counter }}" name="{{ name }}" value="{{ value }}"></p>
{% endfor %}
{% endif %}
<h3>Custom SQL query</h3>
<p><textarea name="sql">select * from {{ tables[0].name|escape_table_name }}</textarea></p>
<p><input type="submit" value="Run SQL"></p>
</form>
{% if rows %}
<p>This data as <a href="{{ url_json }}">.json</a>, <a href="{{ url_jsono }}">.jsono</a></p>
<table>
<thead>
<tr>
{% for column in columns %}<th scope="col">{{ column }}</th>{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for td in row %}
<td>{% if td == None %}{{ "&nbsp;"|safe }}{% else %}{{ td }}{% endif %}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% for table in tables %}
{% if show_hidden or not table.hidden %}
<div class="db-table">
@ -83,20 +42,17 @@
</ul>
{% endif %}
{% if queries %}
<h2>Queries</h2>
<ul>
{% for query in queries %}
<li><a href="/{{ database }}-{{ database_hash }}/{{ query.name|urlencode }}" title="{{ query.sql }}">{{ query.name }}</a></li>
{% endfor %}
</ul>
{% endif %}
<p>Download SQLite DB: <a href="/{{ database }}-{{ database_hash }}.db">{{ database }}.db</a></p>
<script>
var editor = CodeMirror.fromTextArea(document.getElementsByName("sql")[0], {
lineNumbers: true,
mode: "text/x-sql",
lineWrapping: true,
});
editor.setOption("extraKeys", {
"Shift-Enter": function() {
document.getElementsByClassName("sql")[0].submit();
},
Tab: false
});
</script>
{% include "_codemirror_foot.html" %}
{% endblock %}

Wyświetl plik

@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block title %}{{ database }}{% if query and query.sql %}{{ query.sql }}{% endif %}{% endblock %}
{% block extra_head %}
{% if columns %}
<style>
@media only screen and (max-width: 576px) {
{% for column in columns %}
td:nth-of-type({{ loop.index }}):before { content: "{{ column|escape_css_string }}"; }
{% endfor %}
}
</style>
{% endif %}
{% include "_codemirror.html" %}
{% endblock %}
{% block body_class %}query db-{{ database|to_css_class }}{% endblock %}
{% block content %}
<div class="hd"><a href="/">home</a> / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a></div>
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_hash[:6] }}">{{ database }}</h1>
<form class="sql" action="/{{ database }}-{{ database_hash }}{% if canned_query %}/{{ canned_query }}{% endif %}" method="get">
<h3>Custom SQL query{% if rows %} returning {% if truncated %}more than {% endif %}{{ "{:,}".format(rows|length) }} row{% if rows|length == 1 %}{% else %}s{% endif %}{% endif %}</h3>
{% if editable %}
<p><textarea name="sql">{% if query and query.sql %}{{ query.sql }}{% else %}select * from {{ tables[0].name|escape_table_name }}{% endif %}</textarea></p>
{% else %}
<pre>{% if query %}{{ query.sql }}{% endif %}</pre>
{% endif %}
{% if named_parameter_values %}
<h3>Query parameters</h3>
{% for name, value in named_parameter_values.items() %}
<p><label for="qp{{ loop.counter }}">{{ name }}</label> <input type="text" id="qp{{ loop.counter }}" name="{{ name }}" value="{{ value }}"></p>
{% endfor %}
{% endif %}
<p><input type="submit" value="Run SQL"></p>
</form>
{% if rows %}
<p>This data as <a href="{{ url_json }}">.json</a>, <a href="{{ url_jsono }}">.jsono</a></p>
<table>
<thead>
<tr>
{% for column in columns %}<th scope="col">{{ column }}</th>{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for td in row %}
<td>{% if td == None %}{{ "&nbsp;"|safe }}{% else %}{{ td }}{% endif %}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% include "_codemirror_foot.html" %}
{% endblock %}