Renamed canned queries to queries / stored queries in docs

And a few renames in code and YAML as well.
This commit is contained in:
Simon Willison 2026-05-26 15:17:51 -07:00
commit 02a1468f1b
17 changed files with 115 additions and 605 deletions

View file

@ -57,7 +57,7 @@ jobs:
db.route = "alternative-route"
' > plugins/alternative_route.py
cp fixtures.db fixtures2.db
- name: And the counters writable canned query demo
- name: And the counters writable stored query demo
run: |
cat > plugins/counters.py <<EOF
from datasette import hookimpl
@ -69,23 +69,22 @@ jobs:
await db.execute_write("insert or ignore into counters (name, value) values ('counter_a', 0)")
await db.execute_write("insert or ignore into counters (name, value) values ('counter_b', 0)")
await db.execute_write("insert or ignore into counters (name, value) values ('counter_c', 0)")
return inner
@hookimpl
def canned_queries(database):
if database == "counters":
queries = {}
for name in ("counter_a", "counter_b", "counter_c"):
queries["increment_{}".format(name)] = {
"sql": "update counters set value = value + 1 where name = '{}'".format(name),
"on_success_message_sql": "select 'Counter {name} incremented to ' || value from counters where name = '{name}'".format(name=name),
"write": True,
}
queries["decrement_{}".format(name)] = {
"sql": "update counters set value = value - 1 where name = '{}'".format(name),
"on_success_message_sql": "select 'Counter {name} decremented to ' || value from counters where name = '{name}'".format(name=name),
"write": True,
}
return queries
await datasette.add_query(
"counters",
"increment_{}".format(name),
"update counters set value = value + 1 where name = '{}'".format(name),
on_success_message_sql="select 'Counter {name} incremented to ' || value from counters where name = '{name}'".format(name=name),
is_write=True,
)
await datasette.add_query(
"counters",
"decrement_{}".format(name),
"update counters set value = value - 1 where name = '{}'".format(name),
on_success_message_sql="select 'Counter {name} decremented to ' || value from counters where name = '{name}'".format(name=name),
is_write=True,
)
return inner
EOF
# - name: Make some modifications to metadata.json
# run: |

View file

@ -1784,13 +1784,6 @@ class Datasette:
def app_css_hash(self):
return self.static_hash("app.css")
async def get_canned_queries(self, database_name, actor):
page = await self.list_queries(database_name, actor=actor, limit=1000)
return {query["name"]: query for query in page["queries"]}
async def get_canned_query(self, database_name, query_name, actor):
return await self.get_query(database_name, query_name)
def _prepare_connection(self, conn, database):
conn.row_factory = sqlite3.Row
conn.text_factory = lambda x: str(x, "utf-8", "replace")

View file

@ -83,7 +83,7 @@ class Facet:
self.ds = ds
self.request = request
self.database = database
# For foreign key expansion. Can be None for e.g. canned SQL queries:
# For foreign key expansion. Can be None for e.g. stored SQL queries:
self.table = table
self.sql = sql or f"select * from [{table}]"
self.params = params or []

View file

@ -1409,7 +1409,7 @@ svg.dropdown-menu-icon {
border-bottom: 5px solid #666;
}
.canned-query-edit-sql {
.stored-query-edit-sql {
padding-left: 0.5em;
position: relative;
top: 1px;

View file

@ -17,7 +17,7 @@
{% include "_sql_parameter_styles.html" %}
{% endblock %}
{% block body_class %}query db-{{ database|to_css_class }}{% if canned_query %} query-{{ canned_query|to_css_class }}{% endif %}{% endblock %}
{% block body_class %}query db-{{ database|to_css_class }}{% if stored_query %} query-{{ stored_query|to_css_class }}{% endif %}{% endblock %}
{% block crumbs %}
{{ crumbs.nav(request=request, database=database) }}
@ -25,19 +25,19 @@
{% block content %}
{% if canned_query_write and db_is_immutable %}
{% if stored_query_write and db_is_immutable %}
<p class="message-error">This query cannot be executed because the database is immutable.</p>
{% endif %}
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">{{ metadata.title or database }}{% if canned_query and not metadata.title %}: {{ canned_query }}{% endif %}{% if private %} 🔒{% endif %}</h1>
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color }}">{{ metadata.title or database }}{% if stored_query and not metadata.title %}: {{ stored_query }}{% endif %}{% if private %} 🔒{% endif %}</h1>
{% set action_links, action_title = query_actions(), "Query actions" %}
{% include "_action_menu.html" %}
{% if canned_query %}{{ top_stored_query() }}{% else %}{{ top_query() }}{% endif %}
{% if stored_query %}{{ top_stored_query() }}{% else %}{{ top_query() }}{% endif %}
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
<form class="sql core" action="{{ urls.database(database) }}{% if canned_query %}/{{ canned_query }}{% endif %}" method="{% if canned_query_write %}post{% else %}get{% endif %}" data-parameters-url="{{ urls.database(database) }}/-/query/parameters">
<form class="sql core" action="{{ urls.database(database) }}{% if stored_query %}/{{ stored_query }}{% endif %}" method="{% if stored_query_write %}post{% else %}get{% endif %}" data-parameters-url="{{ urls.database(database) }}/-/query/parameters">
<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 %}{% if not query_error %}
<span class="show-hide-sql">(<a href="{{ show_hide_link }}">{{ show_hide_text }}</a>)</span>
{% endif %}</h3>
@ -52,7 +52,7 @@
<pre id="sql-query">{% if query %}{{ query.sql }}{% endif %}</pre>
{% endif %}
{% else %}
{% if not canned_query %}
{% if not stored_query %}
<input type="hidden" name="sql"
value="{% if query and query.sql %}{{ query.sql }}{% elif tables %}select * from {{ tables[0].name|escape_sqlite }}{% endif %}"
>
@ -64,10 +64,10 @@
{% include "_sql_parameters.html" %}
<p>
{% if not hide_sql %}<button id="sql-format" type="button" hidden>Format SQL</button>{% endif %}
<input type="submit" value="Run SQL"{% if canned_query_write and db_is_immutable %} disabled{% endif %}>
<input type="submit" value="Run SQL"{% if stored_query_write and db_is_immutable %} disabled{% endif %}>
{{ show_hide_hidden }}
{% if save_query_url %}<a href="{{ save_query_url }}" class="save-query">Save this query</a>{% endif %}
{% if canned_query and edit_sql_url %}<a href="{{ edit_sql_url }}" class="canned-query-edit-sql">Edit SQL</a>{% endif %}
{% if stored_query and edit_sql_url %}<a href="{{ edit_sql_url }}" class="stored-query-edit-sql">Edit SQL</a>{% endif %}
</p>
</form>
@ -90,7 +90,7 @@
</tbody>
</table></div>
{% else %}
{% if not canned_query_write and not error %}
{% if not stored_query_write and not error %}
<p class="zero-results">0 results</p>
{% endif %}
{% endif %}

