Skip to content
Permalink
Browse files

?_where= parameter on table views, closes #429

From pull request #430
  • Loading branch information...
simonw committed Apr 13, 2019
1 parent e11cb4c commit bc6a9b45646610f362b4287bc4110440991aa4d6
Showing with 92 additions and 2 deletions.
  1. +5 −0 datasette/static/app.css
  2. +11 −0 datasette/templates/table.html
  3. +15 −0 datasette/views/table.py
  4. +15 −0 docs/json_api.rst
  5. +32 −2 tests/test_api.py
  6. +14 −0 tests/test_html.py
@@ -105,6 +105,11 @@ h2 em {
font-style: normal;
font-weight: lighter;
}
.extra-wheres ul, .extra-wheres li {
list-style-type: none;
padding: 0;
margin: 0;
}
form.sql textarea {
border: 1px solid #ccc;
width: 70%;
@@ -91,6 +91,17 @@ <h3>{% if filtered_table_rows_count or filtered_table_rows_count == 0 %}{{ "{:,}
</div>
</form>

{% if extra_wheres_for_ui %}
<div class="extra-wheres">
<h3>{{ extra_wheres_for_ui|length }} extra where clause{% if extra_wheres_for_ui|length != 1 %}s{% endif %}</h3>
<ul>
{% for extra_where in extra_wheres_for_ui %}
<li><code>{{ extra_where.text }}</code> [<a href="{{ extra_where.remove_url }}">remove</a>]</li>
{% endfor %}
</ul>
</div>
{% endif %}

{% if query.sql and config.allow_sql %}
<p><a class="not-underlined" title="{{ query.sql }}" href="{{ database_url(database) }}?{{ {'sql': query.sql}|urlencode|safe }}{% if query.params %}&amp;{{ query.params|urlencode|safe }}{% endif %}">&#x270e; <span class="underlined">View and edit SQL</span></a></p>
{% endif %}
@@ -295,6 +295,20 @@ class TableView(RowTableShared):
filters = Filters(sorted(other_args.items()), units, ureg)
where_clauses, params = filters.build_where_clauses(table)

extra_wheres_for_ui = []
# Add _where= from querystring
if "_where" in request.args:
if not self.ds.config("allow_sql"):
raise DatasetteError("_where= is not allowed", status=400)
else:
where_clauses.extend(request.args["_where"])
extra_wheres_for_ui = [{
"text": text,
"remove_url": path_with_removed_args(
request, {"_where": text}
)
} for text in request.args["_where"]]

# _search support:
fts_table = special_args.get("_fts_table")
fts_table = fts_table or table_metadata.get("fts_table")
@@ -751,6 +765,7 @@ class TableView(RowTableShared):
key=lambda f: (len(f["results"]), f["name"]),
reverse=True
),
"extra_wheres_for_ui": extra_wheres_for_ui,
"form_hidden_args": form_hidden_args,
"facet_hideable": lambda facet: facet not in metadata_facets,
"is_sortable": any(c["sortable"] for c in display_columns),
@@ -206,6 +206,21 @@ The Datasette table view takes a number of special querystring arguments:
Like ``_search=`` but allows you to specify the column to be searched, as
opposed to searching all columns that have been indexed by FTS.

``?_where=SQL-fragment``
If the :ref:`config_allow_sql` config option is enabled, this parameter
can be used to pass one or more additional SQL fragments to be used in the
`WHERE` clause of the SQL used to query the table.

This is particularly useful if you are building a JavaScript application
that needs to do something creative but still wants the other conveniences
provided by the table view (such as faceting) and hence would like not to
have to construct a completely custom SQL query.

Some examples:

* `facetable?_where=state="MI"&_where=city_id=3 <https://latest.datasette.io/facetable?_where=state=%22MI%22&_where=city_id=3>`__
* `facetable?_where=city_id in (select id from facet_cities where name != "Detroit") <https://latest.datasette.io/fixtures/facetable?_where=city_id%20in%20(select%20id%20from%20facet_cities%20where%20name%20!=%20%22Detroit%22)>`__

``?_group_count=COLUMN``
Executes a SQL query that returns a count of the number of rows matching
each unique value in that column, with the most common ordered first.
@@ -443,9 +443,11 @@ def test_allow_sql_off():
for client in make_app_client(config={
'allow_sql': False,
}):
assert 400 == client.get(
response = client.get(
"/fixtures.json?sql=select+sleep(0.01)"
).status
)
assert 400 == response.status
assert 'sql= is not allowed' == response.json['error']


def test_table_json(app_client):
@@ -913,6 +915,34 @@ def test_table_filter_json_arraycontains(app_client):
] == response.json['rows']


def test_table_filter_extra_where(app_client):
response = app_client.get(
"/fixtures/facetable.json?_where=neighborhood='Dogpatch'"
)
assert [
[2, 1, 1, 'CA', 1, 'Dogpatch', '["tag1", "tag3"]']
] == response.json['rows']


def test_table_filter_extra_where_invalid(app_client):
response = app_client.get(
"/fixtures/facetable.json?_where=neighborhood=Dogpatch'"
)
assert 400 == response.status
assert 'Invalid SQL' == response.json['title']


def test_table_filter_extra_where_disabled_if_no_sql_allowed():
for client in make_app_client(config={
'allow_sql': False,
}):
response = client.get(
"/fixtures/facetable.json?_where=neighborhood='Dogpatch'"
)
assert 400 == response.status
assert '_where= is not allowed' == response.json['error']


def test_max_returned_rows(app_client):
response = app_client.get(
'/fixtures.json?sql=select+content+from+no_primary_key'
@@ -869,3 +869,17 @@ def test_show_hide_sql_query(app_client):
] == [
(hidden['name'], hidden['value']) for hidden in hiddens
]


def test_extra_where_clauses(app_client):
response = app_client.get(
"/fixtures/facetable?_where=neighborhood='Dogpatch'&_where=city_id=1"
)
soup = Soup(response.body, "html.parser")
div = soup.select(".extra-wheres")[0]
assert "2 extra where clauses" == div.find("h3").text
hrefs = [a["href"] for a in div.findAll("a")]
assert [
"/fixtures/facetable?_where=city_id%3D1",
"/fixtures/facetable?_where=neighborhood%3D%27Dogpatch%27"
] == hrefs

0 comments on commit bc6a9b4

Please sign in to comment.
You can’t perform that action at this time.