16 KiB
IPC Driver Runtime And Packaging
This document records the current production contract for process-based database IPC drivers.
Runtime Model
The host must keep the transport reader responsive. A driver request is routed by method and resource id:
$/ping,shutdown, and$/cancelRequestare handled directly by the runtime reader path.conn/openruns in a background blocking task. Slow connection creation must not block ping, shutdown, cancel, or connectionless requests.- Requests with a
conn_idrun on that connection's dedicated worker thread. Each connection worker is FIFO and may call synchronous database APIs. cursor_id,stream_id, andimport_idare routed back to the connection worker that created the resource.- Requests without
conn_idor a routed resource id are connectionless and run in a background blocking task, not on a connection worker.
Driver methods that do not need a database connection must not require or inject
conn_id. Otherwise they will be serialized behind that connection worker and
can freeze user-facing workflows.
Timeout and cancellation are cooperative at the protocol boundary:
extension-hostsends$/cancelRequeston request timeout or cancellation token cancellation.extension-driverinterrupts only the worker request that is currently running.- Cancelled queued or running requests are normalized to
REQUEST_CANCELLED.
Driver Layout
A driver directory contains a driver.json manifest and the executable named by
the manifest entry:
ipc-drivers/
duckdb/
driver.json
duckdb_driver
locales/
en.yml
zh-CN.yml
zh-HK.yml
The DuckDB manifest uses a relative executable path:
{
"id": "duckdb",
"name": "DuckDB",
"entry": {
"command": "./duckdb_driver",
"args": []
},
"transport": {
"name": "duckdb-driver.sock",
"connect_timeout_ms": 5000
}
}
Relative commands are resolved against the manifest directory. In development, the driver package should be built and installed by the extension repository, not by this application repository.
Discovery Order
IpcDriverRegistry::load_default() scans installed database driver extensions
from the user config directory:
<config-dir>/extensions/database_drivers
The registry accepts both a root containing driver subdirectories and a direct
single-driver directory containing driver.json.
Packaging
The main application release workflow builds only main. IPC driver binaries
and marketplace manifests are published by extension-specific release pipelines,
currently in the external extensions repository. This repository owns the host
runtime and registry contracts, not DuckDB driver production.
UI Behavior
DuckDB is still presented as the built-in DatabaseType::DuckDB connection type.
When the duckdb IPC driver is available, the DuckDB connection path uses it internally.
The new-connection UI filters that driver out of the generic external driver
list to avoid showing duplicate DuckDB entries.
Third-party drivers are shown as ExternalDatabase entries and persist the
selected driver id in DatabaseType::External { driver_id }. The database type
is the source of truth for external driver identity.
When the connection form edits an existing external connection, it resolves the
driver id from DatabaseType::External { driver_id } and asks
create_external_connection_form_for(driver_id, ...) for that driver's manifest
form. This prevents existing external connections from falling back to a generic
external form.
Driver connection forms are manifest-driven:
ui.form.tabsmaps directly to connection form tabs, so drivers can expose SSH, SSL, advanced, or database-specific groups without host UI changes.CheckboxandFilePathare supported connection field types.visible_whenrules are preserved by the manifest bridge and enforced during rendering, validation, andextra_paramsconstruction. Invisible fields are not validated or saved.- Driver-localized labels are resolved from
ui.locales_dirfirst, then the app locale/raw fallback.
Host Integration Contract
DbManager registers one ExternalDatabasePlugin per manifest driver id. Each
plugin instance owns its concrete IpcDriverManifest and serves only that
driver. DatabaseType::External { driver_id } is routed to the matching plugin.
DatabaseType::DuckDB remains a built-in-facing type for users, but the manager
may back it with the duckdb IPC driver when the manifest is available. This is
a compatibility bridge for DuckDB only; future third-party databases should use
DatabaseType::External { driver_id }.
The connection config sent over IPC includes both:
database_type_key: the storage key such asDuckDBorExternal:iotdb.driver_id: the concrete external driver id.
SSH Tunnel Contract
SSH tunneling is host-managed. IPC drivers must not implement their own SSH client, key loading, agent integration, keepalive, reconnect, or port-forward logic.
When a database connection enables SSH tunneling, the host opens a local port
forward before calling the driver. The driver still receives a normal
conn/open request. The config.host and config.port fields in that request
are the authoritative endpoint the driver must connect to:
{
"driver_id": "iotdb",
"config": {
"host": "127.0.0.1",
"port": 15432,
"username": "root",
"password": "secret",
"database": "main",
"extra_params": {
"ssh_tunnel_enabled": "true",
"ssh_target_host": "db.internal",
"ssh_target_port": "6667"
}
}
}
Driver rules:
- Always connect to
config.hostandconfig.port; do not rebuild the endpoint fromextra_params, stored form fields, or driver defaults. - Use the same endpoint resolution for
conn/openandconn/test. If a driver implementsconn/test, it must testconfig.hostandconfig.portexactly asconn/openwould use them. - Ignore SSH-specific fields such as
ssh_tunnel_enabled,ssh_connection_id,ssh_host,ssh_port,ssh_username,ssh_auth_type,ssh_password,ssh_private_key_path,ssh_private_key_passphrase,ssh_target_host,ssh_target_port, andssh_timeoutfor network dialing. These fields are host inputs, not driver inputs. - Do not call
host/ssh/open_tunnelfrom process-based database drivers for ordinary database connections. The host owns the tunnel lifecycle and keeps it alive for the database connection. - Treat
ConnOptions.ssh_tunnel.session_refas a future host/runtime contract, not as a requirement for current driver implementations. Current IPC database drivers should use the mappedconfig.hostandconfig.port. - If TLS hostname verification is required and the database is reached through
127.0.0.1, expose or honor a driver-specific TLS server-name/hostname override field instead of using SSH fields to recover the original remote address.
Manifest rules:
- A driver may expose SSH fields in its connection form so users can configure a host-managed tunnel.
- Use the field ids above when the form is intended to interoperate with the host-managed tunnel resolver.
- The driver process must still ignore those SSH fields during dialing; their only purpose is to let the host create the local forwarding endpoint before the request reaches the driver.
Manifest SQL And Dialect Contract
The manifest dialect is the host SQL-generation contract for external drivers. It currently includes:
identifier_quote_leftandidentifier_quote_right, with right-quote escaping. This supports asymmetric quoting such as[name].limit_style:limit_offsetoroffset_fetch.bool_trueandbool_falseliterals.explain_template, used as the fallback SQL carried withsql/explain.table_reference_schema_mode:autoorprefer_schema. Useprefer_schemawhen host-generated object SQL should qualify tables asschema.tableeven if the driver also treats schemas as database-like navigation nodes.row_id_columnandrow_id_alias, used by host-generated table-data SQL to expose a stable hidden row identifier for editable result sets.default_order_by, used when paginated table-data SQL needs a deterministic order and the request did not specify one.
Host-side SQL builders must use the driver dialect uniformly. The external
plugin's local fallback covers column changes and index changes, including
DROP INDEX IF EXISTS ... and CREATE [UNIQUE] INDEX .... When a driver
declares DDL methods such as ddl/build_create_table or
ddl/build_alter_table, async table designer paths ask the driver first and
fall back to local SQL only when the method is not supported.
sql/explain is connection-bound. The host builds a wire pseudo-SQL request
containing the original SQL plus dialect fallback SQL so the driver can either
execute native explain behavior or use the fallback template.
Object View Header Customization
Drivers can customize the object-list table rendered by the database object
panel with the optional schema/object_view method. This method is
connection-bound and is called before the host falls back to the older
schema/databases, schema/objects, schema/columns, schema/indexes, and
other fixed mappings.
Declare the method in driver.json when the driver implements it:
{
"methods": [
"schema/object_view",
"schema/databases",
"schema/objects",
"schema/columns"
]
}
If the method is absent from methods, or the driver returns the standard
not-supported error for this method, the host keeps the legacy object view
behavior. This makes the method safe to add incrementally.
Request
The host sends schema/object_view with these params:
{
"conn_id": 12,
"view": "columns",
"database": "main",
"schema": "public",
"table": "events"
}
Fields:
conn_id: injected by the host for connection-bound routing.view: one ofdatabases,schemas,tables,columns,indexes,views,functions,procedures,triggers, orsequences.database: present when the requested view is scoped to a database.schema: present when the requested view is scoped to a schema.table: present for table-scoped views such ascolumnsandindexes.
Response
Return the full table shape the host should render:
{
"title": "Columns",
"columns": [
{ "key": "name", "name": "Field", "width_px": 220 },
{ "key": "type", "name": "Type", "width_px": 160 },
{ "key": "nullable", "name": "Null?", "width_px": 72, "align": "right" },
{ "key": "comment", "name": "Comment", "width_px": 260 }
],
"rows": [
["id", "BIGINT", "false", "primary key"],
["payload", "JSON", "true", ""]
]
}
Response fields:
title: optional. Empty or omitted uses the host default title for that view.columns: required for the custom view to be used. The order is the rendered order, so omit fields the driver does not want to display.columns[].key: stable column identifier, unique within the view.columns[].name: header label shown to the user.columns[].width_px: optional width in pixels. Omit it to use the host default width.columns[].align: optional text alignment:left,center, orright. Omit it for left alignment.rows: each row is an array of strings aligned withcolumns.
The host normalizes row length to the number of rendered columns: extra values are ignored and missing values become empty strings. Drivers should still return exactly one value per column because it is easier to inspect and test.
Choosing Fields
The driver owns the display field set for schema/object_view. For example, a
time-series driver can render measurements with name, tags, fields, and
retention_policy instead of forcing the generic Name / Comment columns.
The same driver can render columns as name, type, nullable,
encoding, and compression if those fields are important for that database.
Keep the first column as the object name when the row represents a clickable database object. The current object panel uses the first cell for the row label and icon pairing.
Rust Driver Example
A driver using extension-driver can handle the method in
DriverConnection::call:
use extension_protocol::{error_codes, method, schema, ProtocolError};
use serde_json::Value;
fn call(&mut self, method_name: &str, params: &Value) -> Result<Value, ProtocolError> {
match method_name {
method::SCHEMA_OBJECT_VIEW => {
let params: schema::ObjectViewParams = serde_json::from_value(params.clone())
.map_err(|error| ProtocolError::new(
error_codes::INVALID_PARAMS,
error.to_string(),
))?;
let view = match params.view {
schema::ObjectViewKind::Columns => schema::ObjectView {
title: "Columns".into(),
columns: vec![
schema::ObjectViewColumn {
key: "name".into(),
name: "Field".into(),
width_px: Some(220.0),
align: None,
},
schema::ObjectViewColumn {
key: "nullable".into(),
name: "Null?".into(),
width_px: Some(72.0),
align: Some(schema::ObjectViewColumnAlign::Right),
},
],
rows: load_column_rows(¶ms.database, ¶ms.schema, ¶ms.table)?,
},
_ => {
return Err(ProtocolError::new(
error_codes::METHOD_NOT_FOUND,
format!("unsupported object view: {}", params.view.as_str()),
));
}
};
serde_json::to_value(view).map_err(|error| ProtocolError::new(
error_codes::INTERNAL_ERROR,
error.to_string(),
))
}
_ => Err(ProtocolError::new(
error_codes::METHOD_NOT_FOUND,
format!("unknown method: {method_name}"),
)),
}
}
Unsupported view values should return METHOD_NOT_FOUND or another error
that the driver adapter maps to DbError::NotSupported; the host treats that as
the signal to fall back to legacy rendering for that object view.
Display And Resources
Driver display metadata resolves from the manifest:
- driver id
- driver name
- icon asset path
Built-in icon names map to app assets. Relative custom driver icons become
driver://{driver_id}/icon or driver://{driver_id}/icon_color. The main app
asset source tries DriverAssetSource first and falls back to bundled GPUI
assets.
Connection tree nodes store external driver metadata for display. Extension menu
when contexts keep connection.kind == "external" for broad compatibility and
add connection.driver_id for driver-specific menu filtering.
Extensibility Rules
Manifest methods should declare supported protocol methods. Unknown standard
method names are rejected to catch typos. Driver-private methods are allowed
under the x/<driver-or-vendor>/... namespace.
When adding a new protocol method:
- Add it to
extension-protocol::method. - Decide whether it is connection-bound, resource-bound, or connectionless.
- Add route handling in
extension-driveronly if it creates or consumes a routed resource id. - Add method gating in
db::ipc::MethodSetif the DB adapter calls it. - Add a manifest declaration test in the driver.
Verification
Useful targeted checks:
cargo test -p extension-driver -- --nocapture
cargo test -p db ipc:: -- --nocapture
cargo test -p db manager::tests -- --nocapture
cargo test -p db_view common:: -- --nocapture
cargo test -p db_view database_view_plugin::tests -- --nocapture
cargo test -p db_view db_tree_view::tests -- --nocapture
cargo test -p db_view connection_form_window::tests -- --nocapture
cargo test -p db_view extension_menu -- --nocapture
cargo test -p db_view table_designer_tab::tests -- --nocapture
cargo test -p extension-runtime database_driver_install -- --nocapture
cargo test -p main new_connection::connection_kind::tests::external_database_kinds_skip_builtin_duckdb_external_driver -- --nocapture
cargo check -p one-core -p db -p db_view -p extension-runtime -p main
cargo fmt --check
git diff --check