#key #attributes #macro #key-value-database #key-index #debugging

macro db-key-macro

Attribute macro to generate fixed-length key-value database keys

2 releases

0.1.1 Nov 4, 2024
0.1.0 Nov 1, 2024

#1025 in Database interfaces

Download history 277/week @ 2024-10-30 47/week @ 2024-11-06

324 downloads per month

MIT license

83KB
2K SLoC

DBKey Macros

The db-key macros generate a structure that can used as a fixed-size key for a key-value database. Both macros take a prototype (or definition) structure and create a tuple struture wrapped around an array.

use db_key_macro::db_key;

#[db_key]
struct ExampleKey {
    byte: u8,
    long: u32,
    array: [u8; 3],
}

let key = ExampleKey::new(0x12, 0x3456789A, [0xBC, 0xDE, 0xF0]);

assert_eq!(key.long(), 0x3456789A);
assert_eq!(key.array(), &[0xBC, 0xDE, 0xF0]);
assert_eq!(key.as_ref(), &[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]);

Currently, the data types that can be used in the definition structure are limited to unsigned integers (except usize), and arrays of u8. The key data is always packed and lexographically ordered so that the definition structure and the key structure will have the same order when sorted.

There are two DBKey macros supplied by this crate: an attribute macro (#[db_key]) and a derive macro (DBKey). They each provide similar functionality of creating a fixed-length key type for key-value databases that use a slice of bytes as the key. The generated code is documented with examples.

For clarification the definition structure is the struture used to define the key, the key struture is the tuple structure wrapping an array, and the argument struture provides an easy way to define a key from the individual components.

# use db_key_macro::db_key;
// Definition struture:
#[db_key]
struct ExampleKey {
    byte: u8,
    long: u32,
    array: [u8; 3],
}
// The generated key struture:
struct ExampleKey([u8; 8]);
// The (generated) argument struture:
struct ExampleKeyArgs {
    pub byte: u8,
    pub long: u32,
    pub array: [u8; 3],
}

The attribute macro consumes the definition structure and generates both a key struture and an argument structure. The derive macro only generates a key struture and the definition struture can be used an the argument struture.

With the attribute macro, the name of the definition structure is used for the key structure and the argument structure uses the definition structure name with Args appended to the end. With the derive macro, the definition structure becomes the argument structure, and the key structure is given the name of the definition structure with Key appended to the end.

Macro Type Definition Name Key Struct Name Argument Struct Name
Attribute ExampleKey ExampleKey ExampleKeyArgs
Derive Example ExampleKey Example

The key options for the attribute macro are defined in the attribute macro call.

# use db_key_macro::db_key;
#[db_key(no_new, custom_debug, alt_name = Args)]
struct Key {
    id: u64,
    index: u32,
}

The key options for the derive macro are defined in a separate key attribute macro.

# use db_key_macro::DBKey;
#[derive(Clone, DBKey, PartialEq, Eq, PartialOrd, Ord)]
#[key(no_new, custom_debug, alt_name = Key)]
struct Args {
    pub id: u64,
    pub index: u32,
}

The key struture always derives Clone, Hash, PartialEq, Eq, PartialOrd, and Ord. If the size of the key is 64-bytes or less, than it also derives Copy as well. A custom Debug implementation is supplied for the key structure. For the attribute macro, the argument structure has all the same derived implementations except Hash and it uses the standard derived Debug.

Since the attribute macro replaces the definition structure, it is more permissive about the data that is allowed in the definition strucure.

# use db_key_macro::db_key;
#[db_key(no_new, custom_debug, alt_name = Args)]
struct Key {
    /// The ID number.
    #[name = "ID"]
    // This isn't allowed in the derive macro. It will trigger an error.
    #[default = 0x123456789ABCDEF_u64]
    // The attribute macro doesn't need this to be public.
    id: u64,
    /// The item index
    #[name = "index value"]
    // This isn't allowed in the derive macro. It will trigger an error.
    #[default = 1_u32]
    index: u32,
    /// The array value
    #[name = "array value"]
    // This isn't allowed in the derive macro. It will trigger an error.
    #[default = [1, 2, 3]]
    array: [u8; 3],
}

Since the derive macro uses the definition structure as the argument structure, it is more restrictive about what is expected.

# use db_key_macro::DBKey;
#[derive(Copy, Clone, DBKey, PartialEq, Eq, PartialOrd, Ord)]
#[key(no_new, custom_debug, alt_name = Key)]
struct Args {
    /// The ID number.
    #[name = "ID"]
    // The derive macro will accept raw values but not with type specifier.
    #[default = 0x123456789ABCDEF]
    // The derive macro need this to be public in order to use it.
    pub id: u64,
    /// The item index
    #[name = "index value"]
    // You can get around the limitation for default values by using a &str.
    #[default = "1_u32"]
    pub index: u32,
    // You can get around the limitation for default values by using a &str.
    #[default = "[1, 2, 3]"]
    array: [u8; 3],
}

Adding Functions

There are non-public constants and functions that can be used when implementing additional methods for the key structure.

For every field in the structure, there are 4 constants defined. There is a size usize constant with the size of the field in bytes. There is a start index and end index usize constants set to the index of the first byte of the field in the key and the byte after the last byte in the key. And there is a range constant with the range of bytes used by the field of the key. The range constant has a Range<usize> type.

For an example field example: u32, the size constant is EXAMPLE_SIZE, the start index constant is EXAMPLE_START, the end index constant is EXAMPLE_END and the range constant is EXAMPLE_RANGE.

Example

use db_key_macro::db_key;
use std::ops::Range;

/// A key for storing food records for tracking what you eat in a day.
#[db_key]
pub struct FoodEntryKey {
    /// The year that the food was consumed.
    year: u16,
    /// The month that the food was consumed.
    month: u8,
    /// The day that the food was consumed.
    day: u8,
    /// The index of the food that was consumed. We are assuming that someone
    /// won't eat more than 255 things in a day.
    index: u8,
}

/// This is a date structure used to combine the year, month, and day.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct Date {
    year: u16,
    month: u8,
    day: u8,
}

