v0.4.0
AimDB v0.4.0 Release Notes
Compile-time safe record keys with #[derive(RecordKey)] macro!
🎯 What's New in v0.4.0
This release introduces compile-time safe record keys via a new derive macro, transforming RecordKey from a struct to a trait. This enables user-defined enum keys with automatic string representation, eliminating runtime key typos and improving embedded system efficiency.
✨ Major Features
🔑 RecordKey Trait + Derive Macro
New crate aimdb-derive provides #[derive(RecordKey)] for compile-time checked keys!
Instead of error-prone string literals, define type-safe enum keys:
use aimdb_core::RecordKey;
#[derive(RecordKey, Clone, Copy, PartialEq, Eq, Debug)]
#[key_prefix = "sensor"]
pub enum SensorKey {
#[key = "temp.indoor"]
TempIndoor,
#[key = "temp.outdoor"]
TempOutdoor,
#[key = "humidity"]
#[link_address = "zigbee/sensors/humidity"] // MQTT topic
Humidity,
}
// Compile-time typo detection!
let producer = db.producer::<Temperature>(SensorKey::TempIndoor)?;
// vs runtime error with string: db.producer::<Temperature>("sensor.temp.indor")?;Benefits:
- 🛡️ Compile-time safety: Typos caught at build time
- 🚀 Zero-allocation: Enum variants are
Copy, no heap allocation - 📍 Connector metadata:
#[link_address = "..."]for MQTT topics, KNX addresses - 🔧 IDE support: Autocomplete and refactoring work correctly
- 📦 no_std compatible: Works on embedded targets
📝 StringKey Type
The previous RecordKey struct is now StringKey with improved memory model:
use aimdb_core::StringKey;
// Static keys (zero allocation)
let key: StringKey = "sensors.temp".into();
// Dynamic keys (interned via Box::leak for O(1) Copy/Clone)
let key = StringKey::intern(dynamic_string);Memory Model:
Static(&'static str)- Zero-allocation for string literalsInterned(&'static str)- UsesBox::leakfor O(1) cloning- Designed for startup-time registration (<1000 keys)
- Debug warning if >1000 interned keys
🐛 MQTT Connector Fix
Fixed initialization deadlock when subscribing to >10 MQTT topics (Issue #63)
The fix implements:
- Spawn-before-subscribe: Event loop spawned before topic subscriptions
- Dynamic channel capacity: Scales with topic count (
topics + 10) - Proper task yielding: Ensures scheduler runs between operations
Also upgraded rumqttc from 0.24 to 0.25.
💥 Breaking Changes
1. RecordKey: Struct → Trait
RecordKey is now a trait, not a struct.
// Before (v0.3.x)
use aimdb_core::RecordKey;
let key: RecordKey = "sensors.temp".into();
// After (v0.4.0)
use aimdb_core::StringKey;
let key: StringKey = "sensors.temp".into();
// Or use derive macro (recommended)
use aimdb_core::RecordKey; // Now a trait
#[derive(RecordKey, Clone, Copy, PartialEq, Eq)]
pub enum AppKey {
#[key = "sensors.temp"]
SensorsTemp,
}2. RecordKey Trait Bounds
If you have generic code over record keys:
// Before (v0.3.x)
fn process_key(key: RecordKey) { ... }
// After (v0.4.0)
fn process_key<K: RecordKey>(key: K) { ... }
// Or with StringKey specifically:
fn process_key(key: StringKey) { ... }📦 Published Crates
New Crate
- 🆕
aimdb-derive@0.1.0-#[derive(RecordKey)]macro for compile-time checked keys
Updated to v0.4.0
- ✅
aimdb-core@0.4.0- RecordKey trait, StringKey type, derive feature - ✅
aimdb-mqtt-connector@0.4.0- Deadlock fix + rumqttc 0.25 - ✅
aimdb-tokio-adapter@0.4.0- Updated for aimdb-core 0.4.0 - ✅
aimdb-embassy-adapter@0.4.0- Updated for aimdb-core 0.4.0 - ✅
aimdb-client@0.4.0- Updated for aimdb-core 0.4.0 - ✅
aimdb-sync@0.4.0- Updated for aimdb-core 0.4.0 - ✅
aimdb-cli@0.4.0- Updated for aimdb-client 0.4.0 - ✅
aimdb-mcp@0.4.0- Updated for aimdb-client 0.4.0
Unchanged
aimdb-executor@0.1.0- No changes (still compatible)aimdb-knx-connector@0.2.0- No changes (still compatible)
🚀 Quick Start
Using Derive Macro (Recommended)
use aimdb_core::{AimDbBuilder, RecordKey, buffer::BufferCfg};
use aimdb_tokio_adapter::TokioAdapter;
use serde::{Serialize, Deserialize};
use std::sync::Arc;
// Define type-safe keys
#[derive(RecordKey, Clone, Copy, PartialEq, Eq, Debug)]
pub enum AppKey {
#[key = "sensors.temperature"]
#[link_address = "home/sensors/temp"] // MQTT topic
Temperature,
#[key = "sensors.humidity"]
#[link_address = "home/sensors/humidity"]
Humidity,
#[key = "config.settings"]
Settings,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct SensorReading {
value: f32,
timestamp: u64,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let runtime = Arc::new(TokioAdapter::new()?);
let mut builder = AimDbBuilder::new().runtime(runtime);
// Register with type-safe keys
builder.configure::<SensorReading>(AppKey::Temperature, |reg| {
reg.buffer(BufferCfg::SingleLatest);
});
builder.configure::<SensorReading>(AppKey::Humidity, |reg| {
reg.buffer(BufferCfg::SingleLatest);
});
let db = builder.build().await?;
// Type-safe producer access
let temp_producer = db.producer::<SensorReading>(AppKey::Temperature)?;
let humidity_producer = db.producer::<SensorReading>(AppKey::Humidity)?;
// Produce data
temp_producer.produce(SensorReading { value: 22.5, timestamp: 1000 }).await?;
humidity_producer.produce(SensorReading { value: 65.0, timestamp: 1001 }).await?;
// Access link address for connector metadata
println!("Temperature MQTT topic: {:?}", AppKey::Temperature.link_address());
// Prints: Some("home/sensors/temp")
Ok(())
}Using StringKey (Dynamic Keys)
use aimdb_core::{AimDbBuilder, StringKey, buffer::BufferCfg};
use aimdb_tokio_adapter::TokioAdapter;
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let runtime = Arc::new(TokioAdapter::new()?);
let mut builder = AimDbBuilder::new().runtime(runtime);
// Static string literals (zero allocation)
builder.configure::<String>("app.logs", |reg| {
reg.buffer(BufferCfg::SpmcRing { capacity: 1000 });
});
// Dynamic keys (for runtime-determined keys)
let tenant_id = "tenant-123";
let dynamic_key = StringKey::intern(format!("tenants.{}.data", tenant_id));
builder.configure::<String>(dynamic_key, |reg| {
reg.buffer(BufferCfg::Mailbox);
});
let db = builder.build().await?;
Ok(())
}📚 Migration Guide
Step 1: Update Dependencies
[dependencies]
aimdb-core = "0.4.0"
aimdb-tokio-adapter = "0.4.0"
# aimdb-mqtt-connector = "0.4.0" # If using MQTT
# Enable derive macro (included by default)
# aimdb-core = { version = "0.4.0", features = ["derive"] }Step 2: Choose Key Strategy
Option A: Keep String Literals (Minimal Change)
// Before (v0.3.x)
builder.configure::<Temperature>("sensors.temp", |reg| { ... });
// After (v0.4.0) - No change needed! &'static str implements RecordKey
builder.configure::<Temperature>("sensors.temp", |reg| { ... });Option B: Adopt Derive Macro (Recommended)
// Define keys once
#[derive(RecordKey, Clone, Copy, PartialEq, Eq)]
pub enum Keys {
#[key = "sensors.temp"]
SensorsTemp,
}
// Use everywhere
builder.configure::<Temperature>(Keys::SensorsTemp, |reg| { ... });
let producer = db.producer::<Temperature>(Keys::SensorsTemp)?;Step 3: Update RecordKey Imports
// Before (v0.3.x)
use aimdb_core::RecordKey;
let key: RecordKey = "name".into();
// After (v0.4.0)
use aimdb_core::StringKey;
let key: StringKey = "name".into();Step 4: Update Generic Code
// Before (v0.3.x)
fn process(key: RecordKey) { ... }
// After (v0.4.0)
fn process<K: RecordKey>(key: K) { ... }
// Or specifically:
fn process(key: impl RecordKey) { ... }
fn process(key: StringKey) { ... }🎯 Derive Macro Reference
Attributes
| Attribute | Level | Required | Description |
|---|---|---|---|
#[key = "..."] |
Variant | Yes | String representation for the key |
#[key_prefix = "..."] |
Enum | No | Prefix prepended to all variant keys |
#[link_address = "..."] |
Variant | No | Connector metadata (MQTT topic, KNX address) |
Example with All Features
use aimdb_core::RecordKey;
#[derive(RecordKey, Clone, Copy, PartialEq, Eq, Debug)]
#[key_prefix = "home.automation"] // Applied to all variants
pub enum HomeKey {
#[key = "lights.living"]
#[link_address = "1/1/1"] // KNX group address
LightsLiving,
#[key = "lights.bedroom"]
#[link_address = "1/1/2"]
LightsBedroom,
#[key = "thermostat"]
#[link_address = "mqtt://home/thermostat"]
Thermostat,
}
// Generated methods:
// HomeKey::LightsLiving.as_str() -> "home.automation.lights.living"
// HomeKey::LightsLiving.link_address() -> Some("1/1/1")Compile-Time Validation
The macro validates at compile time:
- ✅ All variants have
#[key = "..."]attribute - ✅ No duplicate keys (including after prefix)
- ✅ Only unit variants (no tuple/struct variants)
// Compile error: duplicate key
#[derive(RecordKey)]
pub enum BadKeys {
#[key = "same"]
First,
#[key = "same"] // Error: duplicate key "same"
Second,
}
// Compile error: missing key attribute
#[derive(RecordKey)]
pub enum BadKeys {
#[key = "valid"]
First,
Second, // Error: missing #[key = "..."] attribute
}🐛 Bug Fixes
MQTT Connector Deadlock (Issue #63)
Problem: When subscribing to more than 10 MQTT topics, the connector would deadlock during initialization.
Root Cause: The internal channel had a fixed capacity of 10, and subscriptions were made before spawning the event loop, causing the channel to fill up and block.
Solution:
- Event loop now spawned before subscribing to topics
- Channel capacity dynamically scales:
topic_count + 10 - Added
tokio::task::yield_now()to ensure proper task scheduling
Impact: Users with >10 MQTT subscriptions can now initialize without hanging.
📖 Examples
All examples have been updated to use the new derive macro:
git clone https://github.com/aimdb-dev/aimdb.git
cd aimdb
# Tokio MQTT demo with derive keys
cargo run -p tokio-mqtt-connector-demo
# Tokio KNX demo with derive keys
cargo run -p tokio-knx-connector-demo
# Sync API demo
cargo run -p sync-api-demo
# Embedded examples (cross-compile)
cd examples/embassy-mqtt-connector-demo
cargo build --release --target thumbv7em-none-eabihf🤝 Contributing
We welcome contributions! See CONTRIBUTING.md for guidelines.
Quick start:
git clone https://github.com/aimdb-dev/aimdb.git
cd aimdb
make check # Format + clippy + test + embedded cross-compile📄 License
Licensed under Apache License 2.0 - see LICENSE for details.
💬 Community
- Issues: GitHub Issues
- Discussions: GitHub Discussions