mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
WIP better SQL errors, refs #619
This commit is contained in:
parent
5b8b8ae597
commit
ea55267c79
5 changed files with 66 additions and 18 deletions
|
|
@ -16,6 +16,7 @@ from .utils import (
|
||||||
sqlite_timelimit,
|
sqlite_timelimit,
|
||||||
sqlite3,
|
sqlite3,
|
||||||
table_columns,
|
table_columns,
|
||||||
|
QueryInterrupted,
|
||||||
)
|
)
|
||||||
from .inspect import inspect_hash
|
from .inspect import inspect_hash
|
||||||
|
|
||||||
|
|
@ -376,10 +377,6 @@ class WriteTask:
|
||||||
self.reply_queue = reply_queue
|
self.reply_queue = reply_queue
|
||||||
|
|
||||||
|
|
||||||
class QueryInterrupted(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MultipleValues(Exception):
|
class MultipleValues(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@
|
||||||
|
|
||||||
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
|
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<p class="message-error">{{ error }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form class="sql" action="{{ database_url(database) }}{% if canned_query %}/{{ canned_query }}{% endif %}" method="{% if canned_write %}post{% else %}get{% endif %}">
|
<form class="sql" action="{{ database_url(database) }}{% if canned_query %}/{{ canned_query }}{% endif %}" method="{% if canned_write %}post{% else %}get{% endif %}">
|
||||||
<h3>Custom SQL query{% if display_rows %} returning {% if truncated %}more than {% endif %}{{ "{:,}".format(display_rows|length) }} row{% if display_rows|length == 1 %}{% else %}s{% endif %}{% endif %} <span class="show-hide-sql">{% if hide_sql %}(<a href="{{ path_with_removed_args(request, {'_hide_sql': '1'}) }}">show</a>){% else %}(<a href="{{ path_with_added_args(request, {'_hide_sql': '1'}) }}">hide</a>){% endif %}</span></h3>
|
<h3>Custom SQL query{% if display_rows %} returning {% if truncated %}more than {% endif %}{{ "{:,}".format(display_rows|length) }} row{% if display_rows|length == 1 %}{% else %}s{% endif %}{% endif %} <span class="show-hide-sql">{% if hide_sql %}(<a href="{{ path_with_removed_args(request, {'_hide_sql': '1'}) }}">show</a>){% else %}(<a href="{{ path_with_added_args(request, {'_hide_sql': '1'}) }}">hide</a>){% endif %}</span></h3>
|
||||||
{% if not hide_sql %}
|
{% if not hide_sql %}
|
||||||
|
|
@ -76,7 +80,7 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if not canned_write %}
|
{% if not canned_write and not error %}
|
||||||
<p class="zero-results">0 results</p>
|
<p class="zero-results">0 results</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,10 @@ class InvalidSql(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class QueryInterrupted(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
allowed_sql_res = [
|
allowed_sql_res = [
|
||||||
re.compile(r"^select\b"),
|
re.compile(r"^select\b"),
|
||||||
re.compile(r"^explain select\b"),
|
re.compile(r"^explain select\b"),
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,7 @@ class DataView(BaseView):
|
||||||
if isinstance(response_or_template_contexts, Response):
|
if isinstance(response_or_template_contexts, Response):
|
||||||
return response_or_template_contexts
|
return response_or_template_contexts
|
||||||
else:
|
else:
|
||||||
data, _, _ = response_or_template_contexts
|
data = response_or_template_contexts[0]
|
||||||
except (sqlite3.OperationalError, InvalidSql) 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)
|
||||||
|
|
||||||
|
|
@ -307,7 +307,8 @@ class DataView(BaseView):
|
||||||
if next:
|
if next:
|
||||||
kwargs["_next"] = next
|
kwargs["_next"] = next
|
||||||
if not first:
|
if not first:
|
||||||
data, _, _ = await self.data(request, database, hash, **kwargs)
|
bits = await self.data(request, database, hash, **kwargs)
|
||||||
|
data = bits[0]
|
||||||
if first:
|
if first:
|
||||||
await writer.writerow(headings)
|
await writer.writerow(headings)
|
||||||
first = False
|
first = False
|
||||||
|
|
@ -398,9 +399,18 @@ class DataView(BaseView):
|
||||||
)
|
)
|
||||||
if isinstance(response_or_template_contexts, Response):
|
if isinstance(response_or_template_contexts, Response):
|
||||||
return response_or_template_contexts
|
return response_or_template_contexts
|
||||||
|
|
||||||
else:
|
else:
|
||||||
data, extra_template_data, templates = response_or_template_contexts
|
if len(response_or_template_contexts) == 3:
|
||||||
|
data, extra_template_data, templates = response_or_template_contexts
|
||||||
|
elif len(response_or_template_contexts) == 4:
|
||||||
|
(
|
||||||
|
data,
|
||||||
|
extra_template_data,
|
||||||
|
templates,
|
||||||
|
status_code,
|
||||||
|
) = response_or_template_contexts
|
||||||
|
else:
|
||||||
|
assert False, "response_or_template_contexts should be 3 or 4 items"
|
||||||
except QueryInterrupted:
|
except QueryInterrupted:
|
||||||
raise DatasetteError(
|
raise DatasetteError(
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from datasette.utils import (
|
||||||
is_url,
|
is_url,
|
||||||
path_with_added_args,
|
path_with_added_args,
|
||||||
path_with_removed_args,
|
path_with_removed_args,
|
||||||
|
QueryInterrupted,
|
||||||
)
|
)
|
||||||
from datasette.utils.asgi import AsgiFileDownload, Response, Forbidden
|
from datasette.utils.asgi import AsgiFileDownload, Response, Forbidden
|
||||||
from datasette.plugins import pm
|
from datasette.plugins import pm
|
||||||
|
|
@ -34,7 +35,6 @@ class DatabaseView(DataView):
|
||||||
|
|
||||||
if request.args.get("sql"):
|
if request.args.get("sql"):
|
||||||
sql = request.args.get("sql")
|
sql = request.args.get("sql")
|
||||||
validate_sql_select(sql)
|
|
||||||
return await QueryView(self.ds).data(
|
return await QueryView(self.ds).data(
|
||||||
request, database, hash, sql, _size=_size, metadata=metadata
|
request, database, hash, sql, _size=_size, metadata=metadata
|
||||||
)
|
)
|
||||||
|
|
@ -207,6 +207,12 @@ class QueryView(DataView):
|
||||||
|
|
||||||
templates = ["query-{}.html".format(to_css_class(database)), "query.html"]
|
templates = ["query-{}.html".format(to_css_class(database)), "query.html"]
|
||||||
|
|
||||||
|
error = None
|
||||||
|
truncated = False
|
||||||
|
rows = []
|
||||||
|
columns = []
|
||||||
|
http_status = 200
|
||||||
|
|
||||||
# Execute query - as write or as read
|
# Execute query - as write or as read
|
||||||
if write:
|
if write:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
@ -242,6 +248,17 @@ class QueryView(DataView):
|
||||||
message_type = self.ds.INFO
|
message_type = self.ds.INFO
|
||||||
redirect_url = metadata.get("on_success_redirect")
|
redirect_url = metadata.get("on_success_redirect")
|
||||||
ok = True
|
ok = True
|
||||||
|
except QueryInterrupted:
|
||||||
|
raise DatasetteError(
|
||||||
|
"""
|
||||||
|
SQL query took too long. The time limit is controlled by the
|
||||||
|
<a href="https://docs.datasette.io/en/stable/config.html#sql-time-limit-ms">sql_time_limit_ms</a>
|
||||||
|
configuration option.
|
||||||
|
""",
|
||||||
|
title="SQL Interrupted",
|
||||||
|
status=400,
|
||||||
|
messagge_is_html=True,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
message = metadata.get("on_error_message") or str(e)
|
message = metadata.get("on_error_message") or str(e)
|
||||||
message_type = self.ds.ERROR
|
message_type = self.ds.ERROR
|
||||||
|
|
@ -288,10 +305,22 @@ class QueryView(DataView):
|
||||||
params_for_query = MagicParameters(params, request, self.ds)
|
params_for_query = MagicParameters(params, request, self.ds)
|
||||||
else:
|
else:
|
||||||
params_for_query = params
|
params_for_query = params
|
||||||
results = await self.ds.execute(
|
ok = False
|
||||||
database, sql, params_for_query, truncate=True, **extra_args
|
try:
|
||||||
)
|
validate_sql_select(sql)
|
||||||
columns = [r[0] for r in results.description]
|
results = await self.ds.execute(
|
||||||
|
database, sql, params_for_query, truncate=True, **extra_args
|
||||||
|
)
|
||||||
|
rows = results.rows
|
||||||
|
truncated = results.truncated
|
||||||
|
columns = [r[0] for r in results.description]
|
||||||
|
ok = True
|
||||||
|
except Exception as e:
|
||||||
|
rows = []
|
||||||
|
columns = []
|
||||||
|
error = str(e)
|
||||||
|
ok = False
|
||||||
|
http_status = 400
|
||||||
|
|
||||||
if canned_query:
|
if canned_query:
|
||||||
templates.insert(
|
templates.insert(
|
||||||
|
|
@ -303,9 +332,9 @@ class QueryView(DataView):
|
||||||
|
|
||||||
async def extra_template():
|
async def extra_template():
|
||||||
display_rows = []
|
display_rows = []
|
||||||
for row in results.rows:
|
for row in rows:
|
||||||
display_row = []
|
display_row = []
|
||||||
for column, value in zip(results.columns, row):
|
for column, value in zip(columns, row):
|
||||||
display_value = value
|
display_value = value
|
||||||
# Let the plugins have a go
|
# Let the plugins have a go
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
|
|
@ -345,10 +374,13 @@ class QueryView(DataView):
|
||||||
|
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
|
"ok": ok,
|
||||||
|
"error": error,
|
||||||
"database": database,
|
"database": database,
|
||||||
|
"error": error,
|
||||||
"query_name": canned_query,
|
"query_name": canned_query,
|
||||||
"rows": results.rows,
|
"rows": rows,
|
||||||
"truncated": results.truncated,
|
"truncated": truncated,
|
||||||
"columns": columns,
|
"columns": columns,
|
||||||
"query": {"sql": sql, "params": params},
|
"query": {"sql": sql, "params": params},
|
||||||
"private": private,
|
"private": private,
|
||||||
|
|
@ -358,6 +390,7 @@ class QueryView(DataView):
|
||||||
},
|
},
|
||||||
extra_template,
|
extra_template,
|
||||||
templates,
|
templates,
|
||||||
|
http_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue