This guide outlines the process for adding support for a new database (e.g., Oracle, CouchDB, etcd) to Query Pilot.
Query Pilot uses a Unified Adapter Architecture with capability-based traits:
- Backend (
BaseCapability+ paradigm trait): Handles connection lifecycle, execution, and streaming. - Frontend (
BaseAdapter+ paradigm interface): Handles SQL generation (for SQL DBs), introspection queries, and UI operations.
Location: src-tauri/src/adapters/<dbname>/
Choose the appropriate paradigm trait based on your database type:
| Paradigm | Trait | Examples |
|---|---|---|
| SQL | SqlQueryable |
PostgreSQL, MySQL, SQLite, SQL Server |
| Document | DocumentQueryable |
MongoDB, CouchDB, Firestore |
| Key-Value | RichKeyValueOperable or KeyValueOperable |
Redis, etcd, Memcached |
src-tauri/src/adapters/<dbname>/
├── mod.rs # Public exports
├── adapter.rs # The adapter implementation
├── types.rs # Type mapping (DB types -> CellValue)
└── direct_msgpack.rs # Streaming encoder (optional)
All adapters must implement BaseCapability from src-tauri/src/core/capabilities.rs:
use async_trait::async_trait;
use crate::core::capabilities::{BaseCapability, AdapterCapability, CapabilityTestResult};
use crate::error::AppError;
use crate::types::ConnectionProfile;
pub struct CouchDbAdapter {
client: Arc<tokio::sync::RwLock<Option<CouchClient>>>,
}
#[async_trait]
impl BaseCapability for CouchDbAdapter {
async fn connect(&self, profile: &ConnectionProfile) -> Result<(), AppError> {
// Establish connection using interior mutability
let client = /* create client from profile */;
*self.client.write().await = Some(client);
Ok(())
}
async fn disconnect(&self) -> Result<(), AppError> {
if let Some(client) = self.client.write().await.take() {
// cleanup
}
Ok(())
}
async fn test_connection(&self) -> Result<CapabilityTestResult, AppError> {
// Test and return latency
}
fn is_connected(&self) -> bool {
// Check connection state synchronously
}
fn get_capabilities(&self) -> Vec<AdapterCapability> {
vec![AdapterCapability::DocumentQueryable]
}
}#[async_trait]
impl DocumentQueryable for CouchDbAdapter {
async fn find_documents(
&self,
collection: &str,
filter: Value,
options: FindOptions,
) -> Result<Vec<Value>, AppError> {
// Implementation
}
// ... other DocumentQueryable methods
}File: src-tauri/src/core/manager.rs
impl UnifiedAdapter {
pub fn couchdb(adapter: CouchDbAdapter) -> Self {
let boxed = Box::new(adapter);
let ptr = &*boxed as *const CouchDbAdapter;
let doc_ptr: *const dyn DocumentQueryable = ptr;
Self {
inner: boxed,
sql: None,
document: Some(doc_ptr),
keyvalue: None,
// ... other fields
db_type: DbType::CouchDB,
}
}
}File: src-tauri/src/core/manager.rs
fn create_adapter(profile: &ConnectionProfile) -> Result<UnifiedAdapter> {
match profile.db_type {
// ... existing adapters
DbType::CouchDB => Ok(UnifiedAdapter::couchdb(CouchDbAdapter::new())),
}
}File: src-tauri/src/types.rs
pub enum DbType {
// ... existing
CouchDB,
}Location: src/adapters/<dbname>/ or src/adapters/dialects/<DbName>Adapter.ts
Implement the DatabaseAdapter interface:
import { DatabaseAdapter } from '../types';
export class OracleAdapter implements DatabaseAdapter {
constructor(private connectionId: string) {}
// Metadata queries
getTablesQuery(schema?: string): string { ... }
getColumnsQuery(schema: string, table: string): string { ... }
// DML generation
insert(target: TableRef, row: RowData): string { ... }
update(target: TableRef, data: RowData, where: WhereClause): string { ... }
// DDL generation
addColumn(target: TableRef, payload: ColumnAddPayload): string { ... }
}Implement the BaseAdapter interface and paradigm-specific methods:
import { BaseAdapter } from '../types';
export class CouchDBAdapter implements BaseAdapter {
connectionId: string;
dbType: DbType;
paradigm: 'document' = 'document';
// Document operations call Tauri commands
async findDocuments(collection: string, filter: object): Promise<Document[]> {
return invoke('document_execute', {
connId: this.connectionId,
op: { type: 'Find', collection, filter }
});
}
}File: src/adapters/index.ts
For SQL adapters, add to sqlAdapterModules:
const sqlAdapterModules = {
[DbType.Oracle]: () => import('./dialects/OracleAdapter').then(m => m.OracleAdapter),
};For Document/KeyValue, the getAdapter function routes by paradigm automatically.
Location: src-tauri/src/commands/<paradigm>.rs
For new paradigm-specific commands:
// commands/document.rs
#[tauri::command]
pub async fn couchdb_specific_operation(
conn_id: String,
// ... params
state: State<'_, AppState>,
) -> Result<SomeResult, String> {
// Implementation
}Register in src-tauri/src/lib.rs:
.invoke_handler(tauri::generate_handler![
// ... existing commands
commands::document::couchdb_specific_operation,
])-
BaseCapabilityimplemented withconnect/disconnectusing interior mutability - Paradigm trait implemented (
SqlQueryable,DocumentQueryable, orRichKeyValueOperable) -
UnifiedAdapterconstructor added - Factory registration added
-
DbTypevariant added - SSL/TLS handling correct
- Connection pooling appropriate for the database
- Error handling uses
AppError
- Adapter implements correct interface (
DatabaseAdapterfor SQL,BaseAdapterfor others) - Type guards work (
isSqlAdapter,isDocumentAdapter,isKeyValueAdapter) - Registered in adapter factory
- Can connect/disconnect in UI
- Can browse data (tables/collections/keys)
- Can execute queries/operations
- SSH tunneling works (if applicable)
┌─────────────────────────────────────────────────────────────────────┐
│ UnifiedAdapter │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ inner: Box<dyn BaseCapability> │ │
│ │ sql: Option<*const dyn SqlQueryable> │ │
│ │ document: Option<*const dyn DocumentQueryable> │ │
│ │ keyvalue: Option<*const dyn RichKeyValueOperable> │ │
│ │ db_type: DbType │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ Methods: │
│ - connect(), disconnect(), test_connection(), is_connected() │
│ - as_sql() -> Option<&dyn SqlQueryable> │
│ - as_document() -> Option<&dyn DocumentQueryable> │
│ - as_keyvalue() -> Option<&dyn RichKeyValueOperable> │
└─────────────────────────────────────────────────────────────────────┘
No changes needed to UnifiedAdapter methods, commands/ routing, or frontend getAdapter() when adding new databases of an existing paradigm.