mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Improvements + docs for db.execute() and Results class
* Including new results.first() and results.single_value() methods. Closes #685
This commit is contained in:
parent
69e3a855dd
commit
4433306c18
8 changed files with 141 additions and 29 deletions
|
|
@ -26,10 +26,9 @@ from .views.index import IndexView
|
|||
from .views.special import JsonDataView, PatternPortfolioView
|
||||
from .views.table import RowView, TableView
|
||||
from .renderer import json_renderer
|
||||
from .database import Database
|
||||
from .database import Database, QueryInterrupted
|
||||
|
||||
from .utils import (
|
||||
QueryInterrupted,
|
||||
escape_css_string,
|
||||
escape_sqlite,
|
||||
format_bytes,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import uuid
|
|||
|
||||
from .tracer import trace
|
||||
from .utils import (
|
||||
QueryInterrupted,
|
||||
Results,
|
||||
detect_fts,
|
||||
detect_primary_keys,
|
||||
detect_spatialite,
|
||||
|
|
@ -371,3 +369,40 @@ class WriteTask:
|
|||
self.fn = fn
|
||||
self.task_id = task_id
|
||||
self.reply_queue = reply_queue
|
||||
|
||||
|
||||
class QueryInterrupted(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MultipleValues(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Results:
|
||||
def __init__(self, rows, truncated, description):
|
||||
self.rows = rows
|
||||
self.truncated = truncated
|
||||
self.description = description
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
return [d[0] for d in self.description]
|
||||
|
||||
def first(self):
|
||||
if self.rows:
|
||||
return self.rows[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def single_value(self):
|
||||
if self.rows and 1 == len(self.rows) and 1 == len(self.rows[0]):
|
||||
return self.rows[0][0]
|
||||
else:
|
||||
raise MultipleValues
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.rows)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.rows)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import json
|
|||
import urllib
|
||||
import re
|
||||
from datasette import hookimpl
|
||||
from datasette.database import QueryInterrupted
|
||||
from datasette.utils import (
|
||||
escape_sqlite,
|
||||
path_with_added_args,
|
||||
path_with_removed_args,
|
||||
detect_json1,
|
||||
QueryInterrupted,
|
||||
InvalidSql,
|
||||
sqlite3,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -47,27 +47,6 @@ ENV SQLITE_EXTENSIONS /usr/lib/x86_64-linux-gnu/mod_spatialite.so
|
|||
"""
|
||||
|
||||
|
||||
class QueryInterrupted(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Results:
|
||||
def __init__(self, rows, truncated, description):
|
||||
self.rows = rows
|
||||
self.truncated = truncated
|
||||
self.description = description
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
return [d[0] for d in self.description]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.rows)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.rows)
|
||||
|
||||
|
||||
def urlsafe_components(token):
|
||||
"Splits token on commas and URL decodes each component"
|
||||
return [urllib.parse.unquote_plus(b) for b in token.split(",")]
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import pint
|
|||
|
||||
from datasette import __version__
|
||||
from datasette.plugins import pm
|
||||
from datasette.database import QueryInterrupted
|
||||
from datasette.utils import (
|
||||
QueryInterrupted,
|
||||
InvalidSql,
|
||||
LimitedWriter,
|
||||
is_url,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import json
|
|||
import jinja2
|
||||
|
||||
from datasette.plugins import pm
|
||||
from datasette.database import QueryInterrupted
|
||||
from datasette.utils import (
|
||||
CustomRow,
|
||||
QueryInterrupted,
|
||||
RequestParameters,
|
||||
append_querystring,
|
||||
compound_keys_after_sql,
|
||||
|
|
|
|||
|
|
@ -94,13 +94,74 @@ Database class
|
|||
|
||||
Instances of the ``Database`` class can be used to execute queries against attached SQLite databases, and to run introspection against their schemas.
|
||||
|
||||
SQLite only allows one database connection to write at a time. Datasette handles this for you by maintaining a queue of writes to be executed against a given database. Plugins can submit write operations to this queue and they will be executed in the order in which they are received.
|
||||
.. _database_execute:
|
||||
|
||||
await db.execute(sql, ...)
|
||||
--------------------------
|
||||
|
||||
Executes a SQL query against the database and returns the resulting rows (see :ref:`database_results`).
|
||||
|
||||
``sql`` - string (required)
|
||||
The SQL query to execute. This can include ``?`` or ``:named`` parameters.
|
||||
|
||||
``params`` - list or dict
|
||||
A list or dictionary of values to use for the parameters. List for ``?``, dictionary for ``:named``.
|
||||
|
||||
``truncate`` - boolean
|
||||
Should the rows returned by the query be truncated at the maximum page size? Defaults to ``True``, set this to ``False`` to disable truncation.
|
||||
|
||||
``custom_time_limit`` - integer ms
|
||||
A custom time limit for this query. This can be set to a lower value than the Datasette configured default. If a query takes longer than this it will be terminated early and raise a ``dataette.database.QueryInterrupted`` exception.
|
||||
|
||||
``page_size`` - integer
|
||||
Set a custom page size for truncation, over-riding the configured Datasette default.
|
||||
|
||||
``log_sql_errors`` - boolean
|
||||
Should any SQL errors be logged to the console in addition to being raised as an error? Defaults to ``True``.
|
||||
|
||||
.. _database_results:
|
||||
|
||||
Results
|
||||
-------
|
||||
|
||||
The ``db.execute()`` method returns a single ``Results`` object. This can be used to access the rows returned by the query.
|
||||
|
||||
Iterating over a ``Results`` object will yield SQLite `Row objects <https://docs.python.org/3/library/sqlite3.html#row-objects>`__. Each of these can be treated as a tuple or can be accessed using ``row["column"]`` syntax:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
info = []
|
||||
results = await db.execute("select name from sqlite_master")
|
||||
for row in results:
|
||||
info.append(row["name"])
|
||||
|
||||
The ``Results`` object also has the following properties and methods:
|
||||
|
||||
``.truncated`` - boolean
|
||||
Indicates if this query was truncated - if it returned more results than the specified ``page_size``. If this is true then the results object will only provide access to the first ``page_size`` rows in the query result. You can disable truncation by passing ``truncate=False`` to the ``db.query()`` method.
|
||||
|
||||
``.columns`` - list of strings
|
||||
A list of column names returned by the query.
|
||||
|
||||
``.rows`` - list of sqlite3.Row
|
||||
This property provides direct access to the list of rows returned by the database. You can access specific rows by index using ``results.rows[0]``.
|
||||
|
||||
``.first()`` - row or None
|
||||
Returns the first row in the results, or ``None`` if no rows were returned.
|
||||
|
||||
``.single_value()``
|
||||
Returns the value of the first column of the first row of results - but only if the query returned a single row with a single column. Raises a ``datasette.database.MultipleValues`` exception otherwise.
|
||||
|
||||
``.__len__()``
|
||||
Calling ``len(results)`` returns the (truncated) number of returned results.
|
||||
|
||||
.. _database_execute_write:
|
||||
|
||||
await db.execute_write(sql, params=None, block=False)
|
||||
-----------------------------------------------------
|
||||
|
||||
SQLite only allows one database connection to write at a time. Datasette handles this for you by maintaining a queue of writes to be executed against a given database. Plugins can submit write operations to this queue and they will be executed in the order in which they are received.
|
||||
|
||||
This method can be used to queue up a non-SELECT SQL query to be executed against a single write connection to the database.
|
||||
|
||||
You can pass additional SQL parameters as a tuple or dictionary.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,47 @@
|
|||
from datasette.database import Results, MultipleValues
|
||||
from datasette.utils import sqlite3
|
||||
from .fixtures import app_client
|
||||
import pytest
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute1(app_client):
|
||||
db = app_client.ds.databases["fixtures"]
|
||||
results = await db.execute("select * from facetable")
|
||||
assert isinstance(results, Results)
|
||||
assert 15 == len(results)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_results_first(app_client):
|
||||
db = app_client.ds.databases["fixtures"]
|
||||
assert None is (await db.execute("select * from facetable where pk > 100")).first()
|
||||
results = await db.execute("select * from facetable")
|
||||
row = results.first()
|
||||
assert isinstance(row, sqlite3.Row)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"query,expected",
|
||||
[
|
||||
("select 1", 1),
|
||||
("select 1, 2", None),
|
||||
("select 1 as num union select 2 as num", None),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_results_single_value(app_client, query, expected):
|
||||
db = app_client.ds.databases["fixtures"]
|
||||
results = await db.execute(query)
|
||||
if expected:
|
||||
assert expected == results.single_value()
|
||||
else:
|
||||
with pytest.raises(MultipleValues):
|
||||
results.single_value()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tables,exists",
|
||||
(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue