- netboot-base.nix with SSH key auth - Launch scripts for node01/02/03 - Node configuration.nix and disko.nix - Nix modules for first-boot automation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
279 lines
9 KiB
Rust
279 lines
9 KiB
Rust
use crate::error::{Result, SqlError};
|
|
use crate::types::{ColumnDef, DataType, TableMetadata};
|
|
use flaredb_client::RdbClient;
|
|
use std::sync::Arc;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
use tokio::sync::{Mutex, RwLock};
|
|
|
|
const META_PREFIX: &str = "__sql_meta";
|
|
const TABLES_KEY_PREFIX: &str = "__sql_meta:tables";
|
|
const NEXT_TABLE_ID_KEY: &str = "__sql_meta:next_table_id";
|
|
|
|
/// Metadata manager for SQL tables
|
|
pub struct MetadataManager {
|
|
client: Arc<Mutex<RdbClient>>,
|
|
cache: Arc<RwLock<std::collections::HashMap<String, TableMetadata>>>,
|
|
}
|
|
|
|
impl MetadataManager {
|
|
pub fn new(client: Arc<Mutex<RdbClient>>) -> Self {
|
|
Self {
|
|
client,
|
|
cache: Arc::new(RwLock::new(std::collections::HashMap::new())),
|
|
}
|
|
}
|
|
|
|
/// Create a new table
|
|
pub async fn create_table(
|
|
&self,
|
|
table_name: String,
|
|
columns: Vec<ColumnDef>,
|
|
primary_key: Vec<String>,
|
|
) -> Result<TableMetadata> {
|
|
// Check if table already exists
|
|
if self.table_exists(&table_name).await? {
|
|
return Err(SqlError::TableAlreadyExists(table_name));
|
|
}
|
|
|
|
// Validate primary key columns exist
|
|
for pk_col in &primary_key {
|
|
if !columns.iter().any(|c| &c.name == pk_col) {
|
|
return Err(SqlError::ColumnNotFound(
|
|
pk_col.clone(),
|
|
table_name.clone(),
|
|
));
|
|
}
|
|
}
|
|
|
|
// Allocate table ID
|
|
let table_id = self.allocate_table_id().await?;
|
|
|
|
// Create table metadata
|
|
let metadata = TableMetadata {
|
|
table_id,
|
|
table_name: table_name.clone(),
|
|
columns,
|
|
primary_key,
|
|
created_at: SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs(),
|
|
};
|
|
|
|
// Store metadata in KVS
|
|
self.store_metadata(&metadata).await?;
|
|
|
|
// Update cache
|
|
self.cache
|
|
.write()
|
|
.await
|
|
.insert(table_name.clone(), metadata.clone());
|
|
|
|
Ok(metadata)
|
|
}
|
|
|
|
/// Drop an existing table
|
|
pub async fn drop_table(&self, table_name: &str) -> Result<()> {
|
|
// Check if table exists
|
|
if !self.table_exists(table_name).await? {
|
|
return Err(SqlError::TableNotFound(table_name.to_string()));
|
|
}
|
|
|
|
// Get table metadata to find table_id
|
|
let metadata = self.get_table_metadata(table_name).await?;
|
|
|
|
// Delete metadata from KVS
|
|
let key = format!("{}:{}", TABLES_KEY_PREFIX, table_name);
|
|
let mut client = self.client.lock().await;
|
|
client
|
|
.raw_delete(key.as_bytes().to_vec())
|
|
.await
|
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
|
|
|
// Remove from cache
|
|
self.cache.write().await.remove(table_name);
|
|
|
|
// Note: We don't delete table data here for simplicity
|
|
// In production, we'd need to scan and delete all rows with table_id prefix
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get table metadata
|
|
pub async fn get_table_metadata(&self, table_name: &str) -> Result<TableMetadata> {
|
|
// Check cache first
|
|
{
|
|
let cache = self.cache.read().await;
|
|
if let Some(metadata) = cache.get(table_name) {
|
|
return Ok(metadata.clone());
|
|
}
|
|
}
|
|
|
|
// Load from KVS
|
|
let key = format!("{}:{}", TABLES_KEY_PREFIX, table_name);
|
|
let mut client = self.client.lock().await;
|
|
let value = client
|
|
.cas_get(key.as_bytes().to_vec())
|
|
.await
|
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
|
|
|
if let Some((_version, ref bytes)) = value {
|
|
let metadata: TableMetadata = bincode::deserialize(&bytes)
|
|
.map_err(|e| SqlError::SerializationError(e.to_string()))?;
|
|
|
|
// Update cache
|
|
self.cache
|
|
.write()
|
|
.await
|
|
.insert(table_name.to_string(), metadata.clone());
|
|
|
|
Ok(metadata)
|
|
} else {
|
|
Err(SqlError::TableNotFound(table_name.to_string()))
|
|
}
|
|
}
|
|
|
|
/// Check if table exists
|
|
pub async fn table_exists(&self, table_name: &str) -> Result<bool> {
|
|
// Check cache first
|
|
{
|
|
let cache = self.cache.read().await;
|
|
if cache.contains_key(table_name) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
|
|
// Check KVS
|
|
let key = format!("{}:{}", TABLES_KEY_PREFIX, table_name);
|
|
let mut client = self.client.lock().await;
|
|
let value = client
|
|
.cas_get(key.as_bytes().to_vec())
|
|
.await
|
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
|
|
|
Ok(value.is_some())
|
|
}
|
|
|
|
/// List all tables in the namespace
|
|
pub async fn list_tables(&self) -> Result<Vec<String>> {
|
|
let start_key = format!("{}:", TABLES_KEY_PREFIX);
|
|
let end_key = format!("{}~", TABLES_KEY_PREFIX); // '~' is after ':' in ASCII
|
|
|
|
let mut client = self.client.lock().await;
|
|
let (entries, _next_key) = client
|
|
.cas_scan(start_key.as_bytes().to_vec(), end_key.as_bytes().to_vec(), 1000)
|
|
.await
|
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
|
|
|
let mut tables = Vec::new();
|
|
for (key, _value, _version) in entries {
|
|
if let Ok(key_str) = String::from_utf8(key) {
|
|
if let Some(table_name) = key_str.strip_prefix(&format!("{}:", TABLES_KEY_PREFIX)) {
|
|
tables.push(table_name.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(tables)
|
|
}
|
|
|
|
/// Store table metadata in KVS
|
|
async fn store_metadata(&self, metadata: &TableMetadata) -> Result<()> {
|
|
let key = format!("{}:{}", TABLES_KEY_PREFIX, metadata.table_name);
|
|
let value =
|
|
bincode::serialize(metadata).map_err(|e| SqlError::SerializationError(e.to_string()))?;
|
|
|
|
let mut client = self.client.lock().await;
|
|
// Use version 0 for new table (we already checked it doesn't exist)
|
|
let (success, _current_version, _new_version) = client
|
|
.cas(key.as_bytes().to_vec(), value, 0)
|
|
.await
|
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
|
|
|
if !success {
|
|
return Err(SqlError::InternalError("Failed to store table metadata".to_string()));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Allocate a new table ID using CAS for atomicity
|
|
async fn allocate_table_id(&self) -> Result<u32> {
|
|
let mut client = self.client.lock().await;
|
|
let key = NEXT_TABLE_ID_KEY.as_bytes().to_vec();
|
|
|
|
// Retry loop for CAS
|
|
for _attempt in 0..10 {
|
|
// Get current counter with version
|
|
let current = client
|
|
.cas_get(key.clone())
|
|
.await
|
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
|
|
|
let (next_id, expected_version) = if let Some((version, bytes)) = current {
|
|
let current_id = u32::from_be_bytes(
|
|
bytes
|
|
.try_into()
|
|
.map_err(|_| SqlError::InternalError("Invalid table ID format".to_string()))?,
|
|
);
|
|
(current_id + 1, version)
|
|
} else {
|
|
(1u32, 0u64) // Start from 1 if no counter exists
|
|
};
|
|
|
|
// Try to CAS the incremented counter
|
|
let value = next_id.to_be_bytes().to_vec();
|
|
let (success, _current_version, _new_version) = client
|
|
.cas(key.clone(), value, expected_version)
|
|
.await
|
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
|
|
|
if success {
|
|
return Ok(next_id);
|
|
}
|
|
// CAS failed, retry
|
|
}
|
|
|
|
Err(SqlError::InternalError("Failed to allocate table ID after retries".to_string()))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::types::DataType;
|
|
|
|
// Note: These tests require a running FlareDB instance
|
|
// For now, we'll keep them as examples
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires FlareDB server
|
|
async fn test_create_table() {
|
|
let client = RdbClient::connect_direct("127.0.0.1:8001".to_string(), "sqltest".to_string()).await.unwrap();
|
|
let manager = MetadataManager::new(Arc::new(Mutex::new(client)));
|
|
|
|
let columns = vec![
|
|
ColumnDef {
|
|
name: "id".to_string(),
|
|
data_type: DataType::Integer,
|
|
nullable: false,
|
|
default_value: None,
|
|
},
|
|
ColumnDef {
|
|
name: "name".to_string(),
|
|
data_type: DataType::Text,
|
|
nullable: false,
|
|
default_value: None,
|
|
},
|
|
];
|
|
|
|
let metadata = manager
|
|
.create_table("users".to_string(), columns, vec!["id".to_string()])
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(metadata.table_name, "users");
|
|
assert_eq!(metadata.columns.len(), 2);
|
|
assert_eq!(metadata.primary_key, vec!["id".to_string()]);
|
|
}
|
|
}
|