Skip to content

💽 Database usage ​

The bot uses SQLAlchemy ORM in asynchronous mode to work with the database. Documentation.

WARNING

Before you can use the database, you must add the use_db or require_db permission to the permissions list in your module's config.yaml file!

Declaring Models ​

Define your database models using SQLAlchemy's Declarative Mapping (e.g., inheriting from DeclarativeBase). It's common practice to place these in a separate file, like db.py.

python
# db.py
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, Integer

class Base(DeclarativeBase):
    pass

class UserProfile(Base):
    __tablename__ = "user_profiles"

    user_id: Mapped[int] = mapped_column(primary_key=True)
    display_name: Mapped[str] = mapped_column(String(100))
    points: Mapped[int] = mapped_column(Integer, default=0)

# You can define other tables/models here too

Providing Metadata ​

Your main module class must expose the SQLAlchemy MetaData object associated with your models. This is done by overriding the db_meta property. The framework uses this metadata to automatically create the tables when the module loads (if they don't exist).

python
# main.py (inside your BaseModule class)
from .db_models import Base
from sqlalchemy import MetaData

# ... other imports and class definition ...

    @property
    def db_meta(self) -> MetaData:
        """Provides SQLAlchemy MetaData for table creation."""
        return Base.metadata

Database Initialization and Access ​

If database access is permitted (use_db or require_db in config) and the bot's database is enabled, the ModuleLoader will:

  1. Create a Database object (base.db.Database) specifically for your module. By default, this might be a separate SQLite file (e.g., ./ModuleName/module_db.sqlite) or a schema/database in a shared RDBMS, depending on the bot's config.db_url.
  2. Assign this Database object to the self.db attribute of your module instance.
  3. Use self.db_meta to run metadata.create_all(bind=engine) asynchronously.
  4. Call the async def on_db_ready(self) method in your module after the tables are ensured to exist.

The self.db object contains:

  • engine: An AsyncEngine instance connected to your module's specific database/schema.
  • session_maker: An async_sessionmaker bound to the engine, configured with expire_on_commit=False. Use this to create asynchronous sessions.

Performing Database Operations ​

Use the async_sessionmaker (self.db.session_maker) to create sessions within an async with block for database operations.

python
# main.py (inside your BaseModule class)
from .db_models import UserProfile
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update

# ... other methods ...

    async def get_user_points(self, user_id: int) -> int:
        """Fetches user points from the database."""
        async with self.db.session_maker() as session: # Create a session
            session: AsyncSession # Type hint for clarity
            # Use scalar_one_or_none for fetching a single optional record's column
            points = await session.scalar(
                select(UserProfile.points).where(UserProfile.user_id == user_id)
            )
            return points if points is not None else 0

    async def add_points(self, user_id: int, points_to_add: int):
        """Adds points to a user, creating profile if needed."""
        async with self.db.session_maker() as session:
            # Try to update existing user
            stmt = (
                update(UserProfile)
                .where(UserProfile.user_id == user_id)
                .values(points=UserProfile.points + points_to_add)
            )
            result = await session.execute(stmt)

            # If no rows were updated, user doesn't exist yet
            if result.rowcount == 0:
                # Create a new profile (assuming display_name can be fetched elsewhere)
                # In a real scenario, you might fetch the name or handle this differently
                display_name = f"User_{user_id}" # Placeholder
                new_profile = UserProfile(
                    user_id=user_id,
                    display_name=display_name,
                    points=points_to_add
                )
                session.add(new_profile)

            # Commit changes for this session
            await session.commit()

    async def on_db_ready(self):
        """Called after DB is set up. Can be used for initial data loading."""
        self.logger.info("Database is ready! Performing initial checks...")
        # Example: Log the number of existing user profiles
        async with self.db.session_maker() as session:
            count = await session.scalar(select(func.count(UserProfile.user_id)))
            self.logger.info(f"Found {count} user profiles in the database.")

Database Migrations ​

For managing changes to your database schema across module updates, place migration scripts in a db_migrations/ directory within your module. See base.db_migration.DBMigration and the ModuleManager.update_from_git method for how these are potentially applied during updates based on version comparison.