Introduce new `/$DB/-/query` endpoint, soft replaces `/$DB?sql=...` (#2363)

* Introduce new default /$DB/-/query endpoint
* Fix a lot of tests
* Update pyodide test to use query endpoint
* Link to /fixtures/-/query in a few places
* Documentation for QueryView

---------

Co-authored-by: Simon Willison <swillison@gmail.com>
pull/2367/head
Alex Garcia 2024-07-15 10:33:51 -07:00 zatwierdzone przez GitHub
rodzic 56adfff8d2
commit a23c2aee00
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
21 zmienionych plików z 148 dodań i 83 usunięć

Wyświetl plik

@ -37,7 +37,7 @@ from jinja2.exceptions import TemplateNotFound
from .events import Event
from .views import Context
from .views.base import ureg
from .views.database import database_download, DatabaseView, TableCreateView
from .views.database import database_download, DatabaseView, TableCreateView, QueryView
from .views.index import IndexView
from .views.special import (
JsonDataView,
@ -1578,6 +1578,10 @@ class Datasette:
r"/(?P<database>[^\/\.]+)(\.(?P<format>\w+))?$",
)
add_route(TableCreateView.as_view(self), r"/(?P<database>[^\/\.]+)/-/create$")
add_route(
wrap_view(QueryView, self),
r"/(?P<database>[^\/\.]+)/-/query(\.(?P<format>\w+))?$",
)
add_route(
wrap_view(table_view, self),
r"/(?P<database>[^\/\.]+)/(?P<table>[^\/\.]+)(\.(?P<format>\w+))?$",

Wyświetl plik

@ -21,7 +21,7 @@
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
{% if allow_execute_sql %}
<form class="sql" action="{{ urls.database(database) }}" method="get">
<form class="sql" action="{{ urls.database(database) }}/-/query" method="get">
<h3>Custom SQL query</h3>
<p><textarea id="sql-editor" name="sql">{% if tables %}select * from {{ tables[0].name|escape_sqlite }}{% else %}select sqlite_version(){% endif %}</textarea></p>
<p>
@ -36,7 +36,7 @@
<p>The following databases are attached to this connection, and can be used for cross-database joins:</p>
<ul class="bullets">
{% for db_name in attached_databases %}
<li><strong>{{ db_name }}</strong> - <a href="?sql=select+*+from+[{{ db_name }}].sqlite_master+where+type='table'">tables</a></li>
<li><strong>{{ db_name }}</strong> - <a href="{{ urls.database(db_name) }}/-/query?sql=select+*+from+[{{ db_name }}].sqlite_master+where+type='table'">tables</a></li>
{% endfor %}
</ul>
</div>

Wyświetl plik

@ -58,6 +58,11 @@ class DatabaseView(View):
sql = (request.args.get("sql") or "").strip()
if sql:
redirect_url = "/" + request.url_vars.get("database") + "/-/query"
if request.url_vars.get("format"):
redirect_url += "." + request.url_vars.get("format")
redirect_url += "?" + request.query_string
return Response.redirect(redirect_url)
return await QueryView()(request, datasette)
if format_ not in ("html", "json"):
@ -433,6 +438,8 @@ class QueryView(View):
async def get(self, request, datasette):
from datasette.app import TableNotFound
await datasette.refresh_schemas()
db = await datasette.resolve_database(request)
database = db.name
@ -686,6 +693,7 @@ class QueryView(View):
if allow_execute_sql and is_validated_sql and ":_" not in sql:
edit_sql_url = (
datasette.urls.database(database)
+ "/-/query"
+ "?"
+ urlencode(
{

Wyświetl plik

@ -55,6 +55,21 @@ The following tables are hidden by default:
- Tables relating to the inner workings of the SpatiaLite SQLite extension.
- ``sqlite_stat`` tables used to store statistics used by the query optimizer.
.. _QueryView:
Queries
=======
The ``/database-name/-/query`` page can be used to execute an arbitrary SQL query against that database, if the :ref:`permissions_execute_sql` permission is enabled. This query is passed as the ``?sql=`` query string parameter.
This means you can link directly to a query by constructing the following URL:
``/database-name/-/query?sql=SELECT+*+FROM+table_name``
Each configured :ref:`canned query <canned_queries>` has its own page, at ``/database-name/query-name``. Viewing this page will execute the query and display the results.
In both cases adding a ``.json`` extension to the URL will return the results as JSON.
.. _TableView:
Table

Wyświetl plik

@ -40,11 +40,11 @@ async () => {
import setuptools
from datasette.app import Datasette
ds = Datasette(memory=True, settings={'num_sql_threads': 0})
(await ds.client.get('/_memory.json?sql=select+55+as+itworks&_shape=array')).text
(await ds.client.get('/_memory/-/query.json?sql=select+55+as+itworks&_shape=array')).text
\`);
if (JSON.parse(output)[0].itworks != 55) {
throw 'Got ' + output + ', expected itworks: 55';
}
return 'Test passed!';
}
"
"

Wyświetl plik

@ -411,6 +411,7 @@ def query_actions(datasette, database, query_name, sql):
return [
{
"href": datasette.urls.database(database)
+ "/-/query"
+ "?"
+ urllib.parse.urlencode(
{

Wyświetl plik

@ -623,7 +623,7 @@ def test_no_files_uses_memory_database(app_client_no_files):
} == response.json
# Try that SQL query
response = app_client_no_files.get(
"/_memory.json?sql=select+sqlite_version()&_shape=array"
"/_memory/-/query.json?sql=select+sqlite_version()&_shape=array"
)
assert 1 == len(response.json)
assert ["sqlite_version()"] == list(response.json[0].keys())
@ -653,7 +653,7 @@ def test_database_page_for_database_with_dot_in_name(app_client_with_dot):
@pytest.mark.asyncio
async def test_custom_sql(ds_client):
response = await ds_client.get(
"/fixtures.json?sql=select+content+from+simple_primary_key"
"/fixtures/-/query.json?sql=select+content+from+simple_primary_key",
)
data = response.json()
assert data == {
@ -670,7 +670,9 @@ async def test_custom_sql(ds_client):
def test_sql_time_limit(app_client_shorter_time_limit):
response = app_client_shorter_time_limit.get("/fixtures.json?sql=select+sleep(0.5)")
response = app_client_shorter_time_limit.get(
"/fixtures/-/query.json?sql=select+sleep(0.5)",
)
assert 400 == response.status
assert response.json == {
"ok": False,
@ -691,16 +693,22 @@ def test_sql_time_limit(app_client_shorter_time_limit):
@pytest.mark.asyncio
async def test_custom_sql_time_limit(ds_client):
response = await ds_client.get("/fixtures.json?sql=select+sleep(0.01)")
response = await ds_client.get(
"/fixtures/-/query.json?sql=select+sleep(0.01)",
)
assert response.status_code == 200
response = await ds_client.get("/fixtures.json?sql=select+sleep(0.01)&_timelimit=5")
response = await ds_client.get(
"/fixtures/-/query.json?sql=select+sleep(0.01)&_timelimit=5",
)
assert response.status_code == 400
assert response.json()["title"] == "SQL Interrupted"
@pytest.mark.asyncio
async def test_invalid_custom_sql(ds_client):
response = await ds_client.get("/fixtures.json?sql=.schema")
response = await ds_client.get(
"/fixtures/-/query.json?sql=.schema",
)
assert response.status_code == 400
assert response.json()["ok"] is False
assert "Statement must be a SELECT" == response.json()["error"]
@ -883,9 +891,13 @@ async def test_json_columns(ds_client, extra_args, expected):
select 1 as intval, "s" as strval, 0.5 as floatval,
'{"foo": "bar"}' as jsonval
"""
path = "/fixtures.json?" + urllib.parse.urlencode({"sql": sql, "_shape": "array"})
path = "/fixtures/-/query.json?" + urllib.parse.urlencode(
{"sql": sql, "_shape": "array"}
)
path += extra_args
response = await ds_client.get(path)
response = await ds_client.get(
path,
)
assert response.json() == expected
@ -917,7 +929,7 @@ def test_config_force_https_urls():
("/fixtures.json", 200),
("/fixtures/no_primary_key.json", 200),
# A 400 invalid SQL query should still have the header:
("/fixtures.json?sql=select+blah", 400),
("/fixtures/-/query.json?sql=select+blah", 400),
# Write APIs
("/fixtures/-/create", 405),
("/fixtures/facetable/-/insert", 405),
@ -930,7 +942,9 @@ def test_cors(
path,
status_code,
):
response = app_client_with_cors.get(path)
response = app_client_with_cors.get(
path,
)
assert response.status == status_code
assert response.headers["Access-Control-Allow-Origin"] == "*"
assert (
@ -946,7 +960,9 @@ def test_cors(
# should not have those headers - I'm using that fixture because
# regular app_client doesn't have immutable fixtures.db which means
# the test for /fixtures.db returns a 403 error
response = app_client_two_attached_databases_one_immutable.get(path)
response = app_client_two_attached_databases_one_immutable.get(
path,
)
assert response.status == status_code
assert "Access-Control-Allow-Origin" not in response.headers
assert "Access-Control-Allow-Headers" not in response.headers

Wyświetl plik

@ -637,7 +637,9 @@ async def test_delete_row(ds_write, table, row_for_create, pks, delete_path):
# Should be a single row
assert (
await ds_write.client.get(
"/data.json?_shape=arrayfirst&sql=select+count(*)+from+{}".format(table)
"/data/-/query.json?_shape=arrayfirst&sql=select+count(*)+from+{}".format(
table
)
)
).json() == [1]
# Now delete the row
@ -645,7 +647,9 @@ async def test_delete_row(ds_write, table, row_for_create, pks, delete_path):
# Special case for that rowid table
delete_path = (
await ds_write.client.get(
"/data.json?_shape=arrayfirst&sql=select+rowid+from+{}".format(table)
"/data/-/query.json?_shape=arrayfirst&sql=select+rowid+from+{}".format(
table
)
)
).json()[0]
@ -663,7 +667,9 @@ async def test_delete_row(ds_write, table, row_for_create, pks, delete_path):
assert event.pks == str(delete_path).split(",")
assert (
await ds_write.client.get(
"/data.json?_shape=arrayfirst&sql=select+count(*)+from+{}".format(table)
"/data/-/query.json?_shape=arrayfirst&sql=select+count(*)+from+{}".format(
table
)
)
).json() == [0]

Wyświetl plik

@ -412,7 +412,7 @@ def test_magic_parameters_csrf_json(magic_parameters_client, use_csrf, return_js
def test_magic_parameters_cannot_be_used_in_arbitrary_queries(magic_parameters_client):
response = magic_parameters_client.get(
"/data.json?sql=select+:_header_host&_shape=array"
"/data/-/query.json?sql=select+:_header_host&_shape=array"
)
assert 400 == response.status
assert response.json["error"].startswith("You did not supply a value for binding")

Wyświetl plik

@ -250,7 +250,7 @@ def test_plugin_s_overwrite():
"--plugins-dir",
plugins_dir,
"--get",
"/_memory.json?sql=select+prepare_connection_args()",
"/_memory/-/query.json?sql=select+prepare_connection_args()",
],
)
assert result.exit_code == 0, result.output
@ -265,7 +265,7 @@ def test_plugin_s_overwrite():
"--plugins-dir",
plugins_dir,
"--get",
"/_memory.json?sql=select+prepare_connection_args()",
"/_memory/-/query.json?sql=select+prepare_connection_args()",
"-s",
"plugins.name-of-plugin",
"OVERRIDE",
@ -295,7 +295,7 @@ def test_setting_default_allow_sql(default_allow_sql):
"default_allow_sql",
"on" if default_allow_sql else "off",
"--get",
"/_memory.json?sql=select+21&_shape=objects",
"/_memory/-/query.json?sql=select+21&_shape=objects",
],
)
if default_allow_sql:
@ -309,7 +309,7 @@ def test_setting_default_allow_sql(default_allow_sql):
def test_sql_errors_logged_to_stderr():
runner = CliRunner(mix_stderr=False)
result = runner.invoke(cli, ["--get", "/_memory.json?sql=select+blah"])
result = runner.invoke(cli, ["--get", "/_memory/-/query.json?sql=select+blah"])
assert result.exit_code == 1
assert "sql = 'select blah', params = {}: no such column: blah\n" in result.stderr

Wyświetl plik

@ -31,7 +31,7 @@ def test_serve_with_get(tmp_path_factory):
"--plugins-dir",
str(plugins_dir),
"--get",
"/_memory.json?sql=select+sqlite_version()",
"/_memory/-/query.json?sql=select+sqlite_version()",
],
)
assert result.exit_code == 0, result.output

Wyświetl plik

@ -25,7 +25,8 @@ def test_crossdb_join(app_client_two_attached_databases_crossdb_enabled):
fixtures.searchable
"""
response = app_client.get(
"/_memory.json?" + urllib.parse.urlencode({"sql": sql, "_shape": "array"})
"/_memory/-/query.json?"
+ urllib.parse.urlencode({"sql": sql, "_shape": "array"})
)
assert response.status == 200
assert response.json == [
@ -67,9 +68,10 @@ def test_crossdb_attached_database_list_display(
):
app_client = app_client_two_attached_databases_crossdb_enabled
response = app_client.get("/_memory")
response2 = app_client.get("/")
for fragment in (
"databases are attached to this connection",
"<li><strong>fixtures</strong> - ",
"<li><strong>extra database</strong> - ",
'<li><strong>extra database</strong> - <a href="/extra+database/-/query?sql=',
):
assert fragment in response.text

Wyświetl plik

@ -146,14 +146,14 @@ async def test_table_csv_blob_columns(ds_client):
@pytest.mark.asyncio
async def test_custom_sql_csv_blob_columns(ds_client):
response = await ds_client.get(
"/fixtures.csv?sql=select+rowid,+data+from+binary_data"
"/fixtures/-/query.csv?sql=select+rowid,+data+from+binary_data"
)
assert response.status_code == 200
assert response.headers["content-type"] == "text/plain; charset=utf-8"
assert response.text == (
"rowid,data\r\n"
'1,"http://localhost/fixtures.blob?sql=select+rowid,+data+from+binary_data&_blob_column=data&_blob_hash=f3088978da8f9aea479ffc7f631370b968d2e855eeb172bea7f6c7a04262bb6d"\r\n'
'2,"http://localhost/fixtures.blob?sql=select+rowid,+data+from+binary_data&_blob_column=data&_blob_hash=b835b0483cedb86130b9a2c280880bf5fadc5318ddf8c18d0df5204d40df1724"\r\n'
'1,"http://localhost/fixtures/-/query.blob?sql=select+rowid,+data+from+binary_data&_blob_column=data&_blob_hash=f3088978da8f9aea479ffc7f631370b968d2e855eeb172bea7f6c7a04262bb6d"\r\n'
'2,"http://localhost/fixtures/-/query.blob?sql=select+rowid,+data+from+binary_data&_blob_column=data&_blob_hash=b835b0483cedb86130b9a2c280880bf5fadc5318ddf8c18d0df5204d40df1724"\r\n'
"3,\r\n"
)
@ -161,7 +161,7 @@ async def test_custom_sql_csv_blob_columns(ds_client):
@pytest.mark.asyncio
async def test_custom_sql_csv(ds_client):
response = await ds_client.get(
"/fixtures.csv?sql=select+content+from+simple_primary_key+limit+2"
"/fixtures/-/query.csv?sql=select+content+from+simple_primary_key+limit+2"
)
assert response.status_code == 200
assert response.headers["content-type"] == "text/plain; charset=utf-8"
@ -182,7 +182,7 @@ async def test_table_csv_download(ds_client):
@pytest.mark.asyncio
async def test_csv_with_non_ascii_characters(ds_client):
response = await ds_client.get(
"/fixtures.csv?sql=select%0D%0A++%27%F0%9D%90%9C%F0%9D%90%A2%F0%9D%90%AD%F0%9D%90%A2%F0%9D%90%9E%F0%9D%90%AC%27+as+text%2C%0D%0A++1+as+number%0D%0Aunion%0D%0Aselect%0D%0A++%27bob%27+as+text%2C%0D%0A++2+as+number%0D%0Aorder+by%0D%0A++number"
"/fixtures/-/query.csv?sql=select%0D%0A++%27%F0%9D%90%9C%F0%9D%90%A2%F0%9D%90%AD%F0%9D%90%A2%F0%9D%90%9E%F0%9D%90%AC%27+as+text%2C%0D%0A++1+as+number%0D%0Aunion%0D%0Aselect%0D%0A++%27bob%27+as+text%2C%0D%0A++2+as+number%0D%0Aorder+by%0D%0A++number"
)
assert response.status_code == 200
assert response.headers["content-type"] == "text/plain; charset=utf-8"

Wyświetl plik

@ -159,7 +159,7 @@ async def test_database_page(ds_client):
@pytest.mark.asyncio
async def test_invalid_custom_sql(ds_client):
response = await ds_client.get("/fixtures?sql=.schema")
response = await ds_client.get("/fixtures/-/query?sql=.schema")
assert response.status_code == 400
assert "Statement must be a SELECT" in response.text
@ -167,7 +167,7 @@ async def test_invalid_custom_sql(ds_client):
@pytest.mark.asyncio
async def test_disallowed_custom_sql_pragma(ds_client):
response = await ds_client.get(
"/fixtures?sql=SELECT+*+FROM+pragma_not_on_allow_list('idx52')"
"/fixtures/-/query?sql=SELECT+*+FROM+pragma_not_on_allow_list('idx52')"
)
assert response.status_code == 400
pragmas = ", ".join("pragma_{}()".format(pragma) for pragma in allowed_pragmas)
@ -180,7 +180,9 @@ async def test_disallowed_custom_sql_pragma(ds_client):
def test_sql_time_limit(app_client_shorter_time_limit):
response = app_client_shorter_time_limit.get("/fixtures?sql=select+sleep(0.5)")
response = app_client_shorter_time_limit.get(
"/fixtures/-/query?sql=select+sleep(0.5)"
)
assert 400 == response.status
expected_html_fragments = [
"""
@ -207,7 +209,7 @@ def test_row_page_does_not_truncate():
def test_query_page_truncates():
with make_app_client(settings={"truncate_cells_html": 5}) as client:
response = client.get(
"/fixtures?"
"/fixtures/-/query?"
+ urllib.parse.urlencode(
{
"sql": "select 'this is longer than 5' as a, 'https://example.com/' as b"
@ -229,7 +231,7 @@ def test_query_page_truncates():
[
("/", ["index"]),
("/fixtures", ["db", "db-fixtures"]),
("/fixtures?sql=select+1", ["query", "db-fixtures"]),
("/fixtures/-/query?sql=select+1", ["query", "db-fixtures"]),
(
"/fixtures/simple_primary_key",
["table", "db-fixtures", "table-simple_primary_key"],
@ -296,21 +298,24 @@ async def test_row_json_export_link(ds_client):
@pytest.mark.asyncio
async def test_query_json_csv_export_links(ds_client):
response = await ds_client.get("/fixtures?sql=select+1")
response = await ds_client.get("/fixtures/-/query?sql=select+1")
assert response.status_code == 200
assert '<a href="/fixtures.json?sql=select+1">json</a>' in response.text
assert '<a href="/fixtures.csv?sql=select+1&amp;_size=max">CSV</a>' in response.text
assert '<a href="/fixtures/-/query.json?sql=select+1">json</a>' in response.text
assert (
'<a href="/fixtures/-/query.csv?sql=select+1&amp;_size=max">CSV</a>'
in response.text
)
@pytest.mark.asyncio
async def test_query_parameter_form_fields(ds_client):
response = await ds_client.get("/fixtures?sql=select+:name")
response = await ds_client.get("/fixtures/-/query?sql=select+:name")
assert response.status_code == 200
assert (
'<label for="qp1">name</label> <input type="text" id="qp1" name="name" value="">'
in response.text
)
response2 = await ds_client.get("/fixtures?sql=select+:name&name=hello")
response2 = await ds_client.get("/fixtures/-/query?sql=select+:name&name=hello")
assert response2.status_code == 200
assert (
'<label for="qp1">name</label> <input type="text" id="qp1" name="name" value="hello">'
@ -453,7 +458,9 @@ async def test_database_metadata(ds_client):
@pytest.mark.asyncio
async def test_database_metadata_with_custom_sql(ds_client):
response = await ds_client.get("/fixtures?sql=select+*+from+simple_primary_key")
response = await ds_client.get(
"/fixtures/-/query?sql=select+*+from+simple_primary_key"
)
assert response.status_code == 200
soup = Soup(response.text, "html.parser")
# Page title should be the default
@ -591,7 +598,7 @@ async def test_canned_query_with_custom_metadata(ds_client):
@pytest.mark.asyncio
async def test_urlify_custom_queries(ds_client):
path = "/fixtures?" + urllib.parse.urlencode(
path = "/fixtures/-/query?" + urllib.parse.urlencode(
{"sql": "select ('https://twitter.com/' || 'simonw') as user_url;"}
)
response = await ds_client.get(path)
@ -609,7 +616,7 @@ async def test_urlify_custom_queries(ds_client):
@pytest.mark.asyncio
async def test_show_hide_sql_query(ds_client):
path = "/fixtures?" + urllib.parse.urlencode(
path = "/fixtures/-/query?" + urllib.parse.urlencode(
{"sql": "select ('https://twitter.com/' || 'simonw') as user_url;"}
)
response = await ds_client.get(path)
@ -696,15 +703,15 @@ def test_canned_query_show_hide_metadata_option(
@pytest.mark.asyncio
async def test_binary_data_display_in_query(ds_client):
response = await ds_client.get("/fixtures?sql=select+*+from+binary_data")
response = await ds_client.get("/fixtures/-/query?sql=select+*+from+binary_data")
assert response.status_code == 200
table = Soup(response.content, "html.parser").find("table")
expected_tds = [
[
'<td class="col-data"><a class="blob-download" href="/fixtures.blob?sql=select+*+from+binary_data&amp;_blob_column=data&amp;_blob_hash=f3088978da8f9aea479ffc7f631370b968d2e855eeb172bea7f6c7a04262bb6d">&lt;Binary:\xa07\xa0bytes&gt;</a></td>'
'<td class="col-data"><a class="blob-download" href="/fixtures/-/query.blob?sql=select+*+from+binary_data&amp;_blob_column=data&amp;_blob_hash=f3088978da8f9aea479ffc7f631370b968d2e855eeb172bea7f6c7a04262bb6d">&lt;Binary:\xa07\xa0bytes&gt;</a></td>'
],
[
'<td class="col-data"><a class="blob-download" href="/fixtures.blob?sql=select+*+from+binary_data&amp;_blob_column=data&amp;_blob_hash=b835b0483cedb86130b9a2c280880bf5fadc5318ddf8c18d0df5204d40df1724">&lt;Binary:\xa07\xa0bytes&gt;</a></td>'
'<td class="col-data"><a class="blob-download" href="/fixtures/-/query.blob?sql=select+*+from+binary_data&amp;_blob_column=data&amp;_blob_hash=b835b0483cedb86130b9a2c280880bf5fadc5318ddf8c18d0df5204d40df1724">&lt;Binary:\xa07\xa0bytes&gt;</a></td>'
],
['<td class="col-data">\xa0</td>'],
]
@ -719,7 +726,7 @@ async def test_binary_data_display_in_query(ds_client):
[
("/fixtures/binary_data/1.blob?_blob_column=data", "binary_data-1-data.blob"),
(
"/fixtures.blob?sql=select+*+from+binary_data&_blob_column=data&_blob_hash=f3088978da8f9aea479ffc7f631370b968d2e855eeb172bea7f6c7a04262bb6d",
"/fixtures/-/query.blob?sql=select+*+from+binary_data&_blob_column=data&_blob_hash=f3088978da8f9aea479ffc7f631370b968d2e855eeb172bea7f6c7a04262bb6d",
"data-f30889.blob",
),
],
@ -758,7 +765,7 @@ async def test_blob_download_invalid_messages(ds_client, path, expected_message)
@pytest.mark.parametrize(
"path",
[
"/fixtures?sql=select+*+from+[123_starts_with_digits]",
"/fixtures/-/query?sql=select+*+from+[123_starts_with_digits]",
"/fixtures/123_starts_with_digits",
],
)
@ -771,7 +778,7 @@ async def test_zero_results(ds_client, path):
@pytest.mark.asyncio
async def test_query_error(ds_client):
response = await ds_client.get("/fixtures?sql=select+*+from+notatable")
response = await ds_client.get("/fixtures/-/query?sql=select+*+from+notatable")
html = response.text
assert '<p class="message-error">no such table: notatable</p>' in html
assert '<textarea id="sql-editor" name="sql" style="height: 3em' in html
@ -811,7 +818,7 @@ def test_debug_context_includes_extra_template_vars():
"/fixtures/paginated_view",
"/fixtures/facetable",
"/fixtures/facetable?_facet=state",
"/fixtures?sql=select+1",
"/fixtures/-/query?sql=select+1",
],
)
@pytest.mark.parametrize("use_prefix", (True, False))
@ -879,17 +886,17 @@ def test_base_url_affects_metadata_extra_css_urls(app_client_base_url_prefix):
[
(
"/fixtures/neighborhood_search",
"/fixtures?sql=%0Aselect+_neighborhood%2C+facet_cities.name%2C+state%0Afrom+facetable%0A++++join+facet_cities%0A++++++++on+facetable._city_id+%3D+facet_cities.id%0Awhere+_neighborhood+like+%27%25%27+%7C%7C+%3Atext+%7C%7C+%27%25%27%0Aorder+by+_neighborhood%3B%0A&amp;text=",
"/fixtures/-/query?sql=%0Aselect+_neighborhood%2C+facet_cities.name%2C+state%0Afrom+facetable%0A++++join+facet_cities%0A++++++++on+facetable._city_id+%3D+facet_cities.id%0Awhere+_neighborhood+like+%27%25%27+%7C%7C+%3Atext+%7C%7C+%27%25%27%0Aorder+by+_neighborhood%3B%0A&amp;text=",
),
(
"/fixtures/neighborhood_search?text=ber",
"/fixtures?sql=%0Aselect+_neighborhood%2C+facet_cities.name%2C+state%0Afrom+facetable%0A++++join+facet_cities%0A++++++++on+facetable._city_id+%3D+facet_cities.id%0Awhere+_neighborhood+like+%27%25%27+%7C%7C+%3Atext+%7C%7C+%27%25%27%0Aorder+by+_neighborhood%3B%0A&amp;text=ber",
"/fixtures/-/query?sql=%0Aselect+_neighborhood%2C+facet_cities.name%2C+state%0Afrom+facetable%0A++++join+facet_cities%0A++++++++on+facetable._city_id+%3D+facet_cities.id%0Awhere+_neighborhood+like+%27%25%27+%7C%7C+%3Atext+%7C%7C+%27%25%27%0Aorder+by+_neighborhood%3B%0A&amp;text=ber",
),
("/fixtures/pragma_cache_size", None),
(
# /fixtures/𝐜𝐢𝐭𝐢𝐞𝐬
"/fixtures/~F0~9D~90~9C~F0~9D~90~A2~F0~9D~90~AD~F0~9D~90~A2~F0~9D~90~9E~F0~9D~90~AC",
"/fixtures?sql=select+id%2C+name+from+facet_cities+order+by+id+limit+1%3B",
"/fixtures/-/query?sql=select+id%2C+name+from+facet_cities+order+by+id+limit+1%3B",
),
("/fixtures/magic_parameters", None),
],
@ -960,7 +967,7 @@ async def test_navigation_menu_links(
@pytest.mark.asyncio
async def test_trace_correctly_escaped(ds_client):
response = await ds_client.get("/fixtures?sql=select+'<h1>Hello'&_trace=1")
response = await ds_client.get("/fixtures/-/query?sql=select+'<h1>Hello'&_trace=1")
assert "select '<h1>Hello" not in response.text
assert "select &#39;&lt;h1&gt;Hello" in response.text
@ -989,8 +996,8 @@ async def test_trace_correctly_escaped(ds_client):
),
# Custom query page
(
"/fixtures?sql=select+*+from+facetable",
"http://localhost/fixtures.json?sql=select+*+from+facetable",
"/fixtures/-/query?sql=select+*+from+facetable",
"http://localhost/fixtures/-/query.json?sql=select+*+from+facetable",
),
# Canned query page
(

Wyświetl plik

@ -25,15 +25,15 @@ async def test_load_extension_default_entrypoint():
# should fail.
ds = Datasette(sqlite_extensions=[COMPILED_EXTENSION_PATH])
response = await ds.client.get("/_memory.json?_shape=arrays&sql=select+a()")
response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+a()")
assert response.status_code == 200
assert response.json()["rows"][0][0] == "a"
response = await ds.client.get("/_memory.json?_shape=arrays&sql=select+b()")
response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+b()")
assert response.status_code == 400
assert response.json()["error"] == "no such function: b"
response = await ds.client.get("/_memory.json?_shape=arrays&sql=select+c()")
response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+c()")
assert response.status_code == 400
assert response.json()["error"] == "no such function: c"
@ -51,14 +51,14 @@ async def test_load_extension_multiple_entrypoints():
]
)
response = await ds.client.get("/_memory.json?_shape=arrays&sql=select+a()")
response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+a()")
assert response.status_code == 200
assert response.json()["rows"][0][0] == "a"
response = await ds.client.get("/_memory.json?_shape=arrays&sql=select+b()")
response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+b()")
assert response.status_code == 200
assert response.json()["rows"][0][0] == "b"
response = await ds.client.get("/_memory.json?_shape=arrays&sql=select+c()")
response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+c()")
assert response.status_code == 200
assert response.json()["rows"][0][0] == "c"

Wyświetl plik

@ -12,7 +12,7 @@ import pytest
],
)
async def test_add_message_sets_cookie(ds_client, qs, expected):
response = await ds_client.get(f"/fixtures.message?sql=select+1&{qs}")
response = await ds_client.get(f"/fixtures/-/query.message?sql=select+1&{qs}")
signed = response.cookies["ds_messages"]
decoded = ds_client.ds.unsign(signed, "messages")
assert expected == decoded
@ -22,7 +22,7 @@ async def test_add_message_sets_cookie(ds_client, qs, expected):
async def test_messages_are_displayed_and_cleared(ds_client):
# First set the message cookie
set_msg_response = await ds_client.get(
"/fixtures.message?sql=select+1&add_msg=xmessagex"
"/fixtures/-/query.message?sql=select+1&add_msg=xmessagex"
)
# Now access a page that displays messages
response = await ds_client.get("/", cookies=set_msg_response.cookies)

Wyświetl plik

@ -268,7 +268,7 @@ def test_view_query(allow, expected_anon, expected_auth):
def test_execute_sql(config):
schema_re = re.compile("const schema = ({.*?});", re.DOTALL)
with make_app_client(config=config) as client:
form_fragment = '<form class="sql" action="/fixtures"'
form_fragment = '<form class="sql" action="/fixtures/-/query"'
# Anonymous users - should not display the form:
anon_html = client.get("/fixtures").text
@ -276,7 +276,7 @@ def test_execute_sql(config):
# And const schema should be an empty object:
assert "const schema = {};" in anon_html
# This should 403:
assert client.get("/fixtures?sql=select+1").status == 403
assert client.get("/fixtures/-/query?sql=select+1").status == 403
# ?_where= not allowed on tables:
assert client.get("/fixtures/facet_cities?_where=id=3").status == 403
@ -289,7 +289,7 @@ def test_execute_sql(config):
assert set(schema["attraction_characteristic"]) == {"name", "pk"}
assert schema["paginated_view"] == []
assert form_fragment in response_text
query_response = client.get("/fixtures?sql=select+1", cookies=cookies)
query_response = client.get("/fixtures/-/query?sql=select+1", cookies=cookies)
assert query_response.status == 200
schema2 = json.loads(schema_re.search(query_response.text).group(1))
assert set(schema2["attraction_characteristic"]) == {"name", "pk"}
@ -337,7 +337,7 @@ def test_query_list_respects_view_query():
],
),
(
"/fixtures?sql=select+1",
"/fixtures/-/query?sql=select+1",
[
"view-instance",
("view-database", "fixtures"),

Wyświetl plik

@ -45,7 +45,7 @@ def test_plugin_hooks_have_tests(plugin_hook):
@pytest.mark.asyncio
async def test_hook_plugins_dir_plugin_prepare_connection(ds_client):
response = await ds_client.get(
"/fixtures.json?_shape=arrayfirst&sql=select+convert_units(100%2C+'m'%2C+'ft')"
"/fixtures/-/query.json?_shape=arrayfirst&sql=select+convert_units(100%2C+'m'%2C+'ft')"
)
assert response.json()[0] == pytest.approx(328.0839)
@ -53,7 +53,7 @@ async def test_hook_plugins_dir_plugin_prepare_connection(ds_client):
@pytest.mark.asyncio
async def test_hook_plugin_prepare_connection_arguments(ds_client):
response = await ds_client.get(
"/fixtures.json?sql=select+prepare_connection_args()&_shape=arrayfirst"
"/fixtures/-/query.json?sql=select+prepare_connection_args()&_shape=arrayfirst"
)
assert [
"database=fixtures, datasette.plugin_config(\"name-of-plugin\")={'depth': 'root'}"
@ -176,7 +176,7 @@ async def test_hook_render_cell_link_from_json(ds_client):
sql = """
select '{"href": "http://example.com/", "label":"Example"}'
""".strip()
path = "/fixtures?" + urllib.parse.urlencode({"sql": sql})
path = "/fixtures/-/query?" + urllib.parse.urlencode({"sql": sql})
response = await ds_client.get(path)
td = Soup(response.text, "html.parser").find("table").find("tbody").find("td")
a = td.find("a")
@ -205,7 +205,11 @@ async def test_hook_render_cell_demo(ds_client):
@pytest.mark.asyncio
@pytest.mark.parametrize(
"path", ("/fixtures?sql=select+'RENDER_CELL_ASYNC'", "/fixtures/simple_primary_key")
"path",
(
"/fixtures/-/query?sql=select+'RENDER_CELL_ASYNC'",
"/fixtures/simple_primary_key",
),
)
async def test_hook_render_cell_async(ds_client, path):
response = await ds_client.get(path)
@ -423,7 +427,7 @@ def view_names_client(tmp_path_factory):
("/fixtures/units", "table"),
("/fixtures/units/1", "row"),
("/-/versions", "json_data"),
("/fixtures?sql=select+1", "database"),
("/fixtures/-/query?sql=select+1", "database"),
),
)
def test_view_names(view_names_client, path, view_name):
@ -975,13 +979,13 @@ def get_actions_links(html):
@pytest.mark.parametrize(
"path,expected_url",
(
("/fixtures?sql=select+1", "/fixtures?sql=explain+select+1"),
("/fixtures/-/query?sql=select+1", "/fixtures/-/query?sql=explain+select+1"),
(
"/fixtures/pragma_cache_size",
"/fixtures?sql=explain+PRAGMA+cache_size%3B",
"/fixtures/-/query?sql=explain+PRAGMA+cache_size%3B",
),
# Don't attempt to explain an explain
("/fixtures?sql=explain+select+1", None),
("/fixtures/-/query?sql=explain+select+1", None),
),
)
async def test_hook_query_actions(ds_client, path, expected_url):
@ -1475,7 +1479,7 @@ async def test_hook_top_row(ds_client):
async def test_hook_top_query(ds_client):
try:
pm.register(SlotPlugin(), name="SlotPlugin")
response = await ds_client.get("/fixtures?sql=select+1&z=x")
response = await ds_client.get("/fixtures/-/query?sql=select+1&z=x")
assert response.status_code == 200
assert "Xtop_query:fixtures:select 1:x" in response.text
finally:

Wyświetl plik

@ -95,7 +95,7 @@ async def test_db_with_route_databases(ds_with_route):
("/original-name/t", 404),
("/original-name/t/1", 404),
("/custom-route-name", 200),
("/custom-route-name?sql=select+id+from+t", 200),
("/custom-route-name/-/query?sql=select+id+from+t", 200),
("/custom-route-name/t", 200),
("/custom-route-name/t/1", 200),
),

Wyświetl plik

@ -57,7 +57,7 @@ async def test_table_shape_arrays(ds_client):
@pytest.mark.asyncio
async def test_table_shape_arrayfirst(ds_client):
response = await ds_client.get(
"/fixtures.json?"
"/fixtures/-/query.json?"
+ urllib.parse.urlencode(
{
"sql": "select content from simple_primary_key order by id",
@ -699,7 +699,7 @@ async def test_table_through(ds_client):
@pytest.mark.asyncio
async def test_max_returned_rows(ds_client):
response = await ds_client.get(
"/fixtures.json?sql=select+content+from+no_primary_key"
"/fixtures/-/query.json?sql=select+content+from+no_primary_key"
)
data = response.json()
assert data["truncated"]

Wyświetl plik

@ -1199,7 +1199,9 @@ async def test_format_of_binary_links(size, title, length_bytes):
expected = "{}>&lt;Binary:&nbsp;{}&nbsp;bytes&gt;</a>".format(title, length_bytes)
assert expected in response.text
# And test with arbitrary SQL query too
sql_response = await ds.client.get("/{}".format(db_name), params={"sql": sql})
sql_response = await ds.client.get(
"{}/-/query".format(db_name), params={"sql": sql}
)
assert sql_response.status_code == 200
assert expected in sql_response.text