kopia lustrzana https://github.com/simonw/datasette
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 #20sanic-07
rodzic
0cfd7ce59d
commit
a743cdeafc
110
datasette/app.py
110
datasette/app.py
|
@ -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):
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 %}{{ " "|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 %}
|
||||
|
|
|
@ -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 %}{{ " "|safe }}{% else %}{{ td }}{% endif %}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% include "_codemirror_foot.html" %}
|
||||
|
||||
{% endblock %}
|
Ładowanie…
Reference in New Issue