Skip to content

Commit 1bde68a

Browse files
committed
Implement one() and one_or_none()
1 parent d742b8e commit 1bde68a

File tree

8 files changed

+204
-5
lines changed

8 files changed

+204
-5
lines changed

docs/engine.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,9 +384,11 @@ Executing Queries
384384
-----------------
385385

386386
Once you have a :class:`~gino.engine.GinoConnection` instance, you can start
387-
executing queries with it. There are 4 variants of the execute method:
387+
executing queries with it. There are 6 variants of the execute method:
388388
:meth:`~gino.engine.GinoConnection.all`,
389389
:meth:`~gino.engine.GinoConnection.first`,
390+
:meth:`~gino.engine.GinoConnection.one`,
391+
:meth:`~gino.engine.GinoConnection.one_or_none`,
390392
:meth:`~gino.engine.GinoConnection.scalar` and
391393
:meth:`~gino.engine.GinoConnection.status`. They are basically the same:
392394
accepting the same parameters, calling the same underlying methods. The
@@ -399,6 +401,11 @@ difference is how they treat the results:
399401
or ``None`` if there is no result at all. There is usually some optimization
400402
behind the scene to efficiently get only the first result, instead of loading
401403
the full result set into memory.
404+
* :meth:`~gino.engine.GinoConnection.one` returns exactly one result. If there
405+
is no result at all or if there are multiple results, an exception is raised.
406+
* :meth:`~gino.engine.GinoConnection.one_or_none` is similar to
407+
:meth:`~gino.engine.GinoConnection.one`, but it returns ``None`` if there is
408+
no result instead or raising an exception.
402409
* :meth:`~gino.engine.GinoConnection.scalar` is similar to
403410
:meth:`~gino.engine.GinoConnection.first`, it returns the first value of the
404411
first result. Quite convenient to just retrieve a scalar value from database,

docs/schema.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ but return a bit differently. There are also other similar query APIs:
7979
:class:`~sqlalchemy.engine.RowProxy`
8080
* :meth:`~gino.engine.GinoConnection.first` returns one
8181
:class:`~sqlalchemy.engine.RowProxy`, or ``None``
82+
* :meth:`~gino.engine.GinoConnection.one` returns one
83+
:class:`~sqlalchemy.engine.RowProxy`
84+
* :meth:`~gino.engine.GinoConnection.one_or_none` returns one
85+
:class:`~sqlalchemy.engine.RowProxy`, or ``None``
8286
* :meth:`~gino.engine.GinoConnection.scalar` returns a single value, or
8387
``None``
8488
* :meth:`~gino.engine.GinoConnection.iterate` returns an asynchronous iterator

gino/api.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ class GinoExecutor:
2525
await User.query.gino.first()
2626
2727
This allows GINO to add the asynchronous query APIs (:meth:`all`,
28-
:meth:`first`, :meth:`scalar`, :meth:`status`, :meth:`iterate`) to
29-
SQLAlchemy query clauses without messing up with existing synchronous ones.
28+
:meth:`first`, :meth:`one`, :meth:`one_or_none`, :meth:`scalar`,
29+
:meth:`status`, :meth:`iterate`) to SQLAlchemy query clauses without
30+
messing up with existing synchronous ones.
3031
Calling these asynchronous query APIs has the same restriction - the
3132
relevant metadata (the :class:`Gino` instance) must be bound to an engine,
3233
or an :exc:`AttributeError` will be raised.
@@ -135,6 +136,28 @@ async def first(self, *multiparams, **params):
135136
return await self._query.bind.first(self._query, *multiparams,
136137
**params)
137138

139+
async def one_or_none(self, *multiparams, **params):
140+
"""
141+
Returns :meth:`engine.one_or_none() <.engine.GinoEngine.one_or_none>`
142+
with this query as the first argument, and other arguments followed,
143+
where ``engine`` is the :class:`~.engine.GinoEngine` to which the
144+
metadata (:class:`Gino`) is bound, while metadata is found in this
145+
query.
146+
147+
"""
148+
return await self._query.bind.one_or_none(self._query, *multiparams,
149+
**params)
150+
151+
async def one(self, *multiparams, **params):
152+
"""
153+
Returns :meth:`engine.one() <.engine.GinoEngine.one>` with this query
154+
as the first argument, and other arguments followed, where ``engine``
155+
is the :class:`~.engine.GinoEngine` to which the metadata
156+
(:class:`Gino`) is bound, while metadata is found in this query.
157+
158+
"""
159+
return await self._query.bind.one(self._query, *multiparams, **params)
160+
138161
async def scalar(self, *multiparams, **params):
139162
"""
140163
Returns :meth:`engine.scalar() <.engine.GinoEngine.scalar>` with this
@@ -451,6 +474,21 @@ async def first(self, clause, *multiparams, **params):
451474
"""
452475
return await self.bind.first(clause, *multiparams, **params)
453476

