SQL Plugin
Comprehensive guide to the ElizaOS SQL plugin for database management and schema extensions
The SQL plugin (@elizaos/plugin-sql
) is the foundational database plugin for ElizaOS, providing a complete database abstraction layer with support for PostgreSQL and PGlite, dynamic schema migrations, and plugin-based schema extensions.
Overview
The SQL plugin provides:
- Database Adapters: Support for PostgreSQL (production) and PGlite (development/testing)
- Dynamic Schema System: Type-safe schema definitions using Drizzle ORM
- Plugin Schema Extensions: Allow other plugins to extend the database schema
- Vector Search: Built-in support for embeddings and similarity search
- Migration Management: Automatic schema migrations with versioning
- Connection Pooling: Efficient database connection management
The SQL plugin MUST be loaded first in your plugin list as it provides the database adapter that other plugins depend on.
Installation
npm install @elizaos/plugin-sql
pnpm add @elizaos/plugin-sql
bun add @elizaos/plugin-sql
Configuration
Environment Variables
Configure the plugin using environment variables:
# For PostgreSQL (production)
POSTGRES_URL=postgresql://user:password@localhost:5432/eliza
# For PGlite (development)
PGLITE_PATH=./.eliza/.elizadb
# or
DATABASE_PATH=./.eliza/.elizadb
# Connection pool settings (PostgreSQL only)
POSTGRES_MAX_CONNECTIONS=20
POSTGRES_IDLE_TIMEOUT=30000
POSTGRES_CONNECTION_TIMEOUT=5000
Basic Usage
import { plugin as sqlPlugin } from '@elizaos/plugin-sql';
import { plugin as bootstrapPlugin } from '@elizaos/plugin-bootstrap';
const runtime = new AgentRuntime({
character: myCharacter,
plugins: [
sqlPlugin, // Must be first
// ... other plugins
bootstrapPlugin // Should be last
],
});
await runtime.initialize();
Core Schema
The plugin provides a comprehensive set of core tables:
Available Tables
import { schema } from '@elizaos/plugin-sql';
// Core tables available
const {
agentsTable,
memoriesTable,
entitiesTable,
embeddingsTable,
roomsTable,
worldsTable,
participantsTable,
relationshipsTable,
tasksTable,
componentsTable,
logsTable
} = schema;
Table Relationships
Database Adapters
PostgreSQL Adapter
For production environments with full PostgreSQL features:
import { PgDatabaseAdapter } from '@elizaos/plugin-sql';
// Automatically configured when POSTGRES_URL is set
const adapter = new PgDatabaseAdapter(agentId, connectionManager);
// Features:
// - Connection pooling
// - pgvector extension for embeddings
// - Full transaction support
// - Concurrent access
PGlite Adapter
For development and testing with SQLite compatibility:
import { PgliteDatabaseAdapter } from '@elizaos/plugin-sql';
// Automatically configured when no POSTGRES_URL is set
const adapter = new PgliteDatabaseAdapter(agentId, clientManager);
// Features:
// - File-based storage
// - No external dependencies
// - Vector search support
// - Great for development
Schema Extensions
Creating Plugin Schemas
Plugins can extend the database schema by exporting their own tables:
Define Your Schema
Create your table definitions using Drizzle ORM:
import { pgTable, text, uuid, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { schema } from '@elizaos/plugin-sql';
export const customEventsTable = pgTable('my_plugin_events', {
id: uuid('id').defaultRandom().primaryKey(),
agentId: uuid('agent_id')
.references(() => schema.agentsTable.id, { onDelete: 'cascade' })
.notNull(),
eventType: text('event_type').notNull(),
payload: jsonb('payload').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => ({
agentIdIdx: index('idx_my_plugin_events_agent_id').on(table.agentId),
eventTypeIdx: index('idx_my_plugin_events_type').on(table.eventType),
}));
export const customSettingsTable = pgTable('my_plugin_settings', {
id: uuid('id').defaultRandom().primaryKey(),
agentId: uuid('agent_id')
.references(() => schema.agentsTable.id, { onDelete: 'cascade' })
.notNull()
.unique(),
settings: jsonb('settings').default({}).notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Export all tables as schema
export const myPluginSchema = {
customEventsTable,
customSettingsTable,
};
Export Schema in Plugin
Include your schema in the plugin definition:
import { Plugin } from '@elizaos/core';
import { myPluginSchema } from './schema';
export const myPlugin: Plugin = {
name: '@my-org/my-plugin',
description: 'Custom plugin with database extensions',
schema: myPluginSchema, // This enables automatic migrations
init: async (config, runtime) => {
// Plugin initialization
console.log('Plugin initialized with schema extensions');
},
};
Use Your Tables
Access your tables in plugin code:
import { myPluginSchema } from './schema';
export const logEventAction = {
name: 'LOG_EVENT',
handler: async (runtime, message) => {
const db = runtime.adapter.db;
// Insert event
await db.insert(myPluginSchema.customEventsTable).values({
agentId: runtime.agentId,
eventType: 'user_action',
payload: {
message: message.content.text,
timestamp: Date.now()
},
});
// Query events
const recentEvents = await db
.select()
.from(myPluginSchema.customEventsTable)
.where(eq(myPluginSchema.customEventsTable.agentId, runtime.agentId))
.orderBy(desc(myPluginSchema.customEventsTable.createdAt))
.limit(10);
return { success: true, events: recentEvents };
},
};
Vector Search and Embeddings
The SQL plugin includes built-in support for vector embeddings:
Storing Embeddings
// Embeddings are automatically created when storing memories
const memory = {
content: { text: "Important information about the user" },
agentId: runtime.agentId,
roomId: message.roomId,
};
// This will automatically generate and store embeddings
await runtime.createMemory(memory, 'memories');
Searching with Embeddings
// Search for similar memories
const similarMemories = await runtime.searchMemories({
tableName: 'memories',
query: "user preferences",
match_threshold: 0.7,
count: 10,
roomId: message.roomId,
});
// Direct vector search
const embedding = await runtime.useModel(ModelType.TEXT_EMBEDDING, {
text: "search query"
});
const results = await runtime.adapter.searchEmbeddings({
tableName: 'memories',
embedding: embedding,
dimension: 384, // or 512, 768, 1024, 1536, 3072
threshold: 0.8,
limit: 5,
});
Multi-dimensional Support
The plugin supports multiple embedding dimensions:
// Different models produce different dimensions
const dimensions = {
'text-embedding-3-small': 384,
'text-embedding-3-medium': 768,
'text-embedding-3-large': 1024,
'text-embedding-ada-002': 1536,
'gpt-4-embedding': 3072,
};
// The appropriate dimension column is automatically selected
// based on your embedding model
Migration System
Automatic Migrations
Migrations run automatically when the runtime initializes:
// In runtime initialization
async runPluginMigrations(): Promise<void> {
const drizzle = this.adapter.db;
for (const plugin of this.plugins) {
if (plugin.schema) {
await this.adapter.runMigrations(plugin.schema, plugin.name);
}
}
}
Manual Migration Control
For production environments, you may want to control migrations:
import { DatabaseMigrationService } from '@elizaos/plugin-sql';
const migrationService = new DatabaseMigrationService();
// Initialize with database
await migrationService.initializeWithDatabase(db);
// Register plugin schemas
migrationService.discoverAndRegisterPluginSchemas(plugins);
// Run migrations with control
const pendingMigrations = await migrationService.getPendingMigrations();
console.log(`Found ${pendingMigrations.length} pending migrations`);
if (pendingMigrations.length > 0) {
await migrationService.runAllPluginMigrations();
}
Advanced Features
Connection Management
// PostgreSQL connection pooling
const connectionManager = new PostgresConnectionManager({
connectionString: process.env.POSTGRES_URL,
max: 20,
idleTimeoutMillis: 30000,
});
// Test connection health
const isHealthy = await connectionManager.testConnection();
// Get pool statistics
const stats = connectionManager.getPoolStats();
console.log(`Active connections: ${stats.totalCount}`);
Transaction Support
// Use transactions for complex operations
await runtime.adapter.withDatabase(async (db) => {
await db.transaction(async (tx) => {
// Insert entity
const [entity] = await tx.insert(entitiesTable).values({
agentId: runtime.agentId,
names: ['User123'],
}).returning();
// Create relationship
await tx.insert(relationshipsTable).values({
entityAId: runtime.agentId,
entityBId: entity.id,
type: 'knows',
});
// Create memory
await tx.insert(memoriesTable).values({
id: createUuid(),
agentId: runtime.agentId,
entityId: entity.id,
content: { text: 'Met a new user' },
type: 'interaction',
});
});
});
Custom Queries
// Raw SQL when needed
const result = await db.execute(sql`
SELECT m.*, e.dim_384 <=> ${embedding} as distance
FROM memories m
JOIN embeddings e ON m.id = e.memory_id
WHERE m.agent_id = ${agentId}
ORDER BY distance
LIMIT ${limit}
`);
// Complex aggregations
const stats = await db
.select({
type: memoriesTable.type,
count: count(),
avgLength: avg(length(memoriesTable.content)),
})
.from(memoriesTable)
.where(eq(memoriesTable.agentId, agentId))
.groupBy(memoriesTable.type);
Testing
Test Database Setup
import { createTestDatabase } from '@elizaos/plugin-sql/testing';
describe('My Plugin Tests', () => {
let testDb;
beforeEach(async () => {
testDb = await createTestDatabase(testAgentId, [myPlugin]);
});
afterEach(async () => {
await testDb.cleanup();
});
test('should create custom records', async () => {
const { adapter, runtime } = testDb;
// Your tests here
await runtime.adapter.db.insert(myPluginSchema.customEventsTable).values({
agentId: runtime.agentId,
eventType: 'test',
payload: { test: true },
});
const events = await runtime.adapter.db
.select()
.from(myPluginSchema.customEventsTable);
expect(events).toHaveLength(1);
});
});
Migration Testing
test('plugin schema migrations', async () => {
const { adapter } = testDb;
// Verify tables exist
const tables = await adapter.db.execute(sql`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
`);
const tableNames = tables.map(t => t.table_name);
expect(tableNames).toContain('my_plugin_events');
expect(tableNames).toContain('my_plugin_settings');
});
Best Practices
1. Schema Design
- Use UUIDs for primary keys
- Always reference agent_id for multi-tenant data
- Include appropriate indexes for query patterns
- Use JSONB for flexible metadata
2. Plugin Order
// Correct plugin order
const plugins = [
sqlPlugin, // First - provides database
discordPlugin, // Middle - uses database
twitterPlugin, // Middle - uses database
bootstrapPlugin, // Last - depends on others
];
3. Error Handling
try {
await runtime.adapter.createMemory(memory, 'memories');
} catch (error) {
if (error.code === '23505') {
// Handle duplicate key error
console.log('Memory already exists');
} else {
throw error;
}
}
4. Performance
- Use batch operations for bulk inserts
- Implement proper indexes for frequent queries
- Use connection pooling in production
- Consider read replicas for scaling
5. Migrations
- Test migrations thoroughly before production
- Keep migrations idempotent
- Version your schema changes
- Document breaking changes
Troubleshooting
Common Issues
Database adapter not initialized
// Ensure plugin-sql is first in the list
plugins: [sqlPlugin, ...otherPlugins]
Migration failures
// Check for migration errors
try {
await runtime.initialize();
} catch (error) {
if (error.message.includes('migration')) {
console.error('Migration failed:', error);
// Check schema compatibility
}
}
Connection pool exhaustion
# Increase pool size
POSTGRES_MAX_CONNECTIONS=50
API Reference
Plugin Export
export const plugin: Plugin = {
name: '@elizaos/plugin-sql',
description: 'SQL database plugin with dynamic migrations',
priority: 0,
schema: schema, // All core tables
init: async (config, runtime) => {
// Initialization logic
},
};
Schema Exports
import { schema } from '@elizaos/plugin-sql';
// Available tables
schema.agentsTable
schema.memoriesTable
schema.entitiesTable
schema.embeddingsTable
schema.roomsTable
schema.worldsTable
schema.participantsTable
schema.relationshipsTable
schema.tasksTable
schema.componentsTable
schema.logsTable
Adapter Methods
interface IDatabaseAdapter {
// Lifecycle
init(): Promise<void>;
close(): Promise<void>;
isReady(): Promise<boolean>;
// Schema
runMigrations(schema: any, pluginName: string): Promise<void>;
// Direct access
db: DrizzleDatabase;
withDatabase<T>(fn: (db: DrizzleDatabase) => Promise<T>): Promise<T>;
}
Conclusion
The SQL plugin provides a robust, type-safe foundation for database operations in ElizaOS. With support for both PostgreSQL and PGlite, dynamic schema migrations, and comprehensive plugin extensions, it enables developers to build scalable agent systems with proper data persistence and management.