Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust SDK Guide (schemabound)

The schemabound crate is the core Rust runtime. It is the reference implementation of the OAM framework and the foundation that all other SDKs and services build on.

Why Use The Rust SDK Directly

  • You are building SCHEMABOUND backend services, proxies, or custom gRPC adapters.
  • You need the full execution and introspection surface — not just a client.
  • You want zero-overhead integration with Tokio async runtimes.
  • You need to implement a custom MirrorProvider or QueryRuntimeAugmentor.

Installation

Add schemabound to your Cargo.toml:

[dependencies]
schemabound = "0.6"
tokio = { version = "1", features = ["full"] }

Crate Structure

ModulePurpose
schemabound::mirrorSQLite schema introspection — tables, columns, indexes, triggers, UDTs, field mappings
schemabound::executorSchemaService / QueryService traits + SQLite implementations
schemabound::grpc_executorTonic gRPC server wrapping the executor services
schemabound::interceptorTyped event bus — Event enum, EventBus, EventHandler, HandleOutcome
schemabound::handlersBuilt-in CoR handlers — AuditLogHandler, QueryMetricsHandler, SessionActivityHandler, DefaultHandlerChain, SharedHandler
schemabound::tcpTCP JSON-RPC transport and per-client auth
schemabound::policy_engineTool-contract policy evaluation and subquery governance
schemabound::runtime_contextQueryRuntimeContext — the carrier for per-request augmentation metadata
schemabound::rate_limitConnection and request rate limiter
schemabound::mapperMapper trait + LocalMapper (SQLite) + TcpMapper (remote)

Quick Start

Start a gRPC Server

use schemabound::grpc_executor::GrpcExecutor;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let executor = GrpcExecutor::new("path/to/database.db")?;
    let handle = executor.start_server("0.0.0.0:50051").await?;
    handle.await?;
    Ok(())
}

Introspect A SQLite Database

#![allow(unused)]
fn main() {
use schemabound::mirror::introspect_sqlite_path;

let schema = introspect_sqlite_path("path/to/database.db")?;

for table in &schema.tables {
    println!("Table: {}", table.name);
    for col in &table.columns {
        println!("  Column: {} ({}){}", col.name, col.sql_type,
            if col.primary_key { " PK" } else { "" });
    }
    for trigger in &table.triggers {
        println!("  Trigger: {} {} {}", trigger.name, trigger.timing, trigger.event);
    }
    for fm in &table.field_mappings {
        println!("  FieldMapping: {} → {} ({})", fm.physical_name, fm.logical_name, fm.orm_convention);
    }
}

println!("UDTs: {}", schema.user_defined_types.len());
}

Use The MirrorProvider Trait

MirrorProvider is an async trait for pluggable schema introspection:

#![allow(unused)]
fn main() {
use schemabound::{MirrorProvider, SqliteMirrorProvider};

let provider = SqliteMirrorProvider::new("database.db");
let schema = provider.introspect_schema().await?;
}

Implement the trait to integrate custom databases:

#![allow(unused)]
fn main() {
use schemabound::MirrorProvider;
use schemabound::mirror::SchemaModel;

struct MyCustomProvider;

#[async_trait::async_trait]
impl MirrorProvider for MyCustomProvider {
    async fn introspect_schema(&self) -> Result<SchemaModel, String> {
        // custom introspection logic
        Ok(SchemaModel { tables: vec![], user_defined_types: vec![] })
    }
}
}

Register A CoR Handler

Handlers are invoked in registration order for every dispatched event. Use the built-in collection from schemabound::handlers or implement EventHandler yourself:

#![allow(unused)]
fn main() {
use schemabound::{get_event_bus, AuditLogHandler, DefaultHandlerChain, SharedHandler};
use std::sync::Arc;

// Recommended starting point: audit log first, then metrics/session counters
let chain = DefaultHandlerChain::new();
let bus = get_event_bus();

bus.register_handler(Box::new(AuditLogHandler))?;
bus.register_handler(Box::new(SharedHandler(Arc::clone(&chain.query_metrics))))?;
bus.register_handler(Box::new(SharedHandler(Arc::clone(&chain.session_activity))))?;

// Retain chain handles for health probes
let snap = chain.query_metrics.snapshot();
let sessions = chain.session_activity.session_count();
}

See the Event Pipeline architecture guide for the full handler reference and a comparison with subscribers.

Subscribe To Events (fire-and-forget)

#![allow(unused)]
fn main() {
use schemabound::interceptor::{get_event_bus, Event};

let bus = get_event_bus();

let id = bus.register_subscriber(Box::new(|event: &Event| {
    println!("Event: {}", event.event_type());
}))?;

// Unsubscribe when done
bus.unregister_subscriber(id)?;
}

Apply A Policy To A Query

#![allow(unused)]
fn main() {
use schemabound::executor::{QueryServiceImpl, ValidateQueryRequest};
use schemabound::policy_engine::{
    AuthorizationContext, AuthorizedSubqueryShape, PolicyContext, SubqueryPolicy, ToolContract, ToolIntent,
};

let mut service = QueryServiceImpl::new();
service.set_db_path("database.db")?;

let request = ValidateQueryRequest {
    db_identifier: "db".into(),
    query: "SELECT id FROM users WHERE org_id IN (SELECT id FROM organizations)".into(),
    parameters: Default::default(),
};

let policy = PolicyContext {
    tool: ToolContract {
        name: "list-users".into(),
        intent: ToolIntent::ReadSelect,
        subquery_policy: SubqueryPolicy::AllowListed(vec![
            AuthorizedSubqueryShape { table: "organizations".into() },
        ]),
    },
    authorization: AuthorizationContext {
        allowed_intents: vec![ToolIntent::ReadSelect],
        grants: vec!["tool:users.read".into()],
    },
};

let response = service.validate_query_with_policy(request, policy).await?;
assert!(response.valid);
}

Schema Introspection Model

The mirror module returns a tree of Rust structs:

SchemaModel
├── tables: Vec<Table>
│   ├── columns: Vec<Column>          name, sql_type, nullable, primary_key, default_value
│   ├── indexes: Vec<Index>           name, columns
│   ├── unique_indexes: Vec<UniqueIndex>
│   ├── foreign_keys: Vec<ForeignKey>
│   ├── composite_foreign_keys: Vec<CompositeForeignKey>
│   ├── triggers: Vec<Trigger>        name, event, timing, table_name, body
│   └── field_mappings: Vec<FieldMapping>
│       └── logical_name, physical_name, orm_convention  (Hibernate | EntityFramework)
└── user_defined_types: Vec<UserDefinedType>
    └── name, base_type, check_constraint, nullable, default_value

Field mapping convention detection is heuristic:

  • camelCase column names → Hibernate
  • PascalCase column names → EntityFramework (Entity Framework)
  • _prefixed columns → EntityFramework (EF Core shadow property convention)

gRPC Proto Mapping

The table_to_table_def function converts a mirror::Table to a proto TableDef:

#![allow(unused)]
fn main() {
use schemabound::grpc_executor::table_to_table_def;

let proto_table = table_to_table_def(&my_mirror_table);
}

Both unique_indexes and indexes are merged into TableDef.indexes, distinguished by IndexDef.is_unique.

Contributing

See the Contribution Workflow for the full TDD contract — no production code may be written before a failing test exists.

Test Targets

# Unit tests only (fast, no infra)
make test-unit

# Full test suite (schemabound-public)
make test FILTER=schemabound-public

# Proto unit tests
make test-proto