Skip to main content
Zequel supports 9 database systems through a driver architecture. Each database has a dedicated adapter that implements the DatabaseDriver interface.

Architecture Overview

All database drivers extend or implement the DatabaseDriver interface defined in src/main/db/base.ts:
export interface DatabaseDriver {
  readonly type: DatabaseType;
  readonly isConnected: boolean;
  
  // Connection lifecycle
  connect(config: ConnectionConfig): Promise<void>;
  disconnect(): Promise<void>;
  testConnection(config: ConnectionConfig): Promise<TestConnectionResult>;
  
  // Query execution
  execute(sql: string, params?: unknown[], useTransaction?: boolean): Promise<QueryResult>;
  
  // Schema introspection
  getDatabases(): Promise<Database[]>;
  getTables(database: string, schema?: string): Promise<Table[]>;
  getColumns(table: string): Promise<Column[]>;
  getIndexes(table: string): Promise<Index[]>;
  getForeignKeys(table: string): Promise<ForeignKey[]>;
  getTableDDL(table: string): Promise<string>;
  getTableData(table: string, options: DataOptions): Promise<DataResult>;
  
  // Schema editing operations
  addColumn(request: AddColumnRequest): Promise<SchemaOperationResult>;
  modifyColumn(request: ModifyColumnRequest): Promise<SchemaOperationResult>;
  dropColumn(request: DropColumnRequest): Promise<SchemaOperationResult>;
  renameColumn(request: RenameColumnRequest): Promise<SchemaOperationResult>;
  
  createIndex(request: CreateIndexRequest): Promise<SchemaOperationResult>;
  dropIndex(request: DropIndexRequest): Promise<SchemaOperationResult>;
  
  addForeignKey(request: AddForeignKeyRequest): Promise<SchemaOperationResult>;
  dropForeignKey(request: DropForeignKeyRequest): Promise<SchemaOperationResult>;
  
  createTable(request: CreateTableRequest): Promise<SchemaOperationResult>;
  dropTable(request: DropTableRequest): Promise<SchemaOperationResult>;
  renameTable(request: RenameTableRequest): Promise<SchemaOperationResult>;
  
  // Row operations
  insertRow(request: InsertRowRequest): Promise<SchemaOperationResult>;
  deleteRow(request: DeleteRowRequest): Promise<SchemaOperationResult>;
  updateRow(request: UpdateRowRequest): Promise<SchemaOperationResult>;
  
  // View operations
  createView(request: CreateViewRequest): Promise<SchemaOperationResult>;
  dropView(request: DropViewRequest): Promise<SchemaOperationResult>;
  renameView(request: RenameViewRequest): Promise<SchemaOperationResult>;
  getViewDDL(viewName: string): Promise<string>;
  
  // Routines (stored procedures/functions)
  getRoutines(type?: RoutineType): Promise<Routine[]>;
  getRoutineDefinition(name: string, type: RoutineType): Promise<string>;
  
  // Triggers
  getTriggers(table?: string): Promise<Trigger[]>;
  getTriggerDefinition(name: string, table?: string): Promise<string>;
  createTrigger(request: CreateTriggerRequest): Promise<SchemaOperationResult>;
  dropTrigger(request: DropTriggerRequest): Promise<SchemaOperationResult>;
  
  // User management
  getUsers(): Promise<DatabaseUser[]>;
  createUser(request: CreateUserRequest): Promise<SchemaOperationResult>;
  dropUser(request: DropUserRequest): Promise<SchemaOperationResult>;
  
  // Cursor support (streaming)
  createCursor(sql: string, chunkSize: number): Promise<string>;
  createTableCursor(table: string, options: DataOptions, chunkSize: number): Promise<string>;
  
  // Data types
  getDataTypes(): DataTypeInfo[];
}

Existing Drivers

DatabaseDriver FileClient LibraryQuery Builder
PostgreSQLpostgres.tspg + pg-cursorKnex
MySQLmysql.tsmysql2Knex
MariaDBmariadb.tsmysql2 (extends MySQL)Knex
SQLitesqlite.tsbetter-sqlite3Knex
DuckDBduckdb.ts@duckdb/node-apiKnex (custom dialect)
MongoDBmongodb.tsmongodbNone (native driver)
Redisredis.tsioredisNone (key-value)
ClickHouseclickhouse.ts@clickhouse/clientNone (custom SQL)
SQL Serversqlserver.tsmssqlKnex

Adding a New Database

1

Add Database Type Enum

