"""World content persistence models. Implements the schema decided in ``docs/architecture/world_persistence_impl.md`` §4: chunk content (rooms, chunk payloads, chunk revisions) lives in Postgres, alongside ``worlds``, rather than in a per-world SQLite file (``docs/architecture/world.md`` §4, superseded). The chunk *payload* format itself (the 48-byte header + sectioned binary body) is unchanged from ``world.md`` §3.6 and is decoded/encoded by ``app.services.chunk_codec``, not by these models -- these just store the opaque ``BYTEA``. Conventions follow the existing models (``app/models/world.py``, ``app/models/avatar_loadout.py``): declarative ``Column(...)`` style, ``Integer`` surrogate PKs, ``DateTime(timezone=True)`` with ``server_default=func.now()``, JSONB for document-shaped fields. """ from sqlalchemy import ( Boolean, Column, DateTime, ForeignKey, Index, Integer, LargeBinary, String, UniqueConstraint, text, ) from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.database import Base # Default chunk grid, matching Chunk.CHUNK_SIZE_* in scripts/chunk.gd (the # client's source of truth for the same constants) and world.md §2.2. DEFAULT_ROOM_GRID: dict = {"size": [32, 32, 1], "origin": [0, 0, 0]} # The room every world gets automatically (world_persistence_impl.md §4.1). MAIN_ROOM_SLUG = "main" MAIN_ROOM_NAME = "Main" class WorldRoom(Base): """A bounded region within a world -- a level, building, or zone (world.md §2.1). Holds the chunk grid config so a world can later hold rooms with different grids without touching the ``worlds`` table. """ __tablename__ = "world_rooms" id = Column(Integer, primary_key=True, index=True) world_id = Column( Integer, ForeignKey("worlds.id", ondelete="CASCADE"), nullable=False, index=True ) slug = Column(String(64), nullable=False) # url-safe; "main" by default name = Column(String(255), nullable=False) # {"size": [x, y, z], "origin": [x, y, z]} grid = Column( JSONB, nullable=False, server_default='{"size": [32, 32, 1], "origin": [0, 0, 0]}', ) bounds = Column(JSONB, nullable=True) # {"min":[x,y,z], "max":[x,y,z]}; null = unbounded spawn = Column(JSONB, nullable=True) # {"pos":[x,y,z], "yaw": 0.0} created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at = Column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False ) __table_args__ = ( UniqueConstraint("world_id", "slug", name="uq_world_rooms_world_slug"), ) world = relationship("World", back_populates="rooms") chunks = relationship("WorldChunk", back_populates="room", cascade="all, delete-orphan") class WorldChunk(Base): """One chunk's persisted content -- the atomic load/save/stream unit (world.md §2.1). ``payload`` is the world.md §3.6 binary blob, stored verbatim as BYTEA. Deltas from world.md §4.4 (SQLite design): a surrogate integer PK + unique coordinate constraint (rather than ``chunk_key`` as the PK), no ``dirty`` column (write-behind doesn't apply over Postgres -- see world_persistence_impl.md §3.3/§5), and content flags folded into this row as JSONB + an indexed ``is_mature`` boolean rather than a separate ``content_flags`` table (world.md §4.6). """ __tablename__ = "world_chunks" id = Column(Integer, primary_key=True, index=True) world_id = Column( Integer, ForeignKey("worlds.id", ondelete="CASCADE"), nullable=False, index=True ) # denormalized: world-scoped queries without a join through rooms room_id = Column( Integer, ForeignKey("world_rooms.id", ondelete="CASCADE"), nullable=False, index=True ) chunk_key = Column(String(160), nullable=False, unique=True) # world.md §2.3 / §4.5 cx = Column(Integer, nullable=False) cy = Column(Integer, nullable=False) cz = Column(Integer, nullable=False, server_default="0") format_version = Column(Integer, nullable=False, server_default="1") payload = Column(LargeBinary, nullable=False) # world.md §3.6 bytes payload_len = Column(Integer, nullable=False) checksum = Column(String(64), nullable=False) # SHA-256 hex of payload body revision = Column(Integer, nullable=False, server_default="1") content_flags = Column(JSONB, nullable=False, server_default="{}") # world.md §3.4 is_mature = Column(Boolean, nullable=False, server_default="false") updated_by_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at = Column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False ) __table_args__ = ( UniqueConstraint("room_id", "cx", "cy", "cz", name="uq_world_chunks_coord"), Index("ix_world_chunks_room_coord", "room_id", "cx", "cy", "cz"), Index("ix_world_chunks_mature", "world_id", postgresql_where=text("is_mature")), ) room = relationship("WorldRoom", back_populates="chunks") class WorldChunkRevision(Base): """Audit trail of chunk writes (world.md §4.7). ``chunk_key``/``world_id`` are deliberately **not** foreign keys, so deleting a chunk or a world preserves the audit trail. Retention (world_persistence_impl.md §4.3, required): only the newest ``settings.WORLD_CHUNK_REVISION_KEEP`` rows per ``chunk_key`` keep a non-null ``payload``; older rows are pruned (payload nulled, metadata row kept) by ``WorldStore`` in the same transaction as the write that triggers it. """ __tablename__ = "world_chunk_revisions" id = Column(Integer, primary_key=True, index=True) chunk_key = Column(String(160), nullable=False, index=True) world_id = Column(Integer, nullable=False, index=True) revision = Column(Integer, nullable=False) action = Column(String(16), nullable=False) # 'create' | 'update' | 'delete' actor_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True) checksum = Column(String(64), nullable=False) payload = Column(LargeBinary, nullable=True) # full payload at this revision; nulled by retention created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)