477+
async def one_or_none(self, clause, *multiparams, **params):
478+
"""
479+
A delegate of :meth:`GinoEngine.one_or_none()
480+
<.engine.GinoEngine.one_or_none>`.
481+
482+
"""
483+
return await self.bind.one_or_none(clause, *multiparams, **params)
484+
485+
async def one(self, clause, *multiparams, **params):
486+
"""
487+
A delegate of :meth:`GinoEngine.one() <.engine.GinoEngine.first>`.
488+
489+
"""
490+
return await self.bind.one(clause, *multiparams, **params)
491+
454492
async def scalar(self, clause, *multiparams, **params):
455493
"""
456494
A delegate of :meth:`GinoEngine.scalar() <.engine.GinoEngine.scalar>`.

gino/engine.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sqlalchemy.engine import Engine, Connection
99
from sqlalchemy.sql import schema
1010

11+
from .exceptions import MultipleResultsFound, NoResultFound
1112
from .transaction import GinoTransaction
1213

1314

@@ -179,8 +180,8 @@ class GinoConnection:
179180
Represents an actual database connection.
180181
181182
This is the root of all query API like :meth:`all`, :meth:`first`,
182-
:meth:`scalar` or :meth:`status`, those on engine or query are simply
183-
wrappers of methods in this class.
183+
:meth:`one`, :meth:`one_or_none`, :meth:`scalar` or :meth:`status`,
184+
those on engine or query are simply wrappers of methods in this class.
184185
185186
Usually instances of this class are created by :meth:`.GinoEngine.acquire`.
186187
@@ -323,6 +324,50 @@ async def first(self, clause, *multiparams, **params):
323324
result = self._execute(clause, multiparams, params)
324325
return await result.execute(one=True)
325326

327+
async def one_or_none(self, clause, *multiparams, **params):
328+
"""
329+
Runs the given query in database, returns at most one result.
330+
331+
If the query returns no result, this method will return ``None``.
332+
If the query returns multiple results, this method will raise
333+
:class:`~gino.exceptions.MultipleResultsFound`.
334+
335+
See :meth:`all` for common query comments.
336+
337+
"""
338+
result = self._execute(clause, multiparams, params)
339+
ret = await result.execute()
340+
341+
if ret is None or len(ret) == 0:
342+
return None
343+
344+
if len(ret) == 1:
345+
return ret[0]
346+
347+
raise MultipleResultsFound('Multiple rows found for one_or_none().')
348+
349+
async def one(self, clause, *multiparams, **params):
350+
"""
351+
Runs the given query in database, returns exactly one result.
352+
353+
If the query returns no result, this method will raise
354+
:class:`~gino.exceptions.NoResultFound`.
355+
If the query returns multiple results, this method will raise
356+
:class:`~gino.exceptions.MultipleResultsFound`.
357+
358+
See :meth:`all` for common query comments.
359+
360+
"""
361+
try:
362+
ret = await self.one_or_none(clause, *multiparams, **params)
363+
except MultipleResultsFound:
364+
raise MultipleResultsFound('Multiple rows found for one().')
365+
366+
if ret is None:
367+
raise NoResultFound('No row was found for one().')
368+
369+
return ret
370+
326371
async def scalar(self, clause, *multiparams, **params):
327372
"""
328373
Runs the given query in database, returns the first result.
@@ -696,6 +741,22 @@ async def first(self, clause, *multiparams, **params):
696741
async with self.acquire(reuse=True) as conn:
697742
return await conn.first(clause, *multiparams, **params)
698743