Add your database to the DatabaseType enum in src/main/types/index.ts:
export enum DatabaseType {
  PostgreSQL = 'postgresql',
  MySQL = 'mysql',
  MariaDB = 'mariadb',
  SQLite = 'sqlite',
  DuckDB = 'duckdb',
  MongoDB = 'mongodb',
  Redis = 'redis',
  ClickHouse = 'clickhouse',
  SQLServer = 'sqlserver',
  CockroachDB = 'cockroachdb',  // New!
}
2

Install Client Library

Add the Node.js client for your database:
npm install pg-cockroachdb @types/pg-cockroachdb
3

Create Driver Class

Create src/main/db/cockroachdb.ts:
import type { Knex } from 'knex';
import knex from 'knex';
import { DatabaseType } from '@main/types';
import type { ConnectionConfig, QueryResult } from '@main/types';
import type { DatabaseDriver } from './base';

export class CockroachDBDriver implements DatabaseDriver {
  readonly type = DatabaseType.CockroachDB;
  private db: Knex | null = null;
  
  get isConnected(): boolean {
    return this.db !== null;
  }
  
  async connect(config: ConnectionConfig): Promise<void> {
    this.db = knex({
      client: 'cockroachdb',
      connection: {
        host: config.host,
        port: config.port,
        database: config.database,
        user: config.username,
        password: config.password,
        ssl: config.ssl ? { rejectUnauthorized: false } : false,
      },
    });
    
    // Test connection
    await this.db.raw('SELECT 1');
  }
  
  async disconnect(): Promise<void> {
    if (this.db) {
      await this.db.destroy();
      this.db = null;
    }
  }
  
  async execute(sql: string, params?: unknown[]): Promise<QueryResult> {
    if (!this.db) throw new Error('Not connected');
    
    const start = Date.now();
    const result = await this.db.raw(sql, params);
    const executionTime = Date.now() - start;
    
    return {
      rows: result.rows,
      rowCount: result.rowCount,
      fields: result.fields?.map(f => ({
        name: f.name,
        type: f.dataTypeID,
      })),
      executionTime,
    };
  }
  
  async getDatabases(): Promise<Database[]> {
    if (!this.db) throw new Error('Not connected');
    
    const result = await this.db.raw(`
      SELECT datname as name 
      FROM pg_database 
      WHERE datistemplate = false
    `);
    
    return result.rows.map(row => ({ name: row.name }));
  }
  
  async getTables(database: string, schema = 'public'): Promise<Table[]> {
    if (!this.db) throw new Error('Not connected');
    
    const result = await this.db.raw(`
      SELECT tablename as name 
      FROM pg_tables 
      WHERE schemaname = ?
    `, [schema]);
    
    return result.rows.map(row => ({ name: row.name }));
  }
  
  // Implement remaining methods...
  // See src/main/db/postgres.ts for reference
}
4

Add to Connection Manager

Register your driver in src/main/db/manager.ts:
import { CockroachDBDriver } from './cockroachdb';

private createDriver(type: DatabaseType): DatabaseDriver {
  switch (type) {
    case DatabaseType.PostgreSQL:
      return new PostgresDriver();
    case DatabaseType.CockroachDB:
      return new CockroachDBDriver();
    // ...
  }
}
5

Add Cursor Support

Create a cursor class in src/main/db/cursors/CockroachDBCursor.ts:
import { BaseCursor } from './BaseCursor';
import type { Knex } from 'knex';

export class CockroachDBCursor extends BaseCursor {
  private cursor: any;
  
  constructor(
    private db: Knex,
    private sql: string,
    chunkSize: number
  ) {
    super(chunkSize);
  }
  
  async read(): Promise<StreamResult> {
    // Implement cursor logic
    // See PostgresCursor.ts for reference
  }
  
  async cancel(): Promise<void> {
    // Cleanup resources
  }
}
6

Add UI Assets

Add a database logo to src/renderer/assets/images/cockroachdb.svg.
7

Update Connection Form

Add CockroachDB to the database type selector in src/renderer/components/connection/DatabaseTypeCombobox.vue:
const databases = [
  { value: DatabaseType.PostgreSQL, label: 'PostgreSQL', icon: 'postgresql.svg' },
  { value: DatabaseType.CockroachDB, label: 'CockroachDB', icon: 'cockroachdb.svg' },
  // ...
];
8

Add Docker Test Container

Add to docker-compose.yml:
cockroachdb:
  image: cockroachdb/cockroach:latest
  command: start-single-node --insecure
  ports:
    - "26257:26257"
  volumes:
    - cockroachdb_data:/cockroach/cockroach-data
    - ./docker/cockroachdb/init.sql:/docker-entrypoint-initdb.d/init.sql:ro

