Skip to content

Driver Creation ​

Field Manual Section 10 - Armored Engineering ​

Opening a new battlefront means forging a custom 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 shooting range (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

Architecture ​

A driver is a thin composite of five parts:

TraitPurpose
DriverPublic entry point for all the database abstractions
ConnectionLive session running queries and possibly starting a transaction
TransactionAbstraction over transactional database capabilities, borrows mutably a connection
PreparedOwns a compiled statement, binds positional parameters
SqlWriterConverts 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):

toml
[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 ​

rs
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 backend session(s) (pool if applicable)
  • Implement prepare (compile statement) & run (stream QueryResult::{Row,Affected})
  • Optionally implement fast-path bulk append (DuckDB style)

Skeleton:

rs
use crate::{YourDBDriver, YourDBPrepared, YourDBTransaction};
use std::borrow::Cow;
use tank_core::{
    AsQuery, Connection, Error, ErrorContext, Executor, Query, QueryResult, Result,
    stream::{self, Stream},
};
use url::Url;

#[derive(Debug)]
pub struct YourDBConnection {
    pub(crate) url: Url,
}

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(driver: &YourDBDriver, url: Cow<'static, str>) -> Result<Self> {
        let context = "While trying to connect to YourDB";
        let url = Self::sanitize_url(driver, url).context(context)?;
        // Establish connection
        Ok(YourDBConnection { url })
    }

    async fn begin(&mut self) -> Result<YourDBTransaction<'_>> {
        Err(anyhow!("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.

Keep one doctrine in mind: a Prepared value is driver-local executable state, not portable SQL text. Make Display/Debug useful for diagnostics and errors, but execute prepared handles through the owning driver.

rs
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_fragment if divergence

Tip: Start from tank-core's GenericSqlWriter implementation; copy then trim.

rs
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() and rollback() methods, ensure resource release.
  • Expose via the Driver's associated Transaction<'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:

rs
#[cfg(test)]
mod tests {
    use std::sync::Mutex;
    use tank_core::{Driver, PoolConfig};
    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 mut pool = driver
            .connect_pool(URL.into(), PoolConfig::new())
            .await
            .expect("Could not open the database");
        execute_tests(&mut pool).await;
    }
}

Enable feature flags to disable specific functionality until green.

Feature Flags ​

tank-tests exposes opt-out switches to avoid testing the driver on unsupported features:

  • disable-arrays: fixed-size arrays (example: [u8; 13])
  • disable-groups: GROUP BY SQL semantic not supported
  • disable-infinity: infinite values for floating point numbers
  • disable-intervals: interval / duration value types
  • disable-joins: SQL join semantics
  • disable-large-integers: i128 and u128 unsupported
  • disable-large-decimals: decimals beyond IEEE-754 double precision unsupported
  • disable-large-intervals: disable testing very large interval values
  • disable-lists: dynamic list/array-like collections absent
  • disable-maps: avoid testing HashMap, BTreeMap
  • disable-multiple-statements: statement batching unsupported
  • disable-nested-collections: nested collection values unsupported
  • disable-old-dates: historical date ranges unsupported
  • disable-ordering: ordered result sets unsupported
  • disable-references: foreign keys not enforced
  • disable-transactions: no transactional support

7. Tactical Checklist ​

  • URL prefix enforced (yourdb://)
  • Driver::NAME correct and used consistently
  • prepare handles multiple statements cleanly, or rejects them with an explicit error
  • Streams drop promptly (no leaked locks or file handles)
  • SqlWriter prints 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 anyhow!("YourDB connection URL must start with yourdb://")
  • 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.

Released under the Apache-2.0 license.