mirror of
https://github.com/simonw/datasette.git
synced 2026-09-10 02:24:15 +02:00
Pass defaultTable to the SQL editor from table-scoped pages
The table page's 'View and edit SQL' link now carries ?_table=<name>; QueryView validates it against the actor-visible tables/views for the database before exposing it as default_table, so the editor completes that table's columns unprefixed. Stored/canned queries are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
49f1660dcd
commit
cc1a24fb4f
5 changed files with 69 additions and 1 deletions
|
|
@ -15,6 +15,9 @@
|
|||
if (sqlInput) {
|
||||
var editor = (window.editor = cm.editorFromTextArea(sqlInput, {
|
||||
schema,
|
||||
{% if default_table is defined and default_table %}
|
||||
defaultTable: {{ default_table|tojson }},
|
||||
{% endif %}
|
||||
}));
|
||||
if (sqlFormat) {
|
||||
sqlFormat.addEventListener("click", (ev) => {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% if query.sql and allow_execute_sql %}
|
||||
<p><a class="not-underlined" title="{{ query.sql }}" href="{{ urls.database(database) }}?{{ {'sql': query.sql}|urlencode|safe }}{% if query.params %}&{{ query.params|urlencode|safe }}{% endif %}">✎ <span class="underlined">View and edit SQL</span></a></p>
|
||||
<p><a class="not-underlined" title="{{ query.sql }}" href="{{ urls.database(database) }}?{{ {'sql': query.sql, '_table': table}|urlencode|safe }}{% if query.params %}&{{ query.params|urlencode|safe }}{% endif %}">✎ <span class="underlined">View and edit SQL</span></a></p>
|
||||
{% endif %}
|
||||
|
||||
<p class="export-links">This data as {% for name, url in renderers.items() %}<a href="{{ url }}">{{ name }}</a>{{ ", " if not loop.last }}{% endfor %}{% if display_rows %}, <a href="{{ url_csv }}">CSV</a> (<a href="#export">advanced</a>){% endif %}</p>
|
||||
|
|
|
|||
|
|
@ -457,6 +457,11 @@ class QueryContext(Context):
|
|||
"help": "Dictionary mapping table names to lists of column names, used to power SQL autocomplete."
|
||||
}
|
||||
)
|
||||
default_table: str = field(
|
||||
metadata={
|
||||
"help": "Name of the focal table for this query, if any - set when the query page was reached from a table-scoped context (such as the table page's 'View and edit SQL' link) so the SQL editor can complete that table's columns unprefixed. ``None`` otherwise, including for stored/canned queries."
|
||||
}
|
||||
)
|
||||
alternate_url_json: str = field(
|
||||
metadata={"help": "URL for alternate JSON version of this page"}
|
||||
)
|
||||
|
|
@ -716,6 +721,15 @@ class QueryView(View):
|
|||
# Create lookup dict for quick access
|
||||
allowed_dict = {r.child: r for r in allowed_tables_page.resources}
|
||||
|
||||
# If the request carries a ?_table= pointing at a real (visible) table
|
||||
# or view in this database, treat this as a table-scoped query - e.g.
|
||||
# arriving here via the "View and edit SQL" link on a table page - so
|
||||
# the SQL editor can offer that table's columns unprefixed. Anything
|
||||
# else (including stored/canned queries, which may reference more
|
||||
# than one table) leaves this as None.
|
||||
requested_table = request.args.get("_table")
|
||||
default_table = requested_table if requested_table in allowed_dict else None
|
||||
|
||||
# Are we a stored query?
|
||||
stored_query = None
|
||||
stored_query_write = False
|
||||
|
|
@ -1101,6 +1115,7 @@ class QueryView(View):
|
|||
if allow_execute_sql
|
||||
else {}
|
||||
),
|
||||
default_table=default_table,
|
||||
columns=columns,
|
||||
renderers=renderers,
|
||||
url_csv=datasette.urls.path(
|
||||
|
|
|
|||
|
|
@ -168,6 +168,9 @@ The page for arbitrary SQL queries (/database/-/query?sql=...) and stored querie
|
|||
``db_is_immutable`` - ``bool``
|
||||
Boolean indicating if this database is immutable
|
||||
|
||||
``default_table`` - ``str``
|
||||
Name of the focal table for this query, if any - set when the query page was reached from a table-scoped context (such as the table page's 'View and edit SQL' link) so the SQL editor can complete that table's columns unprefixed. ``None`` otherwise, including for stored/canned queries.
|
||||
|
||||
``display_rows`` - ``list``
|
||||
List of result rows formatted for HTML display. Each row is a list of rendered cell values in the same order as ``columns``.
|
||||
|
||||
|
|
|
|||
|
|
@ -284,6 +284,53 @@ async def test_query_page_with_no_sql(ds_client):
|
|||
assert 'class="rows-and-columns"' not in response.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_table_page_view_and_edit_sql_link_carries_table(ds_client):
|
||||
# The table page's "View and edit SQL" link should point at the query
|
||||
# page with a &_table= param identifying the focal table, so the SQL
|
||||
# editor can offer that table's columns unprefixed.
|
||||
response = await ds_client.get("/fixtures/facetable")
|
||||
assert response.status_code == 200
|
||||
soup = Soup(response.content, "html.parser")
|
||||
link = soup.find("span", string="View and edit SQL").find_parent("a")
|
||||
assert link is not None
|
||||
assert "_table=facetable" in link["href"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_page_default_table_from_table_scoped_link(ds_client):
|
||||
# Following the table page's edit-SQL link should result in a query page
|
||||
# whose SQL editor is initialized with defaultTable set to that table.
|
||||
table_response = await ds_client.get("/fixtures/facetable")
|
||||
soup = Soup(table_response.content, "html.parser")
|
||||
href = soup.find("span", string="View and edit SQL").find_parent("a")["href"]
|
||||
response = await ds_client.get(href, follow_redirects=True)
|
||||
assert response.status_code == 200
|
||||
assert 'defaultTable: "facetable"' in response.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_page_no_default_table_without_table_scope(ds_client):
|
||||
# The plain database query page (no focal table) should not set
|
||||
# defaultTable at all.
|
||||
response = await ds_client.get("/fixtures/-/query?sql=select+1")
|
||||
assert response.status_code == 200
|
||||
assert "defaultTable" not in response.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_page_ignores_invalid_table_param(ds_client):
|
||||
# A ?_table= value that isn't a real table/view in this database should
|
||||
# not be reflected back into the page - and should not break execution
|
||||
# of the query itself (leading-underscore params are not treated as SQL
|
||||
# bind parameters unless they appear as :name in the SQL).
|
||||
response = await ds_client.get(
|
||||
"/fixtures/-/query?sql=select+1&_table=not_a_real_table"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert "defaultTable" not in response.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_csv_with_no_sql_is_400(ds_client):
|
||||
# https://github.com/simonw/datasette/issues/2743
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue