diff --git a/eden/fs/cli_rs/edenfs-saved-state/Cargo.toml b/eden/fs/cli_rs/edenfs-saved-state/Cargo.toml index 54d3ea392ea83..6391f9c0410e5 100644 --- a/eden/fs/cli_rs/edenfs-saved-state/Cargo.toml +++ b/eden/fs/cli_rs/edenfs-saved-state/Cargo.toml @@ -10,6 +10,7 @@ license = "GPLv2+" [dependencies] anyhow = "1.0.98" fbinit = { version = "0.2.0", git = "https://github.com/facebookexperimental/rust-shed.git", branch = "main" } +mysql_client = { package = "oss_mysql_client", version = "0.1.0", path = "../oss_mysql_client" } sapling-client = { version = "0.1.0", path = "../sapling-client" } serde = { version = "1.0.219", features = ["derive", "rc"] } diff --git a/eden/fs/cli_rs/edenfs-saved-state/src/lib.rs b/eden/fs/cli_rs/edenfs-saved-state/src/lib.rs index 441c3fdc0028a..999a161e74a46 100644 --- a/eden/fs/cli_rs/edenfs-saved-state/src/lib.rs +++ b/eden/fs/cli_rs/edenfs-saved-state/src/lib.rs @@ -5,7 +5,6 @@ * GNU General Public License version 2. */ -use std::ops::Add; use anyhow::Context; use mysql_client::DbLocator; diff --git a/eden/fs/cli_rs/oss_mysql_client/Cargo.toml b/eden/fs/cli_rs/oss_mysql_client/Cargo.toml new file mode 100644 index 0000000000000..61e8451ca2f94 --- /dev/null +++ b/eden/fs/cli_rs/oss_mysql_client/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "oss_mysql_client" +version = "0.1.0" +authors = ["Facebook Source Control Team "] +edition = "2024" +license = "GPLv2+" + +[dependencies] +anyhow = "1.0.98" +async-trait = "0.1.86" +fbinit = { version = "0.2.0", git = "https://github.com/ben--/rust-shed.git", branch = "scubasample-lifetime" } +mysql_async = "0.31.2" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +tokio = { version = "1.47.1", features = ["full"] } \ No newline at end of file diff --git a/eden/fs/cli_rs/oss_mysql_client/src/lib.rs b/eden/fs/cli_rs/oss_mysql_client/src/lib.rs new file mode 100644 index 0000000000000..dff5dd7f5f150 --- /dev/null +++ b/eden/fs/cli_rs/oss_mysql_client/src/lib.rs @@ -0,0 +1,180 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This software may be used and distributed according to the terms of the + * GNU General Public License version 2. + */ + +//! OSS-compatible mysql_client replacement for edenfs-saved-state +//! +//! ************************************************************************ +//! * WARNING: This crate is (likely) non-functional and is a development vessel for +//! OSS implementation of a MySQL client. It is not intended for production use. +//! +//! Steps: +//! 1. Implement interface compatible with internal mysql_client crate +//! 2. Update edenfs-saved-state to use this crate instead of internal mysql_client +//! +//! 3. Build and test in OSS environment +//! 4. Create contract tests that run on both internal and OSS mysql_client crates +//! +//! * END WARNING +//! ************************************************************************ +//! +//! This crate provides a minimal MySQL client API compatible with the internal +//! mysql_client crate, but implemented using OSS-compatible libraries. + +use std::fmt; + +use anyhow::Context; +use anyhow::Result; +use fbinit::FacebookInit; +use mysql_async::prelude::*; +use mysql_async::{Pool, Row}; + +/// Database locator that specifies which database to connect to +#[derive(Debug, Clone)] +pub struct DbLocator { + pub schema: String, + pub instance_requirement: InstanceRequirement, +} + +impl DbLocator { + pub fn new(schema: &str, instance_requirement: InstanceRequirement) -> Result { + Ok(Self { + schema: schema.to_string(), + instance_requirement, + }) + } +} + +/// Instance requirement for database connections +#[derive(Debug, Clone, Copy)] +pub enum InstanceRequirement { + Master, + Replica, +} + +/// A MySQL query with parameter bindings +#[derive(Debug, Clone)] +pub struct Query { + sql: String, + params: Vec, +} + +impl Query { + pub fn new(sql: &str) -> Self { + Self { + sql: sql.to_string(), + params: Vec::new(), + } + } + + pub fn add(mut self, other: Query) -> Self { + // Append the SQL and combine parameters + self.sql.push(' '); + self.sql.push_str(&other.sql); + self.params.extend(other.params); + self + } + + pub fn bind>(mut self, param: T) -> Self { + self.params.push(param.into()); + self + } +} + +/// Query result that can be converted to various types +pub struct QueryResult { + rows: Vec, +} + +impl QueryResult { + pub fn into_rows(self) -> Result> { + let results: Vec = self.rows + .into_iter() + .map(T::from_row) + .collect(); + Ok(results) + } +} + +/// Main MySQL client +pub struct MysqlCppClient { + pool: Pool, +} + +impl MysqlCppClient { + pub fn new(_fb: FacebookInit) -> Result { + // For OSS usage, we'll use a simple localhost MySQL connection + // In a real deployment, this would be configured via environment or config files + let database_url = std::env::var("MYSQL_DATABASE_URL") + .unwrap_or_else(|_| "mysql://root@localhost/test".to_string()); + + let opts = mysql_async::Opts::from_url(&database_url) + .context("Failed to parse MySQL URL")?; + + let pool = Pool::new(opts); + + Ok(Self { pool }) + } + + pub async fn query(&self, locator: &DbLocator, query: Query) -> Result { + let mut conn = self.pool.get_conn().await + .context("Failed to get MySQL connection")?; + + // Switch to the specified schema + let use_schema = format!("USE `{}`", locator.schema); + conn.query_drop(use_schema).await + .context("Failed to switch to schema")?; + + // Execute the query with parameters + let rows: Vec = conn.exec(&query.sql, query.params).await + .context("Failed to execute query")?; + + Ok(QueryResult { rows }) + } +} + +/// Macro to create parameterized queries +#[macro_export] +macro_rules! query { + ($sql:expr) => { + $crate::Query::new($sql) + }; + ($sql:expr, $($param_name:ident = $param_value:expr),* $(,)?) => {{ + let mut sql_string = $sql.to_string(); + $( + // Replace parameter placeholders with ? for mysql_async compatibility + let param_placeholder = format!("{{{}}}", stringify!($param_name)); + sql_string = sql_string.replace(¶m_placeholder, "?"); + )* + let mut query = $crate::Query::new(&sql_string); + $( + query = query.bind($param_value); + )* + query + }}; +} + +/// Error types for MySQL operations +#[derive(Debug)] +pub struct MysqlError { + pub message: String, +} + +impl fmt::Display for MysqlError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "MySQL error: {}", self.message) + } +} + +impl std::error::Error for MysqlError {} + +impl From for MysqlError { + fn from(err: mysql_async::Error) -> Self { + Self { + message: err.to_string(), + } + } +}