diff --git a/.env b/.env new file mode 100644 index 0000000..c0c68b1 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PORT=3000 \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..eb81371 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "rust-rocksdb"] + path = rust-rocksdb + url = https://github.com/rust-rocksdb/rust-rocksdb.git diff --git a/Cargo.toml b/Cargo.toml index c8d7834..327b573 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +axum = {version = "0.7.4"} serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1.0", features = ["full"] } serde_json = "1.0" +tower-http = { version = "0.4.0", features = ["full"] } +dotenv = "0.15.0" rocksdb = "0.22.0" + +pretty_assertions = "0.7" +select = "0.5" diff --git a/README.md b/README.md new file mode 100644 index 0000000..90f05b8 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Catalog Team 1 diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..24e32ea --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1 @@ +pub mod parameters; diff --git a/src/config/parameters.rs b/src/config/parameters.rs new file mode 100644 index 0000000..b530dac --- /dev/null +++ b/src/config/parameters.rs @@ -0,0 +1,13 @@ +use dotenv; + +// load the env file +pub fn init() { + dotenv::dotenv().ok().expect("Failed to load .env file"); +} + +// get the parameters from the env file and throw errors appropriately +pub fn get(parameter: &str) -> String { + let env_parameter = std::env::var(parameter) + .expect(&format!("{} is not defined in the environment", parameter)); + env_parameter +} diff --git a/src/dto/column_data.rs b/src/dto/column_data.rs new file mode 100644 index 0000000..edafdef --- /dev/null +++ b/src/dto/column_data.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ColumnData { + pub aggregates: Value, + pub value_range: (i32, i32), + pub is_strong_key: bool, + pub is_weak_key: bool, + pub primary_key_col_name: String, +} diff --git a/src/dto/mod.rs b/src/dto/mod.rs index 1e6a204..a5ee4af 100644 --- a/src/dto/mod.rs +++ b/src/dto/mod.rs @@ -1,3 +1,4 @@ +pub mod column_data; pub mod namespace_data; pub mod operator_statistics; pub mod table_data; diff --git a/src/dto/table_data.rs b/src/dto/table_data.rs index 63857a2..6f7b8c0 100644 --- a/src/dto/table_data.rs +++ b/src/dto/table_data.rs @@ -1,3 +1,4 @@ +use crate::dto::column_data::ColumnData; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -8,10 +9,5 @@ pub struct TableData { pub read_properties: Value, pub write_properties: Value, pub file_urls: Vec, - pub columns: Vec>, - pub aggregates: Value, - pub value_range: (i32, i32), - pub is_strong_key: bool, - pub is_weak_key: bool, - pub primary_key_col_name: String, + pub columns: Vec, } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..d291cb8 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,2 @@ +pub mod namespace_handler; +pub mod table_handler; diff --git a/src/handlers/namespace_handler.rs b/src/handlers/namespace_handler.rs new file mode 100644 index 0000000..1e5e1b3 --- /dev/null +++ b/src/handlers/namespace_handler.rs @@ -0,0 +1,73 @@ +use axum::{ + extract::{Json, Path}, + http::StatusCode, + response::IntoResponse, +}; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct Namespace {} + +pub async fn list_namespaces() -> Json> { + // Logic to list namespaces + let namespaces: Vec = vec![ + "accounting".to_string(), + "tax".to_string(), + "paid".to_string(), + ]; + Json(namespaces) +} + +pub async fn create_namespace(new_namespace: Json) -> Json { + // Logic to create a new namespace + + // Logic to persist the namespace and add properties + new_namespace +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct NamespaceMetadata { + // Define your namespace metadata properties here + // Example: pub metadata_property: String, + data: String, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct NamespaceProperties { + data: String, +} + +pub async fn load_namespace_metadata(Path(namespace): Path) -> Json { + print!("Namespaces: {}", namespace); + // Logic to load metadata properties for a namespace + let metadata = NamespaceMetadata { + data: namespace, + // Populate with actual metadata properties + }; + Json(metadata) +} + +pub async fn namespace_exists(Path(namespace): Path) -> impl IntoResponse { + // Logic to check if a namespace exists + // This route just needs to return a status code, no body required + // Return HTTP status code 200 to indicate namespace exists + StatusCode::FOUND +} + +pub async fn drop_namespace(Path(namespace): Path) -> impl IntoResponse { + // Logic to drop a namespace from the catalog + // Ensure the namespace is empty before dropping + // Return HTTP status code 204 to indicate successful deletion + StatusCode::NO_CONTENT +} + +pub async fn set_namespace_properties(Path(namespace): Path) -> Json { + // Logic to set and/or remove properties on a namespace + // Deserialize request body and process properties + // Return HTTP status code 200 to indicate success + + let prop = NamespaceProperties { + data: "namespace properties".to_string(), + }; + + Json(prop) +} diff --git a/src/handlers/table_handler.rs b/src/handlers/table_handler.rs new file mode 100644 index 0000000..9be2910 --- /dev/null +++ b/src/handlers/table_handler.rs @@ -0,0 +1,64 @@ +use axum::{ + extract::{Json, Path}, + http::StatusCode, + response::IntoResponse, +}; + +pub async fn list_tables(Path(namespace): Path) -> Json> { + // Dummy response for demonstration + let tables: Vec = vec![ + "accounting".to_string(), + "tax".to_string(), + "paid".to_string(), + ]; + Json(tables) +} + +pub async fn create_table(Path(namespace): Path) -> impl IntoResponse { + // Logic to create a table in the given namespace + "Table created".to_string() +} + +pub async fn register_table(Path(namespace): Path) -> impl IntoResponse { + // Logic to register a table in the given namespace using metadata file location + "Table registered".to_string() +} + +pub async fn load_table(Path((namespace, table)): Path<(String, String)>) -> impl IntoResponse { + // Logic to load a table from the catalog + Json(table) +} + +pub async fn delete_table(Path((namespace, table)): Path<(String, String)>) -> impl IntoResponse { + // Logic to drop a table from the catalog + "Table dropped".to_string() +} + +pub async fn table_exists(Path((namespace, table)): Path<(String, String)>) -> impl IntoResponse { + // Logic to check if a table exists within a given namespace + StatusCode::OK +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct MetricsReport {} + +// Handler functions +pub async fn rename_table(table_rename: String) -> impl IntoResponse { + // Logic to rename a table from its current name to a new name + "Table renamed".to_string() +} + +pub async fn report_metrics(Path((namespace, table)): Path<(String, String)>) -> impl IntoResponse { + // Logic to process metrics report + Json(table) +} + +pub async fn find_tuple_location( + Path((namespace, table, tuple_id)): Path<(String, String, String)>, +) -> impl IntoResponse { + // Logic to return the physical file location for a given tuple ID + format!( + "Physical file location for tuple ID {} of table {} in namespace {}.", + tuple_id, table, namespace + ) +} diff --git a/src/main.rs b/src/main.rs index 2fe40a1..10a4616 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,17 @@ -fn main() { - println!("hello world!"); +mod config; +mod dto; +mod handlers; +mod routes; +mod tests; + +use crate::config::parameters; + +#[tokio::main] +async fn main() { + parameters::init(); + let host = format!("0.0.0.0:{}", parameters::get("PORT")); + + let listener = tokio::net::TcpListener::bind(host).await.unwrap(); + let app = routes::root::routes(); + axum::serve(listener, app).await.unwrap(); } diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..5b0dfd6 --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,3 @@ +pub mod namespace; +pub mod root; +pub mod table; diff --git a/src/routes/namespace.rs b/src/routes/namespace.rs new file mode 100644 index 0000000..4ffb281 --- /dev/null +++ b/src/routes/namespace.rs @@ -0,0 +1,28 @@ +use crate::handlers::namespace_handler; +use axum::{ + routing::{delete, get, head, post}, + Router, +}; + +pub fn routes() -> Router { + let router = Router::new() + .route("/namespaces", get(namespace_handler::list_namespaces)) + .route("/namespaces", post(namespace_handler::create_namespace)) + .route( + "/namespace/:namespace", + get(namespace_handler::load_namespace_metadata), + ) + .route( + "/namespace/:namespace", + head(namespace_handler::namespace_exists), + ) + .route( + "/namespace/:namespace", + delete(namespace_handler::drop_namespace), + ) + .route( + "/namespace/:namespace/properties", + post(namespace_handler::set_namespace_properties), + ); + return router; +} diff --git a/src/routes/root.rs b/src/routes/root.rs new file mode 100644 index 0000000..9ebd46c --- /dev/null +++ b/src/routes/root.rs @@ -0,0 +1,13 @@ +use crate::routes::{namespace, table}; +use axum::routing::IntoMakeService; +use axum::Router; +use tower_http::trace::TraceLayer; + +pub fn routes() -> Router { + // merge the 2 routes + let app_router = Router::new() + .nest("/", table::routes()) + .nest("/", namespace::routes()); + + app_router +} diff --git a/src/routes/table.rs b/src/routes/table.rs new file mode 100644 index 0000000..586a18e --- /dev/null +++ b/src/routes/table.rs @@ -0,0 +1,44 @@ +use crate::handlers::table_handler; +use axum::{ + routing::{delete, get, head, post}, + Router, +}; + +pub fn routes() -> Router { + let router = Router::new() + .route( + "/namespaces/:namespace/tables", + get(table_handler::list_tables), + ) + .route( + "/namespaces/:namespace/tables", + post(table_handler::create_table), + ) + .route( + "/namespaces/:namespace/register", + post(table_handler::register_table), + ) + .route( + "/namespaces/:namespace/tables/:table", + get(table_handler::load_table), + ) + .route( + "/namespaces/:namespace/tables/:table", + delete(table_handler::delete_table), + ) + .route( + "/namespaces/:namespace/tables/:table", + head(table_handler::table_exists), + ) + .route("/tables/rename", post(table_handler::rename_table)) + .route( + "/namespaces/:namespace/tables/:table/metrics", + post(table_handler::report_metrics), + ) + .route( + "/namespaces/:namespace/tables/:table/find/:tuple_id", + get(table_handler::find_tuple_location), + ); + + return router; +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/tests/namespace_test.rs b/src/tests/namespace_test.rs new file mode 100644 index 0000000..3e6e447 --- /dev/null +++ b/src/tests/namespace_test.rs @@ -0,0 +1,43 @@ +use axum::{http::StatusCode, response::Json}; +use axum::extract::Json as JsonExtractor; +use axum::handler::post; +use axum::routing::Router; +use serde_json::json; +use axum::test::extract; + +use crate::{create_namespace, list_namespaces, Namespace}; + +#[tokio::test] +async fn test_list_namespaces() { + // Create a test router with the list_namespaces route + let app = Router::new().route("/namespaces", post(list_namespaces)); + + // Perform a request to the route + let response = axum::test::call(&app, axum::test::request::Request::post("/namespaces").body(()).unwrap()).await; + + // Ensure that the response status code is OK + assert_eq!(response.status(), StatusCode::OK); + + // Ensure that the response body contains the expected JSON data + let body = extract::>>(response.into_body()).await.unwrap(); + assert_eq!(body.0, vec!["accounting", "tax", "paid"]); +} + +#[tokio::test] +async fn test_create_namespace() { + // Create a test router with the create_namespace route + let app = Router::new().route("/namespaces", post(create_namespace)); + + // Create a JSON payload representing a new namespace + let payload = json!({}); + + // Perform a request to the route with the JSON payload + let response = axum::test::call(&app, axum::test::request::Request::post("/namespaces").body(payload.to_string()).unwrap()).await; + + // Ensure that the response status code is OK + assert_eq!(response.status(), StatusCode::OK); + + // Ensure that the response body contains the expected JSON data + let body = extract::>(response.into_body()).await.unwrap(); + assert_eq!(body, Json(Namespace {})); +}