Sidecar Services
mik-sdk handlers are designed to work with external services for data storage, caching, and other infrastructure concerns. This guide covers patterns for integrating with these services.
Architecture Overview
Section titled “Architecture Overview”SQL Query Builder
Section titled “SQL Query Builder”mik-sdk includes a SQL query builder for generating parameterized queries. The queries are executed by your sidecar database service.
Basic CRUD Operations
Section titled “Basic CRUD Operations”use mik_sdk::prelude::*;
// SELECTlet (sql, params) = sql_read!(users { select: [id, name, email, created_at], filter: { active: true }, order: name, limit: 20,});// sql: SELECT id, name, email, created_at FROM users WHERE active = $1 ORDER BY name LIMIT 20// params: [Value::Bool(true)]
// INSERTlet (sql, params) = sql_create!(users { name: "Alice", email: "alice@example.com", returning: [id, created_at],});// sql: INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, created_at
// UPDATElet (sql, params) = sql_update!(users { set: { name: "Bob" }, filter: { id: 123 },});// sql: UPDATE users SET name = $1 WHERE id = $2
// DELETElet (sql, params) = sql_delete!(users { filter: { id: 123 },});// sql: DELETE FROM users WHERE id = $1Filter Operators
Section titled “Filter Operators”// Comparison operatorssql_read!(users { filter: { age: { $gte: 18 }, // age >= 18 status: { $ne: "banned" }, // status != 'banned' score: { $lt: 100 }, // score < 100 },});
// List operatorssql_read!(users { filter: { role: { $in: ["admin", "moderator"] }, id: { $nin: [1, 2, 3] }, },});
// Text operatorssql_read!(users { filter: { name: { $like: "%alice%" }, email: { $ends_with: "@example.com" }, bio: { $contains: "developer" }, },});
// Logical operatorssql_read!(users { filter: { $or: [ { role: "admin" }, { verified: true } ] },});| Operator | SQL | Example |
|---|---|---|
$eq | = | { age: { $eq: 18 } } |
$ne | != | { status: { $ne: "banned" } } |
$gt | > | { score: { $gt: 100 } } |
$gte | >= | { age: { $gte: 18 } } |
$lt | < | { score: { $lt: 50 } } |
$lte | <= | { age: { $lte: 65 } } |
$in | IN | { id: { $in: [1, 2, 3] } } |
$nin | NOT IN | { id: { $nin: [1, 2, 3] } } |
$like | LIKE | { name: { $like: "%alice%" } } |
$starts_with | LIKE 'x%' | { name: { $starts_with: "A" } } |
$ends_with | LIKE '%x' | { email: { $ends_with: ".com" } } |
$contains | LIKE '%x%' | { bio: { $contains: "rust" } } |
$between | BETWEEN | { age: { $between: [18, 65] } } |
Ordering
Section titled “Ordering”sql_read!(posts { select: [id, title, created_at], order: [-created_at, id], // DESC created_at, ASC id});Prefix with - for descending order.
Pagination
Section titled “Pagination”#[derive(Query)]pub struct PageQuery { #[field(default = 1)] pub page: u32, #[field(default = 20, max = 100)] pub limit: u32,}
fn list_users(query: PageQuery, _req: &Request) -> Response { let (sql, params) = sql_read!(users { select: [id, name], order: id, page: query.page, limit: query.limit, });
// Execute query via sidecar... ok!({ "sql": sql })}use mik_sdk::query::Cursor;
fn list_posts(query: ListQuery, _req: &Request) -> Response { let (sql, params) = sql_read!(posts { select: [id, title, created_at], filter: { published: true }, order: [-created_at, -id], after: query.cursor.as_deref(), limit: 20, });
// Execute query, get results... // let posts = execute_query(sql, params);
// Build next cursor from last item // let next_cursor = if let Some(last) = posts.last() { // Some(Cursor::new() // .string("created_at", &last.created_at) // .int("id", last.id) // .encode()) // } else { // None // };
ok!({ "sql": sql })}SQLite Dialect
Section titled “SQLite Dialect”For SQLite, add sqlite as the first parameter:
let (sql, params) = sql_read!(sqlite, users { filter: { active: true },});// Uses ?1, ?2 placeholders instead of $1, $2HTTP Client for Services
Section titled “HTTP Client for Services”The HTTP client is included by default. Use fetch! to call external services.
Calling a Database Proxy
Section titled “Calling a Database Proxy”use mik_sdk::prelude::*;
fn list_users(_req: &Request) -> Response { let (sql, params) = sql_read!(users { select: [id, name, email], limit: 20, });
// Call database proxy sidecar let response = fetch!(POST "http://db-proxy:8080/query", json: { "sql": sql, "params": params }).send()?;
if response.is_success() { ok!({ "data": response.json() }) } else { error! { status: 500, title: "Database Error", detail: "Failed to execute query" } }}Calling a Cache Service
Section titled “Calling a Cache Service”fn get_user(path: Id, _req: &Request) -> Response { let user_id = path.as_str();
// Try cache first let cache_response = fetch!(GET format!("http://cache:8080/users/{}", user_id)) .send() .ok();
if let Some(resp) = cache_response { if resp.is_success() { // Return cached data return ok!({ "data": resp.json(), "cached": true }); } }
// Cache miss - fetch from database let db_response = fetch!(GET format!("http://db-proxy:8080/users/{}", user_id)) .send()?;
if db_response.is_success() { // Store in cache (fire and forget) let _ = fetch!(PUT format!("http://cache:8080/users/{}", user_id), body: db_response.bytes() ).send();
ok!({ "data": db_response.json(), "cached": false }) } else { not_found!("User not found") }}Batched Loading
Section titled “Batched Loading”Use the ids! macro to extract IDs for batched queries:
fn list_posts_with_authors(_req: &Request) -> Response { // Get posts let (sql, params) = sql_read!(posts { select: [id, title, author_id], limit: 20, }); // let posts = execute_query(sql, params);
// Extract author IDs for batched loading // let author_ids = ids!(posts, author_id);
// Batch load authors (single query, no N+1) // let (sql, params) = sql_read!(users { // filter: { id: { $in: author_ids } }, // }); // let authors = execute_query(sql, params);
ok!({ "message": "Batched loading pattern" })}Next Steps
Section titled “Next Steps”- SQL Reference - Complete SQL macro reference
- HTTP Client - HTTP client reference
- Common Patterns - Service integration patterns