AFI parity: close all 35 gaps — every adapter wires every AFI-common capability
The conformance board (tests/afi/test_capability_parity.py) is now fully green: 90 capability cells + 4 meta-locks + 3 codegen byte-parity = 97 passed. The gaps the prose table used to launder as "Django-only" / "out of scope" are wired, against the pinned-spec model (single-authored spec, byte-identical conformance across languages) — never per-language reimplementation. FastAPI — edge_manifest + PSR (logic single-sourced in mizan_core.manifest), WebSocket RPC (/ws/ through the shared dispatch), SSR (the framework-agnostic SSRBridge relocated to mizan_core.ssr; Django rides it from there), Shapes (SQLAlchemy projection, same declaration surface as django-readers), Forms (Pydantic schema/validate/submit). Rust (Axum + Tauri + cores/mizan-rust) — X-Mizan-Invalidate header, auth= enforcement, origin HMAC cache, edge manifest + PSR, WebSocket handler / IPC subscription channel, multipart upload, SSR bridge, Shapes, Forms; JWT/MWT mint+verify and cache-key derivation byte-pinned to the Python reference (cache_keys_pin, token_pin, invalidate_header_pin). TypeScript — a KDL IR emitter byte-identical to the Python build_ir (so a TS backend can feed the codegen — the largest gap), multipart upload, session-init, WebSocket transport, SSR bridge, JWT/MWT mint (pinned to Python), Shapes, Forms. Verified in the merged tree: core 25, fastapi 74, django 353/21-skip, mizan-rust (incl. cross-language pins) green, axum 10, tauri 8, mizan-ts 103/2-skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
269
backends/mizan-fastapi/tests/test_shapes.py
Normal file
269
backends/mizan-fastapi/tests/test_shapes.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
Shapes behavior — the genuine capability behind the `shapes` probe.
|
||||
|
||||
Proves the SQLAlchemy binding has the same Shape declaration surface and
|
||||
projection/diff semantics as the Django `django-readers` binding:
|
||||
|
||||
- `Shape[Model]` resolves the mapped model + PK from the generic arg;
|
||||
- scalar annotations project columns, Shape-typed annotations project relations;
|
||||
- `.query(session, *stmt_fns, **relation_stmt)` flat / nested / scoped;
|
||||
- nested loads stay flat (selectinload, not N+1);
|
||||
- `.diff()` / `.diff_many()` detect field changes + nested created/updated/deleted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import ForeignKey, create_engine, event
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship
|
||||
|
||||
from mizan_fastapi.shapes import Diff, NestedDiff, Shape
|
||||
|
||||
|
||||
# ─── Mapped models ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class Publisher(Base):
|
||||
__tablename__ = "publisher"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str]
|
||||
country: Mapped[str]
|
||||
authors: Mapped[list["Author"]] = relationship(back_populates="publisher")
|
||||
|
||||
|
||||
class Author(Base):
|
||||
__tablename__ = "author"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str]
|
||||
bio: Mapped[str] = mapped_column(default="")
|
||||
publisher_id: Mapped[int] = mapped_column(ForeignKey("publisher.id"))
|
||||
publisher: Mapped[Publisher] = relationship(back_populates="authors")
|
||||
books: Mapped[list["Book"]] = relationship(back_populates="author")
|
||||
|
||||
|
||||
class Book(Base):
|
||||
__tablename__ = "book"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
title: Mapped[str]
|
||||
is_published: Mapped[bool] = mapped_column(default=True)
|
||||
author_id: Mapped[int] = mapped_column(ForeignKey("author.id"))
|
||||
author: Mapped[Author] = relationship(back_populates="books")
|
||||
|
||||
|
||||
# ─── Shapes (declaration surface identical to the Django adapter) ──────────────
|
||||
|
||||
|
||||
class FlatAuthorShape(Shape[Author]):
|
||||
id: int | None = None
|
||||
name: str
|
||||
|
||||
|
||||
class FlatBookShape(Shape[Book]):
|
||||
id: int | None = None
|
||||
title: str
|
||||
is_published: bool
|
||||
|
||||
|
||||
class BookCardShape(Shape[Book]):
|
||||
id: int | None = None
|
||||
title: str
|
||||
is_published: bool
|
||||
author: FlatAuthorShape # single nested FK
|
||||
|
||||
|
||||
class AuthorCardShape(Shape[Author]):
|
||||
id: int | None = None
|
||||
name: str
|
||||
bio: str
|
||||
books: list[FlatBookShape] = [] # list nested reverse-FK
|
||||
|
||||
|
||||
class PublisherDetailShape(Shape[Publisher]):
|
||||
id: int | None = None
|
||||
name: str
|
||||
authors: list[AuthorCardShape] = [] # 3-level nesting
|
||||
|
||||
|
||||
# ─── Fixtures ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session():
|
||||
engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(engine)
|
||||
with Session(engine) as s:
|
||||
pub = Publisher(name="Orbit", country="UK")
|
||||
ann = Author(name="Ann Leckie", bio="Imperial Radch", publisher=pub)
|
||||
devi = Author(name="Devi Pillai", bio="", publisher=pub)
|
||||
ann.books = [
|
||||
Book(title="Ancillary Justice", is_published=True),
|
||||
Book(title="Provenance", is_published=False),
|
||||
]
|
||||
s.add_all([pub, ann, devi])
|
||||
s.commit()
|
||||
yield s
|
||||
|
||||
|
||||
# ─── Declaration ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_shape_resolves_model_and_pk():
|
||||
assert FlatAuthorShape._model is Author
|
||||
assert FlatAuthorShape._pk_field == "id"
|
||||
|
||||
|
||||
def test_flat_shape_has_no_nested():
|
||||
assert FlatAuthorShape._nested == {}
|
||||
assert FlatAuthorShape._field_names == ["id", "name"]
|
||||
|
||||
|
||||
def test_single_nested_detected():
|
||||
assert BookCardShape._nested == {"author": FlatAuthorShape}
|
||||
|
||||
|
||||
def test_list_nested_detected():
|
||||
assert AuthorCardShape._nested == {"books": FlatBookShape}
|
||||
|
||||
|
||||
# ─── Query ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_flat_query_projects_fields(session):
|
||||
authors = FlatAuthorShape.query(session)
|
||||
assert len(authors) == 2
|
||||
assert {a.name for a in authors} == {"Ann Leckie", "Devi Pillai"}
|
||||
|
||||
|
||||
def test_query_with_stmt_fn_filters(session):
|
||||
authors = FlatAuthorShape.query(session, lambda s: s.where(Author.name == "Ann Leckie"))
|
||||
assert [a.name for a in authors] == ["Ann Leckie"]
|
||||
|
||||
|
||||
def test_single_nested_fk_projected(session):
|
||||
books = BookCardShape.query(session, lambda s: s.where(Book.title == "Ancillary Justice"))
|
||||
assert len(books) == 1
|
||||
assert books[0].author.name == "Ann Leckie"
|
||||
|
||||
|
||||
def test_list_nested_reverse_fk_projected(session):
|
||||
authors = AuthorCardShape.query(session, lambda s: s.where(Author.name == "Ann Leckie"))
|
||||
assert len(authors) == 1
|
||||
assert {b.title for b in authors[0].books} == {"Ancillary Justice", "Provenance"}
|
||||
|
||||
|
||||
def test_empty_nested_list(session):
|
||||
authors = AuthorCardShape.query(session, lambda s: s.where(Author.name == "Devi Pillai"))
|
||||
assert authors[0].books == []
|
||||
|
||||
|
||||
def test_three_level_nesting(session):
|
||||
pubs = PublisherDetailShape.query(session)
|
||||
assert len(pubs) == 1
|
||||
leckie = next(a for a in pubs[0].authors if a.name == "Ann Leckie")
|
||||
assert len(leckie.books) == 2
|
||||
|
||||
|
||||
def test_relation_stmt_scopes_nested_load(session):
|
||||
authors = AuthorCardShape.query(
|
||||
session,
|
||||
lambda s: s.where(Author.name == "Ann Leckie"),
|
||||
books=lambda s: s.where(Book.is_published.is_(True)),
|
||||
)
|
||||
assert [b.title for b in authors[0].books] == ["Ancillary Justice"]
|
||||
assert all(b.is_published for b in authors[0].books)
|
||||
|
||||
|
||||
def test_nested_query_stays_flat(session):
|
||||
"""selectinload keeps the projection at O(depth) queries, not N+1."""
|
||||
counter = {"n": 0}
|
||||
|
||||
@event.listens_for(session.bind, "after_cursor_execute")
|
||||
def _count(*args):
|
||||
counter["n"] += 1
|
||||
|
||||
AuthorCardShape.query(session)
|
||||
# one query for authors + one selectin for books
|
||||
assert counter["n"] == 2
|
||||
|
||||
|
||||
# ─── Diff ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_diff_no_changes(session):
|
||||
book = session.query(Book).filter_by(title="Ancillary Justice").one()
|
||||
shape = FlatBookShape(id=book.id, title="Ancillary Justice", is_published=True)
|
||||
d = shape.diff(session)
|
||||
assert d.is_new is False
|
||||
assert d.changed == {}
|
||||
|
||||
|
||||
def test_diff_detects_field_change(session):
|
||||
book = session.query(Book).filter_by(title="Ancillary Justice").one()
|
||||
shape = FlatBookShape(id=book.id, title="Ancillary Justice (rev)", is_published=True)
|
||||
d = shape.diff(session)
|
||||
assert d.changed["title"] == "Ancillary Justice (rev)"
|
||||
|
||||
|
||||
def test_diff_new_item(session):
|
||||
shape = FlatBookShape(id=None, title="Elantris", is_published=True)
|
||||
d = shape.diff(session)
|
||||
assert d.is_new is True
|
||||
assert "title" in d.changed
|
||||
|
||||
|
||||
def test_diff_nonexistent_pk_raises(session):
|
||||
shape = FlatBookShape(id=999999, title="Ghost", is_published=False)
|
||||
with pytest.raises(LookupError):
|
||||
shape.diff(session)
|
||||
|
||||
|
||||
def test_nested_diff_created_updated_deleted(session):
|
||||
author = session.query(Author).filter_by(name="Ann Leckie").one()
|
||||
books = sorted(author.books, key=lambda b: b.title)
|
||||
# keep one (updated), drop one (deleted), add one (created)
|
||||
shape = AuthorCardShape(
|
||||
id=author.id,
|
||||
name="Ann Leckie",
|
||||
bio="Imperial Radch",
|
||||
books=[
|
||||
FlatBookShape(id=books[0].id, title="Ancillary Justice REWRITTEN", is_published=True),
|
||||
FlatBookShape(id=None, title="Ancillary Sword", is_published=True),
|
||||
],
|
||||
)
|
||||
d = shape.diff(session)
|
||||
assert len(d.books.updated) == 1
|
||||
assert len(d.books.created) == 1
|
||||
assert len(d.books.deleted) == 1
|
||||
|
||||
|
||||
def test_diff_strict_nested_access_raises_on_typo(session):
|
||||
author = session.query(Author).filter_by(name="Ann Leckie").one()
|
||||
shape = FlatAuthorShape(id=author.id, name="Ann Leckie")
|
||||
d = shape.diff(session)
|
||||
with pytest.raises(AttributeError):
|
||||
_ = d.bookz
|
||||
with pytest.raises(KeyError):
|
||||
d.nested("bookz")
|
||||
|
||||
|
||||
def test_diff_many_batches(session):
|
||||
books = session.query(Book).all()
|
||||
items = [FlatBookShape(id=b.id, title=b.title + "!", is_published=b.is_published) for b in books]
|
||||
results = FlatBookShape.diff_many(session, items)
|
||||
assert len(results) == len(books)
|
||||
assert all("title" in d.changed for _, d in results)
|
||||
|
||||
|
||||
def test_diff_many_mixed_new_and_existing(session):
|
||||
book = session.query(Book).first()
|
||||
items = [
|
||||
FlatBookShape(id=book.id, title=book.title, is_published=book.is_published),
|
||||
FlatBookShape(id=None, title="Brand New", is_published=False),
|
||||
]
|
||||
results = FlatBookShape.diff_many(session, items)
|
||||
assert sum(1 for _, d in results if d.is_new) == 1
|
||||
assert sum(1 for _, d in results if not d.is_new) == 1
|
||||
Reference in New Issue
Block a user