impl FoodEntryKey {
    /// Create a new `FoodEntryKey` key using the index and a 32-bit date value
    /// with the year, month, and day stored in a single structure.
    pub fn new_from_date(date: Date, index: u8) -> Self {
        let mut key = [0_u8; Self::KEY_LENGTH];
        key[Self::YEAR_RANGE].copy_from_slice(&date.year.to_be_bytes());
        key[Self::MONTH_START] = date.month;
        key[Self::DAY_START] = date.day;
        key[Self::INDEX_START] = index;
        Self(key)
    }

    /// Get the date value from the key.
    pub fn date(&self) -> Date {
        Date {
            year: self.year(),
            month: self.month(),
            day: self.day(),
        }
    }

    /// Set the date value in a key.
    pub fn set_date(&mut self, date: Date) {
        self.set_year(date.year);
        self.set_month(date.month);
        self.set_day(date.day);
    }
}

let date = Date { year: 2024, month: 10, day: 31 };
let key = FoodEntryKey::new_from_date(date, 0);
assert_eq!(key.year(), 2024);
assert_eq!(key.month(), 10);
assert_eq!(key.day(), 31);
assert_eq!(key.index(), 0);
assert_eq!(key.date(), Date{ year: 2024, month: 10, day: 31 });
assert_eq!(key.as_ref(), &[(2024/256) as u8, (2024%256) as u8, 10, 31, 0]);

Future Plans

I keep on thinking of things I want to add to this, but I want to get it out in the world sometime. Here are a list of features I am thinking of adding in the future.

  • Support signed integers in definition structure.
  • Add option to allow string input/output for arrays (and even integer types).
  • Add option to hide fields from interface (either so the developer can write thier own, or because they want them to always be the default value/padding)
  • Support floating point values in the definition structure. This will partially break the parity between args atructure and key structure.
  • Add option to store keys in little endian order. Breaks ordering.
  • Make as many generated functions const as possible.

Dependencies

~0.5–1MB
~21K SLoC