Skip to content

Commit 226d6df

Browse files
authored
Merge pull request #288 from fantix/t287_model_lookup
Fixed #287, added `lookup()` in `Model`.
2 parents 4a9abd6 + 2ec6943 commit 226d6df

File tree

4 files changed

+92
-27
lines changed

4 files changed

+92
-27
lines changed

gino/crud.py

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def __get__(self, instance, owner):
2828
owner._check_abstract()
2929
q = sa.select([owner.__table__])
3030
if instance is not None:
31-
q = instance.append_where_primary_key(q)
31+
q = q.where(instance.lookup())
3232
return q.execution_options(model=weakref.ref(owner))
3333

3434

@@ -37,7 +37,7 @@ def __get__(self, instance, owner):
3737
def select(*args):
3838
q = sa.select([getattr(owner, x) for x in args])
3939
if instance is not None:
40-
q = instance.append_where_primary_key(q)
40+
q = q.where(instance.lookup())
4141
return q.execution_options(model=weakref.ref(owner),
4242
return_model=False)
4343
return select
@@ -83,10 +83,13 @@ def __init__(self, instance):
8383
self._values = {}
8484
self._props = {}
8585
self._literal = True
86-
if instance.__table__ is None:
87-
self._pk_values = None
88-
else:
89-
self._pk_values = _PrimaryKeyValues(instance)
86+
self._locator = None
87+
if instance.__table__ is not None:
88+
try:
89+
self._locator = instance.lookup()
90+
except LookupError:
91+
# apply() will fail anyway, but still allow updates()
92+
pass
9093

9194
def _set(self, key, value):
9295
self._values[key] = value
@@ -110,9 +113,9 @@ async def apply(self, bind=None, timeout=DEFAULT):
110113
:return: ``self`` for chaining calls.
111114
112115
"""
113-
if self._pk_values is None:
116+
if self._locator is None:
114117
raise TypeError(
115-
'GINO model {} is abstract, no table is defined'.format(
118+
'Model {} has no table, primary key or custom lookup()'.format(
116119
self._instance.__class__.__name__))
117120
cls = type(self._instance)
118121
values = self._values.copy()
@@ -146,8 +149,8 @@ async def apply(self, bind=None, timeout=DEFAULT):
146149
opts = dict(return_model=False)
147150
if timeout is not DEFAULT:
148151
opts['timeout'] = timeout
149-
clause = self._pk_values.append_where_primary_key(
150-
type(self._instance).update
152+
clause = type(self._instance).update.where(
153+
self._locator,
151154
).values(
152155
**values,
153156
).returning(
@@ -252,18 +255,6 @@ def _inspect_alias(target):
252255
return sa.inspection.inspect(target.alias)
253256

254257

255-
class _PrimaryKeyValues:
256-
def __init__(self, instance):
257-
self._values = {}
258-
for c in instance.__table__.primary_key.columns:
259-
self._values[c] = getattr(instance, c.name)
260-
261-
def append_where_primary_key(self, q):
262-
for c, v in self._values.items():
263-
q = q.where(c == v)
264-
return q
265-
266-
267258
class CRUDModel(Model):
268259
"""
269260
The base class for models with CRUD support.
@@ -422,8 +413,7 @@ class CRUDModel(Model):
422413
def __init__(self, **values):
423414
super().__init__()
424415
self.__profile__ = None
425-
# noinspection PyCallingNonCallable
426-
self.update(**values)
416+
self._update_request_cls(self).update(**values)
427417

