Driver Creation
Field Manual Section 9 - Armored Engineering
Opening a new battlefront means forging a fresh Driver — the armored bridge between Tank's high‑level abstractions and a database engine's trenches (type mapping, prepared semantics, transaction doctrine). This section boots a driver crate from cold steel to live fire, then certifies it on the proving ground (tank-tests).
Mission Objectives
- Stand up a new
tank-<backend>crate - Implement core traits:
Driver,Connection+Executor,Transaction+Executor,Prepared,SqlWriter - Specialize dialect printing (override only what diverges from the default functions)
- Integrate with shared test suite (
tank-tests), gate unsupported munitions with feature flags - Ship a lean, consistent crate aligned with existing armor plating
Battlefield Topography
A driver is a thin composite of five moving parts:
| Trait | Purpose |
|---|---|
Driver | Public entry point for all the database abstractions |
Connection | Live session running queries and possibly starting a transaction |
Transaction | Abstraction over transactional database capabilities, borrows mutably a connection |
Prepared | Owns a compiled statement, binds positional parameters |
SqlWriter | Converts Tank's operations and semantic AST fragments into backend query language |
All other machinery (entities, expressions, joins) already speak through these interfaces.
Forge the Crate
Create tank-yourdb in your favorite source repository.
Cargo.toml template (adjust backend dependency + features):
[package]
name = "tank-yourdb"
description = "YourDB driver implementation for Tank: the Rust data layer"
version = "0.1.0"
authors = ["your_name <email@address.com>"]
license = "Apache-2.0"
edition = "2024"
repository = "https://github.com/your_name/tank-yourdb"
categories = ["database"]
[features]
# Example: default = ["bundled"]
# Add backend-specific toggles; keep minimal.
[dependencies]
log = "0"
rust_decimal = "1"
tank-core = "0"
time = "0" # if supported
url = "2"
uuid = "1"
# yourdb-sys = "1"
[dev-dependencies]
tank-tests = { version = "0", features = ["disable-transactions"] }
tokio = "1"Assembly Steps
1. The Driver Shell
use crate::{YourDBConnection, YourDBPrepared, YourDBSqlWriter, YourDBTransaction};
use tank_core::Driver;
#[derive(Debug, Clone, Copy, Default)]
pub struct YourDBDriver;
impl YourDBDriver {
pub const fn new() -> Self {
Self
}
}
impl Driver for YourDBDriver {
type Connection = YourDBConnection;
type SqlWriter = YourDBSqlWriter;
type Prepared = YourDBPrepared;
type Transaction<'c> = YourDBTransaction<'c>;
const NAME: &'static str = "yourdb";
fn sql_writer(&self) -> Self::SqlWriter {
YourDBSqlWriter::default()
}
}2. Connection + Executor
Responsibilities:
- Validate / parse URL (enforce
yourdb://prefix) - Open / pool backend session(s)
- Implement
prepare(compile statement) &run(streamQueryResult::{Row,Affected}) - Optionally implement fast-path bulk
append(DuckDB style)
Skeleton:
use crate::{YourDBDriver, YourDBPrepared, YourDBTransaction};
use std::borrow::Cow;
use tank_core::{
AsQuery, Connection, Driver, Error, Executor, Query, QueryResult, Result,
stream::{self, Stream},
};
pub struct YourDBConnection {}
impl Executor for YourDBConnection {
type Driver = YourDBDriver;
fn driver(&self) -> &Self::Driver {
&YourDBDriver {}
}
async fn prepare(&mut self, query: String) -> Result<Query<Self::Driver>> {
// Return Err if not supported
Ok(Query::Prepared(YourDBPrepared::new()))
}
fn run<'s>(
&'s mut self,
query: impl AsQuery<Self::Driver> + 's,
) -> impl Stream<Item = Result<QueryResult>> + Send {
stream::iter([])
}
}
impl Connection for YourDBConnection {
async fn connect(url: Cow<'static, str>) -> Result<YourDBConnection> {
let context = || format!("While trying to connect to `{}`", url);
let prefix = format!("{}://", <Self::Driver as Driver>::NAME);
if !url.starts_with(&prefix) {
let error = Error::msg(format!(
"YourDB connection url must start with `{}`",
&prefix
))
.context(context());
log::error!("{:#}", error);
return Err(error);
}
Ok(YourDBConnection {})
}
#[allow(refining_impl_trait)]
async fn begin(&mut self) -> Result<YourDBTransaction<'_>> {
Err(Error::msg("Transactions are not supported by YourDB"))
}
}3. Prepared Ordnance
Implement parameter binding according to backend type system. Convert each Rust value from AsValue into the native representation.
use std::fmt::{self, Display, Formatter};
use tank_core::{AsValue, Prepared, Result};
#[derive(Debug)]
pub struct YourDBPrepared {
pub(crate) index: u64,
}
impl YourDBPrepared {
pub(crate) fn new() -> Self {
Self { index: 1 }
}
}
impl Prepared for YourDBPrepared {
fn clear_bindings(&mut self) -> Result<&mut Self> {
// Clear
Ok(self)
}
fn bind(&mut self, value: impl AsValue) -> Result<&mut Self> {
let index = self.index;
self.index += 1;
self.bind_index(value, index)
}
fn bind_index(&mut self, value: impl AsValue, index: u64) -> Result<&mut Self> {
Ok(self)
}
}
impl Display for YourDBPrepared {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("YourDBPrepared")
}
}4. Dialect Scribe (SqlWriter)
Override only differences from the generic fallback:
- Identifier quoting style
- Column type mapping
- Literal escaping quirks (BLOB, INTERVAL, UUID, arrays)
- Parameter placeholder (override
write_expression_operand_question_mark) if not? - Schema operations (skip if engine lacks schemas like SQLite)
- Upsert syntax via
write_insert_update_fragmentif divergence
Tip: Start from tank-core's GenericSqlWriter implementation; copy then trim.
use std::collections::BTreeMap;
use tank_core::{ColumnDef, Context, SqlWriter};
#[derive(Default)]
pub struct YourDBSqlWriter {}
impl SqlWriter for YourDBSqlWriter {
fn as_dyn(&self) -> &dyn SqlWriter {
self
}
fn write_column_overridden_type(
&self,
_context: &mut Context,
out: &mut String,
_column: &ColumnDef,
types: &BTreeMap<&'static str, &'static str>,
) {
if let Some(t) = types
.iter()
.find_map(|(k, v)| if *k == "yourdb" { Some(v) } else { None })
{
out.push_str(t);
}
}
}5. Transactions
- Implement a
YourDBTransaction<'c>type holding a mutable borrow of the connection. - Provide
commit()androllback()on methods, ensure resource release. - Expose via
DriverassociatedTransaction<'c>type
If not supported, return relevant error messages in related functions and enable disable-transactions in tank-tests.
6. Test Range Certification
Add an integration test tests/yourdb.rs:
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use tank_core::Driver;
use tank_tests::{execute_tests, init_logs};
use tank_yourdb::YourDBDriver;
static MUTEX: Mutex<()> = Mutex::new(());
#[tokio::test]
async fn yourdb() {
init_logs();
const URL: &'static str = "yourdb://";
let _guard = MUTEX.lock().unwrap();
let driver = YourDBDriver::new();
let connection = driver
.connect(URL.into())
.await
.expect("Could not open the database");
execute_tests(connection).await;
}
}Enable feature flags to disable specific functionality until green.
Feature Flags Doctrine
tank-tests exposes opt-out switches:
disable-arrays,disable-lists,disable-maps: collections not implementeddisable-intervals: interval types absentdisable-large-integers:i128,u128unsupporteddisable-ordering: yourdb cannot order result setsdisable-references: foreign keys not enforceddisable-transactions: no transactional support
7. Tactical Checklist
- URL prefix enforced (
yourdb://) Driver::NAMEcorrect and used consistentlypreparehandles multiple statements (or rejects cleanly)- Streams drop promptly (no leaked locks / file handles)
SqlWriterprints multi‑statement sequences with proper separators and terminal;- Upsert path (
save()) works if PK exists; documented fallback if not supported
Remove a flag the moment your driver truly supports the capability. Each removed flag unlocks corresponding test sorties.
Performance Brief
- Prefer streaming APIs over buffering entire result sets.
- Implement backend bulk ingestion if native (like DuckDB's appender) for
append(). - Reuse prepared statements internally if engine offers server‑side caching.
Failure Signals
Return early with rich context:
- Wrong URL prefix: immediate
Error::msg("YourDB connection url must start withyourdb://") - Prepare failure: attach truncated query text (
truncate_long!style) to context - Bind failure: specify parameter index and offending value type
Forge the chassis. Calibrate the barrel. Roll new armor onto the field.