volumes:
  cockroachdb_data:
Create docker/cockroachdb/init.sql with seed data.
9

Write Tests

Create unit tests (src/tests/unit/main/cockroachdb.test.ts) and integration tests (src/tests/integration/main/cockroachdb-seed.test.ts).

Database-Specific Features

Some databases have unique features that require custom implementations.

PostgreSQL-Specific

  • Schemas: getSchemas(), setCurrentSchema(), createSchema()
  • Sequences: getSequences(), createSequence(), alterSequence()
  • Materialized Views: getMaterializedViews(), refreshMaterializedView()
  • Extensions: getExtensions(), createExtension()
  • Enums: getEnums(), getAllEnums()
  • Encodings: getPgEncodings(), getPgCollations()

MySQL/MariaDB-Specific

  • Charsets: getCharsets(), getCollations(), setTableCharset()
  • Partitions: getPartitions(), createPartition(), dropPartition()
  • Events: getEvents(), createEvent(), alterEvent()

MongoDB-Specific (NoSQL)

MongoDB doesn’t use the Knex query builder. The driver implements collection operations:
  • getCollections() instead of getTables()
  • getDocuments() instead of getTableData()
  • insertDocument(), updateDocument(), deleteDocument()

Redis-Specific (Key-Value)

Redis has a completely different API:
  • getKeys(pattern) - List keys matching pattern
  • getValue(key) - Get value for key
  • setValue(key, value, ttl) - Set key with optional TTL
  • deleteKey(key) - Delete key
  • getKeyInfo(key) - Get type, TTL, memory usage

Query Builder: Knex vs. Raw SQL

Use Knex for SQL databases (PostgreSQL, MySQL, SQLite, etc.):
  • Type-safe query building
  • Automatic parameterization
  • Schema introspection helpers
  • Migration support
Use raw SQL for:
  • Database-specific syntax (window functions, CTEs)
  • NoSQL databases (MongoDB, Redis)
  • Databases without Knex support
Example (Knex vs. Raw):
const tables = await this.db('information_schema.tables')
  .select('table_name as name')
  .where('table_schema', schema)
  .orderBy('table_name');

Testing Your Driver

Unit Tests

Mock the client library:
// src/tests/unit/main/cockroachdb.test.ts
import { describe, it, expect, vi } from 'vitest';
import { CockroachDBDriver } from '@main/db/cockroachdb';

vi.mock('knex', () => ({
  default: vi.fn(() => ({
    raw: vi.fn(() => ({ rows: [] })),
    destroy: vi.fn(),
  })),
}));

describe('CockroachDBDriver', () => {
  it('should connect successfully', async () => {
    const driver = new CockroachDBDriver();
    await expect(driver.connect(config)).resolves.not.toThrow();
    expect(driver.isConnected).toBe(true);
  });
});

Integration Tests

Test against the Docker container:
// src/tests/integration/main/cockroachdb-seed.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { CockroachDBDriver } from '@main/db/cockroachdb';

describe('CockroachDB integration', () => {
  let driver: CockroachDBDriver;
  
  beforeAll(async () => {
    driver = new CockroachDBDriver();
    try {
      await driver.connect({
        host: 'localhost',
        port: 26257,
        database: 'defaultdb',
        username: 'root',
      });
    } catch (err) {
      console.warn('CockroachDB container not available');
    }
  });
  
  afterAll(async () => {
    if (driver?.isConnected) await driver.disconnect();
  });
  
  it('should fetch databases', async () => {
    if (!driver?.isConnected) return;
    const databases = await driver.getDatabases();
    expect(databases.length).toBeGreaterThan(0);
  });
});

Common Pitfalls

Connection Leaks: Always implement disconnect() properly and call db.destroy() or equivalent.
BigInt Serialization: Use serialize() from @main/utils/serialize to convert BigInt to strings before sending to renderer.
SQL Injection: Always use parameterized queries. Never concatenate user input into SQL strings.
Schema vs. Database: PostgreSQL uses schemas within databases. MySQL/MariaDB use databases (schemas are aliases). Handle this carefully.

Resources

PostgreSQL Driver

Reference implementation (most complete)

MongoDB Driver

Example of NoSQL driver

Knex Documentation

Query builder reference

Electron IPC

Inter-process communication guide

Next Steps

After implementing your driver:
  1. Test thoroughly with unit, integration, and E2E tests
  2. Add documentation for connection setup
  3. Create seed data for integration tests
  4. Submit a PR with clear description and screenshots

Testing Guide

Learn how to write comprehensive tests

Contributing Overview

Submit your contribution

Build docs developers (and LLMs) love