From e8e2890171b14baab665cfcd092d1dc9ec3f143a Mon Sep 17 00:00:00 2001 From: yukkop Date: Sun, 2 Mar 2025 10:48:52 +0000 Subject: [PATCH] feat(pg-schema): init --- flake.nix | 1 + package/postgres/pg-schema/Cargo.lock | 1081 ++++++++++++++++++ package/postgres/pg-schema/Cargo.toml | 17 + package/postgres/pg-schema/default.nix | 26 + package/postgres/pg-schema/src/bin/config.rs | 186 +++ package/postgres/pg-schema/src/main.rs | 379 ++++++ 6 files changed, 1690 insertions(+) create mode 100644 package/postgres/pg-schema/Cargo.lock create mode 100644 package/postgres/pg-schema/Cargo.toml create mode 100644 package/postgres/pg-schema/default.nix create mode 100644 package/postgres/pg-schema/src/bin/config.rs create mode 100644 package/postgres/pg-schema/src/main.rs diff --git a/flake.nix b/flake.nix index 9ed65d7..adc8f44 100644 --- a/flake.nix +++ b/flake.nix @@ -86,6 +86,7 @@ migration-name = pkgs.callPackage ./package/migration-name.nix {}; prettify-log = pkgs.callPackage ./package/prettify-log/default.nix rust.commonArgs; pg-from = pkgs.callPackage ./package/postgres/pg-from/default.nix rust.commonArgs; + pg-schema = pkgs.callPackage ./package/postgres/pg-schema/default.nix rust.commonArgs; pg-migration = pkgs.callPackage ./package/postgres/pg-migration/default.nix rust.commonArgs; }; diff --git a/package/postgres/pg-schema/Cargo.lock b/package/postgres/pg-schema/Cargo.lock new file mode 100644 index 0000000..ccc88c1 --- /dev/null +++ b/package/postgres/pg-schema/Cargo.lock @@ -0,0 +1,1081 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.167" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pg-schema" +version = "0.1.0" +dependencies = [ + "dotenv", + "env_logger", + "hex", + "log", + "postgres", + "regex", + "sha2", + "uuid", + "which", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "postgres" +version = "0.19.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c918733159f4d55d2ceb262950f00b0aebd6af4aa97b5a47bb0655120475ed" +dependencies = [ + "bytes", + "fallible-iterator", + "futures-util", + "log", + "tokio", + "tokio-postgres", +] + +[[package]] +name = "postgres-protocol" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acda0ebdebc28befa84bee35e651e4c5f09073d668c7aed4cf7e23c3cda84b23" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66ea23a2d0e5734297357705193335e0a957696f34bed2f2faefacb2fec336f" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5d3742945bc7d7f210693b0c58ae542c6fd47b17adbbda0885f3dcb34a6bdb" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand", + "socket2", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" +dependencies = [ + "either", + "home", + "rustix", + "winsafe", +] + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", + "web-sys", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/package/postgres/pg-schema/Cargo.toml b/package/postgres/pg-schema/Cargo.toml new file mode 100644 index 0000000..662aa53 --- /dev/null +++ b/package/postgres/pg-schema/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pg-schema" +version = "0.1.0" +edition = "2021" + +[dependencies] +dotenv = "0.15.0" +env_logger = "0.11.5" +hex = "0.4.3" +log = "0.4.22" +postgres = "0.19.9" +regex = "1.11.1" +sha2 = "0.10.8" +uuid = "1.11.0" +which = "7.0.0" + +[workspace] diff --git a/package/postgres/pg-schema/default.nix b/package/postgres/pg-schema/default.nix new file mode 100644 index 0000000..b21399f --- /dev/null +++ b/package/postgres/pg-schema/default.nix @@ -0,0 +1,26 @@ +{ + cargoToml, + nativeBuildInputs, + pkgs, + ... +}: let + src = ./.; + cargo = cargoToml src; +in + pkgs.rustPlatform.buildRustPackage { + pname = cargo.package.name; + version = cargo.package.version; + + inherit nativeBuildInputs src; + + cargoLock.lockFile = ./Cargo.lock; + + cargoTestFlags = [ + "--bin ${cargo.package.name}" + ]; + cargoBuildFlags = [ + "--bin ${cargo.package.name}" + ]; + + doCheck = true; + } diff --git a/package/postgres/pg-schema/src/bin/config.rs b/package/postgres/pg-schema/src/bin/config.rs new file mode 100644 index 0000000..2a03829 --- /dev/null +++ b/package/postgres/pg-schema/src/bin/config.rs @@ -0,0 +1,186 @@ +use postgres::Config; + +/// The macro usage: +/// +/// config! { +/// db_url => "postgres://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}", +/// db_user, +/// db_password, +/// db_host, +/// db_port, +/// db_name, +/// individual_option, +/// another_bundle => "{option_1}@{option_2}", +/// option_1, +/// option_2 +/// } +/// +/// After expansion, you'll get a `Config` struct with fields: +/// - db_url (String) +/// - db_user (String) +/// - db_password (String) +/// - db_host (String) +/// - db_port (String) +/// - db_name (String) +/// - individual_option (String) +/// - another_bundle (String) +/// - option_1 (String) +/// - option_2 (String) +/// +/// Logic: +/// - If `db_url` is provided (env or CLI), it sets `db_url` and ignores `db_user`, `db_password`, `db_host`, `db_port`, `db_name`. +/// - If `db_url` is not provided, tries to construct it from `db_user`, `db_password`, `db_host`, `db_port`, `db_name`. +/// If any is missing, error. +/// - Similarly for `another_bundle`. + +macro_rules! config { + ( + $($key:ident $(=> $template:expr)?),* $(,)? + ) => { + // Identify which keys are bundles (have a template) and which are single options + struct Config { + $( + pub $key: String, + )* + } + + impl Config { + pub fn from_env_and_args() -> Result { + let args: Vec = std::env::args().collect(); + + fn get_arg(name: &str, args: &[String]) -> Option { + let prefix = format!("--{}=", name); + args.iter() + .find(|a| a.starts_with(&prefix)) + .map(|a| a[prefix.len()..].to_string()) + } + + fn get_env(name: &str) -> Option { + std::env::var(name).ok() + } + + // Helper to load a single option: first from args, then from env + fn load_option(name: &str) -> Option { + // command-line option name is the same as the field + // env var name is uppercase + get_arg(name, &args) + .or_else(|| get_env(&name.to_uppercase())) + } + + // We'll store temporary values here + let mut vals = std::collections::HashMap::new(); + $( + // Initialize all options to empty for now + vals.insert(stringify!($key), String::new()); + )* + + $( + // If this is a bundle + $( + if false {} else { + // This branch is for bundles + let bundle_name = stringify!($key); + let maybe_bundle = load_option(bundle_name); + if let Some(bundle_val) = maybe_bundle { + // If the bundle itself is provided, just set it and ignore its components + vals.insert(bundle_name, bundle_val); + } else { + // Bundle not provided. Need to construct from template. + let template_str = $template; + + // Extract placeholders from the template + // placeholders are like {some_option}, we try to fill them + let mut constructed = template_str.to_string(); + // Simple placeholder parsing + let mut placeholders = vec![]; + { + let mut start = 0usize; + while let Some(open) = constructed[start..].find('{') { + let open_idx = start + open; + if let Some(close_idx) = constructed[open_idx..].find('}') { + let close_idx = open_idx + close_idx; + let placeholder = &constructed[(open_idx+1)..close_idx]; + placeholders.push(placeholder.to_string()); + start = close_idx+1; + } else { + break; // malformed, ignore for simplicity + } + } + } + + // For each placeholder, we must load it from env/args + for ph in placeholders { + let maybe_val = load_option(&ph); + let val = maybe_val.ok_or_else(|| format!("Missing required option `{}` for bundle `{}`", ph, bundle_name))?; + // Replace "{ph}" with val + constructed = constructed.replace(&format!("{{{}}}", ph), &val); + // Also store them individually if you want them accessible + vals.insert(&ph, val); + } + vals.insert(bundle_name, constructed); + } + } + )? + + // If this is a single option (not a bundle) + $( ; )? // do nothing if bundle line + )* + + $( + // For single options (those without =>) + // If they haven't been set by a bundle line, set them now + $( + // This empty repetition is a trick + )? + $( + // If no template was provided, this is a single option + if false {} else { + let name = stringify!($key); + // If a bundle line didn't already resolve it: + if !vals.contains_key(name) || vals[name].is_empty() { + if let Some(val) = load_option(name) { + vals.insert(name, val); + } else { + // If not provided and not part of a bundle that was resolved, default empty + // or we can leave it empty silently + vals.insert(name, "".to_string()); + } + } + } + )? + )* + + Ok(Config { + $( + $key: vals.remove(stringify!($key)).unwrap_or_default(), + )* + }) + } + } + } +} + +fn main() -> Result<(), String> { + // Example usage + config! { + db_url => "postgres://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}", + db_user, + db_password, + db_host, + db_port, + db_name, + individual_option, + another_bundle => "{option_1}@{option_2}", + option_1, + option_2 + } + + let cfg = Config::from_env_and_args()?; + println!("db_url: {}", cfg.db_url); + println!("individual_option: {}", cfg.individual_option); + println!("another_bundle: {}", cfg.another_bundle); + + // ... + Ok(()) +} + diff --git a/package/postgres/pg-schema/src/main.rs b/package/postgres/pg-schema/src/main.rs new file mode 100644 index 0000000..5eee100 --- /dev/null +++ b/package/postgres/pg-schema/src/main.rs @@ -0,0 +1,379 @@ +use dotenv::dotenv; +use postgres::{Client, NoTls}; +use regex::Regex; +use std::collections::{HashMap, HashSet}; +use std::env; +use std::process::Command; +use log::info; + +#[derive(Debug)] +struct Column { + name: String, + data_type: String, + is_nullable: bool, +} + +#[derive(Debug)] +struct ForeignKeyGroup { + constraint_name: String, + source_schema: String, + source_table: String, + target_schema: String, + target_table: String, + source_columns: Vec, + target_columns: Vec, + is_unique: bool, +} + +// --- Sanitization functions --- + +fn sanitize_type(data_type: &str) -> String { + let re = Regex::new(r"\s+").unwrap(); + re.replace_all(&data_type.to_lowercase(), "_") + .to_uppercase() + .to_string() +} + +fn sanitize_name(name: &str) -> String { + // Replace spaces and hyphens with underscores and remove invalid characters + let re = Regex::new(r"[\s\-]+").unwrap(); + let name = re.replace_all(name, "_"); + let re = Regex::new(r"[^\w_]").unwrap(); + re.replace_all(&name, "").to_string() +} + +// --- Database Schema queries --- + +fn get_tables(client: &mut Client) -> Result, postgres::Error> { + let rows = client.query( + " + SELECT table_schema, table_name + FROM information_schema.tables + WHERE table_schema NOT IN ('pg_catalog', 'information_schema', 'cron') + AND table_schema NOT LIKE 'pg_toast%' + AND table_type = 'BASE TABLE' + ORDER BY table_schema, table_name; + ", + &[], + )?; + info!("{rows:#?}"); + Ok(rows + .iter() + .map(|row| (row.get::<_, String>(0), row.get::<_, String>(1))) + .collect()) +} + +fn get_columns( + client: &mut Client, + schema: &str, + table: &str, +) -> Result, postgres::Error> { + let rows = client.query( + " + SELECT column_name, data_type, is_nullable + FROM information_schema.columns + WHERE table_schema NOT IN ('pg_catalog', 'information_schema', 'cron') + AND table_schema NOT LIKE 'pg_toast%' + AND table_schema = $1 + AND table_name = $2 + ORDER BY ordinal_position; + ", + &[&schema, &table], + )?; + let mut columns = Vec::new(); + for row in rows { + let col_name: String = row.get(0); + let data_type: String = row.get(1); + let is_nullable_str: String = row.get(2); + let is_nullable = is_nullable_str.to_lowercase() == "yes"; + let sanitized_col_name = sanitize_name(&col_name.to_lowercase()); + let sanitized_type = sanitize_type(&data_type); + columns.push(Column { + name: sanitized_col_name, + data_type: sanitized_type, + is_nullable, + }); + } + Ok(columns) +} + +// Improved foreign key query that gathers constraint_name and uniqueness info, +// so that composite foreign keys are grouped together. +fn get_foreign_keys(client: &mut Client) -> Result, postgres::Error> { + let query = " + SELECT + tc.constraint_name, + tc.table_schema AS source_schema, + tc.table_name AS source_table, + kcu.column_name AS source_column, + ccu.table_schema AS target_schema, + ccu.table_name AS target_table, + ccu.column_name AS target_column, + ( + EXISTS ( + SELECT 1 + FROM information_schema.table_constraints as utc + JOIN information_schema.key_column_usage as ukcu + ON utc.constraint_name = ukcu.constraint_name + WHERE utc.table_schema = tc.table_schema + AND utc.table_name = tc.table_name + AND utc.constraint_type IN ('PRIMARY KEY', 'UNIQUE') + AND ukcu.column_name = kcu.column_name + ) + ) as is_unique + FROM information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + JOIN information_schema.constraint_column_usage AS ccu + ON tc.constraint_name = ccu.constraint_name + WHERE + tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema NOT IN ('pg_catalog', 'information_schema', 'cron') + AND tc.table_schema NOT LIKE 'pg_toast%' + ORDER BY tc.constraint_name, kcu.ordinal_position; + "; + let rows = client.query(query, &[])?; + let mut map: HashMap = HashMap::new(); + for row in rows { + let constraint_name: String = row.get("constraint_name"); + let source_schema: String = row.get("source_schema"); + let source_table: String = row.get("source_table"); + let target_schema: String = row.get("target_schema"); + let target_table: String = row.get("target_table"); + let source_column: String = row.get("source_column"); + let target_column: String = row.get("target_column"); + let is_unique: bool = row.get("is_unique"); + + let entry = map.entry(constraint_name.clone()).or_insert(ForeignKeyGroup { + constraint_name: constraint_name.clone(), + source_schema: source_schema.clone(), + source_table: source_table.clone(), + target_schema: target_schema.clone(), + target_table: target_table.clone(), + source_columns: Vec::new(), + target_columns: Vec::new(), + is_unique: true, + }); + entry.source_columns.push(source_column); + entry.target_columns.push(target_column); + entry.is_unique = entry.is_unique && is_unique; + } + Ok(map.into_iter().map(|(_, v)| v).collect()) +} + +// --- Utility for generating diagram --- + +fn get_hash(text: &str) -> String { + use sha2::{Sha256, Digest}; + let mut hasher = Sha256::new(); + hasher.update(text.as_bytes()); + let result = hasher.finalize(); + format!("_{}", hex::encode(result)) +} + +/// Generates the Mermaid diagram. +/// - `tables`: all non-join tables (after join tables are removed) +/// - `foreign_keys`: grouped foreign key relationships +/// - `join_tables`: a vector of join table keys with their foreign key groups +/// - `join_table_keys`: set of join table identifiers to filter out regular relationships +fn generate_mermaid( + tables: &HashMap<(String, String), Vec>, + foreign_keys: &[ForeignKeyGroup], + join_tables: &Vec<((String, String), Vec<&ForeignKeyGroup>)>, + join_table_keys: &HashSet<(String, String)> +) -> String { + let mut mermaid = String::from("erDiagram\n"); + + // Define entities for non-join tables + for ((schema, table), columns) in tables { + let sanitized_schema = sanitize_name(schema); + let sanitized_table = sanitize_name(table); + let full_name = format!("{}.{}", sanitized_schema, sanitized_table); + mermaid.push_str(&format!(" {}[\"{}\"] {{\n", get_hash(&full_name), full_name)); + for column in columns { + mermaid.push_str(&format!(" {} {}\n", column.data_type, sanitize_name(&column.name))); + } + mermaid.push_str(" }\n"); + } + mermaid.push_str("\n"); + + // Normal relationships (one-to-one or one-to-many) + for fk in foreign_keys { + // Skip relationships that originate from join tables (handled separately) + if join_table_keys.contains(&(fk.source_schema.clone(), fk.source_table.clone())) { + continue; + } + let sanitized_source_table = sanitize_name(&fk.source_table); + let sanitized_target_table = sanitize_name(&fk.target_table); + let sanitized_source_schema = sanitize_name(&fk.source_schema); + let sanitized_target_schema = sanitize_name(&fk.target_schema); + let source_full_name = format!("{}.{}", sanitized_source_schema, sanitized_source_table); + let target_full_name = format!("{}.{}", sanitized_target_schema, sanitized_target_table); + let relationship_label = format!("{} -> {}", fk.source_columns.join(", "), fk.target_columns.join(", ")); + let relationship_type = if fk.is_unique { "||--||" } else { "}o--||" }; + mermaid.push_str(&format!( + " {} {} {} : \"{}\"\n", + get_hash(&source_full_name), + relationship_type, + get_hash(&target_full_name), + relationship_label + )); + } + + // Many-to-many relationships for detected join tables + for (join_key, fk_list) in join_tables { + if fk_list.len() == 2 { + let fk1 = fk_list[0]; + let fk2 = fk_list[1]; + // Draw an edge between the two target tables of the join table. + let source_full_name = format!("{}.{}", sanitize_name(&fk1.target_schema), sanitize_name(&fk1.target_table)); + let target_full_name = format!("{}.{}", sanitize_name(&fk2.target_schema), sanitize_name(&fk2.target_table)); + let label = format!("join: {} <-> {}", fk1.target_columns.join(", "), fk2.target_columns.join(", ")); + mermaid.push_str(&format!( + " {} }}|--|{{ {} : \"{}\"\n", + get_hash(&source_full_name), + get_hash(&target_full_name), + label + )); + } + } + + mermaid +} + +fn generate_svg(mermaid_file: &str, svg_file: &str) -> Result<(), String> { + let mmdc_path = which::which("mmdc").map_err(|_| { + "Mermaid CLI (mmdc) is not installed or not found in PATH.\nPlease install it by running: npm install -g @mermaid-js/mermaid-cli".to_string() + })?; + let status = Command::new(mmdc_path) + .arg("-i") + .arg(mermaid_file) + .arg("-o") + .arg(svg_file) + .status() + .map_err(|e| format!("Failed to execute mmdc: {}", e))?; + if status.success() { + println!("SVG diagram generated successfully as '{}'.", svg_file); + Ok(()) + } else { + Err("An error occurred while generating the SVG.".to_string()) + } +} + +fn main() { + dotenv().ok(); + env_logger::init(); + + // If DB_URL is provided, use it. Otherwise, fall back to individual parameters. + let conn_str = match env::var("DB_URL") { + Ok(url) => { + info!("Using DB_URL environment variable for connection."); + url + }, + Err(_) => { + let db_host = env::var("DB_HOST").unwrap_or_else(|_| { + eprintln!("No DB_HOST environment variable provided."); + std::process::exit(1); + }); + info!("DB_HOST: {:?}", db_host); + + let db_port = env::var("DB_PORT").unwrap_or_else(|_| { + eprintln!("No DB_PORT environment variable provided."); + std::process::exit(1); + }); + let db_name = env::var("DB_NAME").unwrap_or_else(|_| { + eprintln!("No DB_NAME environment variable provided."); + std::process::exit(1); + }); + let db_user = env::var("DB_USER").unwrap_or_else(|_| { + eprintln!("No DB_USER environment variable provided."); + std::process::exit(1); + }); + let db_password = env::var("DB_PASSWORD").unwrap_or_else(|_| { + eprintln!("No DB_PASSWORD environment variable provided."); + std::process::exit(1); + }); + format!("host={} port={} dbname={} user={} password={}", db_host, db_port, db_name, db_user, db_password) + } + }; + + let mut client = match Client::connect(&conn_str, NoTls) { + Ok(c) => c, + Err(e) => { + eprintln!("Unable to connect to the database:\n{}", e); + std::process::exit(1); + } + }; + + // Fetch tables and columns + let tables_list = match get_tables(&mut client) { + Ok(t) => t, + Err(e) => { + eprintln!("Error fetching tables:\n{}", e); + std::process::exit(1); + } + }; + + let mut tables: HashMap<(String, String), Vec> = HashMap::new(); + for (schema, table) in &tables_list { + match get_columns(&mut client, schema, table) { + Ok(cols) => { + tables.insert((schema.clone(), table.clone()), cols); + } + Err(e) => { + eprintln!("Error fetching columns for table '{}':\n{}", table, e); + std::process::exit(1); + } + } + } + + // Fetch grouped foreign keys (composite keys handled together) + let foreign_keys = match get_foreign_keys(&mut client) { + Ok(fks) => fks, + Err(e) => { + eprintln!("Error fetching foreign keys:\n{}", e); + std::process::exit(1); + } + }; + + // --- Detect join tables for many-to-many relationships --- + // Build a map from (schema, table) to foreign key groups where the table is the source. + let mut fk_by_source: HashMap<(String, String), Vec<&ForeignKeyGroup>> = HashMap::new(); + for fk in &foreign_keys { + fk_by_source + .entry((fk.source_schema.clone(), fk.source_table.clone())) + .or_default() + .push(fk); + } + + // A join table is defined as having exactly two columns and exactly two foreign keys. + let mut join_table_keys_vec: Vec<(String, String)> = Vec::new(); + for ((schema, table), columns) in &tables { + if let Some(fk_list) = fk_by_source.get(&(schema.clone(), table.clone())) { + if columns.len() == fk_list.len() && fk_list.len() == 2 { + join_table_keys_vec.push((schema.clone(), table.clone())); + } + } + } + let join_table_keys: HashSet<(String, String)> = join_table_keys_vec.iter().cloned().collect(); + + // Build a vector with join table key and its foreign key groups. + let mut join_tables: Vec<((String, String), Vec<&ForeignKeyGroup>)> = Vec::new(); + for key in join_table_keys_vec { + if let Some(fk_list) = fk_by_source.get(&key) { + join_tables.push((key, fk_list.clone())); + } + } + // Remove join tables from the main entities so they aren’t drawn separately. + for key in &join_table_keys { + tables.remove(key); + } + + // Generate the Mermaid diagram + let mermaid_diagram = generate_mermaid(&tables, &foreign_keys, &join_tables, &join_table_keys); + + // For now, we simply print the diagram (or you could write it to a file) + println!("{mermaid_diagram}"); + + client.close().expect("Failed to close the database connection."); +}