mod codegen;
mod ddl_parser;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
#[proc_macro]
pub fn define_schema(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::LitStr);
let schema = input.value();
match ddl_parser::parse_ddl(&schema) {
Ok(table_defs) => codegen::generate_code(table_defs),
Err(e) => {
panic!("Failed to parse DDL: {}", e);
}
}
}
#[proc_macro_derive(MemdbTable, attributes(memdb_schema))]
pub fn derive_memdb_table(input: TokenStream) -> TokenStream {
let derive_input = parse_macro_input!(input as syn::DeriveInput);
let mut ddl = String::new();
for attr in &derive_input.attrs {
if attr.path().is_ident("memdb_schema") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("ddl") {
let lit = meta.value()?;
let lit_str = lit.parse::<syn::LitStr>()?;
ddl = lit_str.value();
}
Ok(())
})
.unwrap();
}
}
if ddl.is_empty() {
panic!("memdb_schema attribute with ddl parameter is required");
}
match ddl_parser::parse_ddl(&ddl) {
Ok(table_defs) => codegen::generate_code(table_defs),
Err(e) => {
panic!("Failed to parse DDL: {}", e);
}
}
}
use syn::parse::{Parse, ParseStream};
use syn::{Ident, LitInt, Token};
struct Field {
name: Ident,
#[allow(dead_code)]
colon: Token![:],
type_name: Ident,
type_params: Option<LitInt>,
}
impl Parse for Field {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name = input.parse()?;
let colon = input.parse()?;
let type_name = input.parse()?;
let type_params = if input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
let params = content.parse()?;
Some(params)
} else {
None
};
Ok(Self {
name,
colon,
type_name,
type_params,
})
}
}
struct TableArgs {
name: Ident,
max_records: LitInt,
primary_key: Ident,
secondary_index: Option<Ident>,
secondary_index_type: Option<Ident>,
fields: Vec<Field>,
}
impl Parse for TableArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name = input.parse()?;
let _comma1: Token![,] = input.parse()?;
let max_records = input.parse()?;
let _comma2: Token![,] = input.parse()?;
let _primary_key_keyword: Ident = input.parse()?;
let _colon1: Token![:] = input.parse()?;
let primary_key = input.parse()?;
let mut secondary_index = None;
let mut secondary_index_type = None;
if input.peek(Token![,]) {
let _comma3: Token![,] = input.parse()?;
}
loop {
let next = input.lookahead1();
if next.peek(Ident) {
let param_name = input.parse::<Ident>()?;
if param_name == "secondary_index" {
let _colon: Token![:] = input.parse()?;
secondary_index = Some(input.parse()?);
if input.peek(Token![,]) {
let _comma4: Token![,] = input.parse()?;
}
} else if param_name == "secondary_index_type" {
let _colon: Token![:] = input.parse()?;
secondary_index_type = Some(input.parse()?);
if input.peek(Token![,]) {
let _comma5: Token![,] = input.parse()?;
}
} else if param_name == "fields" {
let _colon_fields: Token![:] = input.parse()?;
break;
} else {
return Err(syn::Error::new(param_name.span(), format!("expected 'secondary_index', 'secondary_index_type' or 'fields' keyword, got '{}'", param_name)));
}
} else {
return Err(next.error());
}
}
let content;
syn::braced!(content in input);
let mut fields = Vec::new();
while !content.is_empty() {
let field = content.parse::<Field>()?;
fields.push(field);
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
Ok(Self {
name,
max_records,
primary_key,
secondary_index,
secondary_index_type,
fields,
})
}
}
struct DatabaseArgs {
name: Ident,
tables: Vec<Ident>,
low_power: bool,
low_power_max_records: Option<usize>,
default_max_records: usize,
total_memory: usize,
}
impl Parse for DatabaseArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name = input.parse()?;
let _comma: Token![,] = input.parse()?;
let _tables: Ident = input.parse()?;
let _colon: Token![:] = input.parse()?;
let content;
syn::bracketed!(content in input);
let mut tables = Vec::new();
while !content.is_empty() {
let table = content.parse::<Ident>()?;
tables.push(table);
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
let mut low_power = false;
let mut low_power_max_records = None;
let mut default_max_records = 100000; let mut total_memory = 65536;
while !input.is_empty() {
let _comma: Token![,] = input.parse()?;
let param_name = input.parse::<Ident>()?;
let _colon: Token![:] = input.parse()?;
if param_name == "low_power" {
let lit_bool = input.parse::<syn::LitBool>()?;
low_power = lit_bool.value;
} else if param_name == "low_power_max_records" {
let lit_int = input.parse::<syn::LitInt>()?;
low_power_max_records = Some(lit_int.base10_parse().unwrap_or(0));
} else if param_name == "default_max_records" {
let lit_int = input.parse::<syn::LitInt>()?;
default_max_records = lit_int.base10_parse().unwrap_or(100000);
} else if param_name == "total_memory" {
let lit_int = input.parse::<syn::LitInt>()?;
total_memory = lit_int.base10_parse().unwrap_or(65536);
}
}
Ok(Self {
name,
tables,
low_power,
low_power_max_records,
default_max_records,
total_memory,
})
}
}
#[proc_macro]
pub fn table(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let args = parse_macro_input!(input as TableArgs);
let name = &args.name;
let max_records = &args.max_records;
let primary_key = &args.primary_key;
let secondary_index = &args.secondary_index;
let secondary_index_type = &args.secondary_index_type;
let fields = &args.fields;
let mut offset = 0;
let mut field_defs = Vec::new();
let mut record_size = 0;
let mut primary_key_index = 0usize;
let mut secondary_key_index: Option<usize> = None;
for (i, field) in fields.iter().enumerate() {
let field_name = &field.name;
let type_name = &field.type_name;
let type_params = &field.type_params;
let (data_type, size_val, string_length) = if type_name == "i32" {
(quote!(remdb::types::DataType::Int32), 4, quote!(None))
} else if type_name == "i8" {
(quote!(remdb::types::DataType::Int8), 1, quote!(None))
} else if type_name == "u64" {
(quote!(remdb::types::DataType::UInt64), 8, quote!(None))
} else if type_name == "f64" {
(quote!(remdb::types::DataType::Float64), 8, quote!(None))
} else if type_name == "bool" {
(quote!(remdb::types::DataType::Bool), 1, quote!(None))
} else if type_name == "str" {
let str_size = if let Some(params) = type_params {
params.base10_parse().unwrap_or(32)
} else {
32
};
(
quote!(remdb::types::DataType::VarChar),
str_size,
quote!(Some(#str_size as usize)),
)
} else if type_name == "vector" {
let dim = if let Some(params) = type_params {
params.base10_parse().unwrap_or(128)
} else {
128
};
(
quote!(remdb::types::DataType::Vector),
dim * 4,
quote!(None),
) } else {
(quote!(remdb::types::DataType::Int32), 4, quote!(None))
};
let alignment = if type_name == "u64" || type_name == "f64" || type_name == "i64" {
8
} else if type_name == "i32" || type_name == "u32" || type_name == "f32" {
4
} else if type_name == "i16" || type_name == "u16" {
2
} else {
1
};
offset = ((offset + alignment - 1) / alignment) * alignment;
let is_primary_key = field_name == primary_key;
let primary_key_val = is_primary_key;
let not_null_val = is_primary_key; let unique_val = is_primary_key;
let is_integer_type =
type_name == "i32" || type_name == "i64" || type_name == "u32" || type_name == "u64";
let auto_increment_val = is_primary_key && is_integer_type;
let vector_metadata_code = if type_name == "vector" {
let dim = if let Some(params) = type_params {
params.base10_parse::<u16>().unwrap_or(128)
} else {
128u16
};
quote! {
Some(remdb::types::VectorMetadata {
dimension: #dim,
distance_type: remdb::types::DistanceType::L2,
index_type: remdb::types::VectorIndexType::HNSW,
compression_enabled: false,
compression_scheme: 0,
compression_level: 3,
hnsw_m: 16,
hnsw_ef_construction: 200,
hnsw_ef_search: 128,
ivf_nlist: 1024,
ivf_nprobe: 16,
})
}
} else {
quote! { None }
};
let field_def = quote! {
remdb::types::FieldDef {
name: stringify!(#field_name).to_string(),
data_type: #data_type,
size: #size_val as usize, string_length: #string_length,
offset: #offset as usize, primary_key: #primary_key_val,
not_null: #not_null_val,
unique: #unique_val,
auto_increment: #auto_increment_val,
default_value: None,
vector_metadata: #vector_metadata_code,
json_metadata: None,
}
};
field_defs.push(field_def);
if field_name == primary_key {
primary_key_index = i;
}
if let Some(secondary_field) = secondary_index {
if field_name == secondary_field {
secondary_key_index = Some(i);
}
}
offset += size_val;
record_size = offset;
}
let max_alignment = 8;
record_size = ((record_size + max_alignment - 1) / max_alignment) * max_alignment;
let max_records_usize = max_records.base10_parse::<usize>().unwrap_or(100);
let index_type = match secondary_index_type.as_ref() {
Some(ty) if ty == "btree" => quote!(remdb::types::IndexType::BTree),
Some(ty) if ty == "hash" => quote!(remdb::types::IndexType::Hash),
Some(ty) if ty == "ttree" => quote!(remdb::types::IndexType::TTree),
Some(ty) if ty == "sortedarray" => quote!(remdb::types::IndexType::SortedArray),
_ => quote!(remdb::types::IndexType::BTree),
};
let secondary_index_code = match secondary_key_index {
Some(index) => quote! { Some(vec![#index as usize]) },
None => quote! { None },
};
let output = quote! {
#[allow(non_upper_case_globals)]
pub static #name: std::sync::LazyLock<remdb::types::TableDef> = std::sync::LazyLock::new(|| {
remdb::types::TableDef {
id: 0,
name: stringify!(#name).to_string(),
fields: vec![#(#field_defs,)*],
primary_key: vec![#primary_key_index as usize],
secondary_index: #secondary_index_code,
secondary_index_type: #index_type,
record_size: #record_size as usize,
max_records: #max_records_usize,
version: 1,
created_at: 0,
updated_at: 0,
}
});
};
output.into()
}
#[proc_macro]
pub fn database(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let args = parse_macro_input!(input as DatabaseArgs);
let name = &args.name;
let tables = &args.tables;
let low_power = args.low_power;
let default_max_records = args.default_max_records;
let total_memory = args.total_memory;
let low_power_max_records = match args.low_power_max_records {
Some(val) => quote! { Some(#val) },
None => quote! { None },
};
let output = quote! {
#[allow(non_upper_case_globals)]
pub static #name: std::sync::LazyLock<remdb::config::DbConfig> = std::sync::LazyLock::new(|| {
remdb::config::DbConfig {
tables: vec![#( std::sync::LazyLock::force(&#tables).clone(), )*],
total_memory: #total_memory,
low_power_mode_supported: #low_power,
low_power_max_records: #low_power_max_records,
default_max_records: #default_max_records,
memory_allocator: unsafe {
static mut DEFAULT_ALLOCATOR: remdb::config::DefaultMemoryAllocator = remdb::config::DefaultMemoryAllocator;
&mut DEFAULT_ALLOCATOR
},
wal_config: remdb::config::WALConfig {
log_path: "wal",
log_mode: remdb::config::LogMode::Sync,
checkpoint_interval_ms: 60000, log_file_size_limit: 16 * 1024 * 1024, log_prealloc_size: 1 * 1024 * 1024, log_segment_size: 16 * 1024 * 1024, retained_checkpoints: 3, max_consecutive_invalid: 100,
skip_threshold: 1000,
skip_block_size: 1024 * 1024,
max_skip_attempts: 3,
compression_type: remdb::config::WALCompressionType::None,
compression_level: 3
},
time_series_defaults: remdb::time_series::TimeSeriesConfig::DEFAULT,
#[cfg(feature = "pubsub")]
pubsub_config: None,
#[cfg(feature = "ha")]
ha_config: Some(remdb::ha::HAConfig {
node_id: 1, ha_role: remdb::ha::HARole::Auto,
replication_mode: remdb::ha::ReplicationMode::Async,
heartbeat_interval_ms: 1000, failure_detection_ms: 3000, sync_timeout_ms: 2000, master_address: None,
master_port: None,
replication_port: 5556,
}),
model_worker_config: remdb::config::ModelWorkerConfig::DEFAULT,
}
});
};
output.into()
}