MCPcopy Index your code
hub / github.com/TankHQ/tank

github.com/TankHQ/tank @main

Chat with this repo
repository ↗ · DeepWiki ↗ · + Follow
1,389 symbols 3,727 edges 195 files 133 documented · 10% updated 1d ago★ 43
What it actually does AI analysis from the code graph — generated when you open this
loading…
README
<img width="300" height="300" src="https://github.com/TankHQ/tank/raw/main/docs/public/logo.png" alt="Tank logo: a circular gold emblem with a military tank and a database symbol" />

Tank

Tank (Table Abstraction & Navigation Kit) is the Rust data layer for teams that want one entity model, explicit queries, and the freedom to move across very different databases without rewriting their whole stack.

One entity model. Any terrain.

📘 Docs: https://tankhq.github.io/tank

🖥️ Repo: https://github.com/TankHQ/tank

📦 Crate: https://crates.io/crates/tank

[!IMPORTANT]
Authenticity Notice: This is not AI slop. It was meticulously crafted by me (human) barsdeveloper.

Mission Briefing

In plain terms, Tank is a thin layer over your database workflow, designed for the Rust operator who needs to deploy across multiple environments without changing the kit. It doesn't matter if you are digging into a local SQLite trench, coordinating a distributed ScyllaDB offensive, or managing a Postgres stronghold, Tank provides a unified interface. You define your entities, Tank handles the ballistics.

Known battlefields: - Postgres - SQLite - MySQL/MariaDB - DuckDB - MongoDB - ScyllaDB/Cassandra - Valkey/Redis

Mission Objectives

Tank exists to implement the best possible design for a ORM written in Rust. A clean-slate design focused on ergonomics, flexibility and broad database support.

  • Async operations - Fire and forget.
  • Designed to be extensible - Swap databases like changing magazines mid-battle.
  • SQL and NoSQL support - One Tank, all terrains.
  • Transactions abstraction - Commit on success or rollback and retreat.
  • Rich type arsenal - Automatic conversions between Rust and database types.
  • Optional appender API - High caliber bulk inserts.
  • TLS - No open radios on this battlefield.
  • Joins - Multi unit coordination.
  • Raw SQL - You're never limited by the abstractions provided.
  • Zero setup - Skip training. Go straight to live fire.

No-Fly Zone

  • No schema migrations (just table creation and destroy for fast setup).
  • No implicit joins (no entities as fields, joins are explicit, every alliance is signed).

Why Tank?

Intelligence Report: A quick recon of the battlefield revealed that while existing heavy weaponry is effective, there was a critical need for a more adaptable, cleaner design. Tank was designed from scratch to address these weaknesses.

1. Modular Architecture: Some systems rely on hardcoded enums for database support, which limits flexibility. If a backend isn't in the core list, it cannot be used. Tank is designed to be extensible: a driver can be implemented for any database (SQL or NoSQL) without touching the core library. If it can hold data, Tank can likely target it.

2. Zero Boilerplate: Field operations shouldn't require filling out forms in triplicate. Some tools force data definition twice: once in a complex DSL and again as a Rust struct. Tank keeps it simple: one struct, one definition. The macros handle table creation, selection, and insertion automatically. You can set up tables and get database communication running in just a few lines of code, all through a unified API that works the same regardless of the backend. Perfect for spinning up tests and prototypes rapidly while still scaling to production backends.

Operational Guide

1) Arm your cargo

cargo add tank

2) Choose your battlefield

cargo add tank-duckdb

3) Define unit schematics

use std::borrow::Cow;
use tank::{Entity, Executor, Result};

#[derive(Entity)]
#[tank(schema = "army")]
pub struct Tank {
    #[tank(primary_key)]
    pub name: String,
    pub country: Cow<'static, str>,
    #[tank(name = "caliber")]
    pub caliber_mm: u16,
    #[tank(name = "speed")]
    pub speed_kmh: f32,
    pub is_operational: bool,
    pub units_produced: Option<u32>,
}

4) Fire for effect

use std::{borrow::Cow, collections::HashSet};
use tank::{ConnectionPool, Driver, Entity, PoolConfig, Result, expr, stream::TryStreamExt};
use tank_duckdb::DuckDBDriver;

