cmboulanger's picture
disaster-recovery deploy of pdf-tei-editor
6a49f21 verified
|
Raw
History Blame Contribute Delete
17.5 kB
# 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_id` represent 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 sync
- `sync_status` - Sync state machine
- `sync_hash` - Conflict detection
- `deleted` - Soft delete marker (replaces `.deleted` files)
## 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 defaults
- `users.json` - Default user accounts
- `groups.json` - Default user groups
- `collections.json` - Default collections
- `roles.json` - Role definitions
### Runtime Configuration
Location: `data/db/` directory (gitignored)
- `config.json` - Runtime configuration (copied from `config/` on startup)
- `users.json` - User accounts (copied from `config/` on startup)
- `groups.json` - User groups (copied from `config/` on startup)
- `collections.json` - Collections (copied from `config/` on startup)
- `roles.json` - Roles (copied from `config/` on startup)
### Initialization Pattern
On server startup ([fastapi_app/lib/core/db_init.py](../../fastapi_app/lib/core/db_init.py)):
1. Check if `data/db/*.json` files exist
2. If missing, copy from `config/*.json`
3. Merge missing keys into `data/db/config.json` (preserves existing values)
4. 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:
```sql
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:**
```sql
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:
```sql
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:
```sql
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](../../fastapi_app/lib/core/database.py)
Provides connection management and transactions:
```python
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:**
```python
# 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](../../fastapi_app/lib/repository/file_repository.py)
Provides high-level CRUD operations with Pydantic models:
```python
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.
```python
# 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
```python
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:
```python
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:**
```sql
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
```python
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
```python
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](../../fastapi_app/lib/storage/storage_references.py)
Manages reference counting for safe filesystem cleanup:
```python
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:**
```python
# 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](../../fastapi_app/lib/models/models.py)
Type-safe models for database operations:
```python
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](../../fastapi_app/main.py)
```python
from .lib.db_init import ensure_db_initialized
# Initialize configuration and database
ensure_db_initialized()
```
### For Tests
Tests use isolated temporary directories:
```python
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:
```python
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:
```python
files = file_repo.list_files(collection="manuscripts")
# Documents appear if ANY collection matches
# doc_collections = ["manuscripts", "letters"] matches collection="manuscripts"
```
### Metadata Inheritance Example
```python
# 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 grouping
- `idx_files_file_type` - File type filtering
- `idx_files_deleted` - Exclude deleted files
- `idx_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.Queue` to reuse connections and reduce file open/close overhead.
- **Singleton Pattern**: `_DatabaseManagerSingleton` ensures 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:
```bash
# Remove database
rm data/metadata.db
# Restart server (auto-recreates from filesystem)
npm run start:dev
```
### Schema Updates
Schema changes require:
1. Update `fastapi_app/lib/core/db_schema.py`
2. Add migration logic in `initialize_database()`
3. Test with clean database and existing database
### Backup
```bash
# 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/`
```bash
# 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:
```python
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](architecture.md) - Complete system architecture
- [Configuration Management](configuration.md) - Config files and CLI
- [Access Control](access-control.md) - RBAC and collection-based access
- [Collections](collections.md) - Collection management system
- [Testing](testing.md) - Testing infrastructure