DB Schema Surgeon
Schema management tool for PostgreSQL that supports self-healing startup workflows.
Install
mkdir -p .claude/skills/db-schema-surgeon && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/15705" && unzip -o skill.zip -d .claude/skills/db-schema-surgeon && rm skill.zipInstalls to .claude/skills/db-schema-surgeon
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Gestión del esquema PostgreSQL, Auto-Healing y arquitectura RAG híbrida.Key capabilities
- →Auto-create non-existent PostgreSQL tables on startup
- →Define SQLAlchemy models for database tables
- →Execute idempotent migration scripts for schema changes
- →Manage RAG document metadata in PostgreSQL
- →Synchronize RAG vectors with Supabase
- →Implement dual delete protocol for RAG documents
How it works
The skill employs a self-healing mechanism that creates missing tables on application startup and applies idempotent migration scripts for schema modifications.
Inputs & outputs
When to use DB Schema Surgeon
- →Auto-initialize database tables on startup
- →Develop RAG architectures with PostgreSQL
- →Manage schema updates during development
- →Replace complex migration systems
About this skill
DB Schema Surgeon - Platform AI Solutions
1. Filosofía Auto-Healing (NO Alembic)
El Protocolo
Platform AI Solutions usa Self-Healing: el esquema se auto-repara en cada startup.
# orchestrator_service/main.py
from app.models import Base
from app.db import engine
@app.on_event("startup")
async def startup_event():
async with engine.begin() as conn:
# Crea SOLO las tablas que no existen
await conn.run_sync(Base.metadata.create_all)
# Ejecutar migraciones de esquema
await run_migration_steps()
Ventajas
- Sin historial de migraciones complejo
- Desarrollo rápido (cambios inmediatos)
- Funciona igual en local, staging y producción
- Idempotente (
create_allsolo crea lo que falta)
2. Crear Nuevas Tablas
Paso 1: Definir Modelo SQLAlchemy
# app/models/business_asset.py
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String, Integer, JSONB, ForeignKey
from datetime import datetime
from .base import Base
class BusinessAsset(Base):
__tablename__ = "business_assets"
id: Mapped[str] = mapped_column(String, primary_key=True) # UUID
tenant_id: Mapped[str] = mapped_column(String(50), nullable=False)
asset_type: Mapped[str] = mapped_column(String(50)) # branding, scripts, roi
content: Mapped[dict] = mapped_column(JSONB, nullable=False)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
is_active: Mapped[bool] = mapped_column(default=True)
Paso 2: Importar en main.py
# main.py
from app.models import (
Base,
Tenant,
Agent,
BusinessAsset, # ← Agregar aquí
Credential
)
# Esto asegura que SQLAlchemy conozca todos los modelos
Paso 3: Reiniciar
Al reiniciar el servicio, create_all ejecutará y la tabla se creará automáticamente.
3. Modificar Tablas Existentes
El Problema
create_all NO altera tablas existentes.
Para agregar columnas, cambiar tipos, o renombrar: Migration Script
Solución: Migration Steps
# orchestrator_service/scripts/migration_steps.py
from sqlalchemy import text
async def run_migration_steps(engine):
"""Migraciones idempotentes"""
async with engine.begin() as conn:
# Migración 1: Agregar columna
await conn.execute(text("""
ALTER TABLE agents
ADD COLUMN IF NOT EXISTS template_type VARCHAR(50) DEFAULT 'custom'
"""))
# Migración 2: Crear índice
await conn.execute(text("""
CREATE INDEX IF NOT EXISTS idx_agents_tenant_active
ON agents(tenant_id, is_active)
"""))
# Migración 3: Agregar columna JSONB
await conn.execute(text("""
ALTER TABLE tenants
ADD COLUMN IF NOT EXISTS tool_config JSONB DEFAULT '{}'::jsonb
"""))
Invocar en Startup
# main.py
from scripts.migration_steps import run_migration_steps
@app.on_event("startup")
async def startup_event():
# 1. Crear tablas nuevas
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# 2. Ejecutar migraciones
await run_migration_steps(engine)
4. Arquitectura RAG Híbrida
Separación de Responsabilidades
| Almacenamiento | Responsabilidad | Tecnología |
|---|---|---|
| PostgreSQL Local | Metadata (filename, collection, file_path) | rag_documents table |
| Supabase Remote | Vectores (embeddings) | pgvector extension |
Modelo PostgreSQL
# app/models/rag_document.py
class RAGDocument(Base):
__tablename__ = "rag_documents"
id: Mapped[str] = mapped_column(String, primary_key=True) # UUID
tenant_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("tenants.id"),
index=True
)
filename: Mapped[str] = mapped_column(String(255))
collection: Mapped[str] = mapped_column(String(100)) # General, ADN Personal, Shadow RAG
file_type: Mapped[str] = mapped_column(String(50)) # pdf, txt, docx
file_path: Mapped[str] = mapped_column(String(500), nullable=True)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
Flujo de Creación
# 1. Guardar metadata en PostgreSQL
doc = RAGDocument(
id=str(uuid.uuid4()),
tenant_id=tenant_id,
filename=filename,
collection="General",
file_type=file_extension,
file_path=storage_path
)
session.add(doc)
await session.flush() # Obtener ID sin commit final
# 2. Procesar documento
chunks = process_document(file_content)
# 3. Vectorizar y guardar en Supabase
from app.services.rag.vector_store import SupabaseVectorStore
vector_store = SupabaseVectorStore(tenant_id)
await vector_store.add_documents(
chunks,
metadata={
"tenant_id": tenant_id,
"source_id": doc.id,
"collection": collection
}
)
# 4. Commit
await session.commit()
Flujo de Eliminación (Dual Delete Protocol)
CRÍTICO: Mantener coherencia entre PostgreSQL y Supabase
# Paso 1: Surgical Strike (Remoto) - Eliminar vectores de Supabase
await supabase.from_("documents").delete().eq(
"metadata->>source_id", doc_id
).execute()
# Paso 2: Metadata Cleanup (Local) - Eliminar de PostgreSQL
stmt = delete(RAGDocument).where(
RAGDocument.id == doc_id,
RAGDocument.tenant_id == tenant_id
)
await session.execute(stmt)
# Paso 3: Physical Sweep (Disco) - Best effort
try:
os.remove(file_path)
except FileNotFoundError:
logger.warning(f"File {file_path} not found, skipping physical delete")
await session.commit()
5. Tablas Core (Schema Reference)
credentials (The Vault)
CREATE TABLE IF NOT EXISTS credentials (
id_uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id INTEGER REFERENCES tenants(id),
name TEXT NOT NULL,
value TEXT NOT NULL, -- Encrypted AES-256
category TEXT DEFAULT 'general', -- openai, google, smtp
scope TEXT DEFAULT 'global', -- global o tenant
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(name, tenant_id) -- Unicidad multi-tenant
);
agents (AI Configuration)
CREATE TABLE IF NOT EXISTS agents (
id SERIAL PRIMARY KEY,
tenant_id INTEGER REFERENCES tenants(id),
name TEXT NOT NULL,
role TEXT DEFAULT 'sales',
model_provider TEXT DEFAULT 'openai',
model_version TEXT DEFAULT 'gpt-5-mini',
temperature FLOAT DEFAULT 0.7,
system_prompt_template TEXT NOT NULL,
enabled_tools JSONB DEFAULT '[]',
channels JSONB DEFAULT '["whatsapp", "instagram", "facebook", "web"]',
config JSONB DEFAULT '{}',
template_type VARCHAR(50) DEFAULT 'custom',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
chat_conversations (Omnichannel)
CREATE TABLE IF NOT EXISTS chat_conversations (
id UUID PRIMARY KEY,
tenant_id INTEGER REFERENCES tenants(id),
channel VARCHAR(32) NOT NULL, -- whatsapp, instagram, facebook, web
channel_source VARCHAR(32) DEFAULT 'whatsapp',
display_name VARCHAR(255),
meta JSONB DEFAULT '{}',
last_message_preview TEXT,
last_message_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
tools (Brain Extensions)
CREATE TABLE IF NOT EXISTS tools (
id SERIAL PRIMARY KEY,
tenant_id INTEGER REFERENCES tenants(id), -- NULL para Global
name VARCHAR(255) NOT NULL,
type VARCHAR(32) NOT NULL, -- http, internal
description TEXT,
prompt_injection TEXT,
response_guide TEXT,
config JSONB DEFAULT '{}',
service_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, name)
);
6. Tipos de Datos Comunes
from sqlalchemy import String, Integer, Boolean, DateTime, Text, JSONB, ARRAY
class ExampleModel(Base):
__tablename__ = "example"
# IDs
id_uuid: Mapped[str] = mapped_column(String, primary_key=True)
id_int: Mapped[int] = mapped_column(Integer, primary_key=True)
# Texto
short_text: Mapped[str] = mapped_column(String(255))
long_text: Mapped[str] = mapped_column(Text)
# JSON
metadata: Mapped[dict] = mapped_column(JSONB, default={})
# Arrays
tags: Mapped[list] = mapped_column(JSONB, default=[])
# Booleanos
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# Timestamps
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
7. Identity Resolution (UUID vs INTEGER)
El Problema Crítico
- Tenants:
ides INTEGER - Users:
ides UUID,tenant_ides INTEGER - Agents, Tools, Credentials:
tenant_ides INTEGER (foreign key)
La Solución
# NUNCA asumir que current_user.tenant_id es UUID
# SIEMPRE resolver desde tabla users
user_row = await db.pool.fetchrow(
"SELECT tenant_id FROM users WHERE id = $1",
current_user.id # UUID
)
real_tenant_int = user_row['tenant_id'] # INTEGER
# Usar en queries
stmt = delete(Agent).where(Agent.tenant_id == real_tenant_int)
8. Índices y Performance
Crear Índices
from sqlalchemy import Index
class Message(Base):
__tablename__ = "messages"
# ... columnas ...
__table_args__ = (
Index('idx_messages_conversation', 'conversation_id', 'created_at'),
Index('idx_messages_tenant', 'tenant_id'),
)
Eager Loading (Evitar N+1)
from sqlalchemy.orm import selectinload
# ❌ MAL - N+1 queries
conversations = await session.execute(select(Conversation))
for conv in conversations:
messages = conv.messages # Query adicional
# ✅ BIEN - Single query
stmt = select(Conversation).options(
selectinload(Conversation.messages)
)
conversations = await session.execute(stmt)
9. Colecciones RAG
Tipos de Colecciones
- General: Manuales técnicos, políticas (PDF/DOCX)
- *ADN Personal
Content truncated.
When not to use it
- →When `create_all` is expected to alter existing tables
- →When `tenant_id` is not consistently typed (UUID vs. INTEGER)
- →When `IF NOT EXISTS` is not used in migration scripts
Limitations
- →`create_all` does not alter existing tables
- →Requires explicit migration scripts for altering tables
- →Requires careful synchronization between PostgreSQL and Supabase for RAG data
How it compares
This approach uses an auto-healing schema strategy to simplify database management, contrasting with traditional migration systems that rely on versioned migration files.
Compared to similar skills
DB Schema Surgeon side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| DB Schema Surgeon (this skill) | 0 | 6mo | No flags | Advanced |
| environment-setup | 0 | 6mo | Caution | Beginner |
| drizzle-orm | 32 | 2mo | No flags | Intermediate |
| database-design | 6 | 6mo | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
You might also like
environment-setup
studentdotai
Complete setup for uv, GDAL 3.10.3, PostGIS, and all project dependencies. Use when setting up development environment, installing GDAL, or configuring PostGIS backend.
drizzle-orm
EpicenterHQ
Drizzle ORM patterns for type branding and custom types. Use when working with Drizzle column definitions, branded types, or custom type conversions.
database-design
davila7
Database design principles and decision-making. Schema design, indexing strategy, ORM selection, serverless databases.
database-schema-designer
davila7
Design robust, scalable database schemas for SQL and NoSQL databases. Provides normalization guidelines, indexing strategies, migration patterns, constraint design, and performance optimization. Ensures data integrity, query performance, and maintainable data models.
database-migrations-sql-migrations
sickn33
SQL database migrations with zero-downtime strategies for PostgreSQL, MySQL, SQL Server
postgresql-syntax-reference
pgschema
Consult PostgreSQL's parser and grammar (gram.y) to understand SQL syntax, DDL statement structure, and parsing rules when implementing pgschema features