126 lines
4.8 KiB
Python
126 lines
4.8 KiB
Python
|
|
"""Store schema.
|
||
|
|
|
||
|
|
Tenant keying, from the first migration onward, per
|
||
|
|
`business-app-service-contract_v0.1` § 1.3. Every table holding owned data
|
||
|
|
carries `tenant`, and no query may assume it is the only tenant (§ 1.4). The
|
||
|
|
service is deployed single-tenant today; the key is here so a later
|
||
|
|
consolidation is a data copy rather than a rewrite.
|
||
|
|
|
||
|
|
The format's three distinct things stay distinct:
|
||
|
|
|
||
|
|
- a **package version** is immutable content addressed by `<id>@<version>`
|
||
|
|
(§ 17), scoped to a registry because identity is registry-scoped (§ 3.2);
|
||
|
|
- an **index entry** records how a version arrived in this store (§ 20.3);
|
||
|
|
- **files** are content, stored as blobs rather than parsed into rows.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import datetime as dt
|
||
|
|
|
||
|
|
from sqlalchemy import (
|
||
|
|
BigInteger,
|
||
|
|
DateTime,
|
||
|
|
ForeignKey,
|
||
|
|
Index,
|
||
|
|
LargeBinary,
|
||
|
|
String,
|
||
|
|
Text,
|
||
|
|
UniqueConstraint,
|
||
|
|
)
|
||
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||
|
|
|
||
|
|
from .db import Base
|
||
|
|
|
||
|
|
|
||
|
|
def utcnow() -> dt.datetime:
|
||
|
|
return dt.datetime.now(dt.timezone.utc)
|
||
|
|
|
||
|
|
|
||
|
|
class PackageVersion(Base):
|
||
|
|
"""One immutable `<registry>:<id>@<version>` within one tenant."""
|
||
|
|
|
||
|
|
__tablename__ = "package_versions"
|
||
|
|
__table_args__ = (
|
||
|
|
UniqueConstraint(
|
||
|
|
"tenant", "registry", "package_id", "version", name="uq_package_version"
|
||
|
|
),
|
||
|
|
Index("ix_package_versions_tenant_id", "tenant", "package_id"),
|
||
|
|
)
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||
|
|
tenant: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
|
|
registry: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
|
|
package_id: Mapped[str] = mapped_column(String(512), nullable=False)
|
||
|
|
version: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
|
|
|
||
|
|
# Indexed manifest fields, for discovery without opening every package.
|
||
|
|
name: Mapped[str] = mapped_column(String(256), nullable=False)
|
||
|
|
summary: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||
|
|
package_type: Mapped[str] = mapped_column(String(32), nullable=False, default="template")
|
||
|
|
license: Mapped[str | None] = mapped_column(String(128))
|
||
|
|
tags: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||
|
|
|
||
|
|
# The manifest as published, so a consumer sees exactly what was stored.
|
||
|
|
manifest: Mapped[str] = mapped_column(Text, nullable=False)
|
||
|
|
content_digest: Mapped[str] = mapped_column(String(71), nullable=False)
|
||
|
|
published_at: Mapped[dt.datetime] = mapped_column(
|
||
|
|
DateTime(timezone=True), nullable=False, default=utcnow
|
||
|
|
)
|
||
|
|
|
||
|
|
files: Mapped[list["PackageFile"]] = relationship(
|
||
|
|
back_populates="package_version", cascade="all, delete-orphan"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class PackageFile(Base):
|
||
|
|
"""A file belonging to a package version. Content, not parsed rows."""
|
||
|
|
|
||
|
|
__tablename__ = "package_files"
|
||
|
|
__table_args__ = (
|
||
|
|
UniqueConstraint("package_version_id", "path", name="uq_package_file_path"),
|
||
|
|
Index("ix_package_files_tenant", "tenant"),
|
||
|
|
)
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||
|
|
tenant: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
|
|
package_version_id: Mapped[int] = mapped_column(
|
||
|
|
ForeignKey("package_versions.id", ondelete="CASCADE"), nullable=False
|
||
|
|
)
|
||
|
|
path: Mapped[str] = mapped_column(String(1024), nullable=False)
|
||
|
|
content: Mapped[bytes] = mapped_column(LargeBinary, nullable=False)
|
||
|
|
|
||
|
|
package_version: Mapped[PackageVersion] = relationship(back_populates="files")
|
||
|
|
|
||
|
|
|
||
|
|
class IndexEntry(Base):
|
||
|
|
"""How a package version arrived in this store (§ 20.3).
|
||
|
|
|
||
|
|
Store metadata, deliberately separate from the package: `included_at` is
|
||
|
|
first arrival and is never overwritten; a re-record updates `last_seen_at`.
|
||
|
|
"""
|
||
|
|
|
||
|
|
__tablename__ = "index_entries"
|
||
|
|
__table_args__ = (
|
||
|
|
UniqueConstraint(
|
||
|
|
"tenant", "registry", "package_id", "version", name="uq_index_entry"
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||
|
|
tenant: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
|
|
registry: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
|
|
package_id: Mapped[str] = mapped_column(String(512), nullable=False)
|
||
|
|
version: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
|
|
|
||
|
|
source: Mapped[str] = mapped_column(Text, nullable=False)
|
||
|
|
method: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
|
|
declared_author: Mapped[str | None] = mapped_column(String(256))
|
||
|
|
declared_source: Mapped[str | None] = mapped_column(Text)
|
||
|
|
license: Mapped[str | None] = mapped_column(String(128))
|
||
|
|
|
||
|
|
included_at: Mapped[dt.datetime] = mapped_column(
|
||
|
|
DateTime(timezone=True), nullable=False, default=utcnow
|
||
|
|
)
|
||
|
|
last_seen_at: Mapped[dt.datetime | None] = mapped_column(DateTime(timezone=True))
|