Skip to content

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:

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"
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

rs
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 (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, 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.

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

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

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

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 implemented
  • disable-intervals: interval types absent
  • disable-large-integers: i128, u128 unsupported
  • disable-ordering: yourdb cannot order result sets
  • 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 (or rejects cleanly)
  • Streams drop promptly (no leaked locks / 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 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 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.