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>
270 lines
9.2 KiB
Python
270 lines
9.2 KiB
Python
"""
|
|
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
|