Skip to main content

fluxforge/
lib.rs

1//! # FluxForge
2//!
3//! A database schema converter and migration engine for MySQL and PostgreSQL.
4//!
5//! FluxForge provides a unified interface for extracting, transforming, and replicating
6//! database schemas and data between MySQL and PostgreSQL databases. It supports:
7//!
8//! - Schema extraction and conversion
9//! - Type mapping and transformation
10//! - Data replication with verification
11//! - Dependency-aware table ordering
12//!
13//! # Examples
14//!
15//! ```no_run
16//! use fluxforge::{drivers, core::ForgeConfig};
17//!
18//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
19//! let config = ForgeConfig::default();
20//! let driver = drivers::create_driver("mysql://user:pass@localhost/db", &config, true).await?;
21//! let schema = driver.fetch_schema(&config).await?;
22//! println!("Extracted {} tables", schema.tables.len());
23//! # Ok(())
24//! # }
25//! ```
26
27pub mod config;
28pub mod core;
29pub mod drivers;
30pub mod ops;
31
32// Re-export for easier access
33pub use crate::core::ForgeUniversalDataTransferPacket;
34pub use crate::core::{ForgeConfig, ForgeError};
35pub use crate::core::{ForgeSchema, ForgeSchemaColumn, ForgeSchemaTable};
36pub use crate::core::{ForgeUniversalDataField, ForgeUniversalDataRow};
37
38use async_trait::async_trait;
39use futures::Stream;
40use indexmap::IndexMap;
41use std::pin::Pin;
42
43/// Database driver trait for unified database operations.
44///
45/// This trait provides a common interface for interacting with different database systems
46/// (MySQL, PostgreSQL). Implementations handle database-specific operations while presenting
47/// a unified API for schema extraction, migration, and data replication.
48///
49/// # Examples
50///
51/// ```no_run
52/// use fluxforge::{DatabaseDriver, drivers, core::ForgeConfig};
53///
54/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
55/// let config = ForgeConfig::default();
56/// let driver = drivers::create_driver("postgres://user:pass@localhost/db", &config, true).await?;
57/// let is_empty = driver.db_is_empty().await?;
58/// println!("Database is empty: {}", is_empty);
59/// # Ok(())
60/// # }
61/// ```
62#[async_trait]
63pub trait DatabaseDriver: Send + Sync {
64    /// Checks if the database is empty (contains no tables).
65    ///
66    /// # Examples
67    ///
68    /// ```no_run
69    /// # use fluxforge::{DatabaseDriver, drivers, core::ForgeConfig};
70    /// # async fn example(driver: &dyn DatabaseDriver) -> Result<(), Box<dyn std::error::Error>> {
71    /// if driver.db_is_empty().await? {
72    ///     println!("Database is empty and ready for replication");
73    /// }
74    /// # Ok(())
75    /// # }
76    /// ```
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if the database connection fails or the query cannot be executed.
81    async fn db_is_empty(&self) -> Result<bool, Box<dyn std::error::Error>>;
82
83    /// Fetches the complete database schema including tables, columns, indices, and foreign keys.
84    ///
85    /// # Arguments
86    ///
87    /// * `config` - Configuration for type mappings and transformation rules
88    ///
89    /// # Examples
90    ///
91    /// ```no_run
92    /// # use fluxforge::{DatabaseDriver, core::ForgeConfig};
93    /// # async fn example(driver: &dyn DatabaseDriver) -> Result<(), Box<dyn std::error::Error>> {
94    /// let config = ForgeConfig::default();
95    /// let schema = driver.fetch_schema(&config).await?;
96    /// for table in &schema.tables {
97    ///     println!("Table: {} with {} columns", table.name, table.columns.len());
98    /// }
99    /// # Ok(())
100    /// # }
101    /// ```
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if:
106    /// - Database connection fails
107    /// - Schema metadata cannot be queried
108    /// - Type mapping configuration is invalid
109    async fn fetch_schema(
110        &self,
111        config: &ForgeConfig,
112    ) -> Result<ForgeSchema, Box<dyn std::error::Error>>;
113
114    /// Compares source schema with target database and applies necessary changes.
115    ///
116    /// # Arguments
117    ///
118    /// * `schema` - The source schema to apply
119    /// * `config` - Configuration for type mappings and transformation rules
120    /// * `dry_run` - If true, returns SQL statements without executing them
121    /// * `verbose` - Enable verbose output
122    /// * `destructive` - If true, allows dropping tables and columns not in source schema
123    ///
124    /// # Examples
125    ///
126    /// ```no_run
127    /// # use fluxforge::{DatabaseDriver, ForgeSchema, core::ForgeConfig};
128    /// # async fn example(driver: &dyn DatabaseDriver, schema: &ForgeSchema) -> Result<(), Box<dyn std::error::Error>> {
129    /// let config = ForgeConfig::default();
130    /// let statements = driver.diff_and_apply_schema(
131    ///     schema,
132    ///     &config,
133    ///     true,  // dry_run
134    ///     false, // verbose
135    ///     false  // destructive
136    /// ).await?;
137    /// for sql in statements {
138    ///     println!("Would execute: {}", sql);
139    /// }
140    /// # Ok(())
141    /// # }
142    /// ```
143    ///
144    /// # Errors
145    ///
146    /// Returns an error if:
147    /// - Database connection fails
148    /// - SQL statements cannot be generated or executed
149    /// - Schema conflicts cannot be resolved
150    async fn diff_and_apply_schema(
151        &self,
152        schema: &ForgeSchema,
153        config: &ForgeConfig,
154        dry_run: bool,
155        verbose: bool,
156        destructive: bool,
157    ) -> Result<Vec<String>, Box<dyn std::error::Error>>;
158
159    /// Streams all rows from a table as universal values.
160    ///
161    /// # Arguments
162    ///
163    /// * `table_name` - Name of the table to stream
164    ///
165    /// # Examples
166    ///
167    /// ```no_run
168    /// # use fluxforge::DatabaseDriver;
169    /// # use futures::StreamExt;
170    /// # async fn example(driver: &dyn DatabaseDriver) -> Result<(), Box<dyn std::error::Error>> {
171    /// let mut stream = driver.stream_table_data("users").await?;
172    /// while let Some(row) = stream.next().await {
173    ///     let row = row?;
174    ///     println!("Row: {:?}", row);
175    /// }
176    /// # Ok(())
177    /// # }
178    /// ```
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if:
183    /// - Table does not exist
184    /// - Database connection fails
185    /// - Row data cannot be decoded
186    async fn stream_table_data(
187        &self,
188        table_name: &str,
189    ) -> Result<
190        Pin<
191            Box<
192                dyn Stream<Item = Result<IndexMap<String, ForgeUniversalDataField>, ForgeError>>
193                    + Send
194                    + '_,
195            >,
196        >,
197        Box<dyn std::error::Error>,
198    >;
199
200    /// Streams rows from a table ordered by specified columns.
201    ///
202    /// # Arguments
203    ///
204    /// * `table_name` - Name of the table to stream
205    /// * `order_by` - Column names to order by
206    ///
207    /// # Examples
208    ///
209    /// ```no_run
210    /// # use fluxforge::DatabaseDriver;
211    /// # use futures::StreamExt;
212    /// # async fn example(driver: &dyn DatabaseDriver) -> Result<(), Box<dyn std::error::Error>> {
213    /// let mut stream = driver.stream_table_data_ordered(
214    ///     "users",
215    ///     &["id".to_string()]
216    /// ).await?;
217    /// while let Some(row) = stream.next().await {
218    ///     let row = row?;
219    ///     println!("Row: {:?}", row);
220    /// }
221    /// # Ok(())
222    /// # }
223    /// ```
224    ///
225    /// # Errors
226    ///
227    /// Returns an error if:
228    /// - Table does not exist
229    /// - Order by columns do not exist
230    /// - Database connection fails
231    async fn stream_table_data_ordered(
232        &self,
233        table_name: &str,
234        order_by: &[String],
235    ) -> Result<
236        Pin<
237            Box<
238                dyn Stream<Item = Result<IndexMap<String, ForgeUniversalDataField>, ForgeError>>
239                    + Send
240                    + '_,
241            >,
242        >,
243        Box<dyn std::error::Error>,
244    >;
245
246    /// Inserts a batch of rows into a table.
247    ///
248    /// # Arguments
249    ///
250    /// * `table_name` - Name of the target table
251    /// * `dry_run` - If true, prints SQL without executing
252    /// * `halt_on_error` - If true, stops on first error; if false, logs errors and continues
253    /// * `chunk` - Vector of rows to insert
254    ///
255    /// # Examples
256    ///
257    /// ```no_run
258    /// # use fluxforge::{DatabaseDriver, core::ForgeUniversalDataField};
259    /// # use indexmap::IndexMap;
260    /// # async fn example(driver: &dyn DatabaseDriver) -> Result<(), Box<dyn std::error::Error>> {
261    /// let mut row = IndexMap::new();
262    /// row.insert("id".to_string(), ForgeUniversalDataField::Integer(1));
263    /// row.insert("name".to_string(), ForgeUniversalDataField::Text("Alice".to_string()));
264    /// driver.insert_chunk("users", false, true, vec![row]).await?;
265    /// # Ok(())
266    /// # }
267    /// ```
268    ///
269    /// # Errors
270    ///
271    /// Returns an error if:
272    /// - Table does not exist
273    /// - Column types are incompatible
274    /// - Database constraints are violated
275    /// - `halt_on_error` is true and any insert fails
276    async fn insert_chunk(
277        &self,
278        table_name: &str,
279        dry_run: bool,
280        halt_on_error: bool,
281        chunk: Vec<IndexMap<String, ForgeUniversalDataField>>,
282    ) -> Result<(), Box<dyn std::error::Error>>;
283
284    /// Gets the total number of rows in a table.
285    ///
286    /// # Arguments
287    ///
288    /// * `table_name` - Name of the table
289    ///
290    /// # Examples
291    ///
292    /// ```no_run
293    /// # use fluxforge::DatabaseDriver;
294    /// # async fn example(driver: &dyn DatabaseDriver) -> Result<(), Box<dyn std::error::Error>> {
295    /// let count = driver.get_table_row_count("users").await?;
296    /// println!("Table has {} rows", count);
297    /// # Ok(())
298    /// # }
299    /// ```
300    ///
301    /// # Errors
302    ///
303    /// Returns an error if:
304    /// - Table does not exist
305    /// - Database connection fails
306    async fn get_table_row_count(
307        &self,
308        table_name: &str,
309    ) -> Result<u64, Box<dyn std::error::Error>>;
310}