functions/sync-review-comments-db/models.py (135 lines of code) (raw):
# -*- coding: utf-8 -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
import enum
from datetime import datetime
from typing import List, Optional
from sqlalchemy import ForeignKey, ScalarResult, UniqueConstraint, func, select
from sqlalchemy.orm import (
DeclarativeBase,
Mapped,
Session,
mapped_column,
relationship,
selectinload,
)
class EvaluationAction(enum.Enum):
APPROVE = 1
REJECT = 2
IGNORE = 3
class DiffStatus(enum.Enum):
PENDING = 1
GENERATED = 2
IGNORED = 3
SUBMITTED = 4
REPLACED = 5
class IgnoreReasons(enum.Enum):
SIZE = 1
class SuggestionIgnoreReason(enum.Enum):
NOT_SURE = 1
TRIVIAL = 2
DEVELOPMENT_PHASE = 3
OTHER = 4
REVIEW_TIP = 5
INCORRECT = 6
VALID_REDUNDANT = 7
class ReviewRequestMode(enum.Enum):
NORMAL = 1
EXPERIMENTAL = 2
class Base(DeclarativeBase):
pass
class ReviewRequest(Base):
__tablename__ = "review_requests"
__table_args__ = (UniqueConstraint("diff_id", "mode", "sequence"),)
id: Mapped[int] = mapped_column(primary_key=True)
diff_id: Mapped[int] = mapped_column(index=True)
revision_id: Mapped[int] = mapped_column(index=True)
status: Mapped[DiffStatus]
suggestions: Mapped[List["Suggestion"]] = relationship(
back_populates="review_request"
)
bugbug_version: Mapped[str]
tool_variant: Mapped[Optional[str]]
ignore_reason: Mapped[Optional[IgnoreReasons]]
mode: Mapped[ReviewRequestMode]
sequence: Mapped[int]
# pylint:disable=not-callable
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
server_default=func.now(), server_onupdate=func.now()
)
@property
def is_recently_created(self):
return (datetime.utcnow() - self.created_at).total_seconds() < 240
def has_evaluation(self, session: Session) -> bool:
if self.status == DiffStatus.IGNORED:
# If the diff is ignored, there's no way it has evaluation. We don't
# need to query the database.
return False
stmt = (
select(Evaluation.id)
.join(Suggestion)
.where(Suggestion.review_request_id == self.id)
.limit(1)
)
return session.scalar(stmt) is not None
def is_replaced(self, session: Session) -> bool:
if self.status == DiffStatus.REPLACED:
return True
stmt = select(ReviewRequest.status).where(ReviewRequest.id == self.id)
return session.scalar(stmt) == DiffStatus.REPLACED
def suggestions_with_their_evaluation(
self, session: Session
) -> ScalarResult["Suggestion"]:
stmt = (
select(Suggestion)
.options(selectinload(Suggestion.evaluation))
.where(Suggestion.review_request_id == self.id)
)
return session.scalars(stmt)
class Suggestion(Base):
__tablename__ = "suggestions"
id: Mapped[int] = mapped_column(primary_key=True)
review_request_id: Mapped[int] = mapped_column(ForeignKey(ReviewRequest.id))
review_request: Mapped[ReviewRequest] = relationship(back_populates="suggestions")
file_path: Mapped[str]
line_start: Mapped[int]
line_end: Mapped[int]
has_added_lines: Mapped[bool]
content: Mapped[str]
evaluation: Mapped[Optional["Evaluation"]] = relationship(
back_populates="suggestion"
)
# TODO: Fill the columns for old suggestions, then change the column
# to NOT NULL.
llm_name: Mapped[Optional[str]]
llm_temperature: Mapped[Optional[float]]
# This will be filled once the comment is posted to Phabricator. It will
# store the inline comment ID returned by the Phabricator Conduit API.
inline_comment_id: Mapped[Optional[int]]
# pylint:disable=not-callable
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
server_default=func.now(), server_onupdate=func.now()
)
@property
def final_line_start(self):
"""Return the final line start based on evaluation if present."""
evaluation = self.evaluation
return (
self.line_start
if evaluation is None or evaluation.line_start is None
else evaluation.line_start
)
@property
def final_line_end(self):
"""Return the final line end based on evaluation if present."""
evaluation = self.evaluation
return (
self.line_end
if evaluation is None or evaluation.line_end is None
else evaluation.line_end
)
@property
def final_line_length(self):
"""Return the final line length based on evaluation if present."""
return self.final_line_end - self.final_line_start
class Evaluation(Base):
__tablename__ = "evaluations"
id: Mapped[int] = mapped_column(primary_key=True)
suggestion_id: Mapped[int] = mapped_column(ForeignKey(Suggestion.id), unique=True)
suggestion: Mapped[Suggestion] = relationship(back_populates="evaluation")
user: Mapped[str]
action: Mapped[EvaluationAction]
edited_comment: Mapped[Optional[str]]
line_start: Mapped[Optional[int]]
line_end: Mapped[Optional[int]]
is_latest_diff: Mapped[bool]
ignore_reason: Mapped[Optional[SuggestionIgnoreReason]]
# pylint:disable=not-callable
created_at: Mapped[datetime] = mapped_column(server_default=func.now())