Link to save query from /-/execute-write

This commit is contained in:
Simon Willison 2026-05-26 15:52:16 -07:00
commit d6de8e7520
3 changed files with 67 additions and 1 deletions

View file

@ -119,7 +119,10 @@
{% endif %}
</div>
<p><input type="submit" value="Execute" data-execute-write-submit{% if execute_disabled %} disabled{% endif %}></p>
<p>
<input type="submit" value="Execute" data-execute-write-submit{% if execute_disabled %} disabled{% endif %}>
{% if save_query_base_url %}<a href="{{ save_query_url or save_query_base_url }}" class="save-query" data-save-query-link data-save-query-base-url="{{ save_query_base_url }}"{% if not save_query_url %} hidden{% endif %}>Save this query</a>{% endif %}
</p>
</form>
<script>
@ -140,6 +143,26 @@ window.addEventListener("DOMContentLoaded", () => {
const submitButton = form
? form.querySelector("[data-execute-write-submit]")
: null;
const saveQueryLink = form
? form.querySelector("[data-save-query-link]")
: null;
function updateSaveQueryLink(data) {
if (!saveQueryLink) {
return;
}
const sql = window.editor
? window.editor.state.doc.toString()
: executeWriteSqlInput.value;
if (!sql.trim() || !data.ok || data.execute_disabled) {
saveQueryLink.hidden = true;
return;
}
const url = new URL(saveQueryLink.dataset.saveQueryBaseUrl, window.location.href);
url.searchParams.set("sql", sql);
saveQueryLink.href = url.pathname + url.search + url.hash;
saveQueryLink.hidden = false;
}
window.datasetteSqlParameters.setupSqlParameterRefresh({
form,
@ -150,6 +173,7 @@ window.addEventListener("DOMContentLoaded", () => {
if (submitButton) {
submitButton.disabled = data.execute_disabled;
}
updateSaveQueryLink(data);
},
onError(error) {
window.datasetteSqlAnalysis.renderAnalysis(analysisSection, {
@ -159,6 +183,9 @@ window.addEventListener("DOMContentLoaded", () => {
if (submitButton) {
submitButton.disabled = true;
}
if (saveQueryLink) {
saveQueryLink.hidden = true;
}
},
});
});

View file

@ -1002,6 +1002,26 @@ class ExecuteWriteView(BaseView):
except (QueryValidationError, sqlite3.DatabaseError) as ex:
analysis_error = getattr(ex, "message", str(ex))
allow_save_query = await self.ds.allowed(
action="execute-sql",
resource=DatabaseResource(db.name),
actor=request.actor,
) and await self.ds.allowed(
action="store-query",
resource=DatabaseResource(db.name),
actor=request.actor,
)
save_query_base_url = None
save_query_url = None
if allow_save_query:
save_query_base_url = self.ds.urls.database(db.name) + "/-/queries/store"
if (
sql
and analysis_error is None
and not any(row["allowed"] is False for row in analysis_rows)
):
save_query_url = save_query_base_url + "?" + urlencode({"sql": sql})
response = await self.render(
["execute_write.html"],
request,
@ -1025,6 +1045,8 @@ class ExecuteWriteView(BaseView):
),
"table_columns": table_columns,
"write_template_tables": write_template_tables,
"save_query_url": save_query_url,
"save_query_base_url": save_query_base_url,
},
)
response.status = status

View file

@ -1233,6 +1233,12 @@ async def test_execute_write_get_prepopulates_without_executing():
assert 'data-sql-template="update"' in response.text
assert 'data-sql-template="delete"' in response.text
assert 'data-analyze-url="/data/-/execute-write/analyze"' in response.text
assert 'data-save-query-base-url="/data/-/queries/store"' in response.text
assert "Save this query" in response.text
assert (
"/data/-/queries/store?sql=insert+into+dogs+%28name%29+values+%28%27Cleo%27%29"
in response.text
)
assert 'addEventListener("paste"' in response.text
assert "setupSqlParameterRefresh" in response.text
assert "datasetteSqlAnalysis.renderAnalysis" in response.text
@ -1251,6 +1257,17 @@ async def test_execute_write_get_prepopulates_without_executing():
)
assert '<textarea id="sql-editor" name="sql"></textarea>' in empty_response.text
assert 'executeWriteSqlInput.value = "\\n\\n\\n";' in empty_response.text
assert "hidden>Save this query</a>" in empty_response.text
read_only_response = await ds.client.get(
"/data/-/execute-write?sql=select+*+from+dogs",
actor={"id": "root"},
)
assert (
"Use /-/query for read-only SQL; this endpoint only executes writes"
in read_only_response.text
)
assert "hidden>Save this query</a>" in read_only_response.text
@pytest.mark.asyncio