diff --git a/src/videoag_common/database/__init__.py b/src/videoag_common/database/__init__.py index c29a9a09ca3aa6ecc33ce26155226d18029bb3cf..ad3625c9a1b5b4dc9a204127bbea7c4e106eb8d3 100644 --- a/src/videoag_common/database/__init__.py +++ b/src/videoag_common/database/__init__.py @@ -6,7 +6,7 @@ from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method from sqlalchemy.sql.base import ExecutableOption from sqlalchemy.orm import Session as SessionDb, DeclarativeBase, Mapped, mapped_column, relationship from sqlalchemy.types import Text, String, TIMESTAMP -from sqlalchemy import types, ForeignKey +from sqlalchemy import types, ForeignKey, CheckConstraint, UniqueConstraint, ForeignKeyConstraint from .base import ( Base, diff --git a/src/videoag_common/database/drift_detector.py b/src/videoag_common/database/drift_detector.py index e00a2cbdf6a22aa3d9c47c0cf1574f12db0db409..059af8e341391bcac8266be7b9663e517707e007 100644 --- a/src/videoag_common/database/drift_detector.py +++ b/src/videoag_common/database/drift_detector.py @@ -48,15 +48,15 @@ def _to_string(value) -> str: case PrimaryKeyConstraint() as constraint: assert isinstance(constraint, PrimaryKeyConstraint) - return f"PrimaryKeyConstraint({_to_string(constraint.columns)}, table.name={constraint.table.name})" + return f"PrimaryKeyConstraint({_to_string(constraint.columns)}, table.name='{constraint.table.name}')" case CheckConstraint() as constraint: assert isinstance(constraint, CheckConstraint) - return f"CheckConstraint({_to_string(constraint.columns)}, sqltext={constraint.sqltext} table.name={constraint.table.name})" + return f"CheckConstraint({_to_string(constraint.columns)}, sqltext='{constraint.sqltext}', table.name='{constraint.table.name}')" case UniqueConstraint() as constraint: assert isinstance(constraint, UniqueConstraint) - return f"UniqueConstraint({_to_string(constraint.columns)}, table.name={constraint.table.name})" + return f"UniqueConstraint({_to_string(constraint.columns)}, table.name='{constraint.table.name}')" case ForeignKeyConstraint() as constraint: assert isinstance(constraint, ForeignKeyConstraint) @@ -112,9 +112,14 @@ def _check_constraint_equal(actual_constraint: Constraint, schema_constraint: Co if not isinstance(actual_constraint, CheckConstraint): return False - if schema_constraint.sqltext != actual_constraint.sqltext: # TODO test if this works - return False - return True + + # We try our best to check if they are equal + if schema_constraint.sqltext == actual_constraint.sqltext: + return True + # Sometimes the db returns it without correct brackets. Just strip them + if str(schema_constraint.sqltext).strip("()") == str(actual_constraint.sqltext).strip("()"): + return True + return False case ForeignKeyConstraint(): assert isinstance(schema_constraint, ForeignKeyConstraint) # For pycharm @@ -342,14 +347,14 @@ def _check_table_equal(actual_table: Table, schema_table: Table) -> bool: break else: correct = False - print(f"Missing constraint\n {_to_string(schema_constraint)}\nin database. The following constraints do " - f"not match:") + print(f"Missing constraint\n {_to_string(schema_constraint)}\nin database for table {schema_table.name}. " + f"The following constraints do not match:") print("\n".join(map(lambda c: f" {_to_string(c)}", actual_table.constraints))) if len(unmatched_actual_constraints) > 0: correct = False - print(f"Got unexpected constraint\n {_to_string(next(iter(unmatched_actual_constraints)))}\nin database. The " - f"following schema constraints do not match:") + print(f"Got unexpected constraint\n {_to_string(next(iter(unmatched_actual_constraints)))}\nin database for " + f"table {schema_table.name}. The following schema constraints do not match:") print("\n".join(map(lambda c: f" {_to_string(c)}", schema_table.constraints))) return correct diff --git a/src/videoag_common/objects/job.py b/src/videoag_common/objects/job.py index abaab0eaaad40c5cceb1e3fab147f332a4cb7752..5b9e5a1e9cb49b208f57bfb949714c6cecf2a4fb 100644 --- a/src/videoag_common/objects/job.py +++ b/src/videoag_common/objects/job.py @@ -22,6 +22,17 @@ _JOB_STATE_ENUM = create_enum_type(JobState) class Job(ApiObject, Base): + __table_args__ = ( + CheckConstraint( + "(on_end_event_type IS NULL) = (on_end_event_data IS NULL)", + name="check_event", + comment="Event type and data must be both set or both null" + ), + CheckConstraint( + "cause_job_id IS NULL OR cause_user_id IS NULL", + name="check_only_one_cause" + ) + ) status: Mapped[JobState] = api_mapped( mapped_column(_JOB_STATE_ENUM, nullable=False, index=True, default=JobState.READY), @@ -48,7 +59,6 @@ class Job(ApiObject, Base): ) ) - # TODO check both null or not null on_end_event_type: Mapped[str] = api_mapped( mapped_column(String(collation=STRING_COLLATION), nullable=True), ApiStringField( @@ -79,7 +89,6 @@ class Job(ApiObject, Base): include_in_data=True ) ) - # TODO check only one cause_job_id: Mapped[int] = mapped_column(ForeignKey("job.id"), nullable=True, index=True) cause_user_id: Mapped[int] = mapped_column(ForeignKey("user.id"), nullable=True, index=True) diff --git a/src/videoag_common/objects/medium.py b/src/videoag_common/objects/medium.py index f212e244146412063dd60e54fc41b33f48b666b4..29bedc2cd4e4c20142fa5e91fb198ee412c7c47b 100644 --- a/src/videoag_common/objects/medium.py +++ b/src/videoag_common/objects/medium.py @@ -6,12 +6,24 @@ from videoag_common.miscellaneous import * from videoag_common.api_object import * from videoag_common.media_process import * -from .course import Lecture, Course +from .course import Lecture from .job import Job # A file which was uploaded and needs to be assigned to a lecture, to be converted to a SourceFileTargetMedium class SourceMedium(VisibilityApiObject, DeletableApiObject, Base): + __table_args__ = ( + CheckConstraint( + "NOT tag IS NULL OR lecture_id IS NULL", + name="check_tag_is_set", + comment="Tag must be set if this has a lecture id" + ), + CheckConstraint( + "NOT sha256 IS NULL OR lecture_id IS NULL", + name="check_sha256_is_set", + comment="sha256 must be set if this has a lecture id" + ), + ) file_path: Mapped[str] = api_mapped( mapped_column(String(collation=STRING_COLLATION), nullable=False, index=True), # Can't be unique because of deleted entries @@ -41,6 +53,7 @@ class SourceMedium(VisibilityApiObject, DeletableApiObject, Base): include_in_data=True ) ) + lecture_id: Mapped[int] = mapped_column(ForeignKey("lecture.id"), nullable=True, index=True) sha256: Mapped[str] = api_mapped( mapped_column(String(length=64, collation=STRING_COLLATION), nullable=True), ApiStringField( @@ -48,15 +61,8 @@ class SourceMedium(VisibilityApiObject, DeletableApiObject, Base): data_notes="Only calculated once this is sorted" ) ) - # TODO check course not null if lecture is not null - course_id: Mapped[int] = mapped_column(ForeignKey("course.id"), nullable=True, index=True) - lecture_id: Mapped[int] = mapped_column(ForeignKey("lecture.id"), nullable=True, index=True) tag: Mapped[str] = mapped_column(String(collation=STRING_COLLATION), nullable=True) - course: Mapped[Course] = relationship( - primaryjoin=lambda: Course.id == SourceMedium.course_id, - lazy="raise_on_sql" - ) lecture: Mapped[Lecture] = relationship( back_populates="source_media", primaryjoin=lambda: Lecture.id == SourceMedium.lecture_id,