428418
@classmethod
429419
def _init_table(cls, sub_cls):
@@ -541,8 +531,37 @@ def append_where_primary_key(self, q):
541531
542532
await user.query.gino.first()
543533
534+
.. deprecated:: 0.7.6
535+
Use :meth:`.lookup` instead.
536+
544537
"""
545-
return _PrimaryKeyValues(self).append_where_primary_key(q)
538+
return q.where(self.lookup()) # pragma: no cover
539+
540+
def lookup(self):
541+
"""
542+
Generate where-clause expression to locate this model instance.
543+
544+
By default this method uses current values of all primary keys, and you
545+
can override it to behave differently. Most instance-level CRUD
546+
operations depend on this method internally. Particularly while
547+
:meth:`.lookup` is called in :meth:`.update`, the where condition is
548+
used in :meth:`.UpdateRequest.apply`, so that queries like ``UPDATE ...
549+
SET id = NEW WHERE id = OLD`` could work correctly.
550+
551+
:return:
552+
553+
.. versionadded:: 0.7.6
554+
555+
"""
556+
exps = []
557+
for c in self.__table__.primary_key.columns:
558+
exps.append(c == getattr(self, c.name))
559+
if exps:
560+
return sa.and_(*exps)
561+
else:
562+
raise LookupError('Instance-level CRUD operations not allowed on '
563+
'models without primary keys or lookup(), please'
564+
' use model-level CRUD operations instead.')
546565

547566
def _update(self, **values):
548567
return self._update_request_cls(self).update(**values)
@@ -551,7 +570,7 @@ async def _delete(self, bind=None, timeout=DEFAULT):
551570
cls = type(self)
552571
# noinspection PyUnresolvedReferences,PyProtectedMember
553572
cls._check_abstract()
554-
clause = self.append_where_primary_key(cls.delete)
573+
clause = cls.delete.where(self.lookup())
555574
if timeout is not DEFAULT:
556575
clause = clause.execution_options(timeout=timeout)
557576
if bind is None:

tests/test_crud.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,48 @@ async def test_string_primary_key(engine):
237237
await Relation.create(bind=engine, timeout=10, name=r)
238238
r1 = await Relation.get(relations[0], bind=engine, timeout=10)
239239
assert r1.name == relations[0]
240+
241+
242+
async def test_lookup_287(bind):
243+
from gino.exceptions import NoSuchRowError
244+
245+
class Game(db.Model):
246+
__tablename__ = 'games'
247+
game_id = db.Column(db.String(32), unique=True)
248+
channel_id = db.Column(db.String(1), default='A')
249+
250+
await Game.gino.create()
251+
try:
252+
game_1 = await Game.create(game_id='1', channel_id='X')
253+
game_2 = await Game.create(game_id='2', channel_id='Y')
254+
255+
# ordinary update should be fine
256+
uq = game_1.update(game_id='3')
257+
258+
with pytest.raises(TypeError,
259+
match='Model Game has no table, primary key'):
260+
# but applying the updates to DB should fail
261+
await uq.apply()
262+
263+
with pytest.raises(LookupError,
264+
match='Instance-level CRUD operations not allowed'):
265+
await game_2.delete()
266+
with pytest.raises(LookupError,
267+
match='Instance-level CRUD operations not allowed'):
268+
await game_2.query.gino.all()
269+
with pytest.raises(LookupError,
270+
match='Instance-level CRUD operations not allowed'):
271+
await game_2.select('game_id')
272+
273+
# previous ordinary update still in effect
274+
assert game_1.game_id == '3'
275+
276+
assert await Game.select('game_id').gino.all() == [('1',), ('2',)]
277+
278+
Game.lookup = lambda self: Game.game_id == self.game_id
279+
with pytest.raises(NoSuchRowError):
280+
await game_1.update(channel_id='Z').apply()
281+
await game_2.update(channel_id='Z').apply()
282+
assert await Game.select('channel_id').gino.all() == [('X',), ('Z',)]
283+
finally:
284+
await Game.gino.drop()

tests/test_declarative.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ class AbstractModel(db.Model):
207207

208208
req = am.update()
209209

210-
with pytest.raises(TypeError, match='AbstractModel is abstract'):
210+
with pytest.raises(TypeError, match='AbstractModel has no table'):
211211
await req.apply()
212212

213213
with pytest.raises(TypeError, match='AbstractModel is abstract'):

tests/test_json.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ async def test_non_jsonb(bind):
9393
class News(db.Model):
9494
__tablename__ = 'news'
9595

96+
id = db.Column(db.Integer(), primary_key=True)
9697
profile = db.Column(JSON(), nullable=False, server_default='{}')
9798
visits = db.IntegerProperty(default=0)
9899

0 commit comments

Comments
 (0)