feat(backend): add database models

This commit is contained in:
2025-04-11 00:02:04 +01:00
parent b6f1dd8942
commit 02ee0c219f
5 changed files with 292 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
from src.models.base import Base
from src.models.category import Category
from src.models.pattern import Pattern
from src.models.question import (
Difficulty,
Explanation,
Question,
QuestionCategory,
QuestionPattern,
Solution,
)
__all__ = [
"Base",
"Category",
"Difficulty",
"Explanation",
"Pattern",
"Question",
"QuestionCategory",
"QuestionPattern",
"Solution",
]

View File

@@ -0,0 +1,38 @@
import uuid
from datetime import datetime
from sqlalchemy import DateTime, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
"""Base class for all models."""
pass
class TimestampMixin:
"""Mixin that adds created_at and updated_at timestamps."""
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
class UUIDMixin:
"""Mixin that adds a UUID primary key."""
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)

View File

@@ -0,0 +1,25 @@
from typing import TYPE_CHECKING
from sqlalchemy import String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.models.base import Base, UUIDMixin
if TYPE_CHECKING:
from src.models.question import Question
class Category(Base, UUIDMixin):
"""Question category (e.g., Arrays, Strings, Trees)."""
__tablename__ = "categories"
name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
# Relationships
questions: Mapped[list["Question"]] = relationship(
secondary="question_categories",
back_populates="categories",
)

View File

@@ -0,0 +1,53 @@
from typing import TYPE_CHECKING, Any
from sqlalchemy import Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.models.base import Base, UUIDMixin
if TYPE_CHECKING:
from src.models.question import Question
class Pattern(Base, UUIDMixin):
"""Algorithmic pattern (e.g., Two Pointers, Sliding Window)."""
__tablename__ = "patterns"
# Core fields
name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
when_to_use: Mapped[str | None] = mapped_column(Text, nullable=True)
# Tutorial content fields
metaphor: Mapped[str | None] = mapped_column(Text, nullable=True)
core_concept: Mapped[str | None] = mapped_column(Text, nullable=True)
visualization: Mapped[str | None] = mapped_column(Text, nullable=True)
code_template: Mapped[str | None] = mapped_column(Text, nullable=True)
# Structured data fields (JSONB)
recognition_signals: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True)
common_mistakes: Mapped[list[dict[str, Any]] | None] = mapped_column(JSONB, nullable=True)
variations: Mapped[list[dict[str, Any]] | None] = mapped_column(JSONB, nullable=True)
related_patterns: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True)
prerequisite_patterns: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True)
# Difficulty level (1-5 learning curve)
difficulty_level: Mapped[int | None] = mapped_column(Integer, nullable=True)
# Pattern classification
pattern_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
display_order: Mapped[int | None] = mapped_column(Integer, nullable=True)
# Interactive visualization examples
visualization_examples: Mapped[list[dict[str, Any]] | None] = mapped_column(
JSONB, nullable=True
)
# Relationships
questions: Mapped[list["Question"]] = relationship(
secondary="question_patterns",
back_populates="patterns",
)

View File

@@ -0,0 +1,153 @@
import enum
import uuid
from typing import TYPE_CHECKING, Any
from sqlalchemy import Boolean, Enum, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.models.base import Base, TimestampMixin, UUIDMixin
if TYPE_CHECKING:
from src.models.category import Category
from src.models.pattern import Pattern
class Difficulty(str, enum.Enum):
"""Question difficulty levels."""
EASY = "easy"
MEDIUM = "medium"
HARD = "hard"
# Association tables
class QuestionCategory(Base):
"""Junction table for questions and categories."""
__tablename__ = "question_categories"
question_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("questions.id", ondelete="CASCADE"),
primary_key=True,
)
category_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("categories.id", ondelete="CASCADE"),
primary_key=True,
)
class QuestionPattern(Base):
"""Junction table for questions and patterns."""
__tablename__ = "question_patterns"
question_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("questions.id", ondelete="CASCADE"),
primary_key=True,
)
pattern_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("patterns.id", ondelete="CASCADE"),
primary_key=True,
)
is_optimal: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
# Relationships
question: Mapped["Question"] = relationship(back_populates="question_patterns")
pattern: Mapped["Pattern"] = relationship()
class Question(Base, UUIDMixin, TimestampMixin):
"""Coding interview question."""
__tablename__ = "questions"
title: Mapped[str] = mapped_column(String(255), nullable=False)
slug: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
difficulty: Mapped[Difficulty] = mapped_column(
Enum(
Difficulty,
name="difficulty_enum",
create_type=False,
values_callable=lambda x: [e.value for e in x],
),
nullable=False,
)
description: Mapped[str] = mapped_column(Text, nullable=False)
constraints: Mapped[str | None] = mapped_column(Text, nullable=True)
examples: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
leetcode_id: Mapped[int | None] = mapped_column(Integer, unique=True, nullable=True)
leetcode_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
function_signature: Mapped[str | None] = mapped_column(Text, nullable=True)
test_cases: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
# Relationships
explanation: Mapped["Explanation | None"] = relationship(
back_populates="question",
uselist=False,
cascade="all, delete-orphan",
)
solutions: Mapped[list["Solution"]] = relationship(
back_populates="question",
cascade="all, delete-orphan",
)
categories: Mapped[list["Category"]] = relationship(
secondary="question_categories",
back_populates="questions",
)
patterns: Mapped[list["Pattern"]] = relationship(
secondary="question_patterns",
back_populates="questions",
)
question_patterns: Mapped[list["QuestionPattern"]] = relationship(
back_populates="question",
cascade="all, delete-orphan",
)
class Explanation(Base, UUIDMixin):
"""Educational explanation for a question."""
__tablename__ = "explanations"
question_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("questions.id", ondelete="CASCADE"),
unique=True,
nullable=False,
)
approach: Mapped[str] = mapped_column(Text, nullable=False)
intuition: Mapped[str] = mapped_column(Text, nullable=False)
common_pitfalls: Mapped[list[dict[str, Any]] | None] = mapped_column(JSONB, nullable=True)
key_takeaways: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True)
time_complexity: Mapped[str] = mapped_column(Text, nullable=False)
space_complexity: Mapped[str] = mapped_column(Text, nullable=False)
complexity_explanation: Mapped[str | None] = mapped_column(Text, nullable=True)
pattern_comparison: Mapped[str | None] = mapped_column(Text, nullable=True)
# Relationships
question: Mapped["Question"] = relationship(back_populates="explanation")
class Solution(Base, UUIDMixin):
"""Code solution for a question."""
__tablename__ = "solutions"
question_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("questions.id", ondelete="CASCADE"),
nullable=False,
)
approach_name: Mapped[str] = mapped_column(String(100), nullable=False)
code: Mapped[str] = mapped_column(Text, nullable=False)
language: Mapped[str] = mapped_column(String(20), default="python", nullable=False)
is_optimal: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
explanation: Mapped[str | None] = mapped_column(Text, nullable=True)
# Relationships
question: Mapped["Question"] = relationship(back_populates="solutions")