diff --git a/Cargo.lock b/Cargo.lock index 84a5b98..3bb0322 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -88,6 +100,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "async-trait" +version = "0.1.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atoi" version = "2.0.0" @@ -118,6 +147,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -250,12 +285,60 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e329294a796e9b22329669c1f433a746983f9e324e07f4ef135be81bb2262de4" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -311,6 +394,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "crypto-common" version = "0.1.6" @@ -389,6 +478,15 @@ dependencies = [ "syn", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -404,6 +502,15 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -622,6 +729,15 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -633,13 +749,22 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -862,7 +987,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -898,6 +1023,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1117,6 +1253,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "parking" version = "2.2.1" @@ -1146,6 +1292,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1161,6 +1313,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +dependencies = [ + "memchr", + "thiserror 2.0.11", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1266,6 +1463,18 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags", + "serde", + "serde_derive", +] + [[package]] name = "rsa" version = "0.9.7" @@ -1286,6 +1495,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1521,13 +1741,16 @@ version = "0.1.0" dependencies = [ "chrono", "clap", + "config", "futures", + "heck 0.5.0", "sea-schema", "serde", "sqlx", "thiserror 2.0.11", "tokio", "toml", + "toml_edit", "url", ] @@ -1607,8 +1830,8 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown", - "hashlink", + "hashbrown 0.15.2", + "hashlink 0.10.0", "indexmap", "log", "memchr", @@ -1672,7 +1895,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "bytes", @@ -1715,7 +1938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "chrono", @@ -1875,6 +2098,15 @@ dependencies = [ "syn", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -2006,12 +2238,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -2039,6 +2283,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "url" version = "2.5.4" @@ -2338,6 +2588,17 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "yaml-rust2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1a1c0bc9823338a3bdf8c61f994f23ac004c6fa32c08cd152984499b445e8d" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink 0.9.1", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index be2007a..e3db243 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" [dependencies] chrono = "0.4.39" clap = { version = "4.5.26", features = ["derive", "env"] } +config = { version = "0.15.6", features = ["toml"] } futures = "0.3.31" +heck = "0.5.0" sea-schema = { version = "0.16.1", features = [ "sqlx-mysql", "sqlx-postgres", @@ -15,7 +17,7 @@ sea-schema = { version = "0.16.1", features = [ "probe", "with-serde", ] } -serde = "1.0.217" +serde = { version = "1.0.217", features = ["derive"] } sqlx = { version = "0.8", features = [ "runtime-tokio", "tls-native-tls", @@ -26,4 +28,5 @@ sqlx = { version = "0.8", features = [ thiserror = "2.0.11" tokio = { version = "1.43.0", features = ["full"] } toml = "0.8.19" +toml_edit = "0.22.22" url = "2.5.4" diff --git a/example/schema.toml b/example/schema.toml new file mode 100644 index 0000000..2234a4e --- /dev/null +++ b/example/schema.toml @@ -0,0 +1,21 @@ +[public] + +[public.entries] + +name = "entries" +display_name = "entries" +columns = [ + { name = "id", col_type = "Integer", default = false, not_null = true, reference = { table = "", identity = "", label = "" } }, +] + +[public.sources] + +name = "sources" +display_name = "Sources" + +[[public.sources.columns]] +name = "id" +col_type = "Integer" +default = false +not_null = true +reference = { table = "", identity = "", label = "" } diff --git a/src/sql.rs b/src/sql.rs index 8ea1f30..2f59159 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1,3 +1,5 @@ +use std::io; + use clap::ValueEnum; use sea_schema::postgres::discovery::SchemaDiscovery; use sqlx::PgPool; @@ -5,7 +7,10 @@ use url::Url; use crate::error::{Error, Result}; +use self::schema::{DataModel, Schema}; + mod postgres; +mod schema; #[derive(ValueEnum, Clone)] pub enum Database { @@ -59,7 +64,12 @@ pub async fn discover_scheme(url: String, schema: Option) -> Result<()> let schema = schema_discovery.discover().await?; - println!("{:#?}", schema); + let mut data_model = DataModel::new(); + data_model + .schemas + .insert(schema.schema.to_owned(), Schema::from(schema)); + + data_model.write(&mut io::stdout())?; Ok(()) } diff --git a/src/sql/schema.rs b/src/sql/schema.rs new file mode 100644 index 0000000..5b61eee --- /dev/null +++ b/src/sql/schema.rs @@ -0,0 +1,157 @@ +use heck::ToTitleCase; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt; +use std::io::Write; +use toml_edit::visit_mut::VisitMut; + +use sea_schema::postgres::def as sea_schema_def; + +use crate::error; + +struct InlineTableFix; + +// the toml serializer doesn't generate inline tables by default +impl VisitMut for InlineTableFix { + fn visit_table_like_kv_mut( + &mut self, + mut key: toml_edit::KeyMut<'_>, + node: &mut toml_edit::Item, + ) { + // add the keys of the tables that needs to be inline here + if ["reference", "type"].contains(&key.get()) { + if let toml_edit::Item::Table(table) = node { + // Turn the table into an inline table. + let table = std::mem::replace(table, toml_edit::Table::new()); + let inline_table = table.into_inline_table(); + key.fmt(); + *node = toml_edit::Item::Value(toml_edit::Value::InlineTable(inline_table)); + } + } + + toml_edit::visit_mut::visit_table_like_kv_mut(self, key, node); + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct DataModel { + #[serde(flatten)] + pub schemas: HashMap, +} + +impl DataModel { + pub fn new() -> Self { + Self { + schemas: HashMap::new(), + } + } +} + +impl DataModel { + pub fn write(&self, writter: &mut T) -> error::Result<()> { + writter.write_all(self.to_string().as_bytes())?; + Ok(()) + } +} + +impl fmt::Display for DataModel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let toml = toml::to_string_pretty(self).map_err(|_| fmt::Error)?; + let mut document: toml_edit::DocumentMut = toml.parse().map_err(|_| fmt::Error)?; + + let mut visitor = InlineTableFix; + visitor.visit_document_mut(&mut document); + + write!(f, "{}", document) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Schema { + #[serde(flatten)] + pub tables: HashMap, +} + +impl From for Schema { + fn from(schema: sea_schema_def::Schema) -> Self { + let mut tables: HashMap = HashMap::new(); + + for table in schema.tables { + tables.insert(table.info.to_owned().name, table.into()); + } + + Self { tables } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Table { + /// Default is capitalized name + display_name: Option, + columns: Vec, +} + +impl From for Table { + fn from(table: sea_schema_def::TableDef) -> Self { + Self { + display_name: Some(table.info.name.to_owned().to_title_case()), + columns: table + .columns + .iter() + .map(|column_info| { + let mut col: Column = column_info.to_owned().into(); + + let reference = table + .reference_constraints + .iter() + .find(|reference| reference.columns.contains(&col.name)); + + if let Some(reference) = reference { + col.reference = Some(reference.to_owned().into()); + } + + return col; + }) + .collect(), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Column { + name: String, + #[serde(rename = "type")] + col_type: sea_schema_def::ColumnType, + default: Option, + not_null: bool, + reference: Option, +} + +impl From for Column { + fn from(col: sea_schema_def::ColumnInfo) -> Self { + Self { + name: col.name, + col_type: col.col_type, + default: col.default.map(|expression| expression.0), + not_null: col.not_null.is_some(), + reference: None, + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ColumnReference { + table: String, + identity: String, + label: Option, +} + +impl From for ColumnReference { + fn from(reference: sea_schema_def::References) -> Self { + Self { + table: reference.table, + identity: reference.foreign_columns[0].to_owned(), + label: Some(reference.columns[0].to_title_case()), + } + } +}