744+
async def one_or_none(self, clause, *multiparams, **params):
745+
"""
746+
Runs :meth:`~.GinoConnection.one_or_none`, See :meth:`.all`.
747+
748+
"""
749+
async with self.acquire(reuse=True) as conn:
750+
return await conn.one_or_none(clause, *multiparams, **params)
751+
752+
async def one(self, clause, *multiparams, **params):
753+
"""
754+
Runs :meth:`~.GinoConnection.one`, See :meth:`.all`.
755+
756+
"""
757+
async with self.acquire(reuse=True) as conn:
758+
return await conn.one(clause, *multiparams, **params)
759+
699760
async def scalar(self, clause, *multiparams, **params):
700761
"""
701762
Runs :meth:`~.GinoConnection.scalar`, See :meth:`.all`.

gino/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,11 @@ class UninitializedError(GinoException):
1212

1313
class UnknownJSONPropertyError(GinoException):
1414
pass
15+
16+
17+
class MultipleResultsFound(GinoException):
18+
pass
19+
20+
21+
class NoResultFound(GinoException):
22+
pass

tests/test_engine.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ async def test_basic(engine):
2323
assert isinstance(await engine.scalar(sa.text('select now()')), datetime)
2424
assert isinstance((await engine.first('select now()'))[0], datetime)
2525
assert isinstance((await engine.all('select now()'))[0][0], datetime)
26+
assert isinstance((await engine.one('select now()'))[0], datetime)
27+
assert isinstance((await engine.one_or_none('select now()'))[0], datetime)
2628
status, result = await engine.status('select now()')
2729
assert status == 'SELECT 1'
2830
assert isinstance(result[0][0], datetime)

tests/test_executemany.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22

3+
from gino import MultipleResultsFound, NoResultFound
34
from .models import db, User
45

56
pytestmark = pytest.mark.asyncio
@@ -51,6 +52,65 @@ async def test_first(bind):
5152
assert set(u.nickname for u in rows) == {'1', '2', '3', '4'}
5253

5354

55+
# noinspection PyUnusedLocal
56+
async def test_one_or_none(bind):
57+
row = await User.query.gino.one_or_none()
58+
assert row is None
59+
60+
await User.create(nickname='0')
61+
row = await User.query.gino.one_or_none()
62+
assert row.nickname == '0'
63+
64+
result = await User.insert().returning(User.nickname).gino.one_or_none(
65+
dict(name='1'), dict(name='2'))
66+
assert result is None
67+
rows = await User.query.gino.all()
68+
assert len(await User.query.gino.all()) == 3
69+
assert set(u.nickname for u in rows) == {'0', '1', '2'}
70+
71+
with pytest.raises(MultipleResultsFound):
72+
row = await User.query.gino.one_or_none()
73+
74+
result = await User.insert().gino.one_or_none(
75+
dict(name='3'), dict(name='4'))
76+
assert result is None
77+
rows = await User.query.gino.all()
78+
assert len(rows) == 5
79+
assert set(u.nickname for u in rows) == {'0', '1', '2', '3', '4'}
80+
81+
with pytest.raises(MultipleResultsFound):
82+
row = await User.query.gino.one_or_none()
83+
84+
85+
# noinspection PyUnusedLocal
86+
async def test_one(bind):
87+
with pytest.raises(NoResultFound):
88+
row = await User.query.gino.one()
89+
90+
await User.create(nickname='0')
91+
row = await User.query.gino.one()
92+
assert row.nickname == '0'
93+
94+
with pytest.raises(NoResultFound):
95+
await User.insert().returning(User.nickname).gino.one(
96+
dict(name='1'), dict(name='2'))
97+
rows = await User.query.gino.all()
98+
assert len(await User.query.gino.all()) == 3
99+
assert set(u.nickname for u in rows) == {'0', '1', '2'}
100+
101+
with pytest.raises(MultipleResultsFound):
102+
row = await User.query.gino.one()
103+
104+
with pytest.raises(NoResultFound):
105+
await User.insert().gino.one(dict(name='3'), dict(name='4'))
106+
rows = await User.query.gino.all()
107+
assert len(rows) == 5
108+
assert set(u.nickname for u in rows) == {'0', '1', '2', '3', '4'}
109+
110+
with pytest.raises(MultipleResultsFound):
111+
row = await User.query.gino.one()
112+
113+
54114
# noinspection PyUnusedLocal
55115
async def test_scalar(bind):
56116
result = await User.insert().returning(User.nickname).gino.scalar(

tests/test_loader.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,25 @@ async def test_scalar(user):
4545
assert user.nickname == name
4646

4747

48+
async def test_one_or_none(user):
49+
name = await User.query.gino.load(User.nickname).one_or_none()
50+
assert user.nickname == name
51+
52+
uid, name = await (User.query.gino.load((User.id, User.nickname))
53+
.one_or_none())
54+
assert user.id == uid
55+
assert user.nickname == name
56+
57+
58+
async def test_one(user):
59+
name = await User.query.gino.load(User.nickname).one()
60+
assert user.nickname == name
61+
62+
uid, name = await User.query.gino.load((User.id, User.nickname)).one()
63+
assert user.id == uid
64+
assert user.nickname == name
65+
66+
4867
async def test_model_load(user):
4968
u = await User.query.gino.load(User.load('nickname', User.team_id)).first()
5069
assert isinstance(u, User)

0 commit comments

Comments
 (0)