mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-07-23 09:24:31 +02:00
db.query() now executes its SQL immediately
Previously query() was a generator function, so nothing - including the SQL itself - ran until the result was first iterated. A write statement passed to query() silently did nothing, and SQL errors surfaced far from the call site. The SQL now executes as soon as query() is called, while rows are still fetched lazily during iteration. Statements that return no rows now raise a ValueError directing callers to execute() instead. As a side effect, statements like INSERT ... RETURNING now work naturally with query(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
This commit is contained in:
parent
862944bc38
commit
45af93d463
3 changed files with 49 additions and 2 deletions
|
|
@ -219,6 +219,10 @@ The ``db.query(sql)`` function executes a SQL query and returns an iterator over
|
|||
# {'name': 'Cleo'}
|
||||
# {'name': 'Pancakes'}
|
||||
|
||||
The SQL query is executed as soon as ``db.query()`` is called. The resulting rows are fetched lazily as you iterate, so large result sets are not loaded into memory all at once. Because execution is immediate, an error in your SQL will raise an exception straight away, and a statement such as ``INSERT ... RETURNING`` will take effect even if you do not iterate over its results.
|
||||
|
||||
``db.query()`` can only be used with SQL that returns rows. Passing a statement that returns no rows - an ``INSERT`` or ``UPDATE`` without a ``RETURNING`` clause, for example - will raise a ``ValueError``. Use :ref:`db.execute() <python_api_execute>` for those statements instead.
|
||||
|
||||
.. _python_api_execute:
|
||||
|
||||
db.execute(sql, params)
|
||||
|
|
|
|||
|
|
@ -593,14 +593,28 @@ class Database:
|
|||
"""
|
||||
Execute ``sql`` and return an iterable of dictionaries representing each row.
|
||||
|
||||
The SQL is executed as soon as this method is called - the resulting rows
|
||||
are then fetched lazily as the returned iterable is iterated over.
|
||||
|
||||
:param sql: SQL query to execute
|
||||
:param params: Parameters to use in that query - an iterable for ``where id = ?``
|
||||
parameters, or a dictionary for ``where id = :id``
|
||||
:raises ValueError: if the SQL statement does not return rows - use
|
||||
:meth:`execute` for those statements instead
|
||||
"""
|
||||
cursor = self.execute(sql, params or tuple())
|
||||
if cursor.description is None:
|
||||
raise ValueError(
|
||||
"query() can only be used with SQL that returns rows - "
|
||||
"use execute() for other statements"
|
||||
)
|
||||
keys = [d[0] for d in cursor.description]
|
||||
for row in cursor:
|
||||
yield dict(zip(keys, row))
|
||||
|
||||
def rows() -> Generator[dict, None, None]:
|
||||
for row in cursor:
|
||||
yield dict(zip(keys, row))
|
||||
|
||||
return rows()
|
||||
|
||||
def execute(
|
||||
self, sql: str, parameters: Optional[Union[Sequence, Dict[str, Any]]] = None
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import pytest
|
||||
import sqlite3
|
||||
import types
|
||||
|
||||
|
||||
|
|
@ -8,6 +10,33 @@ def test_query(fresh_db):
|
|||
assert list(results) == [{"name": "Pancakes"}, {"name": "Cleo"}]
|
||||
|
||||
|
||||
def test_query_executes_eagerly(fresh_db):
|
||||
# The SQL runs when query() is called, not when the result is iterated,
|
||||
# so errors are raised at the call site
|
||||
with pytest.raises(sqlite3.OperationalError):
|
||||
fresh_db.query("select * from missing_table")
|
||||
|
||||
|
||||
def test_query_rejects_statements_that_return_no_rows(fresh_db):
|
||||
fresh_db["dogs"].insert({"name": "Cleo"})
|
||||
with pytest.raises(ValueError) as ex:
|
||||
fresh_db.query("update dogs set name = 'Cleopaws'")
|
||||
assert "execute()" in str(ex.value)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sqlite3.sqlite_version_info < (3, 35, 0),
|
||||
reason="RETURNING requires SQLite 3.35.0 or higher",
|
||||
)
|
||||
def test_query_insert_returning(fresh_db):
|
||||
fresh_db["dogs"].insert({"name": "Cleo"})
|
||||
rows = list(
|
||||
fresh_db.query("insert into dogs (name) values ('Pancakes') returning name")
|
||||
)
|
||||
assert rows == [{"name": "Pancakes"}]
|
||||
assert fresh_db["dogs"].count == 2
|
||||
|
||||
|
||||
def test_execute_returning_dicts(fresh_db):
|
||||
# Like db.query() but returns a list, included for backwards compatibility
|
||||
# see https://github.com/simonw/sqlite-utils/issues/290
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue