Spaces:
Build error
Database Architecture
This document describes the database architecture and file metadata management system in the PDF-TEI Editor.
Overview
The application uses SQLite for file metadata storage with a document-centric design. The database stores metadata about PDF and TEI files, their relationships, and sync tracking information. The filesystem contains the actual file content, while the database provides fast querying and relationship management.
Key Design Principles
1. Reconstructable Database
The database does not contain information that cannot be reconstructed from the filesystem (TEI/PDF content). In case of corruption or being outdated after WebDAV sync, it can be rebuilt from the files.
2. Document-Centric Model
- PDF files store document metadata (collections, metadata, etc.)
- TEI files inherit metadata from their associated PDF via
doc_id - All files sharing a
doc_idrepresent artifacts of the same document
3. Soft Deletes
Files are marked as deleted (deleted = 1) rather than removed from the database. This:
- Enables sync tracking (know what was deleted since last sync)
- Preserves history for debugging
- Allows undelete operations
4. Sync-Ready Schema
While sync implementation is complete, the schema was designed from the start to support efficient synchronization:
local_modified_at- Change tracking for delta syncsync_status- Sync state machinesync_hash- Conflict detectiondeleted- Soft delete marker (replaces.deletedfiles)
Database Location
- Development:
data/metadata.db - Production:
data/metadata.db(configurable via settings) - Tests: Temporary directory for isolation
Configuration Management
The application uses a two-tier configuration system:
Default Configuration
Location: config/ directory (version-controlled)
config.json- Application configuration defaultsusers.json- Default user accountsgroups.json- Default user groupscollections.json- Default collectionsroles.json- Role definitions
Runtime Configuration
Location: data/db/ directory (gitignored)
config.json- Runtime configuration (copied fromconfig/on startup)users.json- User accounts (copied fromconfig/on startup)groups.json- User groups (copied fromconfig/on startup)collections.json- Collections (copied fromconfig/on startup)roles.json- Roles (copied fromconfig/on startup)
Initialization Pattern
On server startup (fastapi_app/lib/core/db_init.py):
- Check if
data/db/*.jsonfiles exist - If missing, copy from
config/*.json - Merge missing keys into
data/db/config.json(preserves existing values) - SQLite databases created on first access (on-demand)
This ensures:
- Clean branches - No database file conflicts in git
- Easy reset - Delete
data/db/, restart server - Default recovery - Missing files auto-restore
- Safe updates - Preserves user customizations
Database Schema
Files Table
The files table stores all file metadata:
CREATE TABLE files (
-- Primary key
id TEXT PRIMARY KEY, -- SHA-256 content hash (64 chars)
-- Stable ID for API responses
stable_id TEXT, -- Short stable identifier (6-12 chars)
-- File identity
file_type TEXT NOT NULL, -- 'pdf' | 'tei-xml'
doc_id TEXT NOT NULL, -- Document identifier (groups related files)
-- Document metadata (stored only on PDF files)
doc_collections TEXT, -- JSON array: ["collection1", "collection2"]
doc_metadata TEXT, -- JSON object: document-level metadata
-- File-specific metadata
file_metadata TEXT, -- JSON object: file-specific metadata
variant_id TEXT, -- Variant identifier (e.g., "translation-fr")
is_gold_standard INTEGER DEFAULT 0, -- Boolean: 1 for gold standard TEI files
-- Filesystem location
storage_path TEXT NOT NULL, -- Relative path in data directory
-- Sync tracking
deleted INTEGER DEFAULT 0, -- Soft delete: 1 = deleted
local_modified_at TEXT, -- ISO timestamp of last local change
sync_status TEXT, -- Sync state: 'synced'|'pending'|'conflict'
sync_hash TEXT, -- Hash for conflict detection
-- Audit timestamps
created_at TEXT NOT NULL, -- ISO timestamp
updated_at TEXT NOT NULL -- ISO timestamp
)
Indexes:
CREATE INDEX idx_files_doc_id ON files(doc_id);
CREATE INDEX idx_files_file_type ON files(file_type);
CREATE INDEX idx_files_deleted ON files(deleted);
CREATE INDEX idx_files_sync_status ON files(sync_status);
CREATE INDEX idx_files_stable_id ON files(stable_id);
Sync Metadata Table
Stores WebDAV sync state:
CREATE TABLE sync_metadata (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
)
Used for tracking:
- Last sync timestamp
- Sync server URL
- Sync credentials (encrypted)
Storage References Table
Tracks filesystem references for safe cleanup:
CREATE TABLE storage_references (
hash_prefix TEXT, -- First 2 chars of hash (sharding)
content_hash TEXT, -- Full SHA-256 hash
reference_count INTEGER, -- Number of files referencing this hash
PRIMARY KEY (hash_prefix, content_hash)
)
Enables safe deletion:
- Only delete filesystem files when
reference_count = 0 - Multiple database records can reference same file content
- Git-style hash-sharded storage (
data/files/ab/cd/abcd...)
Database Components
DatabaseManager
Location: fastapi_app/lib/core/database.py
Provides connection management and transactions:
class DatabaseManager:
def __init__(self, db_path: Path, logger=None):
"""Initialize database manager and ensure schema exists."""
@contextmanager
def get_connection(self) -> sqlite3.Connection:
"""Context manager for database connections."""
@contextmanager
def transaction(self) -> sqlite3.Connection:
"""Context manager for transactions with auto-commit/rollback."""
Features:
- Thread-safe connection management
- Automatic schema initialization
- Row factory for dict-like access
- Foreign keys enabled
- Write-Ahead Logging (WAL) mode for better performance
Usage:
# Simple query
with db_manager.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM files WHERE deleted = 0")
files = cursor.fetchall()
# Transaction
with db_manager.transaction() as conn:
cursor = conn.cursor()
cursor.execute("INSERT INTO files ...")
cursor.execute("UPDATE files ...")
# Auto-commit on exit, rollback on exception
FileRepository
Location: fastapi_app/lib/repository/file_repository.py
Provides high-level CRUD operations with Pydantic models:
class FileRepository:
def __init__(self, db_manager: DatabaseManager, logger=None):
"""Initialize repository with storage reference counting."""
Core CRUD Operations
All queries filter deleted = 0 by default unless explicitly requesting deleted files.
# Create
def insert_file(self, file_data: FileCreate) -> FileMetadata:
"""Insert new file record. Returns created FileMetadata."""
# Read
def get_file_by_id(self, file_id: str) -> Optional[FileMetadata]:
"""Get file by full SHA-256 hash (64 chars)."""
def get_file_by_stable_id(self, stable_id: str) -> Optional[FileMetadata]:
"""Get file by short stable ID (6-12 chars)."""
def get_file_by_id_or_stable_id(self, file_id: str) -> Optional[FileMetadata]:
"""Get file by either stable_id or full hash."""
# Update
def update_file(self, file_id: str, updates: FileUpdate) -> FileMetadata:
"""Update file record. Returns updated FileMetadata."""
# Delete (soft)
def delete_file(self, file_id: str) -> None:
"""Mark file as deleted (sets deleted = 1)."""
Document-Centric Queries
def get_files_by_doc_id(self, doc_id: str) -> List[FileMetadata]:
"""Get all files for a document (excludes deleted)."""
def get_pdf_for_document(self, doc_id: str) -> Optional[FileMetadata]:
"""Get the PDF file for a document."""
def get_latest_tei_version(self, doc_id: str) -> Optional[FileMetadata]:
"""Get most recent TEI version (excludes gold standard)."""
def get_gold_standard(self, doc_id: str) -> Optional[FileMetadata]:
"""Get gold standard TEI file for document."""
def get_all_versions(self, doc_id: str) -> List[FileMetadata]:
"""Get all TEI versions (excludes gold standard and deleted)."""
Metadata Inheritance
TEI files inherit doc_collections and doc_metadata from their PDF:
def get_file_with_doc_metadata(
self,
file_id: str
) -> Optional[FileWithDocMetadata]:
"""
Get file with inherited document metadata.
For TEI files, returns both:
- doc_collections/doc_metadata (NULL for TEI)
- inherited_doc_collections/inherited_doc_metadata (from PDF via JOIN)
"""
SQL Implementation:
SELECT
f.*,
pdf.doc_collections as inherited_doc_collections,
pdf.doc_metadata as inherited_doc_metadata
FROM files f
LEFT JOIN files pdf ON f.doc_id = pdf.doc_id AND pdf.file_type = 'pdf'
WHERE f.id = ? AND f.deleted = 0
List and Filter
def list_files(
self,
collection: Optional[str] = None,
variant: Optional[str] = None,
file_type: Optional[str] = None,
include_deleted: bool = False
) -> List[FileMetadata]:
"""
List files with optional filters.
Args:
collection: Filter by collection ID
variant: Filter by variant_id
file_type: Filter by file_type ('pdf' | 'tei-xml')
include_deleted: Include soft-deleted files
"""
Sync Support
def get_deleted_files(self) -> List[FileMetadata]:
"""Get files marked as deleted (for sync reconciliation)."""
def mark_deleted(self, file_id: str) -> None:
"""Soft delete with sync tracking."""
def update_sync_status(self, file_id: str, updates: SyncUpdate) -> None:
"""Update sync tracking fields."""
def get_sync_metadata(self, key: str) -> Optional[str]:
"""Get sync metadata value."""
def set_sync_metadata(self, key: str, value: str) -> None:
"""Set sync metadata value."""
StorageReferenceManager
Location: fastapi_app/lib/storage/storage_references.py
Manages reference counting for safe filesystem cleanup:
class StorageReferenceManager:
def increment_reference(self, content_hash: str) -> None:
"""Increment reference count when file is added."""
def decrement_reference(self, content_hash: str) -> bool:
"""
Decrement reference count when file is deleted.
Returns True if safe to delete file from filesystem.
"""
Usage:
# When creating file
file_repo.insert_file(file_data)
ref_manager.increment_reference(file_data.id)
# When deleting file
file_repo.delete_file(file_id)
can_delete = ref_manager.decrement_reference(file_id)
if can_delete:
# Safe to delete from filesystem
os.remove(storage_path)
Pydantic Models
Location: fastapi_app/lib/models/models.py
Type-safe models for database operations:
class FileMetadata(BaseModel):
"""Complete file record from database."""
id: str
stable_id: Optional[str]
file_type: Literal['pdf', 'tei-xml']
doc_id: str
doc_collections: List[str]
doc_metadata: dict
file_metadata: dict
variant_id: Optional[str]
is_gold_standard: bool
storage_path: str
deleted: bool
local_modified_at: Optional[datetime]
sync_status: Optional[str]
sync_hash: Optional[str]
created_at: datetime
updated_at: datetime
class FileCreate(BaseModel):
"""Model for creating new file records."""
id: str # SHA-256 hash
stable_id: Optional[str]
file_type: Literal['pdf', 'tei-xml']
doc_id: str
# ... (subset of FileMetadata)
class FileUpdate(BaseModel):
"""Model for updating file records (all fields optional)."""
doc_collections: Optional[List[str]]
doc_metadata: Optional[dict]
file_metadata: Optional[dict]
# ...
class FileWithDocMetadata(FileMetadata):
"""File with inherited document metadata from PDF."""
inherited_doc_collections: List[str]
inherited_doc_metadata: dict
File Storage Layout
Files are stored using Git-style hash sharding:
data/
βββ files/
β βββ ab/
β β βββ cd/
β β β βββ abcd1234...5678 # Full SHA-256 hash
β β βββ ef/
β β βββ abef9876...4321
β βββ 12/
β βββ 34/
β βββ 1234abcd...ef56
βββ metadata.db
Benefits:
- Prevents directory with too many files (filesystem limits)
- Fast lookups using hash prefix
- Content-addressable storage (deduplication)
Database Initialization
Application Startup
Location: fastapi_app/main.py
from .lib.db_init import ensure_db_initialized
# Initialize configuration and database
ensure_db_initialized()
For Tests
Tests use isolated temporary directories:
import tempfile
from pathlib import Path
from fastapi_app.lib.core.db_init import initialize_db_from_config
# Create temporary database
with tempfile.TemporaryDirectory() as tmpdir:
db_dir = Path(tmpdir) / "db"
config_dir = Path("config")
initialize_db_from_config(config_dir, db_dir)
# Run tests with isolated database
db_manager = DatabaseManager(db_dir / "metadata.db")
Query Patterns
Document Grouping
Get all files for a document with inherited metadata:
files = file_repo.get_files_by_doc_id("doc123")
pdf = file_repo.get_pdf_for_document("doc123")
gold = file_repo.get_gold_standard("doc123")
versions = file_repo.get_all_versions("doc123")
Collection Filtering
Find all documents in a collection:
files = file_repo.list_files(collection="manuscripts")
# Documents appear if ANY collection matches
# doc_collections = ["manuscripts", "letters"] matches collection="manuscripts"
Metadata Inheritance Example
# PDF file stores metadata
pdf = FileMetadata(
id="abc123...",
file_type="pdf",
doc_id="doc1",
doc_collections=["manuscripts"],
doc_metadata={"title": "Medieval Text"}
)
# TEI file inherits metadata
tei = file_repo.get_file_with_doc_metadata("def456...")
# tei.doc_collections = None (TEI doesn't store)
# tei.inherited_doc_collections = ["manuscripts"] (from PDF)
# tei.inherited_doc_metadata = {"title": "Medieval Text"}
Performance Considerations
Indexes
Critical indexes for common queries:
idx_files_doc_id- Document groupingidx_files_file_type- File type filteringidx_files_deleted- Exclude deleted filesidx_files_stable_id- API lookups
WAL Mode
Write-Ahead Logging enables:
- Concurrent readers with single writer
- Better performance for write-heavy workloads
- Crash recovery
Connection Pooling
Single DatabaseManager instance per application:
- Connection Pooling: Implemented using
queue.Queueto reuse connections and reduce file open/close overhead. - Singleton Pattern:
_DatabaseManagerSingletonensures one pool per database file. - Concurrency: Optimized for high concurrency with WAL mode and explicit transaction management.
- Thread-safe: Context managers handle connection checkout/return and transaction boundaries.
Migration and Maintenance
Database Rebuild
If database becomes corrupted or outdated:
# Remove database
rm data/metadata.db
# Restart server (auto-recreates from filesystem)
npm run start:dev
Schema Updates
Schema changes require:
- Update
fastapi_app/lib/core/db_schema.py - Add migration logic in
initialize_database() - Test with clean database and existing database
Backup
# Backup database
cp data/metadata.db data/metadata.db.backup
# Backup configuration
cp -r data/db/ data/db.backup/
Testing
Unit Tests
Location: tests/unit/fastapi/
# Database initialization tests
uv run python -m pytest tests/unit/fastapi/test_db_init.py -v
# File repository tests
uv run python -m pytest tests/unit/fastapi/test_file_repository.py -v
Test Isolation
Tests use temporary directories:
def test_file_operations():
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "test.db"
db_manager = DatabaseManager(db_path)
file_repo = FileRepository(db_manager)
# Test operations in isolation
# ...
Related Documentation
- Architecture Overview - Complete system architecture
- Configuration Management - Config files and CLI
- Access Control - RBAC and collection-based access
- Collections - Collection management system
- Testing - Testing infrastructure