async fn data() -> Result<()> {
    let driver = DuckDBDriver::new();
    let mut pool = driver
        .connect_pool(
            "duckdb://../target/debug/tests.duckdb?mode=rw".into(),
            PoolConfig::new(),
        )
        .await?;
    let mut connection = pool.get().await?;

    let my_tank = Tank {
        name: "Tiger I".into(),
        country: "Germany".into(),
        caliber_mm: 88,
        speed_kmh: 45.4,
        is_operational: false,
        units_produced: Some(1_347),
    };

    /*
     * CREATE SCHEMA IF NOT EXISTS "army";
     * CREATE TABLE IF NOT EXISTS "army"."tank" (
     *     "name" VARCHAR PRIMARY KEY,
     *     "country" VARCHAR NOT NULL,
     *     "caliber" USMALLINT NOT NULL,
     *     "speed" FLOAT NOT NULL,
     *     "is_operational" BOOLEAN NOT NULL,
     *     "units_produced" UINTEGER);
     */
    Tank::create_table(&mut connection, true, true).await?;

    /*
     * INSERT INTO "army"."tank" ("name", "country", "caliber", "speed", "is_operational", "units_produced") VALUES
     *     ('Tiger I', 'Germany', 88, 45.4, false, 1347)
     * ON CONFLICT ("name") DO UPDATE SET
     *     "country" = EXCLUDED."country",
     *     "caliber" = EXCLUDED."caliber",
     *     "speed" = EXCLUDED."speed",
     *     "is_operational" = EXCLUDED."is_operational",
     *     "units_produced" = EXCLUDED."units_produced";
     */
    my_tank.save(&mut connection).await?;

    /*
     * DuckDB uses the appender API. Other drivers generate an INSERT:
     * INSERT INTO "army"."tank" ("name", "country", "caliber", "speed", "is_operational", "units_produced") VALUES
     *     ('T-34/85', 'Soviet Union', 85, 53.0, false, 49200),
     *     ('M1 Abrams', 'USA', 120, 72.0, true, NULL);
     */
    Tank::insert_many(
        &mut connection,
        &[
            Tank {
                name: "T-34/85".into(),
                country: "Soviet Union".into(),
                caliber_mm: 85,
                speed_kmh: 53.0,
                is_operational: false,
                units_produced: Some(49_200),
            },
            Tank {
                name: "M1 Abrams".into(),
                country: "USA".into(),
                caliber_mm: 120,
                speed_kmh: 72.0,
                is_operational: true,
                units_produced: None,
            },
        ],
    )
    .await?;

    /*
     * SELECT "name", "country", "caliber", "speed", "is_operational", "units_produced"
     * FROM "army"."tank"
     * WHERE "is_operational" = false
     * LIMIT 1000;
     */
    let tanks = Tank::find_many(&mut connection, expr!(Tank::is_operational == false), Some(1000))
        .try_collect::<Vec<_>>()
        .await?;

    assert_eq!(
        tanks
            .iter()
            .map(|t| t.name.to_string())
            .collect::<HashSet<_>>(),
        HashSet::from_iter(["Tiger I".into(), "T-34/85".into()])
    );
    Ok(())
}

Examples

Books

#[derive(Entity, Clone, PartialEq, Debug)]
#[tank(schema = "testing", name = "authors")]
pub struct Author {
    #[tank(primary_key, name = "author_id")]
    pub id: Uuid,
    pub name: String,
    ...
}
#[derive(Entity, Clone, PartialEq, Debug)]
#[tank(schema = "testing", name = "books", primary_key = (Self::title, Self::author))]
pub struct Book {
    #[tank(column_type = (mysql = "VARCHAR(255)"))]
    pub title: String,
    #[tank(references = Author::id)]
    pub author: Uuid,
    ...
}
#[derive(Entity, PartialEq, Debug)]
struct BookAuthorResult {
    #[tank(name = "title")]
    book: String,
    #[tank(name = "name")]
    author: String,
}
let result = executor
    .fetch(
        QueryBuilder::new()
            .select(cols!(B.title, A.name))
            .from(join!(Book B JOIN Author A ON B.author == A.author_id))
            .where_expr(expr!(B.year < 2000))
            .order_by(cols!(B.title DESC))
            .build(&executor.driver()),
    )
    .map_ok(BookAuthorResult::from_row)
    .map(Result::flatten)
    .try_collect::<Vec<_>>()
    .await
    .expect("Failed to query books and authors joined");
assert_eq!(
    result,
    [
        BookAuthorResult{
            book: "The Hobbit".into(),
            author: "J.R.R. Tolkien".into()
        },
        BookAuthorResult{
            book: "Harry Potter and the Philosopher's Stone".into(),
            author: "J.K. Rowling".into(),
        },
    ]
);

Radio Logs

