Coverage for tests/query/test_update_execution.py: 100%
53 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 21:13 +0300
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 21:13 +0300
1"""Update query construction, compilation, and execution acceptance tests."""
3from __future__ import annotations
5from collections.abc import Callable
6from typing import cast
8from snektest import assert_eq, assert_is_none, assert_raises, test
10from snekql import (
11 MISSING,
12 Database,
13 Fetched,
14 Integer,
15 Model,
16 Pending,
17 QueryConstructionError,
18 Text,
19 insert,
20 select,
21 update,
22)
23from snekql.query import compile_write_sql
24from tests.helpers import NULL_LOGGER
27@test(mark="fast")
28def update_compilation_accepts_multiple_quoted_assignments() -> None:
29 """Update compiles multiple assignments in caller-provided order."""
31 class Order[S = Pending](Model[S, "Order[Fetched]"]):
32 """Table model with identifiers requiring SQLite quoting."""
34 __tablename__ = "select"
35 status: Order.Col[str] = Text(nullable=False)
36 where: Order.Col[str] = Text(nullable=False)
38 query = (
39 update(Order)
40 .set(
41 Order.where.to("new"),
42 Order.status.to("done"),
43 )
44 .where(Order.where.ne("old"))
45 )
47 sql, params = compile_write_sql(query)
49 expected_sql = 'UPDATE "select" SET "where" = ?, "status" = ? WHERE ("where" != ?)'
50 assert_eq(sql, expected_sql)
51 assert_eq(params, ("new", "done", "old"))
54@test(mark="fast")
55def update_assignments_must_belong_to_target_model() -> None:
56 """Update set() rejects assignments built from another table model."""
58 class User[S = Pending](Model[S, "User[Fetched]"]):
59 """Target table model for ownership checks."""
61 email: User.Col[str] = Text(nullable=False)
63 class AuditLog[S = Pending](Model[S, "AuditLog[Fetched]"]):
64 """Unrelated table model for ownership checks."""
66 message: AuditLog.Col[str] = Text(nullable=False)
68 set_assignments = cast("Callable[..., object]", update(User).set)
70 with assert_raises(QueryConstructionError):
71 _ = set_assignments(AuditLog.message.to("wrong table"))
74@test(mark="fast")
75def update_rejects_generated_and_primary_key_assignments() -> None:
76 """Generated and primary key columns are not update-assignable."""
78 class User[S = Pending](Model[S, "User[Fetched]"]):
79 """Table model with both protected and updateable columns."""
81 account_id: User.Col[int] = Integer(primary_key=True)
82 email: User.Col[str] = Text(nullable=False)
83 revision: User.GenCol[int] = Integer(default=MISSING)
85 with assert_raises(QueryConstructionError):
86 _ = update(User).set(User.revision.to(2))
88 with assert_raises(QueryConstructionError):
89 _ = update(User).set(User.account_id.to(2))
92@test(mark="medium")
93async def update_execute_returns_none_and_supports_explicit_all() -> None:
94 """tx.execute(update(...).all()) returns None after full-table update."""
96 class User[S = Pending](Model[S, "User[Fetched]"]):
97 """Table model updated through the async runtime."""
99 id: User.GenCol[int] = Integer(primary_key=True, default=MISSING)
100 email: User.Col[str] = Text(nullable=False)
101 status: User.Col[str] = Text(nullable=False, default="active")
103 database = await Database.initialize(
104 logger=NULL_LOGGER, database=":memory:", models=[User]
105 )
106 try:
107 async with database.transaction() as tx:
108 await tx.execute(insert(User(email="a@example.com")))
109 await tx.execute(insert(User(email="b@example.com")))
111 result = await tx.execute(
112 update(User).set(User.status.to("disabled")).all(),
113 )
114 statuses = await tx.fetch_all(
115 select(User.status).all().order_by(User.id.asc()),
116 )
117 finally:
118 await database.close()
120 assert_is_none(result)
121 assert_eq(statuses, ["disabled", "disabled"])