Driver Creation
Field Manual Section 10 - 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 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"
scylla = "1"
tank-core = "0"
time = "0"
url = "2"
uuid = "1"
[dev-dependencies]
tank-tests = { version = "0", features = ["disable-transactions"] }
tokio = "1"Steps
1. The Driver Shell
use crate::{YourDBConnection, YourDBPrepared, YourDBSqlWriter, YourDBTransaction};
use tank_core::Driver;
#[derive(Default, Clone, Copy, Debug)]
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 [&'static str] = &["yourdb"];
fn sql_writer(&self) -> Self::SqlWriter {
YourDBSqlWriter::default()
}
}2. Connection + Executor
Responsibilities:
- Validate and 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, Error, Executor, Query, QueryResult, Result,
stream::{self, Stream},
};
pub struct YourDBConnection {}
impl Executor for YourDBConnection {
type Driver = YourDBDriver;
async fn do_prepare(&mut self, sql: String) -> Result<Query<YourDBDriver>> {
// Return Err if not supported
Ok(Query::Prepared(YourDBPrepared::new()))
}
fn run<'s>(
&'s mut self,
query: impl AsQuery<YourDBDriver> + 's,
) -> impl Stream<Item = Result<QueryResult>> + Send {
stream::iter([])
}
}
impl Connection for YourDBConnection {
async fn connect(url: Cow<'static, str>) -> Result<Self> {
let context = || format!("While trying to connect to `{url}`");
let url = Self::sanitize_url(url);
// Establish connection
Ok(YourDBConnection {})
}
async fn begin(&mut self) -> Result<YourDBTransaction<'_>> {
Err(Error::msg("Transactions are not supported by YourDB"))
}
}3. Prepared Statements
Implement parameter binding according to the backend type system. Convert each Rust value (via 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: 0 }
}
}
impl Prepared for YourDBPrepared {
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any> {
self
}
fn clear_bindings(&mut self) -> Result<&mut Self> {
// Clear
Ok(self)
}
fn bind(&mut self, value: impl AsValue) -> Result<&mut Self> {
self.bind_index(value, self.index)
}
fn bind_index(&mut self, value: impl AsValue, index: u64) -> Result<&mut Self> {
self.index = index + 1;
Ok(self)
}
}
impl Display for YourDBPrepared {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("YourDBPrepared")
}
}4. SQL Writer (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, DynQuery, 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 DynQuery,
_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()methods, ensure resource release. - Expose via the
Driver's associatedTransaction<'c>type
If not supported, return relevant error messages in related functions and enable disable-transactions in tank-tests.
6. Test 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 _lock = 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
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 or 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 Notes
- 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.
Errors
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.