diff --git a/.changepacks/changepack_log_Y7ehoHZ4WG9uPKetaanjN.json b/.changepacks/changepack_log_Y7ehoHZ4WG9uPKetaanjN.json new file mode 100644 index 00000000..21008719 --- /dev/null +++ b/.changepacks/changepack_log_Y7ehoHZ4WG9uPKetaanjN.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch"},"note":"Optimize compile time","date":"2026-03-12T10:42:01.647467800Z"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 77d333bf..ad1f0409 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -452,41 +452,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum_typed_multipart" -version = "0.16.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c8b2ee396b35396ec27f5b9aa101f77000ba842dc82549a381b74c3ae2db7e" -dependencies = [ - "anyhow", - "async-trait", - "axum", - "axum_typed_multipart_macros", - "bytes", - "chrono", - "futures-core", - "futures-util", - "rust_decimal", - "tempfile", - "thiserror", - "tokio", - "uuid", -] - -[[package]] -name = "axum_typed_multipart_macros" -version = "0.16.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27cefbd055910a29c4a3710016559cece5bdb4fb78ec055a1c2e9f8c61e3aa9" -dependencies = [ - "darling 0.23.0", - "heck 0.5.0", - "proc-macro-error2", - "quote", - "syn 2.0.117", - "ubyte", -] - [[package]] name = "base64" version = "0.22.1" @@ -781,9 +746,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -856,18 +821,8 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core", + "darling_macro", ] [[package]] @@ -883,37 +838,13 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", -] - [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.117", ] @@ -1701,9 +1632,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.90" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1783,9 +1714,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libm" @@ -1795,13 +1726,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags", "libc", - "redox_syscall 0.7.0", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -1842,6 +1774,17 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "serde", + "winapi", +] + [[package]] name = "matchit" version = "0.8.4" @@ -1864,6 +1807,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1900,9 +1852,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -1915,6 +1867,19 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -2032,9 +1997,9 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" @@ -2173,6 +2138,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "pluralizer" version = "0.5.0" @@ -2388,9 +2359,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags", ] @@ -2668,9 +2639,9 @@ dependencies = [ [[package]] name = "sea-orm" -version = "2.0.0-rc.36" +version = "2.0.0-rc.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "844d97615ecd2661fe4aba04df28b80a91ad34cc6c476e683f3103381b0c33bd" +checksum = "4b846dc1c7fefbea372c03765ff08307d68894bbad8c73b66176dcd53a3ee131" dependencies = [ "async-stream", "async-trait", @@ -2680,6 +2651,7 @@ dependencies = [ "futures-util", "itertools", "log", + "mac_address", "ouroboros", "pgvector", "rust_decimal", @@ -2712,9 +2684,9 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "2.0.0-rc.36" +version = "2.0.0-rc.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b22cd368ed2053ee7b9ccae5ef33621a55c4499dd3b64cdb518e69661b83e4" +checksum = "b449fe660e4d365f335222025df97ae01e670ef7ad788b3c67db9183b6cb0474" dependencies = [ "heck 0.5.0", "itertools", @@ -2758,7 +2730,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab" dependencies = [ - "darling 0.20.11", + "darling", "heck 0.4.1", "proc-macro2", "quote", @@ -2772,7 +2744,7 @@ version = "1.0.0-rc.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d88ad44b6ad9788c8b9476b6b91f94c7461d1e19d39cd8ea37838b1e6ff5aa8" dependencies = [ - "darling 0.20.11", + "darling", "heck 0.4.1", "proc-macro2", "quote", @@ -2823,9 +2795,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation", @@ -2836,9 +2808,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3062,12 +3034,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3533,9 +3505,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.3+spec-1.1.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0a07913e63758bc95142d9863a5a45173b71515e68b690cad70cf99c3255ce1" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap", "toml_datetime", @@ -3618,12 +3590,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" -[[package]] -name = "ubyte" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea" - [[package]] name = "unicode-bidi" version = "0.3.18" @@ -3695,9 +3661,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -3719,13 +3685,12 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vespera" -version = "0.1.40" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a5811dd7c65c1b092a814c2c141b2fdef884c1c77d67725b2080ee460c379d4" +checksum = "2379c3d45c2af97ca9cfc6a2c2f6656d97314ca9b2a701e8d73705a83d637c32" dependencies = [ "axum", "axum-extra", - "axum_typed_multipart", "chrono", "serde_json", "tempfile", @@ -3737,9 +3702,9 @@ dependencies = [ [[package]] name = "vespera_core" -version = "0.1.40" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab2e8f2a0ac1ef3f3992791a52a320eeb6ac1d4f7b8c9bd0bd98317e7f8bd97" +checksum = "48f6782ea90a8c9acd3817529f0e5e1805357f1e29d2adb4276a59564813f670" dependencies = [ "serde", "serde_json", @@ -3747,9 +3712,9 @@ dependencies = [ [[package]] name = "vespera_macro" -version = "0.1.40" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc52ff53d752b1aa43b7a1d6ee9c2a23af34c334f136fb614364e0f4364d0d" +checksum = "eb90670fca1976f73dcf0adf5cdbea21df41030ada05dd4224a5297b25a06c24" dependencies = [ "proc-macro2", "quote", @@ -3761,15 +3726,17 @@ dependencies = [ [[package]] name = "vespertide" -version = "0.1.50" +version = "0.1.51" dependencies = [ + "sea-orm", + "tokio", "vespertide-core", "vespertide-macro", ] [[package]] name = "vespertide-cli" -version = "0.1.50" +version = "0.1.51" dependencies = [ "anyhow", "assert_cmd", @@ -3798,7 +3765,7 @@ dependencies = [ [[package]] name = "vespertide-config" -version = "0.1.50" +version = "0.1.51" dependencies = [ "clap", "schemars", @@ -3808,7 +3775,7 @@ dependencies = [ [[package]] name = "vespertide-core" -version = "0.1.50" +version = "0.1.51" dependencies = [ "rstest", "schemars", @@ -3820,7 +3787,7 @@ dependencies = [ [[package]] name = "vespertide-exporter" -version = "0.1.50" +version = "0.1.51" dependencies = [ "insta", "rstest", @@ -3832,7 +3799,7 @@ dependencies = [ [[package]] name = "vespertide-loader" -version = "0.1.50" +version = "0.1.51" dependencies = [ "anyhow", "rstest", @@ -3847,14 +3814,12 @@ dependencies = [ [[package]] name = "vespertide-macro" -version = "0.1.50" +version = "0.1.51" dependencies = [ "proc-macro2", - "quote", "runtime-macros", "syn 2.0.117", "tempfile", - "thiserror", "vespertide-config", "vespertide-core", "vespertide-loader", @@ -3864,11 +3829,11 @@ dependencies = [ [[package]] name = "vespertide-naming" -version = "0.1.50" +version = "0.1.51" [[package]] name = "vespertide-planner" -version = "0.1.50" +version = "0.1.51" dependencies = [ "insta", "rstest", @@ -3879,7 +3844,7 @@ dependencies = [ [[package]] name = "vespertide-query" -version = "0.1.50" +version = "0.1.51" dependencies = [ "insta", "rstest", @@ -3946,9 +3911,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -3959,9 +3924,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3969,9 +3934,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -3982,9 +3947,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -4033,6 +3998,28 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -4110,15 +4097,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -4152,30 +4130,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4188,12 +4149,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4206,12 +4161,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4224,24 +4173,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4254,12 +4191,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4272,12 +4203,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4290,12 +4215,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4308,17 +4227,11 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -4457,18 +4370,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "96e13bc581734df6250836c59a5f44f3c57db9f9acb9dc8e3eaabdaf6170254d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "3545ea9e86d12ab9bba9fcd99b54c1556fd3199007def5a03c375623d05fac1c" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5055eea5..ee3de817 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,14 @@ documentation = "https://docs.rs/vespertide" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } [workspace.dependencies] -vespertide-core = { path = "crates/vespertide-core", version = "0.1.51" } -vespertide-config = { path = "crates/vespertide-config", version = "0.1.51" } -vespertide-loader = { path = "crates/vespertide-loader", version = "0.1.51" } +vespertide-core = { path = "crates/vespertide-core", version = "0.1.51", default-features = false } +vespertide-config = { path = "crates/vespertide-config", version = "0.1.51", default-features = false } +vespertide-loader = { path = "crates/vespertide-loader", version = "0.1.51", default-features = false } vespertide-macro = { path = "crates/vespertide-macro", version = "0.1.51" } vespertide-naming = { path = "crates/vespertide-naming", version = "0.1.51" } vespertide-planner = { path = "crates/vespertide-planner", version = "0.1.51" } vespertide-query = { path = "crates/vespertide-query", version = "0.1.51" } vespertide-exporter = { path = "crates/vespertide-exporter", version = "0.1.51" } + +[profile.dev] +debug = 1 # Line tables only — faster DWARF generation for large codegen output diff --git a/crates/vespertide-cli/Cargo.toml b/crates/vespertide-cli/Cargo.toml index 938a5534..0eedd23c 100644 --- a/crates/vespertide-cli/Cargo.toml +++ b/crates/vespertide-cli/Cargo.toml @@ -22,8 +22,8 @@ schemars = "1.2" tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs"] } futures = "0.3" async-recursion = "1" -vespertide-config = { workspace = true } -vespertide-core = { workspace = true } +vespertide-config = { workspace = true, features = ["cli", "schema"] } +vespertide-core = { workspace = true, features = ["schema"] } vespertide-loader = { workspace = true } vespertide-planner = { workspace = true } vespertide-query = { workspace = true } diff --git a/crates/vespertide-config/Cargo.toml b/crates/vespertide-config/Cargo.toml index 511e4602..128ad2c2 100644 --- a/crates/vespertide-config/Cargo.toml +++ b/crates/vespertide-config/Cargo.toml @@ -10,8 +10,13 @@ description = "Manages models/migrations directories and naming-case preferences [dependencies] serde = { version = "1", features = ["derive"] } -clap = { version = "4", features = ["derive"] } -schemars = "1.2" +clap = { version = "4", features = ["derive"], optional = true } +schemars = { version = "1.2", optional = true } + +[features] +default = ["cli", "schema"] +cli = ["dep:clap"] +schema = ["dep:schemars"] [dev-dependencies] serde_json = "1" diff --git a/crates/vespertide-config/src/config.rs b/crates/vespertide-config/src/config.rs index e1ecbbb5..0b7a4822 100644 --- a/crates/vespertide-config/src/config.rs +++ b/crates/vespertide-config/src/config.rs @@ -1,6 +1,5 @@ use std::path::{Path, PathBuf}; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::file_format::FileFormat; @@ -12,7 +11,8 @@ pub fn default_migration_filename_pattern() -> String { } /// SeaORM-specific export configuration. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] pub struct SeaOrmConfig { /// Additional derive macros to add to generated enum types. @@ -78,7 +78,8 @@ impl SeaOrmConfig { } /// Top-level vespertide configuration. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] pub struct VespertideConfig { pub models_dir: PathBuf, diff --git a/crates/vespertide-config/src/file_format.rs b/crates/vespertide-config/src/file_format.rs index be9dec6d..76ba3317 100644 --- a/crates/vespertide-config/src/file_format.rs +++ b/crates/vespertide-config/src/file_format.rs @@ -1,9 +1,9 @@ -use clap::ValueEnum; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// Supported file formats for generated artifacts. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum, JsonSchema)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "cli", derive(clap::ValueEnum))] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "lowercase")] #[derive(Default)] pub enum FileFormat { diff --git a/crates/vespertide-config/src/name_case.rs b/crates/vespertide-config/src/name_case.rs index a47dd134..edd01448 100644 --- a/crates/vespertide-config/src/name_case.rs +++ b/crates/vespertide-config/src/name_case.rs @@ -1,8 +1,8 @@ -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// Supported naming cases. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] pub enum NameCase { Snake, diff --git a/crates/vespertide-core/Cargo.toml b/crates/vespertide-core/Cargo.toml index 127e6711..1ddb0eb8 100644 --- a/crates/vespertide-core/Cargo.toml +++ b/crates/vespertide-core/Cargo.toml @@ -10,10 +10,14 @@ description = "Data models for tables, columns, constraints, indexes, and migrat [dependencies] serde = { version = "1", features = ["derive"] } -schemars = { version = "1.2" } +schemars = { version = "1.2", optional = true } thiserror = "2" vespertide-naming = { workspace = true } +[features] +default = ["schema"] +schema = ["dep:schemars"] + [dev-dependencies] rstest = "0.26" serde_json = "1" diff --git a/crates/vespertide-core/src/action.rs b/crates/vespertide-core/src/action.rs index bcd1db09..50d39384 100644 --- a/crates/vespertide-core/src/action.rs +++ b/crates/vespertide-core/src/action.rs @@ -1,10 +1,10 @@ use crate::schema::{ColumnDef, ColumnName, ColumnType, TableConstraint, TableName}; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] pub struct MigrationPlan { /// Unique identifier for this migration (UUID format). @@ -18,7 +18,8 @@ pub struct MigrationPlan { pub actions: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(tag = "type", rename_all = "snake_case")] pub enum MigrationAction { CreateTable { diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs index 5900eff0..ee838724 100644 --- a/crates/vespertide-core/src/schema/column.rs +++ b/crates/vespertide-core/src/schema/column.rs @@ -1,4 +1,3 @@ -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::schema::{ @@ -8,7 +7,8 @@ use crate::schema::{ str_or_bool::{StrOrBoolOrArray, StringOrBool}, }; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] pub struct ColumnDef { pub name: ColumnName, @@ -28,7 +28,8 @@ pub struct ColumnDef { pub foreign_key: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case", untagged)] pub enum ColumnType { Simple(SimpleColumnType), @@ -146,7 +147,8 @@ impl ColumnType { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] pub enum SimpleColumnType { SmallInt, @@ -246,14 +248,16 @@ impl SimpleColumnType { } /// Integer enum variant with name and numeric value -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct NumValue { pub name: String, pub value: i32, } /// Enum values definition - either all string or all integer -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(untagged)] pub enum EnumValues { String(Vec), @@ -317,7 +321,8 @@ impl From> for EnumValues { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case", tag = "kind")] pub enum ComplexColumnType { Varchar { length: u32 }, diff --git a/crates/vespertide-core/src/schema/constraint.rs b/crates/vespertide-core/src/schema/constraint.rs index e825e8a7..5bdc5878 100644 --- a/crates/vespertide-core/src/schema/constraint.rs +++ b/crates/vespertide-core/src/schema/constraint.rs @@ -1,4 +1,3 @@ -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::schema::{ @@ -6,7 +5,8 @@ use crate::schema::{ names::{ColumnName, TableName}, }; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case", tag = "type")] pub enum TableConstraint { PrimaryKey { diff --git a/crates/vespertide-core/src/schema/foreign_key.rs b/crates/vespertide-core/src/schema/foreign_key.rs index 5586cd73..d59305fc 100644 --- a/crates/vespertide-core/src/schema/foreign_key.rs +++ b/crates/vespertide-core/src/schema/foreign_key.rs @@ -1,9 +1,9 @@ -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::schema::{names::ColumnName, names::TableName, reference::ReferenceAction}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] pub struct ForeignKeyDef { pub ref_table: TableName, @@ -13,7 +13,8 @@ pub struct ForeignKeyDef { } /// Shorthand syntax for foreign key: { "references": "table.column", "on_delete": "cascade" } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] pub struct ReferenceSyntaxDef { /// Reference in "table.column" format @@ -24,7 +25,8 @@ pub struct ReferenceSyntaxDef { pub on_update: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case", untagged)] pub enum ForeignKeySyntax { /// table.column diff --git a/crates/vespertide-core/src/schema/index.rs b/crates/vespertide-core/src/schema/index.rs index b550a557..0f592b92 100644 --- a/crates/vespertide-core/src/schema/index.rs +++ b/crates/vespertide-core/src/schema/index.rs @@ -1,9 +1,9 @@ -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::schema::names::{ColumnName, IndexName}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] pub struct IndexDef { pub name: IndexName, diff --git a/crates/vespertide-core/src/schema/primary_key.rs b/crates/vespertide-core/src/schema/primary_key.rs index f23dad44..366d0b73 100644 --- a/crates/vespertide-core/src/schema/primary_key.rs +++ b/crates/vespertide-core/src/schema/primary_key.rs @@ -1,14 +1,15 @@ -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] pub struct PrimaryKeyDef { #[serde(default)] pub auto_increment: bool, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case", untagged)] pub enum PrimaryKeySyntax { Bool(bool), diff --git a/crates/vespertide-core/src/schema/reference.rs b/crates/vespertide-core/src/schema/reference.rs index c1f51c28..ae895155 100644 --- a/crates/vespertide-core/src/schema/reference.rs +++ b/crates/vespertide-core/src/schema/reference.rs @@ -1,7 +1,7 @@ -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] pub enum ReferenceAction { Cascade, diff --git a/crates/vespertide-core/src/schema/str_or_bool.rs b/crates/vespertide-core/src/schema/str_or_bool.rs index 9e20633b..61bef6e3 100644 --- a/crates/vespertide-core/src/schema/str_or_bool.rs +++ b/crates/vespertide-core/src/schema/str_or_bool.rs @@ -1,7 +1,7 @@ -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case", untagged)] pub enum StrOrBoolOrArray { Str(String), @@ -11,7 +11,8 @@ pub enum StrOrBoolOrArray { /// A value that can be a string, boolean, or number. /// This is used for default values where columns can use literal values directly. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(untagged)] pub enum DefaultValue { Bool(bool), diff --git a/crates/vespertide-core/src/schema/table.rs b/crates/vespertide-core/src/schema/table.rs index dd9a492e..56b4859f 100644 --- a/crates/vespertide-core/src/schema/table.rs +++ b/crates/vespertide-core/src/schema/table.rs @@ -1,5 +1,3 @@ -use schemars::JsonSchema; - use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -46,7 +44,8 @@ impl std::fmt::Display for TableValidationError { impl std::error::Error for TableValidationError {} -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "snake_case")] pub struct TableDef { pub name: TableName, diff --git a/crates/vespertide-loader/src/migrations.rs b/crates/vespertide-loader/src/migrations.rs index 762bee94..1fa928ce 100644 --- a/crates/vespertide-loader/src/migrations.rs +++ b/crates/vespertide-loader/src/migrations.rs @@ -113,9 +113,70 @@ pub fn load_migrations_at_compile_time() -> Result, Box Self { + let original = env::current_dir().unwrap(); + env::set_current_dir(dir).unwrap(); + Self { original } + } + } + + impl Drop for CwdGuard { + fn drop(&mut self) { + let _ = env::set_current_dir(&self.original); + } + } + + fn write_config(dir: &std::path::Path) { + let cfg = VespertideConfig::default(); + let text = serde_json::to_string_pretty(&cfg).unwrap(); + fs::write(dir.join("vespertide.json"), text).unwrap(); + } + + #[test] + #[serial] + fn test_load_migrations_returns_empty_when_no_migrations_dir() { + let temp_dir = TempDir::new().unwrap(); + let _guard = CwdGuard::new(&temp_dir.path().to_path_buf()); + write_config(temp_dir.path()); + + let result = load_migrations(&VespertideConfig::default()).unwrap(); + assert!(result.is_empty()); + } + + #[test] + #[serial] + fn test_load_migrations_reads_json_and_sorts_versions() { + let temp_dir = TempDir::new().unwrap(); + let _guard = CwdGuard::new(&temp_dir.path().to_path_buf()); + write_config(temp_dir.path()); + fs::create_dir_all("migrations").unwrap(); + fs::write( + "migrations/0002_second.json", + r#"{"version": 2, "actions": []}"#, + ) + .unwrap(); + fs::write( + "migrations/0001_first.json", + r#"{"version": 1, "actions": []}"#, + ) + .unwrap(); + + let plans = load_migrations(&VespertideConfig::default()).unwrap(); + assert_eq!( + plans.iter().map(|plan| plan.version).collect::>(), + vec![1, 2] + ); + } + #[test] fn test_load_migrations_from_dir_with_no_migrations_dir() { let temp_dir = TempDir::new().unwrap(); @@ -245,6 +306,32 @@ actions: assert_eq!(plans[0].version, 1); } + #[test] + #[serial] + fn test_load_migrations_reads_yaml_for_runtime_loader() { + let temp_dir = TempDir::new().unwrap(); + let _guard = CwdGuard::new(&temp_dir.path().to_path_buf()); + write_config(temp_dir.path()); + fs::create_dir_all("migrations").unwrap(); + + let migration_content = r#"--- +version: 1 +actions: + - type: create_table + table: users + columns: + - name: id + type: integer + nullable: false + constraints: [] +"#; + fs::write("migrations/0001_test.yaml", migration_content).unwrap(); + + let plans = load_migrations(&VespertideConfig::default()).unwrap(); + assert_eq!(plans.len(), 1); + assert_eq!(plans[0].version, 1); + } + #[test] fn test_load_migrations_from_dir_with_invalid_json() { let temp_dir = TempDir::new().unwrap(); diff --git a/crates/vespertide-macro/Cargo.toml b/crates/vespertide-macro/Cargo.toml index 518f06d4..45406d32 100644 --- a/crates/vespertide-macro/Cargo.toml +++ b/crates/vespertide-macro/Cargo.toml @@ -17,9 +17,7 @@ vespertide-config = { workspace = true } vespertide-loader = { workspace = true } vespertide-query = { workspace = true } vespertide-planner = { workspace = true } -thiserror = "2" -syn = { version = "2.0", features = ["full"] } -quote = "1.0" +syn = { version = "2.0", features = ["parsing", "proc-macro"] } proc-macro2 = "1.0" [dev-dependencies] diff --git a/crates/vespertide-macro/src/lib.rs b/crates/vespertide-macro/src/lib.rs index 8c596ade..dbc65d6a 100644 --- a/crates/vespertide-macro/src/lib.rs +++ b/crates/vespertide-macro/src/lib.rs @@ -1,27 +1,29 @@ // MigrationOptions and MigrationError are now in vespertide-core use std::env; +use std::fmt::Write; use std::path::PathBuf; use proc_macro::TokenStream; -use quote::quote; use syn::parse::{Parse, ParseStream}; -use syn::{Expr, Ident, Token}; -use vespertide_loader::{ - load_config_or_default, load_migrations_at_compile_time, load_models_at_compile_time, -}; +use syn::{Ident, Token}; +use vespertide_loader::{load_config_or_default, load_migrations_at_compile_time}; use vespertide_planner::apply_action; use vespertide_query::{DatabaseBackend, build_plan_queries}; struct MacroInput { - pool: Expr, + pool: proc_macro2::TokenStream, version_table: Option, verbose: bool, } impl Parse for MacroInput { fn parse(input: ParseStream) -> syn::Result { - let pool = input.parse()?; + let mut pool_tokens = Vec::new(); + while !input.is_empty() && !input.peek(Token![,]) { + pool_tokens.push(input.parse::()?); + } + let pool: proc_macro2::TokenStream = pool_tokens.into_iter().collect(); let mut version_table = None; let mut verbose = false; @@ -54,13 +56,28 @@ impl Parse for MacroInput { } } +/// Generated migration block with raw SQL strings for string-based codegen. +#[derive(Debug)] +pub(crate) struct MigrationBlock { + /// Migration version number + pub version: u32, + /// Migration ID for validation + pub migration_id: String, + /// Migration comment (for verbose logging) + pub comment: String, + /// PostgreSQL SQL statements + pub pg_sqls: Vec, + /// MySQL SQL statements + pub mysql_sqls: Vec, + /// SQLite SQL statements + pub sqlite_sqls: Vec, +} + pub(crate) fn build_migration_block( migration: &vespertide_core::MigrationPlan, baseline_schema: &mut Vec, - verbose: bool, -) -> Result { +) -> Result { let version = migration.version; - let migration_id = &migration.id; // Use the current baseline schema (from all previous migrations) let queries = build_plan_queries(migration, baseline_schema).map_err(|e| { @@ -75,252 +92,86 @@ pub(crate) fn build_migration_block( let _ = apply_action(baseline_schema, action); } - // Generate version guard and SQL execution block - let version_str = format!("v{}", version); - let comment_str = migration.comment.as_deref().unwrap_or("").to_string(); - - let block = if verbose { - // Verbose mode: preserve per-action grouping with action descriptions - let total_sql_count: usize = queries - .iter() - .map(|q| q.postgres.len().max(q.mysql.len()).max(q.sqlite.len())) - .sum(); - let total_sql_count_lit = total_sql_count; - - let mut action_blocks = Vec::new(); - let mut global_idx: usize = 0; - - for (action_idx, q) in queries.iter().enumerate() { - let action_desc = format!("{}", q.action); - let action_num = action_idx + 1; - let total_actions = queries.len(); - - let pg: Vec = q - .postgres - .iter() - .map(|s| s.build(DatabaseBackend::Postgres)) - .collect(); - let mysql: Vec = q - .mysql - .iter() - .map(|s| s.build(DatabaseBackend::MySql)) - .collect(); - let sqlite: Vec = q - .sqlite - .iter() - .map(|s| s.build(DatabaseBackend::Sqlite)) - .collect(); - - // Build per-SQL execution with global index - let sql_count = pg.len().max(mysql.len()).max(sqlite.len()); - let mut sql_exec_blocks = Vec::new(); - - for i in 0..sql_count { - let idx = global_idx + i + 1; - let pg_sql = pg.get(i).cloned().unwrap_or_default(); - let mysql_sql = mysql.get(i).cloned().unwrap_or_default(); - let sqlite_sql = sqlite.get(i).cloned().unwrap_or_default(); - - sql_exec_blocks.push(quote! { - { - let sql: &str = match backend { - sea_orm::DatabaseBackend::Postgres => #pg_sql, - sea_orm::DatabaseBackend::MySql => #mysql_sql, - sea_orm::DatabaseBackend::Sqlite => #sqlite_sql, - _ => #pg_sql, - }; - if !sql.is_empty() { - eprintln!("[vespertide] [{}/{}] {}", #idx, #total_sql_count_lit, sql); - let stmt = sea_orm::Statement::from_string(backend, sql); - __txn.execute_raw(stmt).await.map_err(|e| { - ::vespertide::MigrationError::DatabaseError(format!("Failed to execute SQL '{}': {}", sql, e)) - })?; - } - } - }); - } - global_idx += sql_count; + // Flatten all SQL into per-backend string arrays + let mut pg_sqls = Vec::new(); + let mut mysql_sqls = Vec::new(); + let mut sqlite_sqls = Vec::new(); - action_blocks.push(quote! { - eprintln!("[vespertide] Action {}/{}: {}", #action_num, #total_actions, #action_desc); - #(#sql_exec_blocks)* - }); + for q in &queries { + for stmt in &q.postgres { + pg_sqls.push(stmt.build(DatabaseBackend::Postgres)); } - - quote! { - if __version < #version { - // Validate migration id against database if version already tracked - if let Some(db_id) = __version_ids.get(&#version) { - let expected_id: &str = #migration_id; - if !expected_id.is_empty() && !db_id.is_empty() && db_id != expected_id { - return Err(::vespertide::MigrationError::IdMismatch { - version: #version, - expected: expected_id.to_string(), - found: db_id.clone(), - }); - } - } - - eprintln!("[vespertide] Applying migration {} ({})", #version_str, #comment_str); - #(#action_blocks)* - - let insert_sql = format!("INSERT INTO {q}{}{q} (version, id) VALUES ({}, '{}')", __version_table, #version, #migration_id); - let stmt = sea_orm::Statement::from_string(backend, insert_sql); - __txn.execute_raw(stmt).await.map_err(|e| { - ::vespertide::MigrationError::DatabaseError(format!("Failed to insert version: {}", e)) - })?; - - eprintln!("[vespertide] Migration {} applied successfully", #version_str); - } + for stmt in &q.mysql { + mysql_sqls.push(stmt.build(DatabaseBackend::MySql)); } - } else { - // Non-verbose: flatten all SQL into one array (minimal overhead) - let mut pg_sqls = Vec::new(); - let mut mysql_sqls = Vec::new(); - let mut sqlite_sqls = Vec::new(); - - for q in &queries { - for stmt in &q.postgres { - pg_sqls.push(stmt.build(DatabaseBackend::Postgres)); - } - for stmt in &q.mysql { - mysql_sqls.push(stmt.build(DatabaseBackend::MySql)); - } - for stmt in &q.sqlite { - sqlite_sqls.push(stmt.build(DatabaseBackend::Sqlite)); - } + for stmt in &q.sqlite { + sqlite_sqls.push(stmt.build(DatabaseBackend::Sqlite)); } + } - quote! { - if __version < #version { - // Validate migration id against database if version already tracked - if let Some(db_id) = __version_ids.get(&#version) { - let expected_id: &str = #migration_id; - if !expected_id.is_empty() && !db_id.is_empty() && db_id != expected_id { - return Err(::vespertide::MigrationError::IdMismatch { - version: #version, - expected: expected_id.to_string(), - found: db_id.clone(), - }); - } - } - - let sqls: &[&str] = match backend { - sea_orm::DatabaseBackend::Postgres => &[#(#pg_sqls),*], - sea_orm::DatabaseBackend::MySql => &[#(#mysql_sqls),*], - sea_orm::DatabaseBackend::Sqlite => &[#(#sqlite_sqls),*], - _ => &[#(#pg_sqls),*], - }; - - for sql in sqls { - if !sql.is_empty() { - let stmt = sea_orm::Statement::from_string(backend, *sql); - __txn.execute_raw(stmt).await.map_err(|e| { - ::vespertide::MigrationError::DatabaseError(format!("Failed to execute SQL '{}': {}", sql, e)) - })?; - } - } - - let insert_sql = format!("INSERT INTO {q}{}{q} (version, id) VALUES ({}, '{}')", __version_table, #version, #migration_id); - let stmt = sea_orm::Statement::from_string(backend, insert_sql); - __txn.execute_raw(stmt).await.map_err(|e| { - ::vespertide::MigrationError::DatabaseError(format!("Failed to insert version: {}", e)) - })?; - } - } - }; + let comment = migration.comment.as_deref().unwrap_or("").to_string(); - Ok(block) + Ok(MigrationBlock { + version, + migration_id: migration.id.clone(), + comment, + pg_sqls, + mysql_sqls, + sqlite_sqls, + }) } fn generate_migration_code( - pool: &Expr, + pool: &proc_macro2::TokenStream, version_table: &str, - migration_blocks: Vec, + migration_blocks: Vec, verbose: bool, ) -> proc_macro2::TokenStream { - let verbose_current_version = if verbose { - quote! { - eprintln!("[vespertide] Current database version: {}", __version); - } - } else { - quote! {} - }; - - quote! { - async { - use sea_orm::{ConnectionTrait, TransactionTrait}; - let __pool = &#pool; - let __version_table = #version_table; - let backend = __pool.get_database_backend(); - let q = if matches!(backend, sea_orm::DatabaseBackend::MySql) { '`' } else { '"' }; - - // Create version table if it does not exist (outside transaction) - let create_table_sql = format!( - "CREATE TABLE IF NOT EXISTS {q}{}{q} (version INTEGER PRIMARY KEY, id TEXT DEFAULT '', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)", - __version_table - ); - let stmt = sea_orm::Statement::from_string(backend, create_table_sql); - __pool.execute_raw(stmt).await.map_err(|e| { - ::vespertide::MigrationError::DatabaseError(format!("Failed to create version table: {}", e)) - })?; - - // Add id column for existing tables that don't have it yet (backward compatibility). - // We use a try-and-ignore approach: if the column already exists, the ALTER will fail - // and we simply ignore the error. - let alter_sql = format!( - "ALTER TABLE {q}{}{q} ADD COLUMN id TEXT DEFAULT ''", - __version_table - ); - let stmt = sea_orm::Statement::from_string(backend, alter_sql); - let _ = __pool.execute_raw(stmt).await; - - // Single transaction for the entire migration process. - // This prevents race conditions when multiple connections exist - // (e.g. SQLite with max_connections > 1). - let __txn = __pool.begin().await.map_err(|e| { - ::vespertide::MigrationError::DatabaseError(format!("Failed to begin transaction: {}", e)) - })?; - - // Read current maximum version inside the transaction (holds lock) - let select_sql = format!("SELECT MAX(version) as version FROM {q}{}{q}", __version_table); - let stmt = sea_orm::Statement::from_string(backend, select_sql); - let version_result = __txn.query_one_raw(stmt).await.map_err(|e| { - ::vespertide::MigrationError::DatabaseError(format!("Failed to read version: {}", e)) - })?; - - let __version = version_result - .and_then(|row| row.try_get::("", "version").ok()) - .unwrap_or(0) as u32; - - // Load all existing (version, id) pairs for id mismatch validation - let select_ids_sql = format!("SELECT version, id FROM {q}{}{q}", __version_table); - let stmt = sea_orm::Statement::from_string(backend, select_ids_sql); - let id_rows = __txn.query_all_raw(stmt).await.map_err(|e| { - ::vespertide::MigrationError::DatabaseError(format!("Failed to read version ids: {}", e)) - })?; - - let mut __version_ids = std::collections::HashMap::::new(); - for row in &id_rows { - if let Ok(v) = row.try_get::("", "version") { - let id = row.try_get::("", "id").unwrap_or_default(); - __version_ids.insert(v as u32, id); - } - } + // Build entire output as a String, parse once at the end. + // This avoids per-token IPC overhead from quote! (see rust-lang/rust#65080). + let pool_str = pool.to_string(); + let mut code = String::with_capacity(1_048_576); + + code.push_str("{\n"); + + // Emit compact SQL blobs (outside async block for zero runtime cost). + for b in &migration_blocks { + write_sql_blob(&mut code, &format!("__V{}_PG", b.version), &b.pg_sqls); + write_sql_blob(&mut code, &format!("__V{}_MYSQL", b.version), &b.mysql_sqls); + write_sql_blob( + &mut code, + &format!("__V{}_SQLITE", b.version), + &b.sqlite_sqls, + ); + } - #verbose_current_version + code.push_str( + "static __VESPERTIDE_MIGRATIONS: &[::vespertide::runtime::EmbeddedMigration] = &[\n", + ); + for b in &migration_blocks { + writeln!(code, "::vespertide::runtime::EmbeddedMigration::new({}u32, {:?}, {:?}, __V{}_PG, __V{}_MYSQL, __V{}_SQLITE),", b.version, b.migration_id, b.comment, b.version, b.version, b.version).unwrap(); + } + code.push_str("];\n"); - // Execute each migration block within the same transaction - #(#migration_blocks)* + code.push_str("async {\n"); + writeln!(code, "let __pool = &{pool_str};").unwrap(); + writeln!(code, "::vespertide::runtime::run_embedded_migrations(__pool, {version_table:?}, {verbose}, __VESPERTIDE_MIGRATIONS).await").unwrap(); + code.push_str("}\n"); // async block + code.push_str("}\n"); // outer block - // Commit the entire migration - __txn.commit().await.map_err(|e| { - ::vespertide::MigrationError::DatabaseError(format!("Failed to commit transaction: {}", e)) - })?; + // Single parse — ONE IPC call to rustc tokenizer + code.parse().unwrap_or_else(|e| panic!("vespertide_migration codegen produced invalid Rust:\n{e}\n\nGenerated code (first 2000 chars):\n{}", &code[..code.len().min(2000)])) +} - Ok::<(), ::vespertide::MigrationError>(()) - } +/// Write a compact SQL blob declaration into the code string. +fn write_sql_blob(code: &mut String, ident: &str, sqls: &[String]) { + let mut blob = String::new(); + for sql in sqls { + blob.push_str(sql); + blob.push('\0'); } + + writeln!(code, "static {ident}: &str = {blob:?};").unwrap(); } /// Inner implementation that works with proc_macro2::TokenStream for testability. @@ -371,18 +222,6 @@ pub(crate) fn vespertide_migration_impl( .to_compile_error(); } }; - let _models = match load_models_at_compile_time() { - Ok(models) => models, - #[cfg(not(tarpaulin_include))] - Err(e) => { - return syn::Error::new( - proc_macro2::Span::call_site(), - format!("Failed to load models at compile time: {}", e), - ) - .to_compile_error(); - } - }; - // Apply prefix to migrations and build SQL using incremental baseline schema let mut baseline_schema = Vec::new(); let mut migration_blocks = Vec::new(); @@ -391,7 +230,7 @@ pub(crate) fn vespertide_migration_impl( for migration in &migrations { // Apply prefix to migration table names let prefixed_migration = migration.clone().with_prefix(prefix); - match build_migration_block(&prefixed_migration, &mut baseline_schema, verbose) { + match build_migration_block(&prefixed_migration, &mut baseline_schema) { Ok(block) => migration_blocks.push(block), Err(e) => { return syn::Error::new(proc_macro2::Span::call_site(), e).to_compile_error(); @@ -522,6 +361,24 @@ mod tests { } } + /// Concatenate all SQL strings from a block for assertion testing. + fn block_to_string(block: &MigrationBlock) -> String { + let mut result = String::new(); + for sql in &block.pg_sqls { + result.push_str(sql); + result.push(' '); + } + for sql in &block.mysql_sqls { + result.push_str(sql); + result.push(' '); + } + for sql in &block.sqlite_sqls { + result.push_str(sql); + result.push(' '); + } + result + } + #[test] fn test_build_migration_block_create_table() { let migration = MigrationPlan { @@ -537,15 +394,15 @@ mod tests { }; let mut baseline = Vec::new(); - let result = build_migration_block(&migration, &mut baseline, false); + let result = build_migration_block(&migration, &mut baseline); assert!(result.is_ok()); let block = result.unwrap(); - let block_str = block.to_string(); + let block_str = block_to_string(&block); - // Verify the generated block contains expected elements - assert!(block_str.contains("version < 1u32")); + // Verify statics contain SQL and metadata is correct assert!(block_str.contains("CREATE TABLE")); + assert_eq!(block.version, 1); // Verify baseline schema was updated assert_eq!(baseline.len(), 1); @@ -568,7 +425,7 @@ mod tests { }; let mut baseline = Vec::new(); - let _ = build_migration_block(&create_migration, &mut baseline, false); + let _ = build_migration_block(&create_migration, &mut baseline); // Now add a column let add_column_migration = MigrationPlan { @@ -593,12 +450,12 @@ mod tests { }], }; - let result = build_migration_block(&add_column_migration, &mut baseline, false); + let result = build_migration_block(&add_column_migration, &mut baseline); assert!(result.is_ok()); let block = result.unwrap(); - let block_str = block.to_string(); + let block_str = block_to_string(&block); - assert!(block_str.contains("version < 2u32")); + assert_eq!(block.version, 2); assert!(block_str.contains("ALTER TABLE")); assert!(block_str.contains("ADD COLUMN")); } @@ -625,7 +482,7 @@ mod tests { }; let mut baseline = Vec::new(); - let result = build_migration_block(&migration, &mut baseline, false); + let result = build_migration_block(&migration, &mut baseline); assert!(result.is_ok()); assert_eq!(baseline.len(), 2); @@ -633,7 +490,7 @@ mod tests { #[test] fn test_generate_migration_code() { - let pool: Expr = syn::parse_str("db_pool").unwrap(); + let pool: proc_macro2::TokenStream = "db_pool".parse().unwrap(); let version_table = "test_versions"; // Create a simple migration block @@ -650,7 +507,7 @@ mod tests { }; let mut baseline = Vec::new(); - let block = build_migration_block(&migration, &mut baseline, false).unwrap(); + let block = build_migration_block(&migration, &mut baseline).unwrap(); let generated = generate_migration_code(&pool, version_table, vec![block], false); let generated_str = generated.to_string(); @@ -659,13 +516,14 @@ mod tests { assert!(generated_str.contains("async")); assert!(generated_str.contains("db_pool")); assert!(generated_str.contains("test_versions")); - assert!(generated_str.contains("CREATE TABLE IF NOT EXISTS")); - assert!(generated_str.contains("SELECT MAX")); + assert!(generated_str.contains("run_embedded_migrations")); + assert!(generated_str.contains("EmbeddedMigration")); + assert!(generated_str.contains("1u32")); } #[test] fn test_generate_migration_code_empty_migrations() { - let pool: Expr = syn::parse_str("pool").unwrap(); + let pool: proc_macro2::TokenStream = "pool".parse().unwrap(); let version_table = "vespertide_version"; let generated = generate_migration_code(&pool, version_table, vec![], false); @@ -678,7 +536,7 @@ mod tests { #[test] fn test_generate_migration_code_multiple_blocks() { - let pool: Expr = syn::parse_str("connection").unwrap(); + let pool: proc_macro2::TokenStream = "connection".parse().unwrap(); let mut baseline = Vec::new(); @@ -693,7 +551,7 @@ mod tests { constraints: vec![], }], }; - let block1 = build_migration_block(&migration1, &mut baseline, false).unwrap(); + let block1 = build_migration_block(&migration1, &mut baseline).unwrap(); let migration2 = MigrationPlan { id: String::new(), @@ -706,14 +564,43 @@ mod tests { constraints: vec![], }], }; - let block2 = build_migration_block(&migration2, &mut baseline, false).unwrap(); + let block2 = build_migration_block(&migration2, &mut baseline).unwrap(); let generated = generate_migration_code(&pool, "migrations", vec![block1, block2], false); let generated_str = generated.to_string(); - // Both version checks should be present - assert!(generated_str.contains("version < 1u32")); - assert!(generated_str.contains("version < 2u32")); + // Both migration versions should be present in the metadata array + assert!(generated_str.contains("1u32")); + assert!(generated_str.contains("2u32")); + assert!(generated_str.contains("__VESPERTIDE_MIGRATIONS")); + } + + #[test] + fn test_generate_migration_code_delegates_runtime_execution() { + let pool: proc_macro2::TokenStream = "db_pool".parse().unwrap(); + + let migration = MigrationPlan { + id: String::new(), + version: 1, + comment: Some("initial".into()), + created_at: None, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![test_column("id")], + constraints: vec![], + }], + }; + + let mut baseline = Vec::new(); + let block = build_migration_block(&migration, &mut baseline).unwrap(); + + let generated = generate_migration_code(&pool, "vespertide_version", vec![block], false); + let generated_str = generated.to_string(); + + assert!(generated_str.contains("run_embedded_migrations")); + assert!(generated_str.contains("EmbeddedMigration")); + assert!(!generated_str.contains("SELECT MAX")); + assert!(!generated_str.contains("execute_raw")); } #[test] @@ -731,15 +618,29 @@ mod tests { }; let mut baseline = Vec::new(); - let result = build_migration_block(&migration, &mut baseline, false); + let result = build_migration_block(&migration, &mut baseline); assert!(result.is_ok()); - let block_str = result.unwrap().to_string(); + let block = result.unwrap(); + + // All three backend SQL arrays should be populated + assert!( + !block.pg_sqls.is_empty(), + "PostgreSQL SQL should not be empty" + ); + assert!( + !block.mysql_sqls.is_empty(), + "MySQL SQL should not be empty" + ); + assert!( + !block.sqlite_sqls.is_empty(), + "SQLite SQL should not be empty" + ); - // The generated block should have backend matching - assert!(block_str.contains("DatabaseBackend :: Postgres")); - assert!(block_str.contains("DatabaseBackend :: MySql")); - assert!(block_str.contains("DatabaseBackend :: Sqlite")); + // Each should contain CREATE TABLE SQL + assert!(block.pg_sqls.iter().any(|s| s.contains("CREATE TABLE"))); + assert!(block.mysql_sqls.iter().any(|s| s.contains("CREATE TABLE"))); + assert!(block.sqlite_sqls.iter().any(|s| s.contains("CREATE TABLE"))); } #[test] @@ -758,7 +659,7 @@ mod tests { }; let mut baseline = Vec::new(); - let _ = build_migration_block(&create_migration, &mut baseline, false); + let _ = build_migration_block(&create_migration, &mut baseline); assert_eq!(baseline.len(), 1); // Now delete it @@ -772,10 +673,10 @@ mod tests { }], }; - let result = build_migration_block(&delete_migration, &mut baseline, false); + let result = build_migration_block(&delete_migration, &mut baseline); assert!(result.is_ok()); - let block_str = result.unwrap().to_string(); - assert!(block_str.contains("DROP TABLE")); + let block = result.unwrap(); + assert!(block.pg_sqls.iter().any(|s| s.contains("DROP TABLE"))); // Baseline should be empty after delete assert_eq!(baseline.len(), 0); @@ -809,7 +710,7 @@ mod tests { }; let mut baseline = Vec::new(); - let result = build_migration_block(&migration, &mut baseline, false); + let result = build_migration_block(&migration, &mut baseline); assert!(result.is_ok()); // Table should be normalized with index @@ -834,7 +735,7 @@ mod tests { }; let mut baseline = Vec::new(); - let result = build_migration_block(&migration, &mut baseline, false); + let result = build_migration_block(&migration, &mut baseline); assert!(result.is_err()); let err = result.unwrap_err(); @@ -909,8 +810,8 @@ mod tests { output_str ); assert!( - output_str.contains("CREATE TABLE IF NOT EXISTS"), - "Expected version table creation, got: {}", + output_str.contains("run_embedded_migrations"), + "Expected runtime helper delegation, got: {}", output_str ); @@ -941,15 +842,16 @@ mod tests { }; let mut baseline = Vec::new(); - let result = build_migration_block(&migration, &mut baseline, true); + let result = build_migration_block(&migration, &mut baseline); assert!(result.is_ok()); - let block_str = result.unwrap().to_string(); + let block = result.unwrap(); - // Verbose mode should contain eprintln statements with action descriptions - assert!(block_str.contains("vespertide")); - assert!(block_str.contains("Action")); - assert!(block_str.contains("version < 1u32")); + // Metadata should capture comment for verbose logging in generate_migration_code + assert_eq!(block.version, 1); + assert_eq!(block.comment, "initial setup"); + // SQL should contain CREATE TABLE + assert!(block.pg_sqls.iter().any(|s| s.contains("CREATE TABLE"))); } #[test] @@ -974,14 +876,12 @@ mod tests { }; let mut baseline = Vec::new(); - let result = build_migration_block(&migration, &mut baseline, true); + let result = build_migration_block(&migration, &mut baseline); assert!(result.is_ok()); - let block_str = result.unwrap().to_string(); - - // Should have action numbering for both actions - assert!(block_str.contains("Action")); assert_eq!(baseline.len(), 2); + // Metadata should be set even with multiple actions + assert_eq!(result.as_ref().unwrap().version, 1); } #[test] @@ -999,9 +899,9 @@ mod tests { }], }; let mut baseline = Vec::new(); - let _ = build_migration_block(&create, &mut baseline, true); + let _ = build_migration_block(&create, &mut baseline); - // Add column in verbose mode + // Add column let add_col = MigrationPlan { id: String::new(), version: 2, @@ -1024,16 +924,18 @@ mod tests { }], }; - let result = build_migration_block(&add_col, &mut baseline, true); + let result = build_migration_block(&add_col, &mut baseline); assert!(result.is_ok()); - let block_str = result.unwrap().to_string(); - assert!(block_str.contains("vespertide")); - assert!(block_str.contains("version < 2u32")); + let block = result.unwrap(); + assert_eq!(block.version, 2); + assert_eq!(block.comment, "add email"); + // SQL should contain ALTER TABLE for the add column action + assert!(block.pg_sqls.iter().any(|s| s.contains("ALTER TABLE"))); } #[test] fn test_generate_migration_code_verbose() { - let pool: Expr = syn::parse_str("db_pool").unwrap(); + let pool: proc_macro2::TokenStream = "db_pool".parse().unwrap(); let version_table = "test_versions"; let migration = MigrationPlan { @@ -1049,13 +951,12 @@ mod tests { }; let mut baseline = Vec::new(); - let block = build_migration_block(&migration, &mut baseline, true).unwrap(); + let block = build_migration_block(&migration, &mut baseline).unwrap(); let generated = generate_migration_code(&pool, version_table, vec![block], true); let generated_str = generated.to_string(); - // Verbose mode should include current version eprintln - assert!(generated_str.contains("Current database version")); + assert!(generated_str.contains("run_embedded_migrations")); assert!(generated_str.contains("async")); } @@ -1139,4 +1040,79 @@ mod tests { } } } + + #[test] + fn test_vespertide_migration_impl_ignores_invalid_models() { + use std::fs; + + let dir = tempdir().unwrap(); + let project_dir = dir.path(); + + let config_content = r#"{ + "modelsDir": "models", + "migrationsDir": "migrations", + "tableNamingCase": "snake", + "columnNamingCase": "snake", + "modelFormat": "json" + }"#; + fs::write(project_dir.join("vespertide.json"), config_content).unwrap(); + + fs::create_dir_all(project_dir.join("models")).unwrap(); + fs::create_dir_all(project_dir.join("migrations")).unwrap(); + + fs::write( + project_dir.join("models").join("broken.json"), + r#"{ + "name": "broken", + "columns": [ + {"name": "user_id", "type": "integer", "nullable": false, "foreign_key": "invalid_format"} + ], + "constraints": [] + }"#, + ) + .unwrap(); + + fs::write( + project_dir.join("migrations").join("0001_initial.json"), + r#"{ + "version": 1, + "actions": [ + { + "type": "create_table", + "table": "users", + "columns": [ + {"name": "id", "type": "integer", "nullable": false} + ], + "constraints": [] + } + ] + }"#, + ) + .unwrap(); + + let original = std::env::var("CARGO_MANIFEST_DIR").ok(); + unsafe { + std::env::set_var("CARGO_MANIFEST_DIR", project_dir); + } + + let input: proc_macro2::TokenStream = "pool".parse().unwrap(); + let output = vespertide_migration_impl(input); + let output_str = output.to_string(); + + assert!( + output_str.contains("async"), + "Expected migration code generation to ignore invalid models, got: {}", + output_str + ); + + if let Some(val) = original { + unsafe { + std::env::set_var("CARGO_MANIFEST_DIR", val); + } + } else { + unsafe { + std::env::remove_var("CARGO_MANIFEST_DIR"); + } + } + } } diff --git a/crates/vespertide-schema-gen/Cargo.toml b/crates/vespertide-schema-gen/Cargo.toml index e747181e..f89ffa27 100644 --- a/crates/vespertide-schema-gen/Cargo.toml +++ b/crates/vespertide-schema-gen/Cargo.toml @@ -14,8 +14,8 @@ anyhow = "1" clap = { version = "4", features = ["derive"] } schemars = "1.2" serde_json = "1" -vespertide-core = { workspace = true } -vespertide-config = { workspace = true } +vespertide-core = { workspace = true, features = ["schema"] } +vespertide-config = { workspace = true, features = ["schema"] } [dev-dependencies] tempfile = "3" diff --git a/crates/vespertide/Cargo.toml b/crates/vespertide/Cargo.toml index ce75867e..13e65b53 100644 --- a/crates/vespertide/Cargo.toml +++ b/crates/vespertide/Cargo.toml @@ -11,3 +11,8 @@ description = "Rust workspace for defining database schemas in JSON and generati [dependencies] vespertide-macro = { workspace = true } vespertide-core = { workspace = true } +sea-orm = { version = "2.0.0-rc.37", default-features = false } + +[dev-dependencies] +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +sea-orm = { version = "2.0.0-rc.37", default-features = false, features = ["runtime-tokio-native-tls", "sqlx-sqlite"] } diff --git a/crates/vespertide/src/lib.rs b/crates/vespertide/src/lib.rs index 70001e74..a74e2c59 100644 --- a/crates/vespertide/src/lib.rs +++ b/crates/vespertide/src/lib.rs @@ -1,3 +1,5 @@ +pub mod runtime; + // Re-export macro for convenient usage #[doc(inline)] pub use vespertide_macro::vespertide_migration; diff --git a/crates/vespertide/src/runtime.rs b/crates/vespertide/src/runtime.rs new file mode 100644 index 00000000..a250493b --- /dev/null +++ b/crates/vespertide/src/runtime.rs @@ -0,0 +1,402 @@ +use sea_orm::{ConnectionTrait, DatabaseBackend, DatabaseConnection, Statement, TransactionTrait}; + +use crate::MigrationError; + +#[derive(Debug, Clone, Copy)] +pub struct EmbeddedMigration { + pub version: u32, + pub migration_id: &'static str, + pub comment: &'static str, + pub postgres_sql_blob: &'static str, + pub mysql_sql_blob: &'static str, + pub sqlite_sql_blob: &'static str, +} + +impl EmbeddedMigration { + pub const fn new( + version: u32, + migration_id: &'static str, + comment: &'static str, + postgres_sql_blob: &'static str, + mysql_sql_blob: &'static str, + sqlite_sql_blob: &'static str, + ) -> Self { + Self { + version, + migration_id, + comment, + postgres_sql_blob, + mysql_sql_blob, + sqlite_sql_blob, + } + } + + pub const fn sql_blob(self, backend: DatabaseBackend) -> &'static str { + if matches!(backend, DatabaseBackend::MySql) { + self.mysql_sql_blob + } else if matches!(backend, DatabaseBackend::Sqlite) { + self.sqlite_sql_blob + } else { + self.postgres_sql_blob + } + } +} + +pub fn split_sql_blob(blob: &str) -> impl Iterator { + blob.split_terminator('\0').filter(|sql| !sql.is_empty()) +} + +pub async fn run_embedded_migrations( + pool: &DatabaseConnection, + version_table: &str, + verbose: bool, + migrations: &[EmbeddedMigration], +) -> Result<(), MigrationError> { + let backend = pool.get_database_backend(); + let q = if matches!(backend, DatabaseBackend::MySql) { + '`' + } else { + '"' + }; + + let create_table_sql = format!( + "CREATE TABLE IF NOT EXISTS {q}{}{q} (version INTEGER PRIMARY KEY, id TEXT DEFAULT '', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)", + version_table + ); + let stmt = Statement::from_string(backend, create_table_sql); + pool.execute_raw(stmt).await.map_err(|e| { + MigrationError::DatabaseError(format!("Failed to create version table: {}", e)) + })?; + + let alter_sql = format!( + "ALTER TABLE {q}{}{q} ADD COLUMN id TEXT DEFAULT ''", + version_table + ); + let stmt = Statement::from_string(backend, alter_sql); + let _ = pool.execute_raw(stmt).await; + + let txn = pool.begin().await.map_err(|e| { + MigrationError::DatabaseError(format!("Failed to begin transaction: {}", e)) + })?; + + let select_sql = format!( + "SELECT MAX(version) as version FROM {q}{}{q}", + version_table + ); + let stmt = Statement::from_string(backend, select_sql); + let version_result = txn + .query_one_raw(stmt) + .await + .map_err(|e| MigrationError::DatabaseError(format!("Failed to read version: {}", e)))?; + let version = version_result + .and_then(|row| row.try_get::("", "version").ok()) + .unwrap_or(0) as u32; + + let select_ids_sql = format!("SELECT version, id FROM {q}{}{q}", version_table); + let stmt = Statement::from_string(backend, select_ids_sql); + let id_rows = txn + .query_all_raw(stmt) + .await + .map_err(|e| MigrationError::DatabaseError(format!("Failed to read version ids: {}", e)))?; + let mut version_ids = std::collections::HashMap::::new(); + for row in &id_rows { + if let Ok(found_version) = row.try_get::("", "version") { + let id = row.try_get::("", "id").unwrap_or_default(); + version_ids.insert(found_version as u32, id); + } + } + + if verbose { + eprintln!("[vespertide] Current database version: {}", version); + } + + for migration in migrations { + if version >= migration.version { + continue; + } + + if let Some(db_id) = version_ids.get(&migration.version) + && !migration.migration_id.is_empty() + && !db_id.is_empty() + && db_id != migration.migration_id + { + return Err(MigrationError::IdMismatch { + version: migration.version, + expected: migration.migration_id.to_string(), + found: db_id.clone(), + }); + } + + if verbose { + eprintln!( + "[vespertide] Applying migration v{} ({})", + migration.version, migration.comment + ); + } + + let sql_blob = migration.sql_blob(backend); + let sqls: Vec<_> = split_sql_blob(sql_blob).collect(); + + for (sql_idx, sql) in sqls.iter().enumerate() { + if verbose { + eprintln!("[vespertide] [{}/{}] {}", sql_idx + 1, sqls.len(), sql); + } + + let stmt = Statement::from_string(backend, (*sql).to_owned()); + txn.execute_raw(stmt).await.map_err(|e| { + MigrationError::DatabaseError(format!("Failed to execute SQL '{}': {}", sql, e)) + })?; + } + + let insert_sql = format!( + "INSERT INTO {q}{}{q} (version, id) VALUES ({}, '{}')", + version_table, migration.version, migration.migration_id + ); + let stmt = Statement::from_string(backend, insert_sql); + txn.execute_raw(stmt).await.map_err(|e| { + MigrationError::DatabaseError(format!("Failed to insert version: {}", e)) + })?; + + if verbose { + eprintln!( + "[vespertide] Migration v{} applied successfully", + migration.version + ); + } + } + + txn.commit().await.map_err(|e| { + MigrationError::DatabaseError(format!("Failed to commit transaction: {}", e)) + })?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use sea_orm::{ConnectionTrait, Database, DatabaseBackend, Statement}; + + use crate::MigrationError; + + use super::{EmbeddedMigration, run_embedded_migrations, split_sql_blob}; + + async fn sqlite_memory_db() -> sea_orm::DatabaseConnection { + Database::connect("sqlite::memory:").await.unwrap() + } + + async fn read_versions(db: &sea_orm::DatabaseConnection) -> Vec<(i32, String)> { + let stmt = Statement::from_string( + DatabaseBackend::Sqlite, + "SELECT version, id FROM \"vespertide_migrations\" ORDER BY version".to_owned(), + ); + let rows = db.query_all_raw(stmt).await.unwrap(); + rows.into_iter() + .map(|row| { + ( + row.try_get::("", "version").unwrap(), + row.try_get::("", "id").unwrap(), + ) + }) + .collect() + } + + #[test] + fn split_sql_blob_ignores_empty_segments() { + let sqls: Vec<_> = + split_sql_blob("CREATE TABLE users ();\0\0ALTER TABLE users;\0").collect(); + + assert_eq!(sqls, vec!["CREATE TABLE users ();", "ALTER TABLE users;"]); + } + + #[test] + fn embedded_migration_selects_backend_blob() { + let migration = EmbeddedMigration::new(1, "id", "comment", "pg\0", "mysql\0", "sqlite\0"); + + assert_eq!(migration.sql_blob(DatabaseBackend::Postgres), "pg\0"); + assert_eq!(migration.sql_blob(DatabaseBackend::MySql), "mysql\0"); + assert_eq!(migration.sql_blob(DatabaseBackend::Sqlite), "sqlite\0"); + } + + #[tokio::test] + async fn run_embedded_migrations_applies_pending_versions_and_records_ids() { + let db = sqlite_memory_db().await; + let migrations = [ + EmbeddedMigration::new( + 1, + "init", + "create users", + "CREATE TABLE users (id INTEGER PRIMARY KEY);\0", + "CREATE TABLE users (id INTEGER PRIMARY KEY);\0", + "CREATE TABLE users (id INTEGER PRIMARY KEY);\0", + ), + EmbeddedMigration::new( + 2, + "add_name", + "add name column", + "ALTER TABLE users ADD COLUMN name TEXT;\0", + "ALTER TABLE users ADD COLUMN name TEXT;\0", + "ALTER TABLE users ADD COLUMN name TEXT;\0", + ), + ]; + + run_embedded_migrations(&db, "vespertide_migrations", true, &migrations) + .await + .unwrap(); + + let versions = read_versions(&db).await; + assert_eq!( + versions, + vec![(1, "init".to_string()), (2, "add_name".to_string())] + ); + + let stmt = Statement::from_string( + DatabaseBackend::Sqlite, + "PRAGMA table_info('users')".to_owned(), + ); + let rows = db.query_all_raw(stmt).await.unwrap(); + let names: Vec<_> = rows + .into_iter() + .map(|row| row.try_get::("", "name").unwrap()) + .collect(); + assert_eq!(names, vec!["id".to_string(), "name".to_string()]); + } + + #[tokio::test] + async fn run_embedded_migrations_skips_versions_that_are_already_applied() { + let db = sqlite_memory_db().await; + run_embedded_migrations( + &db, + "vespertide_migrations", + false, + &[EmbeddedMigration::new( + 1, + "init", + "create users", + "CREATE TABLE users (id INTEGER PRIMARY KEY);\0", + "CREATE TABLE users (id INTEGER PRIMARY KEY);\0", + "CREATE TABLE users (id INTEGER PRIMARY KEY);\0", + )], + ) + .await + .unwrap(); + + run_embedded_migrations( + &db, + "vespertide_migrations", + true, + &[ + EmbeddedMigration::new( + 1, + "init", + "should skip existing", + "ALTER TABLE users ADD COLUMN skipped TEXT;\0", + "ALTER TABLE users ADD COLUMN skipped TEXT;\0", + "ALTER TABLE users ADD COLUMN skipped TEXT;\0", + ), + EmbeddedMigration::new( + 2, + "add_name", + "apply only new version", + "ALTER TABLE users ADD COLUMN name TEXT;\0", + "ALTER TABLE users ADD COLUMN name TEXT;\0", + "ALTER TABLE users ADD COLUMN name TEXT;\0", + ), + ], + ) + .await + .unwrap(); + + let versions = read_versions(&db).await; + assert_eq!( + versions, + vec![(1, "init".to_string()), (2, "add_name".to_string())] + ); + + let stmt = Statement::from_string( + DatabaseBackend::Sqlite, + "PRAGMA table_info('users')".to_owned(), + ); + let rows = db.query_all_raw(stmt).await.unwrap(); + let names: Vec<_> = rows + .into_iter() + .map(|row| row.try_get::("", "name").unwrap()) + .collect(); + assert!(!names.iter().any(|name| name == "skipped")); + assert!(names.iter().any(|name| name == "name")); + } + + #[tokio::test] + async fn run_embedded_migrations_surfaces_sql_errors() { + let db = sqlite_memory_db().await; + let stmt = Statement::from_string(DatabaseBackend::Sqlite, "CREATE TABLE \"vespertide_migrations\" (version INTEGER PRIMARY KEY, id TEXT DEFAULT '', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)".to_owned()); + db.execute_raw(stmt).await.unwrap(); + let stmt = Statement::from_string( + DatabaseBackend::Sqlite, + "INSERT INTO \"vespertide_migrations\" (version, id) VALUES (2, 'different')" + .to_owned(), + ); + db.execute_raw(stmt).await.unwrap(); + + let result = run_embedded_migrations( + &db, + "vespertide_migrations", + true, + &[EmbeddedMigration::new( + 3, + "broken", + "invalid sql", + "THIS IS NOT SQL;\0", + "THIS IS NOT SQL;\0", + "THIS IS NOT SQL;\0", + )], + ) + .await; + + assert!( + matches!(result, Err(MigrationError::DatabaseError(message)) if message.contains("Failed to execute SQL 'THIS IS NOT SQL;'")) + ); + } + + #[tokio::test] + async fn run_embedded_migrations_detects_existing_version_id_mismatch() { + let db = sqlite_memory_db().await; + let stmt = Statement::from_string(DatabaseBackend::Sqlite, "CREATE TABLE \"vespertide_migrations\" (version INTEGER PRIMARY KEY, id TEXT DEFAULT '', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)".to_owned()); + db.execute_raw(stmt).await.unwrap(); + let stmt = Statement::from_string( + DatabaseBackend::Sqlite, + "INSERT INTO \"vespertide_migrations\" (version, id) VALUES (2, 'different')" + .to_owned(), + ); + db.execute_raw(stmt).await.unwrap(); + let stmt = Statement::from_string( + DatabaseBackend::Sqlite, + "INSERT INTO \"vespertide_migrations\" (version, id) VALUES (2147483648, 'overflow')" + .to_owned(), + ); + db.execute_raw(stmt).await.unwrap(); + + let result = run_embedded_migrations( + &db, + "vespertide_migrations", + true, + &[EmbeddedMigration::new( + 2, + "expected", + "mismatch", + "ALTER TABLE users ADD COLUMN name TEXT;\0", + "ALTER TABLE users ADD COLUMN name TEXT;\0", + "ALTER TABLE users ADD COLUMN name TEXT;\0", + )], + ) + .await; + + assert!(matches!( + result, + Err(MigrationError::IdMismatch { + version: 2, + expected, + found, + }) if expected == "expected" && found == "different" + )); + } +} diff --git a/examples/app/Cargo.toml b/examples/app/Cargo.toml index ca799f90..3bf60141 100644 --- a/examples/app/Cargo.toml +++ b/examples/app/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] vespertide = { path = "../../crates/vespertide" } tokio = { version = "1", features = ["full"] } -sea-orm = { version = "2.0.0-rc.36", features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-native-tls", "macros"] } +sea-orm = { version = "2.0.0-rc.37", features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-native-tls", "macros"] } anyhow = "1" serde = { version = "1", features = ["derive"] } -vespera = "0.1.40" +vespera = "0.1.43"