Database Layer: Peewee ORM & Models
Peewee ORM models, database schema, custom field types, and multi-tenancy enforcement at the data access layer.
Purpose & Architecture
The db/ sub-module contains:
- Peewee Models:
db_models.pydefines 15+ Peewee model classes corresponding to MySQL tables (User, Tenant, Document, Dialog, Task, Chunk, Conversation, Message, KnowledgeBase, etc.). - Domain Services:
services/contains 15+ domain service classes (DocumentService, DialogService, TaskService, etc.) that encapsulate business logic and use Peewee models to query/update data. - Joint Services:
joint_services/contains cross-domain coordination services (UserAccountService, MemoryMessageService, TenantModelService) for atomic multi-table mutations. - Custom Field Types: Peewee extensions (JSONField, ListField, SerializedField) for portable field types across MySQL and PostgreSQL.
- Multi-Tenancy Enforcement: All queries automatically filter by tenant_id at the ORM level; services validate tenant ownership before mutations.
Database Models Overview
RAGFlow's database schema includes 15+ Peewee models. Here are the core entities:
| Model Class | MySQL Table | Purpose | Key Fields |
|---|---|---|---|
| User | user | User accounts and authentication | id, email, password_hash, tenant_id, created_at, is_active |
| Tenant | tenant | Multi-tenant org boundaries | id, name, description, created_by (FK User), created_at, updated_at |
| Document | document | Uploaded documents (PDFs, Word, etc.) | id, tenant_id, name, file_path, file_type, created_by (FK User), created_at, updated_at, deleted_at (soft delete) |
| Chunk | chunk | Document chunks (embeddings + text) | id, document_id (FK Document), content, tokens, embedding (vector), created_at, deleted_at |
| Dialog | dialog | Conversation definitions | id, tenant_id, name, description, created_by (FK User), created_at, updated_at |
| Conversation | conversation | Chat sessions within a dialog | id, dialog_id (FK Dialog), user_id (FK User), created_at, updated_at |
| Message | message | Individual chat messages | id, conversation_id (FK Conversation), role (user/assistant), content, created_at |
| Task | task | Background processing tasks | id, tenant_id, document_id (FK Document), status (pending/processing/done/error), progress_json (JSONField), created_at, updated_at |
| KnowledgeBase | knowledge_base | Groupings of documents + chunks | id, tenant_id, name, description, created_by (FK User), created_at, updated_at |
| Memory | memory | User memory/context for conversations | id, user_id (FK User), content, created_at, updated_at |
Schema Relationships & Foreign Keys
Key relationships in the schema:
- Tenant multi-tenancy: User → Tenant (many users per tenant), Document → Tenant (docs isolated by tenant), Dialog → Tenant, Task → Tenant.
- User isolation: Document.created_by → User (FK), Dialog.created_by → User (FK), Conversation.user_id → User (FK).
- Document relationships: Document → KnowledgeBase (documents belong to knowledge bases), Document → Chunk (documents have many chunks).
- Conversation relationships: Dialog → Conversation (many conversations per dialog), Conversation → Message (many messages per conversation).
- Task queuing: Document → Task (one document upload = one task), Task.status tracks processing state.
Custom Peewee Field Types
RAGFlow defines custom Peewee field types to handle complex data structures:
JSONField
Stores JSON objects in TEXT columns. Used for semi-structured data (metadata, config, progress).
# Example in db_models.py
class Task(BaseModel):
progress_json = JSONField(default={})
# Usage in service
task = Task.get(Task.id == task_id)
task.progress_json = {'percent': 50, 'stage': 'embedding'}
task.save()
ListField
Stores Python lists as JSON arrays. Useful for tags, keywords, etc.
class Document(BaseModel):
tags = ListField(default=[])
# Usage
doc.tags = ['important', 'finance', 'q4-2024']
doc.save()
SerializedField
Serializes Python objects using pickle or JSON. Used for complex objects that don't fit JSON.
class ConversationContext(BaseModel):
context_obj = SerializedField()
# Usage
ctx = ConversationContext()
ctx.context_obj = {'user_preferences': {...}, 'session_state': {...}}
ctx.save()
Domain Services Layer
Services in db/services/ are the primary interface for business logic. Each service encapsulates one entity's behavior:
| Service Class | Module | Responsibilities |
|---|---|---|
| DocumentService | document_service.py | CRUD for documents, file path resolution, soft delete, tenant isolation |
| ChunkService | chunk_service.py | Chunk creation/retrieval, embedding queries, vector similarity search |
| DialogService | dialog_service.py | Conversation template management, tenant-scoped queries |
| ConversationService | conversation_service.py | Chat session lifecycle, message coordination |
| MessageService | message_service.py | Message persistence, role-based filtering (user vs assistant) |
| TaskService | task_service.py | Background task lifecycle, status updates, progress tracking |
| KnowledgebaseService | knowledgebase_service.py | Knowledge base CRUD, document grouping, tenant isolation |
| LLMService | llm_service.py | LLM model configuration, credential management |
| MemoryService | memory_service.py | User context/memory management for conversations |
| FileService | file_service.py | File uploads to S3/blob storage, temp file cleanup |
Joint Services for Cross-Domain Mutations
Some operations span multiple entities and must be atomic. Joint services coordinate these:
- UserAccountService: Creates User + Tenant together. Ensures user is owner of created tenant.
- MemoryMessageService: Coordinates Memory and Message creation when a user sends a message that should be remembered.
- TenantModelService: Manages tenant settings and related model configurations atomically.
Multi-Tenancy Enforcement in Database Layer
Multi-tenancy is enforced at THREE levels:
- ORM Level: Peewee queries are auto-filtered by tenant_id where applicable. Services should use tenant_id in WHERE clauses.
- Service Level: Services validate tenant_id before mutations. Example:
def delete_document(self, doc_id, tenant_id): doc = Document.get_by_id(doc_id) if doc.tenant_id != tenant_id: raise TenantMismatch(f"Doc {doc_id} not in tenant {tenant_id}") doc.deleted_at = datetime.now() doc.save() - API Layer: All handlers pass
g.tenant_idto service calls. Never trust request parameters for tenant_id.
Connection Pooling & Performance
RAGFlow uses Peewee's connection pool (typically 10–20 connections) to handle concurrent requests:
- Pool size: Configured at app startup. Default is 10; can be tuned for throughput.
- Connection reuse: Each async handler borrows a connection from the pool, executes queries, returns it.
- Deadlock retry: MySQL InnoDB can deadlock under contention. Services implement exponential backoff (retry up to 3x) for failed transactions.
- Query caching: Peewee doesn't cache results. Services can implement caching at a higher level (Redis) for frequently-accessed, slowly-changing data (LLM models, KnowledgeBases).
Soft Deletes & Data Integrity
Most entities use soft deletes (a deleted_at timestamp column) instead of hard deletes:
# Example in db_models.py
class Document(BaseModel):
name = CharField()
deleted_at = DateTimeField(null=True, default=None)
# Soft delete in service
doc.deleted_at = datetime.now()
doc.save()
# Query only active documents
active_docs = Document.select().where(Document.deleted_at.is_null())
Benefits:
- Allows "undo" operations (restore deleted documents)
- Maintains referential integrity (chunks still reference deleted documents)
- Supports audit trails (can see when entity was "deleted")
Error Handling & Constraints
Peewee raises exceptions for constraint violations. Services catch and convert to domain exceptions:
from peewee import IntegrityError, DoesNotExist
class DocumentService:
def create_document(self, tenant_id, name):
try:
doc = Document.create(
tenant_id=tenant_id,
name=name,
created_by=current_user_id
)
return doc
except IntegrityError as e:
if "UNIQUE" in str(e):
raise DocumentNameExists(f"Document '{name}' already exists in tenant")
raise InternalError(f"DB constraint violation: {e}")
except Exception as e:
logger.exception(f"Failed to create document: {e}")
raise InternalError("Failed to create document")
Key Files & Modules
db/db_models.py— All 15+ Peewee model definitionsdb/base_model.py— Base class for all models (common fields, timestamps)db/services/— 10+ domain service classesdb/joint_services/— Cross-domain coordination (UserAccountService, MemoryMessageService, etc.)db/common_service.py— CommonService base class with shared methods (pagination, tenant filtering, error handling)common/exceptions.py— Custom exception classes raised by services
Design Decisions & Trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| Peewee ORM (not SQLAlchemy) | Simpler query API, less boilerplate, better for rapid development | Less mature than SQLAlchemy, fewer advanced features (eager loading, relationship configuration) |
| Soft deletes instead of hard delete | Preserves audit trail, enables undo, avoids orphaned foreign keys | Requires WHERE clauses filtering deleted_at.is_null(); uses extra storage |
| Custom field types (JSONField, ListField) | Handles semi-structured data without separate tables; portable across MySQL/PostgreSQL | Less efficient for querying (can't easily search within JSON); requires deserialization on fetch |
| Explicit tenant_id checks in services | Prevents data leaks; makes tenant isolation explicit and auditable | Boilerplate in every service; easy to forget a check |
| Connection pooling (10–20 conns) | Prevents connection exhaustion; reuses connections for efficiency | Pool contention at high concurrency; deadlock retry overhead |
Security Implications
- SQL Injection: Peewee parameterizes all queries; no risk of SQL injection if using ORM (not raw SQL).
- Tenant Isolation: Must always check tenant_id before mutations. Missing check = data leak vulnerability.
- Password Storage: User.password_hash is hashed with bcrypt. Never store plaintext passwords.
- Credential Leaks: LLM credentials (API keys) should be encrypted in the database. Decrypt only when needed for API calls.
Future Enhancements
- Connection Pooling Tuning: Monitor pool contention and deadlock rates; adjust pool size dynamically.
- Caching Layer: Implement Redis caching for slowly-changing data (LLM models, KnowledgeBases) to reduce DB load.
- Sharding: Partition data by tenant_id (shard key) for horizontal scaling at high customer count.
- Read Replicas: Deploy read-only replicas for analytics and reporting queries.
- Query Optimization: Add indexes on commonly-filtered columns (tenant_id, created_by, deleted_at).