Coverage for tests/test_public_api.py: 98%
111 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"""Public API contract tests for snekql."""
3from __future__ import annotations
5from collections.abc import Callable
6from typing import cast
8from snektest import test
9from snektest.assertions import (
10 assert_eq,
11 assert_in,
12 assert_is,
13 assert_isinstance,
14 assert_ne,
15)
17import snekql
18from snekql.testing import mariadb as testing_mariadb
21def _assert_has_specific_docstring(value: object) -> None:
22 docstring = getattr(value, "__doc__", None)
23 assert_ne(docstring, None)
24 assert_ne(docstring, "")
25 assert_ne(docstring, object.__doc__)
28def _catch_as_snekql_error(error: snekql.SnekqlError) -> None:
29 try:
30 raise error
31 except snekql.SnekqlError as caught_error:
32 assert_is(caught_error, error)
35@test()
36def public_contract_exports_canonical_names() -> None:
37 """The package root explicitly curates canonical PRD names."""
39 expected_names = (
40 "MISSING",
41 "Assignment",
42 "Attr",
43 "Blob",
44 "Boolean",
45 "Col",
46 "CurrentTimestamp",
47 "Database",
48 "DatabaseCloseTimeoutError",
49 "DatabaseClosedError",
50 "DatabaseClosingError",
51 "DatabaseRuntimeError",
52 "DateTime",
53 "DeleteQuery",
54 "ExecutionError",
55 "Fetched",
56 "FrozenModelError",
57 "GenCol",
58 "Index",
59 "InsertQuery",
60 "Integer",
61 "Json",
62 "Missing",
63 "Model",
64 "ModelDeclarationError",
65 "ModelError",
66 "ModelMeta",
67 "ModelValidationError",
68 "OrderBy",
69 "Pending",
70 "PoolTimeoutError",
71 "Predicate",
72 "QueryCompilationError",
73 "QueryConstructionError",
74 "QueryError",
75 "Real",
76 "SchemaError",
77 "SchemaPolicy",
78 "SchemaVerificationError",
79 "SelectModelQuery",
80 "SelectTupleQuery",
81 "SelectValueQuery",
82 "SnekqlError",
83 "StructuredLogger",
84 "Table",
85 "Text",
86 "Transaction",
87 "TransactionClosedError",
88 "UpdateQuery",
89 "delete",
90 "insert",
91 "mariadb",
92 "select",
93 "sqlite",
94 "update",
95 )
97 assert_eq(tuple(snekql.__all__), expected_names)
98 for name in expected_names:
99 assert_in(name, snekql.__all__)
100 assert_eq(getattr(snekql, name), getattr(snekql, name))
103@test()
104def testing_mariadb_namespace_exports_test_server_names() -> None:
105 """The testing namespace exposes MariaDB test-server support directly."""
107 assert_eq(
108 tuple(testing_mariadb.__all__),
109 (
110 "MariaDBAuth",
111 "MariaDBCommandResult",
112 "MariaDBTransport",
113 "TemporaryMariaDBServer",
114 "TemporaryMariaDBServerError",
115 "temporary_mariadb_server",
116 ),
117 )
118 assert_in("mariadb", __import__("snekql.testing").testing.__all__)
119 assert_in("temporary_mariadb_server", testing_mariadb.__all__)
120 assert_isinstance(
121 testing_mariadb.TemporaryMariaDBServerError("failure"),
122 snekql.SnekqlError,
123 )
124 assert "testing" not in snekql.__all__
127@test()
128def query_factory_functions_reject_empty_selects() -> None:
129 """Selecting no model or fields is package-originated query misuse."""
131 select_fn = cast("Callable[..., object]", snekql.select)
133 try:
134 _ = select_fn()
135 except snekql.QueryConstructionError:
136 return
138 msg = "select() should reject empty selection"
139 raise AssertionError(msg)
142@test()
143def column_declarations_produce_query_attributes() -> None:
144 """Column declarations leave public descriptors on table model classes."""
146 class AttributeUser(snekql.Model[snekql.Pending, "AttributeUser[snekql.Fetched]"]):
147 """Table model for descriptor smoke checks."""
149 email: AttributeUser.Col[str] = snekql.Text(nullable=False)
151 assert_isinstance(AttributeUser.email, snekql.Attr)
152 assert_isinstance(AttributeUser.email.eq("alice@example.com"), snekql.Predicate)
153 assert_isinstance(AttributeUser.email.asc(), snekql.OrderBy)
154 assert_isinstance(AttributeUser.email.to("new@example.com"), snekql.Assignment)
157@test()
158def sqlite_namespace_exports_backend_specific_names() -> None:
159 """The SQLite namespace exposes the future backend-specific model shape."""
161 assert_is(snekql.sqlite.Model, snekql.Model)
162 assert_is(snekql.sqlite.Index, snekql.Index)
163 assert_is(snekql.sqlite.Integer, snekql.Integer)
164 assert_is(snekql.sqlite.Text, snekql.Text)
165 assert_in("Config", snekql.sqlite.__all__)
167 class SqliteUser(snekql.sqlite.Model[snekql.Pending, "SqliteUser[snekql.Fetched]"]):
168 """SQLite table model declared through the SQLite namespace."""
170 email: SqliteUser.Col[str] = snekql.sqlite.Text(nullable=False)
172 assert_isinstance(SqliteUser.email, snekql.sqlite.Attr)
173 assert_isinstance(SqliteUser.email.eq("alice@example.com"), snekql.Predicate)
176@test()
177def mutation_query_chain_methods_return_query_objects() -> None:
178 """Public update/delete chain methods keep returning mutation query objects."""
180 class MutationUser(snekql.Model[snekql.Pending, "MutationUser[snekql.Fetched]"]):
181 """Table model for mutation chain smoke checks."""
183 email: MutationUser.Col[str] = snekql.Text(nullable=False)
184 status: MutationUser.Col[str] = snekql.Text(nullable=False)
186 assignment = MutationUser.status.to("disabled")
187 predicate = MutationUser.email.eq("alice@example.com")
189 update_query = snekql.update(MutationUser)
190 delete_query = snekql.delete(MutationUser)
192 assert_isinstance(update_query.set(assignment), snekql.UpdateQuery)
193 assert_isinstance(update_query.where(predicate), snekql.UpdateQuery)
194 assert_isinstance(update_query.all(), snekql.UpdateQuery)
195 assert_isinstance(delete_query.where(predicate), snekql.DeleteQuery)
196 assert_isinstance(delete_query.all(), snekql.DeleteQuery)
199@test()
200def select_query_chain_methods_return_query_objects() -> None:
201 """Public select chain methods keep returning select query objects."""
203 class ChainUser(snekql.Model[snekql.Pending, "ChainUser[snekql.Fetched]"]):
204 """Table model for select chain smoke checks."""
206 query = snekql.select(ChainUser)
208 assert_isinstance(query.all(), snekql.SelectModelQuery)
209 assert_isinstance(query.limit(10), snekql.SelectModelQuery)
210 assert_isinstance(query.offset(5), snekql.SelectModelQuery)
213@test()
214def query_factory_functions_return_public_query_objects() -> None:
215 """Query builder entry points return stable public query classes."""
217 class QueryUser(snekql.Model[snekql.Pending, "QueryUser[snekql.Fetched]"]):
218 """Table model for query factory smoke checks."""
220 row = object.__new__(QueryUser)
222 assert_isinstance(snekql.select(QueryUser), snekql.SelectModelQuery)
223 assert_isinstance(snekql.insert(row), snekql.InsertQuery)
224 assert_isinstance(snekql.update(QueryUser), snekql.UpdateQuery)
225 assert_isinstance(snekql.delete(QueryUser), snekql.DeleteQuery)
228@test()
229def public_classes_have_specific_docstrings() -> None:
230 """Public marker, error, column, query, and runtime classes explain intent."""
232 documented_classes = (
233 snekql.Assignment,
234 snekql.Attr,
235 snekql.Blob,
236 snekql.Boolean,
237 snekql.CurrentTimestamp,
238 snekql.Database,
239 snekql.DatabaseClosedError,
240 snekql.DatabaseCloseTimeoutError,
241 snekql.DatabaseClosingError,
242 snekql.DatabaseRuntimeError,
243 snekql.DateTime,
244 snekql.DeleteQuery,
245 snekql.ExecutionError,
246 snekql.Fetched,
247 snekql.FrozenModelError,
248 snekql.Index,
249 snekql.InsertQuery,
250 snekql.Integer,
251 snekql.Json,
252 snekql.Missing,
253 snekql.Model,
254 snekql.ModelDeclarationError,
255 snekql.ModelError,
256 snekql.ModelMeta,
257 snekql.ModelValidationError,
258 snekql.OrderBy,
259 snekql.Pending,
260 snekql.PoolTimeoutError,
261 snekql.Predicate,
262 snekql.QueryCompilationError,
263 snekql.QueryConstructionError,
264 snekql.QueryError,
265 snekql.Real,
266 snekql.SchemaError,
267 snekql.SchemaVerificationError,
268 snekql.SelectModelQuery,
269 snekql.SelectTupleQuery,
270 snekql.SelectValueQuery,
271 snekql.SnekqlError,
272 snekql.Table,
273 snekql.Text,
274 snekql.Transaction,
275 snekql.TransactionClosedError,
276 snekql.UpdateQuery,
277 )
279 for documented_class in documented_classes:
280 _assert_has_specific_docstring(documented_class)
283@test()
284def missing_sentinel_has_stable_singleton_behavior() -> None:
285 """MISSING is the only Missing value applications need to compare with."""
287 assert_is(snekql.Missing(), snekql.MISSING)
288 assert_eq(repr(snekql.MISSING), "MISSING")
291@test()
292def public_error_hierarchy_is_rooted_at_snekql_error() -> None:
293 """All intentional public errors can be caught as SnekqlError."""
295 errors = (
296 snekql.DatabaseClosedError("package-originated failure"),
297 snekql.DatabaseCloseTimeoutError("package-originated failure"),
298 snekql.DatabaseClosingError("package-originated failure"),
299 snekql.ExecutionError(
300 "package-originated failure",
301 sql="SELECT ?",
302 params=(1,),
303 ),
304 snekql.FrozenModelError("package-originated failure"),
305 snekql.ModelDeclarationError("package-originated failure"),
306 snekql.ModelValidationError("package-originated failure"),
307 snekql.PoolTimeoutError("package-originated failure"),
308 snekql.QueryCompilationError("package-originated failure"),
309 snekql.QueryConstructionError("package-originated failure"),
310 snekql.SchemaVerificationError("package-originated failure"),
311 snekql.TransactionClosedError("package-originated failure"),
312 )
314 catches: tuple[Callable[[], None], ...] = tuple(
315 lambda error=error: _catch_as_snekql_error(error) for error in errors
316 )
318 for catch in catches:
319 catch()
322@test()
323def execution_error_preserves_sql_and_params() -> None:
324 """Execution failures expose query context through the public exception."""
326 error = snekql.ExecutionError(
327 "insert failed",
328 sql='INSERT INTO "user" ("email") VALUES (?)',
329 params=("alice@example.com",),
330 )
332 assert_eq(error.sql, 'INSERT INTO "user" ("email") VALUES (?)')
333 assert_eq(error.params, ("alice@example.com",))
334 assert_in("insert failed", str(error))
335 assert_in('INSERT INTO "user"', str(error))
336 assert_in("alice@example.com", str(error))