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",