feat: prepare postgres sbom cutover
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a028f0-a42f-7582-89a8-ebaad7343834
This commit is contained in:
parent
cf7e3acb78
commit
ba535e1f8f
26 changed files with 1573 additions and 411 deletions
128
src/sbom_nexus/database.py
Normal file
128
src/sbom_nexus/database.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
"""Portable SQLAlchemy schema shared by the runtime store and Alembic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Boolean,
|
||||
Column,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
MetaData,
|
||||
String,
|
||||
Table,
|
||||
Text,
|
||||
create_engine,
|
||||
event,
|
||||
)
|
||||
from sqlalchemy.engine import Engine
|
||||
|
||||
NAMING_CONVENTION = {
|
||||
"ix": "ix_%(table_name)s_%(column_0_name)s",
|
||||
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||
"ck": "ck_%(table_name)s_%(constraint_name)s",
|
||||
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||
"pk": "pk_%(table_name)s",
|
||||
}
|
||||
|
||||
metadata = MetaData(naming_convention=NAMING_CONVENTION)
|
||||
|
||||
repositories = Table(
|
||||
"repositories",
|
||||
metadata,
|
||||
Column("id", String(36), primary_key=True),
|
||||
Column("slug", String(200), nullable=False, unique=True),
|
||||
Column("checkout_path", Text, nullable=True),
|
||||
Column("active", Boolean, nullable=False),
|
||||
Column("last_attempt_at", DateTime(timezone=True), nullable=True),
|
||||
Column("last_success_at", DateTime(timezone=True), nullable=True),
|
||||
Column("last_status", String(50), nullable=True),
|
||||
Column("last_source", String(200), nullable=True),
|
||||
Column("created_at", DateTime(timezone=True), nullable=False),
|
||||
Column("updated_at", DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
|
||||
snapshots = Table(
|
||||
"snapshots",
|
||||
metadata,
|
||||
Column("id", String(36), primary_key=True),
|
||||
Column(
|
||||
"repo_id",
|
||||
String(36),
|
||||
ForeignKey("repositories.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("snapshot_at", DateTime(timezone=True), nullable=False),
|
||||
Column("source", String(200), nullable=False),
|
||||
Column("status", String(50), nullable=False),
|
||||
Column("entry_count", Integer, nullable=False),
|
||||
Column("source_revision", String(200), nullable=True),
|
||||
Column("sources_json", JSON, nullable=False),
|
||||
Column("errors_json", JSON, nullable=False),
|
||||
Column("legacy_id", String(100), nullable=True, unique=True),
|
||||
Column("created_at", DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
|
||||
entries = Table(
|
||||
"entries",
|
||||
metadata,
|
||||
Column("id", String(36), primary_key=True),
|
||||
Column(
|
||||
"repo_id",
|
||||
String(36),
|
||||
ForeignKey("repositories.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
),
|
||||
Column(
|
||||
"snapshot_id",
|
||||
String(36),
|
||||
ForeignKey("snapshots.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("package_name", String(300), nullable=False),
|
||||
Column("package_version", String(100), nullable=True),
|
||||
Column("ecosystem", String(50), nullable=False),
|
||||
Column("license_spdx", String(100), nullable=True),
|
||||
Column("is_direct", Boolean, nullable=False),
|
||||
Column("is_dev", Boolean, nullable=False),
|
||||
Column("source_path", Text, nullable=True),
|
||||
Column("created_at", DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
|
||||
Index("ix_snapshots_repo_time", snapshots.c.repo_id, snapshots.c.snapshot_at)
|
||||
Index("ix_entries_snapshot", entries.c.snapshot_id)
|
||||
Index("ix_entries_repo", entries.c.repo_id)
|
||||
Index("ix_entries_license", entries.c.license_spdx)
|
||||
|
||||
|
||||
def database_url(value: str | Path) -> str:
|
||||
"""Normalize a filesystem path or supported URL into a SQLAlchemy URL."""
|
||||
text = str(value)
|
||||
if "://" not in text:
|
||||
return f"sqlite:///{Path(text).resolve()}"
|
||||
if text.startswith("postgres://"):
|
||||
return text.replace("postgres://", "postgresql+psycopg://", 1)
|
||||
if text.startswith("postgresql://"):
|
||||
return text.replace("postgresql://", "postgresql+psycopg://", 1)
|
||||
return text
|
||||
|
||||
|
||||
def create_database_engine(value: str | Path) -> Engine:
|
||||
url = database_url(value)
|
||||
options: dict[str, object] = {"pool_pre_ping": True}
|
||||
if url.startswith("sqlite:"):
|
||||
options["connect_args"] = {"check_same_thread": False}
|
||||
engine = create_engine(url, **options)
|
||||
if engine.dialect.name == "sqlite":
|
||||
|
||||
@event.listens_for(engine, "connect")
|
||||
def _enable_sqlite_foreign_keys(dbapi_connection, _connection_record) -> None:
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA foreign_keys = ON")
|
||||
cursor.close()
|
||||
|
||||
return engine
|
||||
Loading…
Add table
Add a link
Reference in a new issue