#[derive(Entity)]
#[tank(schema = "operations", name = "radio_operator")]
pub struct Operator {
    #[tank(primary_key)]
    pub id: Uuid,
    pub callsign: String,
    #[tank(name = "rank")]
    pub service_rank: String,
    #[tank(name = "enlistment_date")]
    pub enlisted: Date,
    pub is_certified: bool,
}
#[derive(Entity)]
#[tank(schema = "operations")]
pub struct RadioLog {
    #[tank(primary_key)]
    pub id: Uuid,
    #[tank(references = Operator::id)]
    pub operator: Uuid,
    pub message: String,
    pub unit_callsign: String,
    #[tank(name = "tx_time")]
    pub transmission_time: OffsetDateTime,
    #[tank(name = "rssi")]
    pub signal_strength: i8,
}
let operator = Operator {
    id: Uuid::parse_str("21c90df5-00db-4062-9f5a-bcfa2e759e78").unwrap(),
    callsign: "SteelHammer".into(),
    service_rank: "Major".into(),
    enlisted: date!(2015 - 06 - 20),
    is_certified: true,
};
Operator::insert_one(executor, &operator).await?;
let op_id = operator.id;
let logs: Vec<RadioLog> = (0..5)
    .map(|i| RadioLog {
        id: Uuid::new_v4(),
        operator: op_id,
        message: format!("Ping #{i}"),
        unit_callsign: "Alpha-1".into(),
        transmission_time: OffsetDateTime::now_utc(),
        signal_strength: 42,
    })
    .collect();
RadioLog::insert_many(executor, &logs).await?;
if let Some(radio_log) = RadioLog::find_one(
    executor,
    expr!(RadioLog::unit_callsign == "Alpha-%" as LIKE),
)
.await?
{
    log::debug!("Found radio log: {:?}", radio_log.id);
}
let mut query =
    RadioLog::prepare_find(executor, expr!(RadioLog::signal_strength > ?), None).await?;
query.bind(40)?;
let _messages: Vec<_> = executor
    .fetch(query)
    .map_ok(|row| row.values[0].clone())
    .try_collect()
    .await?;

Support the Mission

Building and maintaining drivers for half a dozen different databases is a massive effort. If Tank saves your company time, infrastructure headaches, or boilerplate, please consider supporting its development.

Donations ensure that Tank stays maintained, well-tested, and gets new capabilities (like more advanced driver features) faster.

🔗 Sponsor the Commander via GitHub Sponsors

Rustaceans don't hide behind ORMs, they drive Tanks.

Extension points exported contracts — how you extend this code

Driver (Interface)
Backend connector and dialect. [8 implementers]
tank-core/src/driver.rs
NullCheck (Interface)
(no doc) [6 implementers]
tank-duckdb/src/cbox.rs
NullCheck (Interface)
(no doc) [5 implementers]
tank-sqlite/src/cbox.rs
Executor (Interface)
Async query execution. Implemented by connections. [13 implementers]
tank-core/src/executor.rs
Connection (Interface)
A live database handle capable of executing queries and spawning transactions. Extends [`Executor`] with connection and [10 …
tank-core/src/connection.rs
Dataset (Interface)
Queryable data source (table or join tree). Implementors know how to render themselves inside a FROM clause. [6 implementers]
tank-core/src/dataset.rs
AsValue (Interface)
Bidirectional conversion between Rust types and `Value`. [22 implementers]
tank-core/src/as_value.rs

Core symbols most depended-on inside this repo

push_str
called by 293
tank-core/src/query/dyn_query.rs
push
called by 193
tank-core/src/query/dyn_query.rs
as_value
called by 105
tank-tests/src/enums.rs
write_query
called by 82
tank-core/src/join.rs
select
called by 76
tank-core/src/query/builder/mod.rs
len
called by 75
tank-core/src/row.rs
into_iter
called by 73
tank-core/src/row.rs
driver
called by 64
tank-core/src/pool.rs

Shape

Method 706
Function 404
Class 236
Enum 22
Interface 21

Languages

Rust100%

Modules by API surface

tests/util.rs79 symbols
tests/value.rs70 symbols
tank-core/src/writer/sql_writer.rs57 symbols
tank-mongodb/src/sql_writer.rs39 symbols
tests/query.rs38 symbols
tank-core/src/interval.rs29 symbols
tank-core/src/pool.rs26 symbols
tests/expression.rs25 symbols
tank-mongodb/src/payload.rs22 symbols
tank-scylladb/src/sql_writer.rs19 symbols
tank-core/src/expression/visitor.rs19 symbols
tests/join.rs18 symbols

Datastores touched

(mysql)Database · 1 repos
(mongodb)Database · 1 repos
militaryDatabase · 1 repos
mysql_databaseDatabase · 1 repos
adminDatabase · 1 repos
dbDatabase · 1 repos
mariadb_databaseDatabase · 1 repos
operations_dbDatabase · 1 repos

For agents

$ claude mcp add tank \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact