diff --git a/datasette/templates/_sql_parameter_scripts.html b/datasette/templates/_sql_parameter_scripts.html
index 159a141c44..ec095cdf42 100644
--- a/datasette/templates/_sql_parameter_scripts.html
+++ b/datasette/templates/_sql_parameter_scripts.html
@@ -22,21 +22,28 @@
};
}
+ function parameterName(control) {
+ return control.dataset.parameterName || control.name;
+ }
+
function syncParameterState(manager) {
manager.parameterState = new Map();
manager.section
.querySelectorAll("[data-parameter-control]")
.forEach((control) => {
- manager.parameterState.set(control.name, controlState(control));
+ manager.parameterState.set(parameterName(control), controlState(control));
});
}
- function createControl(parameter, id, state) {
+ function createControl(parameter, id, state, namePrefix) {
const control = document.createElement(state.expanded ? "textarea" : "input");
control.id = id;
- control.name = parameter;
+ control.name = `${namePrefix || ""}${parameter}`;
control.value = state.value;
control.setAttribute("data-parameter-control", "");
+ if (namePrefix) {
+ control.dataset.parameterName = parameter;
+ }
if (state.expanded) {
control.rows = 5;
} else {
@@ -53,10 +60,16 @@
value,
selectionStart
) {
- const replacement = createControl(control.name, control.id, {
- value: value === undefined ? control.value : value,
- expanded: expand,
- });
+ const parameter = parameterName(control);
+ const replacement = createControl(
+ parameter,
+ control.id,
+ {
+ value: value === undefined ? control.value : value,
+ expanded: expand,
+ },
+ manager.namePrefix
+ );
button.textContent = expand ? "Collapse" : "Expand";
button.setAttribute("aria-expanded", expand ? "true" : "false");
control.replaceWith(replacement);
@@ -64,7 +77,7 @@
if (selectionStart !== undefined && replacement.setSelectionRange) {
replacement.setSelectionRange(selectionStart, selectionStart);
}
- manager.parameterState.set(replacement.name, controlState(replacement));
+ manager.parameterState.set(parameter, controlState(replacement));
}
function renderParameters(manager, parameters) {
@@ -99,7 +112,7 @@
label.htmlFor = id;
label.textContent = parameter;
- const control = createControl(parameter, id, state);
+ const control = createControl(parameter, id, state, manager.namePrefix);
row.append(label, control);
if (manager.allowExpand) {
@@ -124,7 +137,7 @@
if (!control.matches || !control.matches("[data-parameter-control]")) {
return;
}
- manager.parameterState.set(control.name, controlState(control));
+ manager.parameterState.set(parameterName(control), controlState(control));
});
if (!manager.allowExpand) {
@@ -230,6 +243,7 @@
? section.dataset.allowExpand === "1"
: false
: options.allowExpand,
+ namePrefix: section ? section.dataset.parameterNamePrefix || "" : "",
parameterState: new Map(),
};
if (section) {
diff --git a/datasette/templates/_sql_parameters.html b/datasette/templates/_sql_parameters.html
index 58801d4016..62cea3db96 100644
--- a/datasette/templates/_sql_parameters.html
+++ b/datasette/templates/_sql_parameters.html
@@ -1,9 +1,10 @@
-
+{% set sql_parameter_name_prefix = sql_parameter_name_prefix|default("") %}
+
{% if parameter_names %}
Parameters
{% for parameter in parameter_names %}
{% set parameter_id = (sql_parameter_id_prefix|default("qp")) ~ loop.index %}
-
{% if sql_parameters_allow_expand|default(false) %} {% endif %}
+
{% if sql_parameters_allow_expand|default(false) %} {% endif %}
{% endfor %}
{% endif %}
diff --git a/datasette/views/execute_write.py b/datasette/views/execute_write.py
index c5d55b80df..2817f56e49 100644
--- a/datasette/views/execute_write.py
+++ b/datasette/views/execute_write.py
@@ -9,6 +9,7 @@
from .database import display_rows as display_query_rows
from .query_helpers import (
QueryValidationError,
+ SQL_PARAMETER_FORM_PREFIX,
_analysis_is_write,
_analysis_rows,
_analysis_rows_with_permissions,
@@ -295,6 +296,7 @@ async def _render_form(
"execute_write_columns": execute_write_columns,
"execute_write_display_rows": execute_write_display_rows,
"execute_write_truncated": execute_write_truncated,
+ "sql_parameter_name_prefix": SQL_PARAMETER_FORM_PREFIX,
"execute_disabled": bool(execute_disabled_reason),
"execute_disabled_reason": execute_disabled_reason,
"table_columns": table_columns,
diff --git a/datasette/views/query_helpers.py b/datasette/views/query_helpers.py
index f30a30bc1a..a12b71efef 100644
--- a/datasette/views/query_helpers.py
+++ b/datasette/views/query_helpers.py
@@ -49,6 +49,8 @@
"on_error_redirect",
}
+SQL_PARAMETER_FORM_PREFIX = "_sql_param_"
+
class QueryValidationError(Exception):
def __init__(self, message, status=400, *, flash=False):
@@ -289,11 +291,13 @@ def _coerce_execute_write_payload(data, is_json):
)
params = data.get("params") or {}
else:
- params = {
- key: value
- for key, value in data.items()
- if key not in {"sql", "csrftoken", "_json"}
- }
+ params = {}
+ for key, value in data.items():
+ if key in {"sql", "csrftoken", "_json"}:
+ continue
+ if key.startswith(SQL_PARAMETER_FORM_PREFIX):
+ key = key[len(SQL_PARAMETER_FORM_PREFIX) :]
+ params[key] = value
if not isinstance(params, dict):
raise QueryValidationError("params must be a dictionary")
return data.get("sql"), params
diff --git a/tests/test_api_write.py b/tests/test_api_write.py
index 64f91701d5..b7ceb6b28b 100644
--- a/tests/test_api_write.py
+++ b/tests/test_api_write.py
@@ -794,6 +794,44 @@ async def test_update_row_alter(ds_write):
assert response.json() == {"ok": True}
+@pytest.mark.asyncio
+async def test_execute_write_form_parameter_called_sql():
+ ds = Datasette(memory=True, default_deny=True)
+ ds.root_enabled = True
+ db = ds.add_memory_database("execute_write_parameter_sql", name="data")
+ await db.execute_write("create table docs (id integer primary key, title text)")
+ await db.execute_write("insert into docs (id, title) values (1, 'Initial')")
+ await ds.invoke_startup()
+
+ form_response = await ds.client.get(
+ "/data/-/execute-write",
+ actor={"id": "root"},
+ params={"sql": "update docs set title = :sql where id = :id"},
+ )
+ assert form_response.status_code == 200
+ assert 'data-parameter-name-prefix="_sql_param_"' in form_response.text
+ assert '
' in form_response.text
+ assert 'name="_sql_param_sql"' in form_response.text
+ assert 'data-parameter-name="sql"' in form_response.text
+ assert 'name="_sql_param_id"' in form_response.text
+
+ response = await ds.client.post(
+ "/data/-/execute-write",
+ actor={"id": "root"},
+ data={
+ "sql": "update docs set title = :sql where id = :id",
+ "_sql_param_sql": "Updated",
+ "_sql_param_id": "1",
+ },
+ )
+
+ assert response.status_code == 200
+ assert "Query executed, 1 row affected" in response.text
+ assert (await db.execute("select title from docs where id = 1")).first()[
+ 0
+ ] == "Updated"
+
+
@pytest.mark.asyncio
@pytest.mark.parametrize(
"input,expected_errors",