Skip to content

v0.4.0

Choose a tag to compare

@lxsaah lxsaah released this 25 Dec 20:44
· 91 commits to main since this release

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 literals
  • Interned(&'static str) - Uses Box::leak for 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:

  1. Spawn-before-subscribe: Event loop spawned before topic subscriptions
  2. Dynamic channel capacity: Scales with topic count (topics + 10)
  3. 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:

  1. Event loop now spawned before subscribing to topics
  2. Channel capacity dynamically scales: topic_count + 10
  3. 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