use axum::{
extract::{Path, State},
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::error::DbError;
use crate::server::handlers::AppState;
use crate::storage::columnar::{
AggregateOp, ColumnDef, ColumnFilter, ColumnarCollection, CompressionType,
};
#[derive(Debug, Deserialize)]
pub struct CreateColumnarRequest {
pub name: String,
pub columns: Vec<ColumnDefRequest>,
#[serde(default)]
pub compression: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct ColumnDefRequest {
pub name: String,
#[serde(rename = "type")]
pub data_type: String,
#[serde(default)]
pub nullable: bool,
#[serde(default)]
pub indexed: bool,
}
#[derive(Debug, Serialize)]
pub struct CreateColumnarResponse {
pub status: String,
pub name: String,
pub columns: usize,
}
#[derive(Debug, Deserialize)]
pub struct InsertColumnarRequest {
pub rows: Vec<Value>,
}
#[derive(Debug, Serialize)]
pub struct InsertColumnarResponse {
pub status: String,
pub inserted: usize,
pub ids: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct AggregateRequest {
pub column: String,
pub operation: String,
#[serde(default)]
pub group_by: Option<Vec<String>>,
}
#[derive(Debug, Serialize)]
pub struct AggregateResponse {
pub result: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub groups: Option<Vec<Value>>,
}
#[derive(Debug, Deserialize)]
pub struct QueryColumnarRequest {
pub columns: Vec<String>,
#[serde(default)]
pub filter: Option<FilterRequest>,
#[serde(default)]
pub limit: Option<usize>,
}
#[derive(Debug, Deserialize)]
pub struct FilterRequest {
pub column: String,
pub op: String,
pub value: Value,
}
#[derive(Debug, Serialize)]
pub struct QueryColumnarResponse {
pub result: Vec<Value>,
pub count: usize,
}
pub async fn create_columnar_handler(
State(state): State<AppState>,
Path(db_name): Path<String>,
Json(req): Json<CreateColumnarRequest>,
) -> Result<impl IntoResponse, DbError> {
let db = state.storage.get_database(&db_name)?;
let columns: Vec<ColumnDef> = req
.columns
.into_iter()
.map(|c| ColumnDef {
name: c.name,
data_type: parse_column_type(&c.data_type),
nullable: c.nullable,
indexed: c.indexed,
index_type: None, })
.collect();
let compression = match req.compression.as_deref() {
Some("none") => CompressionType::None,
Some("lz4") | None => CompressionType::Lz4,
Some(other) => {
return Err(DbError::BadRequest(format!(
"Unknown compression type: {}. Supported: none, lz4",
other
)))
}
};
let cf_name = format!("_columnar_{}", req.name);
db.create_collection(cf_name.clone(), None)?;
let col = ColumnarCollection::new(
req.name.clone(),
&db_name,
db.db_arc(),
columns.clone(),
compression,
)?;
Ok(Json(CreateColumnarResponse {
status: "created".to_string(),
name: col.name,
columns: columns.len(),
}))
}
pub async fn list_columnar_handler(
State(state): State<AppState>,
Path(db_name): Path<String>,
) -> Result<impl IntoResponse, DbError> {
let db = state.storage.get_database(&db_name)?;
let collection_names: Vec<String> = db
.list_collections()
.into_iter()
.filter(|name| name.starts_with("_columnar_"))
.map(|name| name.trim_start_matches("_columnar_").to_string())
.collect();
let mut collections = Vec::new();
for name in &collection_names {
if let Ok(col) = ColumnarCollection::load(name.clone(), &db_name, db.db_arc()) {
if let Ok(meta) = col.metadata() {
collections.push(serde_json::json!({
"name": name,
"row_count": meta.row_count,
"columns": meta.columns,
"compression": format!("{:?}", meta.compression),
"created_at": meta.created_at
}));
}
}
}
Ok(Json(serde_json::json!({
"collections": collections,
"count": collections.len()
})))
}
pub async fn get_columnar_handler(
State(state): State<AppState>,
Path((db_name, collection)): Path<(String, String)>,
) -> Result<impl IntoResponse, DbError> {
let db = state.storage.get_database(&db_name)?;
let col = ColumnarCollection::load(collection, &db_name, db.db_arc())?;
let meta = col.metadata()?;
let stats = col.stats()?;
Ok(Json(serde_json::json!({
"name": meta.name,
"columns": meta.columns,
"row_count": meta.row_count,
"compression": meta.compression,
"created_at": meta.created_at,
"last_updated_at": meta.last_updated_at,
"stats": {
"compressed_size_bytes": stats.compressed_size_bytes,
"uncompressed_size_bytes": stats.uncompressed_size_bytes,
"compression_ratio": stats.compression_ratio
}
})))
}
pub async fn delete_columnar_handler(
State(state): State<AppState>,
Path((db_name, collection)): Path<(String, String)>,
) -> Result<impl IntoResponse, DbError> {
let db = state.storage.get_database(&db_name)?;
let cf_name = format!("_columnar_{}", collection);
db.delete_collection(&cf_name)?;
Ok(Json(serde_json::json!({
"status": "deleted",
"name": collection
})))
}
pub async fn insert_columnar_handler(
State(state): State<AppState>,
Path((db_name, collection)): Path<(String, String)>,
Json(req): Json<InsertColumnarRequest>,
) -> Result<impl IntoResponse, DbError> {
let db = state.storage.get_database(&db_name)?;
let col = ColumnarCollection::load(collection.clone(), &db_name, db.db_arc())?;
let rows_for_log = req.rows.clone();
let inserted_ids = col.insert_rows(req.rows)?;
if let Some(ref log) = state.replication_log {
for (id, row) in inserted_ids.iter().zip(rows_for_log.iter()) {
let row_data = serde_json::to_vec(row).ok();
log.append_columnar(
&db_name,
&collection,
crate::sync::protocol::Operation::ColumnarInsert,
id.clone(),
row_data,
);
}
}
Ok(Json(InsertColumnarResponse {
status: "ok".to_string(),
inserted: inserted_ids.len(),
ids: inserted_ids,
}))
}
pub async fn aggregate_columnar_handler(
State(state): State<AppState>,
Path((db_name, collection)): Path<(String, String)>,
Json(req): Json<AggregateRequest>,
) -> Result<impl IntoResponse, DbError> {
let db = state.storage.get_database(&db_name)?;
let col = ColumnarCollection::load(collection, &db_name, db.db_arc())?;
let op = AggregateOp::from_str(&req.operation).ok_or_else(|| {
DbError::BadRequest(format!(
"Unknown aggregation operation: {}. Supported: SUM, AVG, COUNT, MIN, MAX, COUNT_DISTINCT",
req.operation
))
})?;
if let Some(group_cols) = req.group_by {
use crate::storage::columnar::GroupByColumn;
let group_defs: Vec<GroupByColumn> = group_cols
.iter()
.map(|s| GroupByColumn::Simple(s.clone()))
.collect();
let groups = col.group_by(&group_defs, &req.column, op)?;
Ok(Json(AggregateResponse {
result: Value::Null,
groups: Some(groups),
}))
} else {
let result = col.aggregate(&req.column, op)?;
Ok(Json(AggregateResponse {
result,
groups: None,
}))
}
}
pub async fn query_columnar_handler(
State(state): State<AppState>,
Path((db_name, collection)): Path<(String, String)>,
Json(req): Json<QueryColumnarRequest>,
) -> Result<impl IntoResponse, DbError> {
let db = state.storage.get_database(&db_name)?;
let col = ColumnarCollection::load(collection, &db_name, db.db_arc())?;
let col_refs: Vec<&str> = req.columns.iter().map(|s| s.as_str()).collect();
let mut results = if let Some(filter_req) = req.filter {
let filter = parse_filter(&filter_req)?;
col.scan_filtered(&filter, &col_refs)?
} else {
col.read_columns(&col_refs, None)?
};
if let Some(limit) = req.limit {
results.truncate(limit);
}
let count = results.len();
Ok(Json(QueryColumnarResponse {
result: results,
count,
}))
}
fn parse_column_type(type_str: &str) -> crate::storage::columnar::ColumnType {
use crate::storage::columnar::ColumnType;
match type_str.to_uppercase().as_str() {
"INT64" | "INTEGER" | "INT" | "BIGINT" => ColumnType::Int64,
"FLOAT64" | "FLOAT" | "DOUBLE" | "NUMBER" => ColumnType::Float64,
"STRING" | "TEXT" | "VARCHAR" => ColumnType::String,
"BOOL" | "BOOLEAN" => ColumnType::Bool,
"TIMESTAMP" | "DATETIME" | "DATE" => ColumnType::Timestamp,
"JSON" | "OBJECT" | "ARRAY" => ColumnType::Json,
_ => ColumnType::String, }
}
fn parse_filter(req: &FilterRequest) -> Result<ColumnFilter, DbError> {
match req.op.to_uppercase().as_str() {
"EQ" | "=" | "==" => Ok(ColumnFilter::Eq(req.column.clone(), req.value.clone())),
"NE" | "!=" | "<>" => Ok(ColumnFilter::Ne(req.column.clone(), req.value.clone())),
"GT" | ">" => Ok(ColumnFilter::Gt(req.column.clone(), req.value.clone())),
"GTE" | ">=" => Ok(ColumnFilter::Gte(req.column.clone(), req.value.clone())),
"LT" | "<" => Ok(ColumnFilter::Lt(req.column.clone(), req.value.clone())),
"LTE" | "<=" => Ok(ColumnFilter::Lte(req.column.clone(), req.value.clone())),
"IN" => {
if let Value::Array(arr) = &req.value {
Ok(ColumnFilter::In(req.column.clone(), arr.clone()))
} else {
Err(DbError::BadRequest(
"IN operator requires an array value".to_string(),
))
}
}
other => Err(DbError::BadRequest(format!(
"Unknown filter operator: {}. Supported: EQ, NE, GT, GTE, LT, LTE, IN",
other
))),
}
}
#[derive(Debug, Deserialize)]
pub struct CreateIndexRequest {
pub column: String,
#[serde(default)]
pub index_type: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CreateIndexResponse {
pub status: String,
pub column: String,
pub index_type: String,
}
pub async fn create_columnar_index_handler(
State(state): State<AppState>,
Path((db_name, collection)): Path<(String, String)>,
Json(req): Json<CreateIndexRequest>,
) -> Result<impl IntoResponse, DbError> {
let db = state.storage.get_database(&db_name)?;
let col = ColumnarCollection::load(collection.clone(), &db_name, db.db_arc())?;
let index_type = match req.index_type.as_deref() {
Some("hash") => crate::storage::columnar::ColumnarIndexType::Hash,
Some("sorted") | None => crate::storage::columnar::ColumnarIndexType::Sorted,
Some("bitmap") => crate::storage::columnar::ColumnarIndexType::Bitmap,
Some("minmax") => crate::storage::columnar::ColumnarIndexType::MinMax,
Some("bloom") => crate::storage::columnar::ColumnarIndexType::Bloom,
Some(other) => {
return Err(DbError::BadRequest(format!(
"Unknown index type: {}. Supported: sorted, hash, bitmap, minmax, bloom",
other
)))
}
};
col.create_index(&req.column, index_type.clone())?;
Ok(Json(CreateIndexResponse {
status: "created".to_string(),
column: req.column,
index_type: format!("{:?}", index_type).to_lowercase(),
}))
}
pub async fn list_columnar_indexes_handler(
State(state): State<AppState>,
Path((db_name, collection)): Path<(String, String)>,
) -> Result<impl IntoResponse, DbError> {
let db = state.storage.get_database(&db_name)?;
let col = ColumnarCollection::load(collection, &db_name, db.db_arc())?;
let indexes = col.list_indexes()?;
let indexes_json: Vec<Value> = indexes
.into_iter()
.map(|idx| {
serde_json::json!({
"column": idx.column,
"index_type": format!("{:?}", idx.index_type).to_lowercase(),
"created_at": idx.created_at
})
})
.collect();
Ok(Json(serde_json::json!({
"indexes": indexes_json,
"count": indexes_json.len()
})))
}
pub async fn delete_columnar_index_handler(
State(state): State<AppState>,
Path((db_name, collection, column)): Path<(String, String, String)>,
) -> Result<impl IntoResponse, DbError> {
let db = state.storage.get_database(&db_name)?;
let col = ColumnarCollection::load(collection, &db_name, db.db_arc())?;
col.drop_index(&column)?;
Ok(Json(serde_json::json!({
"status": "deleted",
"column": column
})))
}