View file

@ -100,12 +100,12 @@ class DatabaseView(View):
limit=5,
include_private=True,
)
canned_queries = queries_page["queries"]
stored_queries = queries_page["queries"]
queries_more = queries_page["has_more"]
queries_count = (
await datasette.count_queries(database, actor=request.actor)
if queries_more
else len(canned_queries)
else len(stored_queries)
)
async def database_actions():
@ -137,7 +137,7 @@ class DatabaseView(View):
"tables": tables,
"hidden_count": len([t for t in tables if t["hidden"]]),
"views": sql_views,
"queries": canned_queries,
"queries": stored_queries,
"queries_more": queries_more,
"queries_count": queries_count,
"allow_execute_sql": allow_execute_sql,
@ -172,7 +172,7 @@ class DatabaseView(View):
tables=tables,
hidden_count=len([t for t in tables if t["hidden"]]),
views=sql_views,
queries=canned_queries,
queries=stored_queries,
queries_more=queries_more,
queries_count=queries_count,
allow_execute_sql=allow_execute_sql,
@ -271,7 +271,7 @@ class QueryContext(Context):
query: dict = field(
metadata={"help": "The SQL query object containing the `sql` string"}
)
canned_query: str = field(
stored_query: str = field(
metadata={"help": "The name of the stored query if this is a stored query"}
)
private: bool = field(
@ -280,7 +280,7 @@ class QueryContext(Context):
# urls: dict = field(
# metadata={"help": "Object containing URL helpers like `database()`"}
# )
canned_query_write: bool = field(
stored_query_write: bool = field(
metadata={
"help": "Boolean indicating if this is a stored query that allows writes"
}
@ -1629,10 +1629,10 @@ class QueryView(View):
await datasette.resolve_table(request)
table_found = True
except TableNotFound as table_not_found:
canned_query = await datasette.get_canned_query(
table_not_found.database_name, table_not_found.table, request.actor
stored_query = await datasette.get_query(
table_not_found.database_name, table_not_found.table
)
if canned_query is None:
if stored_query is None:
raise
if table_found:
# That should not have happened
@ -1640,13 +1640,13 @@ class QueryView(View):
if not await datasette.allowed(
action="view-query",
resource=QueryResource(database=db.name, query=canned_query["name"]),
resource=QueryResource(database=db.name, query=stored_query["name"]),
actor=request.actor,
):
raise Forbidden("You do not have permission to view this query")
await _ensure_stored_query_execution_permissions(
datasette, db, canned_query, request.actor
datasette, db, stored_query, request.actor
)
# If database is immutable, return an error
@ -1674,19 +1674,19 @@ class QueryView(View):
or params.get("_json")
)
params_for_query = MagicParameters(
canned_query["sql"], params, request, datasette
stored_query["sql"], params, request, datasette
)
await params_for_query.execute_params()
ok = None
redirect_url = None
try:
cursor = await db.execute_write(
canned_query["sql"], params_for_query, request=request
stored_query["sql"], params_for_query, request=request
)
# success message can come from on_success_message or on_success_message_sql
message = None
message_type = datasette.INFO
on_success_message_sql = canned_query.get("on_success_message_sql")
on_success_message_sql = stored_query.get("on_success_message_sql")
if on_success_message_sql:
try:
message_result = (
@ -1698,18 +1698,18 @@ class QueryView(View):
message = "Error running on_success_message_sql: {}".format(ex)
message_type = datasette.ERROR
if not message:
message = canned_query.get(
message = stored_query.get(
"on_success_message"
) or "Query executed, {} row{} affected".format(
cursor.rowcount, "" if cursor.rowcount == 1 else "s"
)
redirect_url = canned_query.get("on_success_redirect")
redirect_url = stored_query.get("on_success_redirect")
ok = True
except Exception as ex:
message = canned_query.get("on_error_message") or str(ex)
message = stored_query.get("on_error_message") or str(ex)
message_type = datasette.ERROR
redirect_url = canned_query.get("on_error_redirect")
redirect_url = stored_query.get("on_error_redirect")
ok = False
if should_return_json:
return Response.json(
@ -1743,33 +1743,33 @@ class QueryView(View):
allowed_dict = {r.child: r for r in allowed_tables_page.resources}
# Are we a stored query?
canned_query = None
canned_query_write = False
stored_query = None
stored_query_write = False
if "table" in request.url_vars:
try:
await datasette.resolve_table(request)
except TableNotFound as table_not_found:
# Was this actually a stored query?
canned_query = await datasette.get_canned_query(
table_not_found.database_name, table_not_found.table, request.actor
stored_query = await datasette.get_query(
table_not_found.database_name, table_not_found.table
)
if canned_query is None:
if stored_query is None:
raise
canned_query_write = bool(canned_query.get("write"))
stored_query_write = bool(stored_query.get("write"))
private = False
if canned_query:
if stored_query:
# Respect stored query permissions
visible, private = await datasette.check_visibility(
request.actor,
action="view-query",
resource=QueryResource(database=database, query=canned_query["name"]),
resource=QueryResource(database=database, query=stored_query["name"]),
)
if not visible:
raise Forbidden("You do not have permission to view this query")
if not canned_query_write:
if not stored_query_write:
await _ensure_stored_query_execution_permissions(
datasette, db, canned_query, request.actor
datasette, db, stored_query, request.actor
)
else:
@ -1783,15 +1783,15 @@ class QueryView(View):
params = {key: request.args.get(key) for key in request.args}
sql = None
if canned_query:
sql = canned_query["sql"]
if stored_query:
sql = stored_query["sql"]
elif "sql" in params:
sql = params.pop("sql")
# Extract any :named parameters
named_parameters = []
if canned_query and canned_query.get("params"):
named_parameters = canned_query["params"]
if stored_query and stored_query.get("params"):
named_parameters = stored_query["params"]
if not named_parameters and sql:
named_parameters = derive_named_parameters(sql)
named_parameter_values = {
@ -1817,9 +1817,9 @@ class QueryView(View):
params_for_query = params
if sql and not canned_query_write:
if sql and not stored_query_write:
try:
if not canned_query:
if not stored_query:
# For regular queries we only allow SELECT, plus other rules
validate_sql_select(sql)
else:
@ -1879,7 +1879,7 @@ class QueryView(View):
columns=columns,
rows=rows,
sql=sql,
query_name=canned_query["name"] if canned_query else None,
query_name=stored_query["name"] if stored_query else None,
database=database,
table=None,
request=request,
@ -1911,10 +1911,10 @@ class QueryView(View):
elif format_ == "html":
headers = {}
templates = [f"query-{to_css_class(database)}.html", "query.html"]
if canned_query:
if stored_query:
templates.insert(
0,
f"query-{to_css_class(database)}-{to_css_class(canned_query['name'])}.html",
f"query-{to_css_class(database)}-{to_css_class(stored_query['name'])}.html",
)
environment = datasette.get_jinja_environment(request)
@ -1932,8 +1932,8 @@ class QueryView(View):
}
)
metadata = await datasette.get_database_metadata(database)
if canned_query:
metadata = dict(canned_query)
if stored_query:
metadata = dict(stored_query)
metadata.pop("source", None)
renderers = {}
@ -1968,7 +1968,7 @@ class QueryView(View):
)
show_hide_hidden = ""
if canned_query and canned_query.get("hide_sql"):
if stored_query and stored_query.get("hide_sql"):
if bool(params.get("_show_sql")):
show_hide_link = path_with_removed_args(request, {"_show_sql"})
show_hide_text = "hide"
@ -2018,7 +2018,7 @@ class QueryView(View):
)
save_query_url = None
if (
not canned_query
not stored_query
and allow_execute_sql
and allow_store_query
and is_validated_sql
@ -2036,7 +2036,7 @@ class QueryView(View):
datasette=datasette,
actor=request.actor,
database=database,
query_name=canned_query["name"] if canned_query else None,
query_name=stored_query["name"] if stored_query else None,
request=request,
sql=sql,
params=params,
@ -2056,15 +2056,15 @@ class QueryView(View):
"sql": sql,
"params": params,
},
canned_query=canned_query["name"] if canned_query else None,
stored_query=stored_query["name"] if stored_query else None,
private=private,
canned_query_write=canned_query_write,
stored_query_write=stored_query_write,
db_is_immutable=not db.is_mutable,
error=query_error,
hide_sql=hide_sql,
show_hide_link=datasette.urls.path(show_hide_link),
show_hide_text=show_hide_text,
editable=not canned_query,
editable=not stored_query,
allow_execute_sql=allow_execute_sql,
save_query_url=save_query_url,
tables=await get_tables(datasette, request, db, allowed_dict),
@ -2100,7 +2100,7 @@ class QueryView(View):
datasette,
request,
database=database,
query_name=canned_query["name"] if canned_query else None,
query_name=stored_query["name"] if stored_query else None,
),
query_actions=query_actions,
),

View file

@ -964,11 +964,11 @@ async def table_view_traced(datasette, request):
resolved = await datasette.resolve_table(request)
except TableNotFound as not_found:
# Was this actually a stored query?
canned_query = await datasette.get_canned_query(
not_found.database_name, not_found.table, request.actor
stored_query = await datasette.get_query(
not_found.database_name, not_found.table
)
# If this is a stored query, not a table, then dispatch to QueryView instead
if canned_query:
if stored_query:
return await QueryView()(request, datasette)
else:
raise

View file

@ -121,7 +121,7 @@ This configuration will deny access to everyone except the user with ``id`` of `
How permissions are resolved
----------------------------
Datasette performs permission checks using the internal :ref:`datasette_allowed`, method which accepts keyword arguments for ``action``, ``resource`` and an optional ``actor``.
Datasette performs permission checks using the internal :ref:`datasette_allowed`, method which accepts keyword arguments for ``action``, ``resource`` and an optional ``actor``.
``resource`` should be an instance of the appropriate ``Resource`` subclass from :mod:`datasette.resources`—for example ``InstanceResource()``, ``DatabaseResource(database="...``)`` or ``TableResource(database="...", table="...")``. This defaults to ``InstanceResource()`` if not specified.
@ -468,7 +468,7 @@ You can control the following:
* Access to the entire Datasette instance
* Access to specific databases
* Access to specific tables and views
* Access to specific :ref:`queries <canned_queries>`
* Access to specific :ref:`queries <queries>`
If a user has permission to view a table they will be able to view that table, independent of if they have permission to view the database or instance that the table exists within.
@ -496,7 +496,7 @@ Here's how to restrict access to your entire Datasette instance to just the ``"i
title: My private Datasette instance
allow:
id: root
.. tab:: datasette.json
@ -644,7 +644,7 @@ This works for SQL views as well - you can list their names in the ``"tables"``
Access to specific queries
--------------------------
:ref:`Queries <canned_queries>` allow you to configure named SQL queries in your ``datasette.yaml`` that can be executed by users. These queries can be set up to both read and write to the database, so controlling who can execute them can be important.
:ref:`Queries <queries>` allow you to configure named SQL queries in your ``datasette.yaml`` that can be executed by users. These queries can be set up to both read and write to the database, so controlling who can execute them can be important.
To limit access to the ``add_name`` query in your ``dogs.db`` database to just the :ref:`root user<authentication_root>`:
@ -1020,7 +1020,7 @@ You can also restrict permissions such that they can only be used within specifi
The resulting token will only be able to insert rows, and only to tables in the ``mydatabase`` database.
Finally, you can restrict permissions to individual resources - tables, SQL views and :ref:`named queries <canned_queries>` - within a specific database::
Finally, you can restrict permissions to individual resources - tables, SQL views and :ref:`named queries <queries>` - within a specific database::
datasette create-token root --resource mydatabase mytable insert-row

View file

@ -11,7 +11,8 @@ Unreleased
- Fixed a bug where visiting ``/<database>/-/query`` without a ``?sql=`` parameter returned a 500 error. (:issue:`2743`)
- The ``top_canned_query()`` plugin hook has been renamed to :ref:`top_stored_query() <plugin_hook_top_stored_query>`. (:issue:`2747`)
- The ``canned_queries()`` plugin hook has been removed. Plugins can use the new ``datasette.add_query()``, ``datasette.update_query()`` and ``datasette.remove_query()`` methods to managed stored queries instead.
- The ``canned_queries()`` plugin hook has been removed. Plugins can use the new ``datasette.add_query()``, ``datasette.update_query()`` and ``datasette.remove_query()`` methods to manage stored queries instead.
- The ``datasette.get_canned_query()`` and ``datasette.get_canned_queries()`` methods have been removed. Plugins can use ``datasette.get_query()`` and ``datasette.list_queries()`` instead.
.. _v1_0_a30:
@ -658,7 +659,7 @@ For more information and workarounds, read `the security advisory <https://githu
Also in this alpha:
- The new ``datasette plugins --requirements`` option outputs a list of currently installed plugins in Python ``requirements.txt`` format, useful for duplicating that installation elsewhere. (:issue:`2133`)
- :ref:`canned_queries_writable` can now define a ``on_success_message_sql`` field in their configuration, containing a SQL query that should be executed upon successful completion of the write operation in order to generate a message to be shown to the user. (:issue:`2138`)
- :ref:`queries_writable` can now define a ``on_success_message_sql`` field in their configuration, containing a SQL query that should be executed upon successful completion of the write operation in order to generate a message to be shown to the user. (:issue:`2138`)
- The automatically generated border color for a database is now shown in more places around the application. (:issue:`2119`)
- Every instance of example shell script code in the documentation should now include a working copy button, free from additional syntax. (:issue:`2140`)
@ -1052,7 +1053,7 @@ Other small fixes
- The ``base.html`` template now wraps everything other than the ``<footer>`` in a ``<div class="not-footer">`` element, to help with advanced CSS customization. (:issue:`1446`)
- The :ref:`render_cell() <plugin_hook_render_cell>` plugin hook can now return an awaitable function. This means the hook can execute SQL queries. (:issue:`1425`)
- :ref:`plugin_register_routes` plugin hook now accepts an optional ``datasette`` argument. (:issue:`1404`)
- New ``hide_sql`` canned query option for defaulting to hiding the SQL query used by a canned query, see :ref:`canned_queries_options`. (:issue:`1422`)
- New ``hide_sql`` canned query option for defaulting to hiding the SQL query used by a canned query, see :ref:`queries_options`. (:issue:`1422`)
- New ``--cpu`` option for :ref:`datasette publish cloudrun <publish_cloud_run>`. (:issue:`1420`)
- If `Rich <https://github.com/willmcgugan/rich>`__ is installed in the same virtual environment as Datasette, it will be used to provide enhanced display of error tracebacks on the console. (:issue:`1416`)
- ``datasette.utils`` :ref:`internals_utils_parse_metadata` function, used by the new `datasette-remote-metadata plugin <https://datasette.io/plugins/datasette-remote-metadata>`__, is now a documented API. (:issue:`1405`)
@ -1426,7 +1427,7 @@ See also `Datasette 0.50: The annotated release notes <https://simonwillison.net
See also `Datasette 0.49: The annotated release notes <https://simonwillison.net/2020/Sep/15/datasette-0-49/>`__.
- Writable canned queries now expose a JSON API, see :ref:`canned_queries_json_api`. (:issue:`880`)
- Writable canned queries now expose a JSON API, see :ref:`queries_json_api`. (:issue:`880`)
- New mechanism for defining page templates with custom path parameters - a template file called ``pages/about/{slug}.html`` will be used to render any requests to ``/about/something``. See :ref:`custom_pages_parameters`. (:issue:`944`)
- ``register_output_renderer()`` render functions can now return a ``Response``. (:issue:`953`)
- New ``--upgrade`` option for ``datasette install``. (:issue:`945`)
@ -1518,7 +1519,7 @@ Magic parameters for canned queries, a log out feature, improved plugin document
Magic parameters for canned queries
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Canned queries now support :ref:`canned_queries_magic_parameters`, which can be used to insert or select automatically generated values. For example::
Canned queries now support :ref:`queries_magic_parameters`, which can be used to insert or select automatically generated values. For example::
insert into logs
(user_id, timestamp)
@ -1549,7 +1550,7 @@ New plugin hooks
- :ref:`plugin_hook_register_magic_parameters` can be used to define new types of magic canned query parameters.
- :ref:`plugin_hook_startup` can run custom code when Datasette first starts up. `datasette-init <https://github.com/simonw/datasette-init>`__ is a new plugin that uses this hook to create database tables and views on startup if they have not yet been created. (:issue:`834`)
- :ref:`plugin_hook_canned_queries` lets plugins provide additional canned queries beyond those defined in Datasette's metadata. See `datasette-saved-queries <https://github.com/simonw/datasette-saved-queries>`__ for an example of this hook in action. (:issue:`852`)
- ``canned_queries()`` lets plugins provide additional canned queries beyond those defined in Datasette's metadata. See `datasette-saved-queries <https://github.com/simonw/datasette-saved-queries>`__ for an example of this hook in action. (:issue:`852`)
- :ref:`plugin_hook_forbidden` is a hook for customizing how Datasette responds to 403 forbidden errors. (:issue:`812`)
Smaller changes
@ -1624,7 +1625,7 @@ A new debug page at ``/-/permissions`` shows recent permission checks, to help a
Writable canned queries
~~~~~~~~~~~~~~~~~~~~~~~
Datasette's :ref:`canned_queries` feature lets you define SQL queries in ``metadata.json`` which can then be executed by users visiting a specific URL. https://latest.datasette.io/fixtures/neighborhood_search for example.
Datasette's :ref:`queries` feature lets you define SQL queries in ``metadata.json`` which can then be executed by users visiting a specific URL. https://latest.datasette.io/fixtures/neighborhood_search for example.
Canned queries were previously restricted to ``SELECT``, but Datasette 0.44 introduces the ability for canned queries to execute ``INSERT`` or ``UPDATE`` queries as well, using the new ``"write": true`` property (:issue:`800`):
@ -1643,7 +1644,7 @@ Canned queries were previously restricted to ``SELECT``, but Datasette 0.44 intr
}
}
See :ref:`canned_queries_writable` for more details.
See :ref:`queries_writable` for more details.
Flash messages
~~~~~~~~~~~~~~
@ -1698,7 +1699,7 @@ Smaller changes
- New ``request.cookies`` property.
- ``/-/plugins`` endpoint now shows a list of hooks implemented by each plugin, e.g. https://latest.datasette.io/-/plugins?all=1
- ``request.post_vars()`` method no longer discards empty values.
- New "params" canned query key for explicitly setting named parameters, see :ref:`canned_queries_named_parameters`. (:issue:`797`)
- New "params" canned query key for explicitly setting named parameters, see :ref:`queries_named_parameters`. (:issue:`797`)
- ``request.args`` is now a :ref:`MultiParams <internals_multiparams>` object.
- Fixed a bug with the ``datasette plugins`` command. (:issue:`802`)
- Nicer pattern for using ``make_app_client()`` in tests. (:issue:`395`)
@ -1732,7 +1733,7 @@ The main focus of this release is a major upgrade to the :ref:`plugin_register_o
* Visually distinguish float and integer columns - useful for figuring out why order-by-column might be returning unexpected results. (:issue:`729`)
* The :ref:`internals_request`, which is passed to several plugin hooks, is now documented. (:issue:`706`)
* New ``metadata.json`` option for setting a custom default page size for specific tables and views, see :ref:`table_configuration_size`. (:issue:`751`)
* Canned queries can now be configured with a default URL fragment hash, useful when working with plugins such as `datasette-vega <https://github.com/simonw/datasette-vega>`__, see :ref:`canned_queries_options`. (:issue:`706`)
* Canned queries can now be configured with a default URL fragment hash, useful when working with plugins such as `datasette-vega <https://github.com/simonw/datasette-vega>`__, see :ref:`queries_options`. (:issue:`706`)
* Fixed a bug in ``datasette publish`` when running on operating systems where the ``/tmp`` directory lives in a different volume, using a backport of the Python 3.8 ``shutil.copytree()`` function. (:issue:`744`)
* Every plugin hook is now covered by the unit tests, and a new unit test checks that each plugin hook has at least one corresponding test. (:issue:`771`, :issue:`773`)
@ -2249,7 +2250,7 @@ A number of small new features:
- Documentation for :ref:`datasette publish and datasette package <publishing>`, closes `#337 <https://github.com/simonw/datasette/issues/337>`_
- Fixed compatibility with Python 3.7
- ``datasette publish heroku`` now supports app names via the ``-n`` option, which can also be used to overwrite an existing application [Russ Garrett]
- Title and description metadata can now be set for :ref:`canned SQL queries <canned_queries>`, closes `#342 <https://github.com/simonw/datasette/issues/342>`_
- Title and description metadata can now be set for :ref:`canned SQL queries <queries>`, closes `#342 <https://github.com/simonw/datasette/issues/342>`_
- New ``force_https_on`` config option, fixes ``https://`` API URLs when deploying to Zeit Now - closes `#333 <https://github.com/simonw/datasette/issues/333>`_
- ``?_json_infinity=1`` query string argument for handling Infinity/-Infinity values in JSON, closes `#332 <https://github.com/simonw/datasette/issues/332>`_
- URLs displayed in the results of custom SQL queries are now URLified, closes `#298 <https://github.com/simonw/datasette/issues/298>`_

View file

@ -434,12 +434,12 @@ Here is a simple example:
:ref:`authentication_permissions_config` has the full details.
.. _configuration_reference_canned_queries:
.. _configuration_reference_queries:
Queries configuration
~~~~~~~~~~~~~~~~~~~~~
:ref:`Queries <canned_queries>` are named SQL queries that appear in the Datasette interface. They can be configured in ``datasette.yaml`` using the ``queries`` key at the database level:
:ref:`Queries <queries>` are named SQL queries that appear in the Datasette interface. They can be configured in ``datasette.yaml`` using the ``queries`` key at the database level:
.. [[[cog
from metadata_doc import config_example, config_example
@ -484,7 +484,7 @@ Queries configuration
}
.. [[[end]]]
See the :ref:`queries documentation <canned_queries>` for more, including how to configure :ref:`writable queries <canned_queries_writable>`.
See the :ref:`queries documentation <queries>` for more, including how to configure :ref:`writable queries <queries_writable>`.
.. _configuration_reference_css_js:

View file

@ -1207,16 +1207,6 @@ Potential use-cases:
Examples: `datasette-saved-queries <https://datasette.io/plugins/datasette-saved-queries>`__, `datasette-init <https://datasette.io/plugins/datasette-init>`__
.. _plugin_hook_canned_queries:
canned_queries(datasette, database, actor)
------------------------------------------
This hook has been removed. Plugins that need to add stored queries should use
the :ref:`plugin_hook_startup` hook and call ``await datasette.add_query(...)``.
Example: `datasette-saved-queries <https://datasette.io/plugins/datasette-saved-queries>`__
.. _plugin_hook_actor_from_request:
actor_from_request(datasette, request)
@ -1635,7 +1625,7 @@ register_magic_parameters(datasette)
``datasette`` - :ref:`internals_datasette`
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``.
:ref:`canned_queries_magic_parameters` can be used to add automatic parameters to :ref:`configured queries <canned_queries>`. This plugin hook allows additional magic parameters to be defined by plugins.
:ref:`queries_magic_parameters` can be used to add automatic parameters to :ref:`configured queries <queries>`. This plugin hook allows additional magic parameters to be defined by plugins.
Magic parameters all take this format: ``_prefix_rest_of_parameter``. The prefix indicates which magic parameter function should be called - the rest of the parameter is passed as an argument to that function.

View file

@ -30,7 +30,7 @@ Warning
The following steps are recommended:
- Disable arbitrary SQL queries by untrusted users. See :ref:`authentication_permissions_execute_sql` for ways to do this. The easiest is to start Datasette with the ``datasette --setting default_allow_sql off`` option.
- Define :ref:`queries <canned_queries>` with the SQL queries that use SpatiaLite functions that you want people to be able to execute.
- Define :ref:`queries <queries>` with the SQL queries that use SpatiaLite functions that you want people to be able to execute.
The `Datasette SpatiaLite tutorial <https://datasette.io/tutorials/spatialite>`__ includes detailed instructions for running SpatiaLite safely using these techniques

View file

@ -66,7 +66,7 @@ You can also use the `sqlite-utils <https://sqlite-utils.datasette.io/>`__ tool
sqlite-utils create-view sf-trees.db demo_view "select qSpecies from Street_Tree_List"
.. _canned_queries:
.. _queries:
Queries
-------
@ -173,7 +173,7 @@ You can opt out of this behavior for a configured query using ``is_trusted: fals
sql: select * from report
is_trusted: false
.. _canned_queries_named_parameters:
.. _queries_named_parameters:
Query parameters
~~~~~~~~~~~~~~~~
@ -313,7 +313,7 @@ You can alternatively provide an explicit list of named parameters using the ``"
}
.. [[[end]]]
.. _canned_queries_options:
.. _queries_options:
Additional query options
~~~~~~~~~~~~~~~~~~~~~~~~
@ -389,7 +389,7 @@ This example demonstrates both ``fragment`` and ``hide_sql``:
`See here <https://latest.datasette.io/fixtures#queries>`__ for a demo of this in action.
.. _canned_queries_writable:
.. _queries_writable:
Writable queries
~~~~~~~~~~~~~~~~
@ -524,7 +524,7 @@ You can pre-populate form fields when the page first loads using a query string,
If you specify a query in ``"on_success_message_sql"``, that query will be executed after the main query. The first column of the first row return by that query will be displayed as a success message. Named parameters from the main query will be made available to the success message query as well.
.. _canned_queries_magic_parameters:
.. _queries_magic_parameters:
Magic parameters
~~~~~~~~~~~~~~~~
@ -621,7 +621,7 @@ The form presented at ``/mydatabase/add_message`` will have just a field for ``m
Additional custom magic parameters can be added by plugins using the :ref:`plugin_hook_register_magic_parameters` hook.
.. _canned_queries_json_api:
.. _queries_json_api:
JSON API for writable queries
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -244,7 +244,7 @@ except (KeyError, TypeError):
New code:
```python
try:
query_info = await datasette.get_canned_query(database, query_name, request.actor)
query_info = await datasette.get_query(database, query_name)
if query_info and "title" in query_info:
title = query_info["title"]
except (KeyError, TypeError):
@ -253,7 +253,7 @@ except (KeyError, TypeError):
### Update render functions to async
If your plugin's render function needs to call `datasette.get_canned_query()` or other async Datasette methods, it must be declared as async:
If your plugin's render function needs to call `datasette.get_query()` or other async Datasette methods, it must be declared as async:
Old code:
```python
@ -268,7 +268,7 @@ New code:
async def render_atom(datasette, request, sql, columns, rows, database, table, query_name, view_name, data):
# ...
if query_name:
query_info = await datasette.get_canned_query(database, query_name, request.actor)
query_info = await datasette.get_query(database, query_name)
if query_info and "title" in query_info:
title = query_info["title"]
```

View file

@ -1,473 +0,0 @@
from bs4 import BeautifulSoup as Soup
from asgiref.sync import async_to_sync
import json
import pytest
import re
from .fixtures import make_app_client
def update_query(client, name, **kwargs):
async_to_sync(client.ds.invoke_startup)()
async_to_sync(client.ds.update_query)("data", name, **kwargs)
@pytest.fixture
def canned_write_client(tmpdir):
template_dir = tmpdir / "canned_write_templates"
template_dir.mkdir()
(template_dir / "query-data-update_name.html").write_text(
"""
{% extends "query.html" %}
{% block content %}!!!CUSTOM_UPDATE_NAME_TEMPLATE!!!{{ super() }}{% endblock %}
""",
"utf-8",
)
with make_app_client(
extra_databases={"data.db": "create table names (name text)"},
template_dir=str(template_dir),
config={
"databases": {
"data": {
"queries": {
"canned_read": {"sql": "select * from names"},
"add_name": {
"sql": "insert into names (name) values (:name)",
"write": True,
"on_success_redirect": "/data/add_name?success",
},
"add_name_specify_id": {
"sql": "insert into names (rowid, name) values (:rowid, :name)",
"on_success_message_sql": "select 'Name added: ' || :name || ' with rowid ' || :rowid",
"write": True,
"on_error_redirect": "/data/add_name_specify_id?error",
},
"add_name_specify_id_with_error_in_on_success_message_sql": {
"sql": "insert into names (rowid, name) values (:rowid, :name)",
"on_success_message_sql": "select this is bad SQL",
"write": True,
},
"delete_name": {
"sql": "delete from names where rowid = :rowid",
"write": True,
"on_success_message": "Name deleted",
"allow": {"id": "root"},
},
"update_name": {
"sql": "update names set name = :name where rowid = :rowid",
"params": ["rowid", "name", "extra"],
"write": True,
},
}
}
}
},
) as client:
yield client
@pytest.fixture
def canned_write_immutable_client():
with make_app_client(
is_immutable=True,
config={
"databases": {
"fixtures": {
"queries": {
"add": {
"sql": "insert into sortable (text) values (:text)",
"write": True,
},
}
}
}
},
) as client:
yield client
@pytest.mark.asyncio
async def test_canned_query_with_named_parameter(ds_client):
response = await ds_client.get(
"/fixtures/neighborhood_search.json?text=town&_shape=arrays"
)
assert response.json()["rows"] == [
["Corktown", "Detroit", "MI"],
["Downtown", "Los Angeles", "CA"],
["Downtown", "Detroit", "MI"],
["Greektown", "Detroit", "MI"],
["Koreatown", "Los Angeles", "CA"],
["Mexicantown", "Detroit", "MI"],
]
def test_insert(canned_write_client):
response = canned_write_client.post(
"/data/add_name",
{"name": "Hello"},
csrftoken_from=True,
cookies={"foo": "bar"},
)
messages = canned_write_client.ds.unsign(
response.cookies["ds_messages"], "messages"
)
assert messages == [["Query executed, 1 row affected", 1]]
assert response.status == 302
assert response.headers["Location"] == "/data/add_name?success"
def test_insert_blocked_cross_site(canned_write_client):
# A cross-site POST (browser-originated) must be blocked
response = canned_write_client.post(
"/data/add_name",
{"name": "Hello"},
headers={"sec-fetch-site": "cross-site"},
)
assert 403 == response.status
def test_insert_no_cookies_no_csrf(canned_write_client):
response = canned_write_client.post("/data/add_name", {"name": "Hello"})
assert 302 == response.status
assert "/data/add_name?success" == response.headers["Location"]
def test_custom_success_message(canned_write_client):
response = canned_write_client.post(
"/data/delete_name",
{"rowid": 1},
cookies={"ds_actor": canned_write_client.actor_cookie({"id": "root"})},
csrftoken_from=True,
)
assert 302 == response.status
messages = canned_write_client.ds.unsign(
response.cookies["ds_messages"], "messages"
)
assert [["Name deleted", 1]] == messages
def test_insert_error(canned_write_client):
canned_write_client.post("/data/add_name", {"name": "Hello"}, csrftoken_from=True)
response = canned_write_client.post(
"/data/add_name_specify_id",
{"rowid": 1, "name": "Should fail"},
csrftoken_from=True,
)
assert 302 == response.status
assert "/data/add_name_specify_id?error" == response.headers["Location"]
messages = canned_write_client.ds.unsign(
response.cookies["ds_messages"], "messages"
)
assert [["UNIQUE constraint failed: names.rowid", 3]] == messages
# How about with a custom error message?
update_query(canned_write_client, "add_name_specify_id", on_error_message="ERROR")
response = canned_write_client.post(
"/data/add_name_specify_id",
{"rowid": 1, "name": "Should fail"},
csrftoken_from=True,
)
assert [["ERROR", 3]] == canned_write_client.ds.unsign(
response.cookies["ds_messages"], "messages"
)
def test_on_success_message_sql(canned_write_client):
response = canned_write_client.post(
"/data/add_name_specify_id",
{"rowid": 5, "name": "Should be OK"},
csrftoken_from=True,
)
assert response.status == 302
assert response.headers["Location"] == "/data/add_name_specify_id"
messages = canned_write_client.ds.unsign(
response.cookies["ds_messages"], "messages"
)
assert messages == [["Name added: Should be OK with rowid 5", 1]]
def test_error_in_on_success_message_sql(canned_write_client):
response = canned_write_client.post(
"/data/add_name_specify_id_with_error_in_on_success_message_sql",
{"rowid": 1, "name": "Should fail"},
csrftoken_from=True,
)
messages = canned_write_client.ds.unsign(
response.cookies["ds_messages"], "messages"
)
assert messages == [
["Error running on_success_message_sql: no such column: bad", 3]
]
def test_custom_params(canned_write_client):
response = canned_write_client.get("/data/update_name?extra=foo")
assert (
'<input type="text" id="qp3" name="extra" value="foo" data-parameter-control>'
in response.text
)
def test_canned_query_pages_no_vary_header(canned_write_client):
# These pages no longer embed per-cookie CSRF tokens, so they must not
# set Vary: Cookie - they should be cacheable across users.
assert "vary" not in canned_write_client.get("/data").headers
assert "vary" not in canned_write_client.get("/data/update_name").headers
def test_json_post_body(canned_write_client):
response = canned_write_client.post(
"/data/add_name",
body=json.dumps({"name": ["Hello", "there"]}),
)
assert 302 == response.status
assert "/data/add_name?success" == response.headers["Location"]
rows = canned_write_client.get("/data/names.json?_shape=array").json
assert rows == [{"rowid": 1, "name": "['Hello', 'there']"}]
@pytest.mark.parametrize(
"headers,body,querystring",
(
(None, "name=NameGoesHere", "?_json=1"),
({"Accept": "application/json"}, "name=NameGoesHere", None),
(None, "name=NameGoesHere&_json=1", None),
(None, '{"name": "NameGoesHere", "_json": 1}', None),
),
)
def test_json_response(canned_write_client, headers, body, querystring):
response = canned_write_client.post(
"/data/add_name" + (querystring or ""),
body=body,
headers=headers,
)
assert 200 == response.status
assert response.headers["content-type"] == "application/json; charset=utf-8"
assert response.json == {
"ok": True,
"message": "Query executed, 1 row affected",
"redirect": "/data/add_name?success",
}
rows = canned_write_client.get("/data/names.json?_shape=array").json
assert rows == [{"rowid": 1, "name": "NameGoesHere"}]
def test_canned_query_permissions_on_database_page(canned_write_client):
# Without auth shows the five public queries
anon_response = canned_write_client.get("/data.json")
query_names = {q["name"] for q in anon_response.json["queries"]}
assert query_names == {
"add_name_specify_id_with_error_in_on_success_message_sql",
"update_name",
"add_name_specify_id",
"canned_read",
"add_name",
}
assert anon_response.json["queries_more"] is False
# With auth the database page preview shows the first five queries
response = canned_write_client.get(
"/data.json",
cookies={"ds_actor": canned_write_client.actor_cookie({"id": "root"})},
)
assert response.status == 200
query_names_and_private = sorted(
[
{"name": q["name"], "private": q["private"]}
for q in response.json["queries"]
],
key=lambda q: q["name"],
)
assert query_names_and_private == [
{"name": "add_name", "private": False},
{"name": "add_name_specify_id", "private": False},
{
"name": "add_name_specify_id_with_error_in_on_success_message_sql",
"private": False,
},
{"name": "canned_read", "private": False},
{"name": "delete_name", "private": True},
]
assert response.json["queries_more"] is True
# The full query list endpoint includes the remaining query
response = canned_write_client.get(
"/data/-/queries.json?_size=10",
cookies={"ds_actor": canned_write_client.actor_cookie({"id": "root"})},
)
assert response.status == 200
query_names_and_private = sorted(
[
{"name": q["name"], "private": q["private"]}
for q in response.json["queries"]
],
key=lambda q: q["name"],
)
assert query_names_and_private == [
{"name": "add_name", "private": False},
{"name": "add_name_specify_id", "private": False},
{
"name": "add_name_specify_id_with_error_in_on_success_message_sql",
"private": False,
},
{"name": "canned_read", "private": False},
{"name": "delete_name", "private": True},
{"name": "update_name", "private": False},
]
def test_canned_query_permissions(canned_write_client):
assert 403 == canned_write_client.get("/data/delete_name").status
assert 200 == canned_write_client.get("/data/update_name").status
cookies = {"ds_actor": canned_write_client.actor_cookie({"id": "root"})}
assert 200 == canned_write_client.get("/data/delete_name", cookies=cookies).status
assert 200 == canned_write_client.get("/data/update_name", cookies=cookies).status
@pytest.fixture(scope="session")
def magic_parameters_client():
with make_app_client(
extra_databases={"data.db": "create table logs (line text)"},
config={
"databases": {
"data": {
"queries": {
"runme_post": {"sql": "", "write": True},
"runme_get": {"sql": ""},
}
}
}
},
) as client:
yield client
@pytest.mark.parametrize(
"magic_parameter,expected_re",
[
("_actor_id", "root"),
("_header_host", "localhost"),
("_header_not_a_thing", ""),
("_cookie_foo", "bar"),
("_now_epoch", r"^\d+$"),
("_now_date_utc", r"^\d{4}-\d{2}-\d{2}$"),
("_now_datetime_utc", r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$"),
("_random_chars_1", r"^\w$"),
("_random_chars_10", r"^\w{10}$"),
],
)
def test_magic_parameters(magic_parameters_client, magic_parameter, expected_re):
update_query(
magic_parameters_client,
"runme_post",
sql=f"insert into logs (line) values (:{magic_parameter})",
)
update_query(
magic_parameters_client,
"runme_get",
sql=f"select :{magic_parameter} as result",
)
cookies = {
"ds_actor": magic_parameters_client.actor_cookie({"id": "root"}),
"foo": "bar",
}
# Test the GET version
get_response = magic_parameters_client.get(
"/data/runme_get.json?_shape=array", cookies=cookies
)
get_actual = get_response.json[0]["result"]
assert re.match(expected_re, str(get_actual))
# Test the form
form_response = magic_parameters_client.get("/data/runme_post")
soup = Soup(form_response.body, "html.parser")
# The magic parameter should not be represented as a form field
assert None is soup.find("input", {"name": magic_parameter})
# Submit the form to create a log line
response = magic_parameters_client.post(
"/data/runme_post?_json=1", {}, csrftoken_from=True, cookies=cookies
)
assert response.json == {
"ok": True,
"message": "Query executed, 1 row affected",
"redirect": None,
}
post_actual = magic_parameters_client.get(
"/data/logs.json?_sort_desc=rowid&_shape=array"
).json[0]["line"]
assert re.match(expected_re, post_actual)
@pytest.mark.parametrize("use_csrf", [True, False])
@pytest.mark.parametrize("return_json", [True, False])
def test_magic_parameters_csrf_json(magic_parameters_client, use_csrf, return_json):
update_query(
magic_parameters_client,
"runme_post",
sql="insert into logs (line) values (:_header_host)",
)
qs = ""
if return_json:
qs = "?_json=1"
response = magic_parameters_client.post(
f"/data/runme_post{qs}",
{},
csrftoken_from=use_csrf or None,
)
if return_json:
assert response.status == 200
assert response.json["ok"], response.json
else:
assert response.status == 302
messages = magic_parameters_client.ds.unsign(
response.cookies["ds_messages"], "messages"
)
assert [["Query executed, 1 row affected", 1]] == messages
post_actual = magic_parameters_client.get(
"/data/logs.json?_sort_desc=rowid&_shape=array"
).json[0]["line"]
assert post_actual == "localhost"
def test_magic_parameters_cannot_be_used_in_arbitrary_queries(magic_parameters_client):
response = magic_parameters_client.get(
"/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")
def test_canned_write_custom_template(canned_write_client):
response = canned_write_client.get("/data/update_name")
assert response.status == 200
assert "!!!CUSTOM_UPDATE_NAME_TEMPLATE!!!" in response.text
assert (
"<!-- Templates considered: *query-data-update_name.html, query-data.html, query.html -->"
in response.text
)
# And test for link rel=alternate while we're here:
assert (
'<link rel="alternate" type="application/json+datasette" href="http://localhost/data/update_name.json">'
in response.text
)
assert (
response.headers["link"]
== '<http://localhost/data/update_name.json>; rel="alternate"; type="application/json+datasette"'
)
def test_canned_write_query_disabled_for_immutable_database(
canned_write_immutable_client,
):
response = canned_write_immutable_client.get("/fixtures/add")
assert response.status == 200
assert (
"This query cannot be executed because the database is immutable."
in response.text
)
assert '<input type="submit" value="Run SQL" disabled>' in response.text
# Submitting form should get a forbidden error
response = canned_write_immutable_client.post(
"/fixtures/add",
{"text": "text"},
csrftoken_from=True,
)
assert response.status == 403
assert "Database is immutable" in response.text

View file

@ -633,7 +633,7 @@ async def test_404_content_type(ds_client):
@pytest.mark.asyncio
async def test_canned_query_default_title(ds_client):
async def test_stored_query_default_title(ds_client):
response = await ds_client.get("/fixtures/magic_parameters")
assert response.status_code == 200
soup = Soup(response.content, "html.parser")
@ -641,7 +641,7 @@ async def test_canned_query_default_title(ds_client):
@pytest.mark.asyncio
async def test_canned_query_with_custom_metadata(ds_client):
async def test_stored_query_with_custom_metadata(ds_client):
response = await ds_client.get("/fixtures/neighborhood_search?text=town")
assert response.status_code == 200
soup = Soup(response.content, "html.parser")
@ -700,7 +700,7 @@ async def test_show_hide_sql_query(ds_client):
@pytest.mark.asyncio
async def test_canned_query_with_hide_has_no_hidden_sql(ds_client):
async def test_stored_query_with_hide_has_no_hidden_sql(ds_client):
# For a stored query the show/hide should NOT have a hidden SQL field
# https://github.com/simonw/datasette/issues/1411
response = await ds_client.get("/fixtures/pragma_cache_size?_hide_sql=1")
@ -720,7 +720,7 @@ async def test_canned_query_with_hide_has_no_hidden_sql(ds_client):
(True, "?_show_sql=1", "_show_sql", "/_memory/one", "hide"),
),
)
def test_canned_query_show_hide_metadata_option(
def test_stored_query_show_hide_metadata_option(
hide_sql,
querystring,
expected_hidden,
@ -981,10 +981,10 @@ def test_base_url_affects_metadata_extra_css_urls(app_client_base_url_prefix):
("/fixtures/magic_parameters", None),
],
)
async def test_edit_sql_link_on_canned_queries(ds_client, path, expected):
async def test_edit_sql_link_on_stored_queries(ds_client, path, expected):
response = await ds_client.get(path)
assert response.status_code == 200
expected_link = f'<a href="{expected}" class="canned-query-edit-sql">Edit SQL</a>'
expected_link = f'<a href="{expected}" class="stored-query-edit-sql">Edit SQL</a>'
if expected:
assert expected_link in response.text
else:

View file

@ -76,7 +76,7 @@ async def ds_for_jump():
@pytest.mark.asyncio
async def test_jump_searches_tables_databases_views_and_canned_queries(ds_for_jump):
async def test_jump_searches_tables_databases_views_and_stored_queries(ds_for_jump):
response = await ds_for_jump.client.get(
"/-/jump.json?q=content", actor={"id": "user"}
)
@ -98,7 +98,7 @@ async def test_jump_searches_tables_databases_views_and_canned_queries(ds_for_ju
@pytest.mark.asyncio
async def test_jump_uses_canned_query_names_not_titles(ds_for_jump):
async def test_jump_uses_stored_query_names_not_titles(ds_for_jump):
response = await ds_for_jump.client.get(
"/-/jump.json?q=datasette", actor={"id": "user"}
)