diff --git a/Cargo.lock b/Cargo.lock index a1add0b4ea..6e195dbe4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,6 +140,17 @@ dependencies = [ "getrandom 0.2.7", ] +[[package]] +name = "bigdecimal" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aaf33151a6429fe9211d1b276eafdf70cdff28b071e76c0b0e1503221ea3744" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bincode" version = "1.3.3" @@ -149,6 +160,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", +] + [[package]] name = "bindle" version = "0.8.0" @@ -197,6 +227,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -226,6 +268,51 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive", + "hashbrown 0.11.2", +] + +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate", + "proc-macro2", + "syn", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bstr" version = "0.2.17" @@ -244,6 +331,27 @@ version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +[[package]] +name = "bytecheck" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d11cac2c12b5adc6570dad2ee1b87eff4955dac476fe12d81e5fdd352e52406f" +dependencies = [ + "bytecheck_derive", + "ptr_meta", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e576ebe98e605500b3c8041bb888e966653577172df6dd97398714eb30b9bf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "byteorder" version = "1.4.3" @@ -344,6 +452,15 @@ dependencies = [ "jobserver", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -375,6 +492,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "clang-sys" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "2.34.0" @@ -456,6 +584,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "cmake" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" +dependencies = [ + "cc", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -604,7 +741,7 @@ checksum = "0a6dccc0b16b7b8c1278162e436beebb35f3d321743b639d2b578138d630f43e" dependencies = [ "cranelift-entity", "fxhash", - "hashbrown", + "hashbrown 0.12.3", "indexmap", "log", "smallvec", @@ -711,6 +848,20 @@ dependencies = [ "itertools", ] +[[package]] +name = "crossbeam" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" +dependencies = [ + "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" version = "0.5.6" @@ -745,6 +896,16 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.12" @@ -1200,6 +1361,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ "crc32fast", + "libz-sys", "miniz_oxide", ] @@ -1233,6 +1395,70 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "frunk" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89c703bf50009f383a0873845357cc400a95fc535f836feddfe015d7df6e1e0" +dependencies = [ + "frunk_core", + "frunk_derives", + "frunk_proc_macros", +] + +[[package]] +name = "frunk_core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a446d01a558301dca28ef43222864a9fa2bd9a2e71370f769d5d5d5ec9f3537" + +[[package]] +name = "frunk_derives" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b83164912bb4c97cfe0772913c7af7387ee2e00cb6d4636fb65a35b3d0c8f173" +dependencies = [ + "frunk_proc_macro_helpers", + "quote", + "syn", +] + +[[package]] +name = "frunk_proc_macro_helpers" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "015425591bbeb0f5b8a75593340f1789af428e9f887a4f1e36c0c471f067ef50" +dependencies = [ + "frunk_core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "frunk_proc_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea01524f285deab48affffb342b97f186e657b119c3f1821ac531780e0fbfae0" +dependencies = [ + "frunk_core", + "frunk_proc_macros_impl", + "proc-macro-hack", +] + +[[package]] +name = "frunk_proc_macros_impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a802d974cc18ee7fe1a7868fc9ce31086294fd96ba62f8da64ecb44e92a2653" +dependencies = [ + "frunk_core", + "frunk_proc_macro_helpers", + "proc-macro-hack", + "quote", + "syn", +] + [[package]] name = "fs-set-times" version = "0.17.1" @@ -1260,6 +1486,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.24" @@ -1459,6 +1691,15 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1697,7 +1938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", "serde", ] @@ -1836,12 +2077,91 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "leb128" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +[[package]] +name = "lexical" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7aefb36fd43fef7003334742cbf77b243fcd36418a1d1bdd480d613a67968f6" +dependencies = [ + "lexical-core", +] + +[[package]] +name = "lexical-core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" +dependencies = [ + "lexical-util", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.135" @@ -1860,6 +2180,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libz-sys" version = "1.1.8" @@ -1970,7 +2300,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" dependencies = [ - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -2046,6 +2376,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.5.4" @@ -2067,6 +2403,75 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mysql_async" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456207bb9636a0fdade67a64cea7bdebe6730c3c16ee5e34f2c481838ee5a39e" +dependencies = [ + "bytes", + "crossbeam", + "flate2", + "futures-core", + "futures-sink", + "futures-util", + "lazy_static", + "lru", + "mio", + "mysql_common", + "native-tls", + "once_cell", + "pem", + "percent-encoding", + "pin-project", + "serde", + "serde_json", + "socket2", + "thiserror", + "tokio", + "tokio-native-tls", + "tokio-util 0.7.4", + "twox-hash", + "url", +] + +[[package]] +name = "mysql_common" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "522f2f30f72de409fc04f88df25a031f98cfc5c398a94e0b892cabb33a1464cb" +dependencies = [ + "base64", + "bigdecimal", + "bindgen", + "bitflags", + "bitvec", + "byteorder", + "bytes", + "cc", + "cmake", + "crc32fast", + "flate2", + "frunk", + "lazy_static", + "lexical", + "num-bigint", + "num-traits", + "rand 0.8.5", + "regex", + "rust_decimal", + "saturating", + "serde", + "serde_json", + "sha-1", + "sha2 0.10.6", + "smallvec", + "subprocess", + "thiserror", + "time 0.3.15", + "uuid", +] + [[package]] name = "native-tls" version = "0.2.10" @@ -2109,6 +2514,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2195,7 +2610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" dependencies = [ "crc32fast", - "hashbrown", + "hashbrown 0.12.3", "indexmap", "memchr", ] @@ -2306,6 +2721,19 @@ dependencies = [ "wit-bindgen-wasmtime", ] +[[package]] +name = "outbound-mysql" +version = "0.6.0" +dependencies = [ + "anyhow", + "mysql_async", + "mysql_common", + "spin-core", + "tokio", + "tracing", + "wit-bindgen-wasmtime", +] + [[package]] name = "outbound-pg" version = "0.6.0" @@ -2414,6 +2842,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pem" version = "1.1.0" @@ -2592,6 +3026,15 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2664,6 +3107,26 @@ dependencies = [ "cc", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pulldown-cmark" version = "0.8.0" @@ -2684,6 +3147,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -2866,6 +3335,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "rend" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79af64b4b6362ffba04eef3a4e10829718a4896dac19daa741851c86781edf95" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.12" @@ -2925,6 +3403,31 @@ dependencies = [ "winapi", ] +[[package]] +name = "rkyv" +version = "0.7.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cec2b3485b07d96ddfd3134767b8a447b45ea4eb91448d0a35180ec0ffd5ed15" +dependencies = [ + "bytecheck", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eaedadc88b53e36dd32d940ed21ae4d850d5916f2581526921f553a72ac34c4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "rpassword" version = "7.0.0" @@ -2935,12 +3438,36 @@ dependencies = [ "winapi", ] +[[package]] +name = "rust_decimal" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c321ee4e17d2b7abe12b5d20c1231db708dd36185c8a21e9de5fed6da4dbe9" +dependencies = [ + "arrayvec", + "borsh", + "bytecheck", + "byteorder", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustify" version = "0.5.3" @@ -3052,6 +3579,12 @@ dependencies = [ "regex", ] +[[package]] +name = "saturating" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" + [[package]] name = "schannel" version = "0.1.20" @@ -3084,6 +3617,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.7.0" @@ -3197,6 +3736,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.5", +] + [[package]] name = "sha1" version = "0.6.1" @@ -3265,6 +3815,12 @@ dependencies = [ "dirs 4.0.0", ] +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "signal-hook" version = "0.3.14" @@ -3731,6 +4287,7 @@ dependencies = [ "dirs 4.0.0", "futures", "outbound-http", + "outbound-mysql", "outbound-pg", "outbound-redis", "sanitize-filename", @@ -3755,6 +4312,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.2" @@ -3845,6 +4408,12 @@ dependencies = [ "winx", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.38" @@ -4232,6 +4801,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "rand 0.8.5", + "static_assertions", +] + [[package]] name = "typenum" version = "1.15.0" @@ -5112,6 +5692,15 @@ dependencies = [ "wast 35.0.2", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xattr" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 35561812e5..8d128af04d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,7 @@ e2e-tests = [] outbound-redis-tests = [] config-provider-tests = [] outbound-pg-tests = [] +outbound-mysql-tests = [] [workspace] members = [ diff --git a/Makefile b/Makefile index f434c61fd7..703a971719 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,10 @@ test-config-provider: test-outbound-pg: RUST_LOG=$(LOG_LEVEL) cargo test --test integration --features outbound-pg-tests --no-fail-fast -- --nocapture +.PHONY: test-outbound-mysql +test-outbound-mysql: + RUST_LOG=$(LOG_LEVEL) cargo test --test integration --features outbound-mysql-tests --no-fail-fast -- --nocapture + .PHONY: test-sdk-go test-sdk-go: $(MAKE) -C sdk/go test diff --git a/crates/outbound-mysql/Cargo.toml b/crates/outbound-mysql/Cargo.toml new file mode 100644 index 0000000000..4ea40cfeb2 --- /dev/null +++ b/crates/outbound-mysql/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "outbound-mysql" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[lib] +doctest = false + +[dependencies] +anyhow = "1.0" +mysql_async = "0.30.0" +mysql_common = "0.29.1" +spin-core = { path = "../core" } +tokio = { version = "1", features = [ "rt-multi-thread" ] } +tracing = { version = "0.1", features = [ "log" ] } +wit-bindgen-wasmtime = { workspace = true } diff --git a/crates/outbound-mysql/src/lib.rs b/crates/outbound-mysql/src/lib.rs new file mode 100644 index 0000000000..a139e9e544 --- /dev/null +++ b/crates/outbound-mysql/src/lib.rs @@ -0,0 +1,254 @@ +use mysql_async::consts::ColumnType; +use mysql_async::{from_value_opt, prelude::*}; +pub use outbound_mysql::add_to_linker; +use spin_core::HostComponent; +use std::collections::HashMap; +use std::sync::Arc; +use wit_bindgen_wasmtime::async_trait; + +wit_bindgen_wasmtime::export!({paths: ["../../wit/ephemeral/outbound-mysql.wit"], async: *}); +use outbound_mysql::*; + +/// A simple implementation to support outbound mysql connection +#[derive(Default)] +pub struct OutboundMysql { + pub connections: HashMap, +} + +impl HostComponent for OutboundMysql { + type Data = Self; + + fn add_to_linker( + linker: &mut spin_core::Linker, + get: impl Fn(&mut spin_core::Data) -> &mut Self::Data + Send + Sync + Copy + 'static, + ) -> anyhow::Result<()> { + outbound_mysql::add_to_linker(linker, get) + } + + fn build_data(&self) -> Self::Data { + Default::default() + } +} + +#[async_trait] +impl outbound_mysql::OutboundMysql for OutboundMysql { + async fn execute( + &mut self, + address: &str, + statement: &str, + params: Vec>, + ) -> Result<(), MysqlError> { + let db_params = params + .iter() + .map(to_sql_parameter) + .collect::>>() + .map_err(|e| MysqlError::QueryFailed(format!("{:?}", e)))?; + + let parameters = mysql_async::Params::Positional(db_params); + + self.get_conn(address) + .await + .map_err(|e| MysqlError::ConnectionFailed(format!("{:?}", e)))? + .exec_batch(statement, &[parameters]) + .await + .map_err(|e| MysqlError::QueryFailed(format!("{:?}", e)))?; + + Ok(()) + } + + async fn query( + &mut self, + address: &str, + statement: &str, + params: Vec>, + ) -> Result { + let db_params = params + .iter() + .map(to_sql_parameter) + .collect::>>() + .map_err(|e| MysqlError::QueryFailed(format!("{:?}", e)))?; + + let parameters = mysql_async::Params::Positional(db_params); + + let mut query_result = self + .get_conn(address) + .await + .map_err(|e| MysqlError::ConnectionFailed(format!("{:?}", e)))? + .exec_iter(statement, parameters) + .await + .map_err(|e| MysqlError::QueryFailed(format!("{:?}", e)))?; + + // We have to get these before collect() destroys them + let columns = convert_columns(query_result.columns()); + + match query_result.collect::().await { + Err(e) => Err(MysqlError::OtherError(format!("{:?}", e))), + Ok(result_set) => { + let rows = result_set + .into_iter() + .map(|row| convert_row(row, &columns)) + .collect::, _>>() + .map_err(|e| MysqlError::QueryFailed(format!("{:?}", e)))?; + + Ok(RowSet { columns, rows }) + } + } + } +} + +fn to_sql_parameter(value: &ParameterValue) -> anyhow::Result { + match value { + ParameterValue::Boolean(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::Int32(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::Int64(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::Int8(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::Int16(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::Floating32(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::Floating64(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::Uint8(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::Uint16(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::Uint32(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::Uint64(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::Str(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::Binary(v) => Ok(mysql_async::Value::from(v)), + ParameterValue::DbNull => Ok(mysql_async::Value::NULL), + } +} + +fn convert_columns(columns: Option>) -> Vec { + match columns { + Some(columns) => columns.iter().map(convert_column).collect(), + None => vec![], + } +} + +fn convert_column(column: &mysql_async::Column) -> Column { + let name = column.name_str().to_string(); + let data_type = convert_data_type(column); + + Column { name, data_type } +} + +fn convert_data_type(column: &mysql_async::Column) -> DbDataType { + let column_type = column.column_type(); + + if column_type.is_numeric_type() { + convert_numeric_type(column) + } else if column_type.is_character_type() { + convert_character_type(column) + } else { + DbDataType::Other + } +} + +fn convert_character_type(column: &mysql_async::Column) -> DbDataType { + match (column.column_type(), is_binary(column)) { + (ColumnType::MYSQL_TYPE_BLOB, false) => DbDataType::Str, // TEXT type + (ColumnType::MYSQL_TYPE_BLOB, _) => DbDataType::Binary, + (ColumnType::MYSQL_TYPE_LONG_BLOB, _) => DbDataType::Binary, + (ColumnType::MYSQL_TYPE_MEDIUM_BLOB, _) => DbDataType::Binary, + (ColumnType::MYSQL_TYPE_STRING, true) => DbDataType::Binary, // BINARY type + (ColumnType::MYSQL_TYPE_STRING, _) => DbDataType::Str, + (ColumnType::MYSQL_TYPE_VAR_STRING, true) => DbDataType::Binary, // VARBINARY type + (ColumnType::MYSQL_TYPE_VAR_STRING, _) => DbDataType::Str, + (_, _) => DbDataType::Other, + } +} + +fn convert_numeric_type(column: &mysql_async::Column) -> DbDataType { + match (column.column_type(), is_signed(column)) { + (ColumnType::MYSQL_TYPE_DOUBLE, _) => DbDataType::Floating64, + (ColumnType::MYSQL_TYPE_FLOAT, _) => DbDataType::Floating32, + (ColumnType::MYSQL_TYPE_INT24, true) => DbDataType::Int32, + (ColumnType::MYSQL_TYPE_INT24, false) => DbDataType::Uint32, + (ColumnType::MYSQL_TYPE_LONG, true) => DbDataType::Int32, + (ColumnType::MYSQL_TYPE_LONG, false) => DbDataType::Uint32, + (ColumnType::MYSQL_TYPE_LONGLONG, true) => DbDataType::Int64, + (ColumnType::MYSQL_TYPE_LONGLONG, false) => DbDataType::Uint64, + (ColumnType::MYSQL_TYPE_SHORT, true) => DbDataType::Int16, + (ColumnType::MYSQL_TYPE_SHORT, false) => DbDataType::Uint16, + (ColumnType::MYSQL_TYPE_TINY, true) => DbDataType::Int8, + (ColumnType::MYSQL_TYPE_TINY, false) => DbDataType::Uint8, + (_, _) => DbDataType::Other, + } +} + +fn is_signed(column: &mysql_async::Column) -> bool { + !column + .flags() + .contains(mysql_async::consts::ColumnFlags::UNSIGNED_FLAG) +} + +fn is_binary(column: &mysql_async::Column) -> bool { + column + .flags() + .contains(mysql_async::consts::ColumnFlags::BINARY_FLAG) +} + +fn convert_row(mut row: mysql_async::Row, columns: &[Column]) -> Result, MysqlError> { + let mut result = Vec::with_capacity(row.len()); + for index in 0..row.len() { + result.push(convert_entry(&mut row, index, columns)?); + } + Ok(result) +} + +fn convert_entry( + row: &mut mysql_async::Row, + index: usize, + columns: &[Column], +) -> Result { + match (row.take(index), columns.get(index)) { + (None, _) => Ok(DbValue::DbNull), // TODO: is this right or is this an "index out of range" thing + (_, None) => Err(MysqlError::OtherError(format!( + "Can't get column at index {}", + index + ))), + (Some(mysql_async::Value::NULL), _) => Ok(DbValue::DbNull), + (Some(value), Some(column)) => convert_value(value, column), + } +} + +fn convert_value(value: mysql_async::Value, column: &Column) -> Result { + match column.data_type { + DbDataType::Binary => convert_value_to::>(value).map(DbValue::Binary), + DbDataType::Boolean => convert_value_to::(value).map(DbValue::Boolean), + DbDataType::Floating32 => convert_value_to::(value).map(DbValue::Floating32), + DbDataType::Floating64 => convert_value_to::(value).map(DbValue::Floating64), + DbDataType::Int8 => convert_value_to::(value).map(DbValue::Int8), + DbDataType::Int16 => convert_value_to::(value).map(DbValue::Int16), + DbDataType::Int32 => convert_value_to::(value).map(DbValue::Int32), + DbDataType::Int64 => convert_value_to::(value).map(DbValue::Int64), + DbDataType::Str => convert_value_to::(value).map(DbValue::Str), + DbDataType::Uint8 => convert_value_to::(value).map(DbValue::Uint8), + DbDataType::Uint16 => convert_value_to::(value).map(DbValue::Uint16), + DbDataType::Uint32 => convert_value_to::(value).map(DbValue::Uint32), + DbDataType::Uint64 => convert_value_to::(value).map(DbValue::Uint64), + DbDataType::Other => Err(MysqlError::ValueConversionFailed(format!( + "Cannot convert value {:?} in column {} data type {:?}", + value, column.name, column.data_type + ))), + } +} + +impl OutboundMysql { + async fn get_conn(&mut self, address: &str) -> anyhow::Result<&mut mysql_async::Conn> { + let client = match self.connections.entry(address.to_owned()) { + std::collections::hash_map::Entry::Occupied(o) => o.into_mut(), + std::collections::hash_map::Entry::Vacant(v) => v.insert(build_conn(address).await?), + }; + Ok(client) + } +} + +async fn build_conn(address: &str) -> Result { + tracing::log::debug!("Build new connection: {}", address); + + let connection_pool = mysql_async::Pool::new(address); + + connection_pool.get_conn().await +} + +fn convert_value_to(value: mysql_async::Value) -> Result { + from_value_opt::(value).map_err(|e| MysqlError::ValueConversionFailed(format!("{}", e))) +} diff --git a/crates/trigger/Cargo.toml b/crates/trigger/Cargo.toml index 0b29002ddb..8b7a101314 100644 --- a/crates/trigger/Cargo.toml +++ b/crates/trigger/Cargo.toml @@ -11,9 +11,10 @@ clap = { version = "3.1.15", features = ["derive", "env"] } ctrlc = { version = "3.2", features = ["termination"] } dirs = "4" futures = "0.3" -outbound-http = { path = "../outbound-http" } +outbound-http = { path = "../outbound-http" } outbound-redis = { path = "../outbound-redis" } outbound-pg = { path = "../outbound-pg" } +outbound-mysql = { path = "../outbound-mysql" } sanitize-filename = "0.4" serde = "1.0" serde_json = "1.0" diff --git a/crates/trigger/src/lib.rs b/crates/trigger/src/lib.rs index 896766794f..3a89705fe7 100644 --- a/crates/trigger/src/lib.rs +++ b/crates/trigger/src/lib.rs @@ -94,6 +94,7 @@ impl TriggerExecutorBuilder { if !self.disable_default_host_components { builder.add_host_component(outbound_redis::OutboundRedisComponent)?; builder.add_host_component(outbound_pg::OutboundPg::default())?; + builder.add_host_component(outbound_mysql::OutboundMysql::default())?; self.loader.add_dynamic_host_component( &mut builder, outbound_http::OutboundHttpComponent, diff --git a/examples/rust-outbound-mysql/.cargo/config.toml b/examples/rust-outbound-mysql/.cargo/config.toml new file mode 100644 index 0000000000..6b77899cb3 --- /dev/null +++ b/examples/rust-outbound-mysql/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/examples/rust-outbound-mysql/.gitignore b/examples/rust-outbound-mysql/.gitignore new file mode 100644 index 0000000000..2f7896d1d1 --- /dev/null +++ b/examples/rust-outbound-mysql/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/examples/rust-outbound-mysql/Cargo.toml b/examples/rust-outbound-mysql/Cargo.toml new file mode 100644 index 0000000000..4620fd2102 --- /dev/null +++ b/examples/rust-outbound-mysql/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "rust-outbound-mysql" +authors = ["itowlson "] +description = "Demo of calling MySQL from a Spin application" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = [ "cdylib" ] + +[dependencies] +# Useful crate to handle errors. +anyhow = "1" +# Crate to simplify working with bytes. +bytes = "1" +# General-purpose crate with common HTTP types. +http = "0.2" +serde = "1.0.144" +serde_json = "1.0.85" +# The Spin SDK. +spin-sdk = { path = "../../sdk/rust" } +# Crate that generates Rust Wasm bindings from a WebAssembly interface. +wit-bindgen-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "cb871cfa1ee460b51eb1d144b175b9aab9c50aba" } + +[workspace] diff --git a/examples/rust-outbound-mysql/db/pets.sql b/examples/rust-outbound-mysql/db/pets.sql new file mode 100644 index 0000000000..bbf91d68ad --- /dev/null +++ b/examples/rust-outbound-mysql/db/pets.sql @@ -0,0 +1,4 @@ +CREATE TABLE pets (id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, prey VARCHAR(100), is_finicky BOOL NOT NULL); +INSERT INTO pets VALUES (1, 'Splodge', NULL, false); +INSERT INTO pets VALUES (2, 'Kiki', 'Cicadas', false); +INSERT INTO pets VALUES (3, 'Slats', 'Temptations', true); diff --git a/examples/rust-outbound-mysql/spin.toml b/examples/rust-outbound-mysql/spin.toml new file mode 100644 index 0000000000..86f2efc10f --- /dev/null +++ b/examples/rust-outbound-mysql/spin.toml @@ -0,0 +1,15 @@ +spin_version = "1" +authors = ["itowlson "] +description = "Demo of calling MySQL from a Spin application" +name = "rust-outbound-mysql" +trigger = { type = "http", base = "/" } +version = "0.1.0" + +[[component]] +environment = { DB_URL = "mysql://spin:spin@127.0.0.1/spin_dev" } +id = "rust-outbound-mysql" +source = "target/wasm32-wasi/release/rust_outbound_mysql.wasm" +[component.trigger] +route = "/..." +[component.build] +command = "cargo build --target wasm32-wasi --release" diff --git a/examples/rust-outbound-mysql/src/lib.rs b/examples/rust-outbound-mysql/src/lib.rs new file mode 100644 index 0000000000..2e735aa493 --- /dev/null +++ b/examples/rust-outbound-mysql/src/lib.rs @@ -0,0 +1,204 @@ +use anyhow::{anyhow, Result}; +use http::{HeaderValue, Method}; +use spin_sdk::{ + http::{Request, Response}, + http_component, + mysql::{self, ParameterValue}, +}; +use std::{collections::HashMap, str::FromStr}; + +use crate::model::as_pet; + +mod model; + +// The environment variable set in `spin.toml` that points to the +// address of the MySQL server that the component will write to +const DB_URL_ENV: &str = "DB_URL"; + +enum RequestAction { + List, + Get(i32), + Create(String, Option, bool), + Error(u16), +} + +#[http_component] +fn rust_outbound_mysql(req: Request) -> Result { + match parse_request(req) { + RequestAction::List => list(), + RequestAction::Get(id) => get(id), + RequestAction::Create(name, prey, is_finicky) => create(&name, &prey, is_finicky), + RequestAction::Error(status) => error(status), + } +} + +fn parse_request(req: Request) -> RequestAction { + match *req.method() { + Method::GET => match req.headers().get("spin-path-info") { + None => RequestAction::Error(500), + Some(header_val) => match header_val_to_int(header_val) { + Ok(None) => RequestAction::List, + Ok(Some(id)) => RequestAction::Get(id), + Err(()) => RequestAction::Error(404), + }, + }, + Method::POST => { + match body_json_to_map(&req) { + Ok(map) => { + let name = match map.get("name") { + Some(n) => n.to_owned(), + None => return RequestAction::Error(400), // If this were a real app it would have error messages + }; + let prey = map.get("prey").cloned(); + let is_finicky = map + .get("is_finicky") + .map(|s| s == "true") + .unwrap_or_default(); + RequestAction::Create(name, prey, is_finicky) + } + Err(_) => RequestAction::Error(400), // Sorry no this isn't helpful either + } + } + _ => RequestAction::Error(405), + } +} + +fn header_val_to_int(header_val: &HeaderValue) -> Result, ()> { + match header_val.to_str() { + Ok(path) => { + let path_parts = &(path.split('/').skip(1).collect::>()[..]); + match *path_parts { + [""] => Ok(None), + [id_str] => match i32::from_str(id_str) { + Ok(id) => Ok(Some(id)), + Err(_) => Err(()), + }, + _ => Err(()), + } + } + Err(_) => Err(()), + } +} + +fn body_json_to_map(req: &Request) -> Result> { + // TODO: easier way? + let body = match req.body().as_ref() { + Some(bytes) => bytes.slice(..), + None => bytes::Bytes::default(), + }; + Ok(serde_json::from_slice::>(&body)?) +} + +fn list() -> Result { + let address = std::env::var(DB_URL_ENV)?; + + let sql = "SELECT id, name, prey, is_finicky FROM pets"; + let rowset = mysql::query(&address, sql, &[])?; + + let column_summary = rowset + .columns + .iter() + .map(format_col) + .collect::>() + .join(", "); + + let mut response_lines = vec![]; + + for row in rowset.rows { + let pet = as_pet(&row); + println!("{:#?}", pet); + response_lines.push(format!("{:#?}", pet)); + } + + let response = format!( + "Found {} pet(s) as follows:\n{}\n\n(Column info: {})\n", + response_lines.len(), + response_lines.join("\n"), + column_summary, + ); + + Ok(http::Response::builder() + .status(200) + .body(Some(response.into()))?) +} + +fn get(id: i32) -> Result { + let address = std::env::var(DB_URL_ENV)?; + + let sql = "SELECT id, name, prey, is_finicky FROM pets WHERE id = ?"; + let params = vec![ParameterValue::Int32(id)]; + let rowset = mysql::query(&address, sql, ¶ms)?; + + match rowset.rows.first() { + None => Ok(http::Response::builder().status(404).body(None)?), + Some(row) => { + let pet = as_pet(row)?; + let response = format!("{:?}", pet); + Ok(http::Response::builder() + .status(200) + .body(Some(response.into()))?) + } + } +} + +fn create(name: &str, prey: &Option, is_finicky: bool) -> Result { + let address = std::env::var(DB_URL_ENV)?; + + let id = max_pet_id(&address)? + 1; + + let prey_param = match prey { + None => ParameterValue::DbNull, + Some(str) => ParameterValue::Str(str), + }; + + let is_finicky_param = ParameterValue::Int8(to_i8_bool(is_finicky)); + + let sql = "INSERT INTO pets (id, name, prey, is_finicky) VALUES (?, ?, ?, ?)"; + let params = vec![ + ParameterValue::Int32(id), + ParameterValue::Str(name), + prey_param, + is_finicky_param, + ]; + mysql::execute(&address, sql, ¶ms)?; + + let location_url = format!("/{}", id); + + Ok(http::Response::builder() + .status(201) + .header("Location", location_url) + .body(None)?) +} + +fn error(status: u16) -> Result { + Ok(http::Response::builder().status(status).body(None)?) +} + +fn format_col(column: &mysql::Column) -> String { + format!("{}: {:?}", column.name, column.data_type) +} + +fn max_pet_id(address: &str) -> Result { + let sql = "SELECT MAX(id) FROM pets"; + let rowset = mysql::query(address, sql, &[])?; + + match rowset.rows.first() { + None => Ok(0), + Some(row) => match row.first() { + None => Ok(0), + Some(mysql::DbValue::Int32(i)) => Ok(*i), + Some(other) => Err(anyhow!( + "Unexpected non-integer ID {:?}, can't insert", + other + )), + }, + } +} + +fn to_i8_bool(value: bool) -> i8 { + if value { + 1 + } else { + 0 + } +} diff --git a/examples/rust-outbound-mysql/src/model.rs b/examples/rust-outbound-mysql/src/model.rs new file mode 100644 index 0000000000..18dcea0ccb --- /dev/null +++ b/examples/rust-outbound-mysql/src/model.rs @@ -0,0 +1,27 @@ +use anyhow::Result; +use spin_sdk::mysql::{self, Decode}; + +// Such logic, very business + +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub(crate) struct Pet { + id: i32, + name: String, + prey: Option, + is_finicky: bool, +} + +pub(crate) fn as_pet(row: &mysql::Row) -> Result { + let id = i32::decode(&row[0])?; + let name = String::decode(&row[1])?; + let prey = Option::::decode(&row[2])?; + let is_finicky = bool::decode(&row[3])?; + + Ok(Pet { + id, + name, + prey, + is_finicky, + }) +} diff --git a/sdk/rust/src/lib.rs b/sdk/rust/src/lib.rs index 7b46adb5ff..aa995a5175 100644 --- a/sdk/rust/src/lib.rs +++ b/sdk/rust/src/lib.rs @@ -49,6 +49,9 @@ pub mod redis { /// Implementation of the spin postgres db interface. pub mod pg; +/// Implementation of the Spin MySQL database interface. +pub mod mysql; + /// Implementation of the spin config interface. #[allow(missing_docs)] pub mod config { diff --git a/sdk/rust/src/mysql.rs b/sdk/rust/src/mysql.rs new file mode 100644 index 0000000000..965df5fc67 --- /dev/null +++ b/sdk/rust/src/mysql.rs @@ -0,0 +1,296 @@ +//! Conversions between Rust, WIT and **MySQL** types. +//! +//! # Types +//! +//! | Rust type | WIT (db-value) | MySQL type(s) | +//! |-----------|---------------------|-------------------------| +//! | `bool` | int8(s8) | TINYINT(1), BOOLEAN | +//! | `i8` | int8(s8) | TINYINT | +//! | `i16` | int16(s16) | SMALLINT | +//! | `i32` | int32(s32) | MEDIUM, INT | +//! | `i64` | int64(s64) | BIGINT | +//! | `u8` | uint8(u8) | TINYINT UNSIGNED | +//! | `u16` | uint16(u16) | SMALLINT UNSIGNED | +//! | `u32` | uint32(u32) | INT UNSIGNED | +//! | `u64` | uint64(u64) | BIGINT UNSIGNED | +//! | `f32` | floating32(float32) | FLOAT | +//! | `f64` | floating64(float64) | DOUBLE | +//! | `String` | str(string) | VARCHAR, CHAR, TEXT | +//! | `Vec` | binary(list) | VARBINARY, BINARY, BLOB | + +#![allow(missing_docs)] +wit_bindgen_rust::import!("../../wit/ephemeral/outbound-mysql.wit"); + +/// Exports the generated outbound MySQL items. +pub use outbound_mysql::*; + +impl std::error::Error for MysqlError {} + +impl ::std::fmt::Display for MysqlError { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + match self { + MysqlError::ConnectionFailed(err_msg) + | MysqlError::BadParameter(err_msg) + | MysqlError::QueryFailed(err_msg) + | MysqlError::ValueConversionFailed(err_msg) + | MysqlError::OtherError(err_msg) => write!(f, "MySQL error: {}", err_msg), + MysqlError::Success => panic!("Unexpected error: Success isn't supposed to be used"), + } + } +} + +/// A MySQL error +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Failed to deserialize [`DbValue`] + #[error("error value decoding: {0}")] + Decode(String), + /// MySQL query failed with an error + #[error(transparent)] + MysqlError(#[from] MysqlError), +} + +/// A type that can be decoded from the database. +pub trait Decode: Sized { + /// Decode a new value of this type using a [`DbValue`]. + fn decode(value: &DbValue) -> Result; +} + +impl Decode for Option +where + T: Decode, +{ + fn decode(value: &DbValue) -> Result { + match value { + DbValue::DbNull => Ok(None), + v => Ok(Some(T::decode(v)?)), + } + } +} + +impl Decode for bool { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Int8(0) => Ok(false), + DbValue::Int8(1) => Ok(true), + _ => Err(Error::Decode(format_decode_err( + "TINYINT(1), BOOLEAN", + value, + ))), + } + } +} + +impl Decode for i8 { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Int8(n) => Ok(*n), + _ => Err(Error::Decode(format_decode_err("TINYINT", value))), + } + } +} + +impl Decode for i16 { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Int16(n) => Ok(*n), + _ => Err(Error::Decode(format_decode_err("SMALLINT", value))), + } + } +} + +impl Decode for i32 { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Int32(n) => Ok(*n), + _ => Err(Error::Decode(format_decode_err("INT", value))), + } + } +} + +impl Decode for i64 { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Int64(n) => Ok(*n), + _ => Err(Error::Decode(format_decode_err("BIGINT", value))), + } + } +} + +impl Decode for u8 { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Uint8(n) => Ok(*n), + _ => Err(Error::Decode(format_decode_err("UNSIGNED TINYINT", value))), + } + } +} + +impl Decode for u16 { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Uint16(n) => Ok(*n), + _ => Err(Error::Decode(format_decode_err("UNSIGNED SMALLINT", value))), + } + } +} + +impl Decode for u32 { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Uint32(n) => Ok(*n), + _ => Err(Error::Decode(format_decode_err( + "UNISIGNED MEDIUMINT, UNSIGNED INT", + value, + ))), + } + } +} + +impl Decode for u64 { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Uint64(n) => Ok(*n), + _ => Err(Error::Decode(format_decode_err("UNSIGNED BIGINT", value))), + } + } +} + +impl Decode for f32 { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Floating32(n) => Ok(*n), + _ => Err(Error::Decode(format_decode_err("FLOAT", value))), + } + } +} + +impl Decode for f64 { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Floating64(n) => Ok(*n), + _ => Err(Error::Decode(format_decode_err("DOUBLE", value))), + } + } +} + +impl Decode for Vec { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Binary(n) => Ok(n.to_owned()), + _ => Err(Error::Decode(format_decode_err("BINARY, VARBINARY", value))), + } + } +} + +impl Decode for String { + fn decode(value: &DbValue) -> Result { + match value { + DbValue::Str(s) => Ok(s.to_owned()), + _ => Err(Error::Decode(format_decode_err( + "CHAR, VARCHAR, TEXT", + value, + ))), + } + } +} + +fn format_decode_err(types: &str, value: &DbValue) -> String { + format!("Expected {} from the DB but got {:?}", types, value) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn boolean() { + assert!(bool::decode(&DbValue::Int8(1)).unwrap()); + assert!(bool::decode(&DbValue::Int8(3)).is_err()); + assert!(bool::decode(&DbValue::Int32(0)).is_err()); + assert!(Option::::decode(&DbValue::DbNull).unwrap().is_none()); + } + + #[test] + fn int8() { + assert_eq!(i8::decode(&DbValue::Int8(0)).unwrap(), 0); + assert!(i8::decode(&DbValue::Int32(0)).is_err()); + assert!(Option::::decode(&DbValue::DbNull).unwrap().is_none()); + } + + #[test] + fn int16() { + assert_eq!(i16::decode(&DbValue::Int16(0)).unwrap(), 0); + assert!(i16::decode(&DbValue::Int32(0)).is_err()); + assert!(Option::::decode(&DbValue::DbNull).unwrap().is_none()); + } + + #[test] + fn int32() { + assert_eq!(i32::decode(&DbValue::Int32(0)).unwrap(), 0); + assert!(i32::decode(&DbValue::Boolean(false)).is_err()); + assert!(Option::::decode(&DbValue::DbNull).unwrap().is_none()); + } + + #[test] + fn int64() { + assert_eq!(i64::decode(&DbValue::Int64(0)).unwrap(), 0); + assert!(i64::decode(&DbValue::Boolean(false)).is_err()); + assert!(Option::::decode(&DbValue::DbNull).unwrap().is_none()); + } + + #[test] + fn uint8() { + assert_eq!(u8::decode(&DbValue::Uint8(0)).unwrap(), 0); + assert!(u8::decode(&DbValue::Uint32(0)).is_err()); + assert!(Option::::decode(&DbValue::DbNull).unwrap().is_none()); + } + + #[test] + fn uint16() { + assert_eq!(u16::decode(&DbValue::Uint16(0)).unwrap(), 0); + assert!(u16::decode(&DbValue::Uint32(0)).is_err()); + assert!(Option::::decode(&DbValue::DbNull).unwrap().is_none()); + } + + #[test] + fn uint32() { + assert_eq!(u32::decode(&DbValue::Uint32(0)).unwrap(), 0); + assert!(u32::decode(&DbValue::Boolean(false)).is_err()); + assert!(Option::::decode(&DbValue::DbNull).unwrap().is_none()); + } + + #[test] + fn uint64() { + assert_eq!(u64::decode(&DbValue::Uint64(0)).unwrap(), 0); + assert!(u64::decode(&DbValue::Boolean(false)).is_err()); + assert!(Option::::decode(&DbValue::DbNull).unwrap().is_none()); + } + + #[test] + fn floating32() { + assert!(f32::decode(&DbValue::Floating32(0.0)).is_ok()); + assert!(f32::decode(&DbValue::Boolean(false)).is_err()); + assert!(Option::::decode(&DbValue::DbNull).unwrap().is_none()); + } + + #[test] + fn floating64() { + assert!(f64::decode(&DbValue::Floating64(0.0)).is_ok()); + assert!(f64::decode(&DbValue::Boolean(false)).is_err()); + assert!(Option::::decode(&DbValue::DbNull).unwrap().is_none()); + } + + #[test] + fn str() { + assert_eq!( + String::decode(&DbValue::Str(String::from("foo"))).unwrap(), + String::from("foo") + ); + + assert!(String::decode(&DbValue::Int32(0)).is_err()); + assert!(Option::::decode(&DbValue::DbNull) + .unwrap() + .is_none()); + } +} diff --git a/tests/integration.rs b/tests/integration.rs index d191e60fcc..76e24cc861 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -677,6 +677,33 @@ mod integration_tests { } } + #[cfg(feature = "outbound-mysql-tests")] + mod outbound_mysql_tests { + use super::*; + + const RUST_OUTBOUND_MYSQL_INTEGRATION_TEST: &str = + "tests/outbound-mysql/http-rust-outbound-mysql"; + + #[tokio::test] + async fn test_outbound_mysql_rust_local() -> Result<()> { + let s = SpinTestController::with_manifest( + &format!( + "{}/{}", + RUST_OUTBOUND_MYSQL_INTEGRATION_TEST, DEFAULT_MANIFEST_LOCATION + ), + &[], + &[], + None, + ) + .await?; + + assert_status(&s, "/test_numeric_types", 200).await?; + assert_status(&s, "/test_character_types", 200).await?; + + Ok(()) + } + } + #[tokio::test] async fn test_static_assets_without_bindle() -> Result<()> { let s = SpinTestController::with_manifest( diff --git a/tests/outbound-mysql/http-rust-outbound-mysql/.cargo/config.toml b/tests/outbound-mysql/http-rust-outbound-mysql/.cargo/config.toml new file mode 100644 index 0000000000..6b77899cb3 --- /dev/null +++ b/tests/outbound-mysql/http-rust-outbound-mysql/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/tests/outbound-mysql/http-rust-outbound-mysql/Cargo.toml b/tests/outbound-mysql/http-rust-outbound-mysql/Cargo.toml new file mode 100644 index 0000000000..cd56000c5c --- /dev/null +++ b/tests/outbound-mysql/http-rust-outbound-mysql/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "http-rust-outbound-mysql" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = [ "cdylib" ] + +[dependencies] +# Useful crate to handle errors. +anyhow = "1" +# Crate to simplify working with bytes. +bytes = "1" +# General-purpose crate with common HTTP types. +http = "0.2" +# The Spin SDK. +spin-sdk = { path = "../../../sdk/rust" } +# Crate that generates Rust Wasm bindings from a WebAssembly interface. +wit-bindgen-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "cb871cfa1ee460b51eb1d144b175b9aab9c50aba" } + +[workspace] diff --git a/tests/outbound-mysql/http-rust-outbound-mysql/spin.toml b/tests/outbound-mysql/http-rust-outbound-mysql/spin.toml new file mode 100644 index 0000000000..c851325917 --- /dev/null +++ b/tests/outbound-mysql/http-rust-outbound-mysql/spin.toml @@ -0,0 +1,14 @@ +spin_version = "1" +authors = ["Fermyon Engineering "] +name = "rust-outbound-mysql-example" +trigger = { type = "http", base = "/" } +version = "0.1.0" + +[[component]] +environment = { DB_URL = "mysql://spin:spin@127.0.0.1/spin_dev" } +id = "outbound-mysql" +source = "target/wasm32-wasi/release/http_rust_outbound_mysql.wasm" +[component.trigger] +route = "/..." +[component.build] +command = "cargo build --target wasm32-wasi --release" diff --git a/tests/outbound-mysql/http-rust-outbound-mysql/src/lib.rs b/tests/outbound-mysql/http-rust-outbound-mysql/src/lib.rs new file mode 100644 index 0000000000..623d9c1ddf --- /dev/null +++ b/tests/outbound-mysql/http-rust-outbound-mysql/src/lib.rs @@ -0,0 +1,239 @@ +#![allow(dead_code)] +use anyhow::{anyhow, Result}; +use spin_sdk::{ + http::{Request, Response}, + http_component, mysql::{self, Decode}, +}; + +// The environment variable set in `spin.toml` that points to the +// address of the Pg server that the component will write to +const DB_URL_ENV: &str = "DB_URL"; + +#[derive(Debug, Clone)] +struct NumericRow { + rtiny: i8, + rsmall: i16, + rmedium: i32, + rint: i32, + rbig: i64, + rfloat: f32, + rdouble: f64, + rutiny: u8, + rusmall: u16, + rumedium: u32, + ruint: u32, + rubig: u64, + rtinyint1: bool, + rbool: bool, +} + +#[derive(Debug, Clone)] +struct CharacterRow { + rvarchar: String, + rtext: String, + rchar: String, + rbinary: Vec, + rvarbinary: Vec, + rblob: Vec +} + +#[http_component] +fn process(req: Request) -> Result { + match req.uri().path() { + "/test_character_types" => test_character_types(req), + "/test_numeric_types" => test_numeric_types(req), + _ => Ok(http::Response::builder() + .status(404) + .body(Some("Not found".into()))?), + } +} + +fn test_numeric_types(_req: Request) -> Result { + let address = std::env::var(DB_URL_ENV)?; + + let create_table_sql = r#" + CREATE TEMPORARY TABLE test_numeric_types ( + rtiny TINYINT NOT NULL, + rsmall SMALLINT NOT NULL, + rmedium MEDIUMINT NOT NULL, + rint INT NOT NULL, + rbig BIGINT NOT NULL, + rfloat FLOAT NOT NULL, + rdouble DOUBLE NOT NULL, + rutiny TINYINT UNSIGNED NOT NULL, + rusmall SMALLINT UNSIGNED NOT NULL, + rumedium MEDIUMINT UNSIGNED NOT NULL, + ruint INT UNSIGNED NOT NULL, + rubig BIGINT UNSIGNED NOT NULL, + rtinyint1 TINYINT(1) NOT NULL, + rbool BOOLEAN NOT NULL + ); + "#; + + mysql::execute(&address, create_table_sql, &[])?; + + let insert_sql = r#" + INSERT INTO test_numeric_types + (rtiny, rsmall, rmedium, rint, rbig, rfloat, rdouble, rutiny, rusmall, rumedium, ruint, rubig, rtinyint1, rbool) + VALUES + (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1); + "#; + + mysql::execute(&address, insert_sql, &[])?; + + let sql = r#" + SELECT + rtiny, + rsmall, + rmedium, + rint, + rbig, + rfloat, + rdouble, + rutiny, + rusmall, + rumedium, + ruint, + rubig, + rtinyint1, + rbool + FROM test_numeric_types; + "#; + + let rowset = mysql::query(&address, sql, &[])?; + + let column_summary = rowset + .columns + .iter() + .map(format_col) + .collect::>() + .join(", "); + + let mut response_lines = vec![]; + + for row in rowset.rows { + let rtiny = i8::decode(&row[0])?; + let rsmall = i16::decode(&row[1])?; + let rmedium = i32::decode(&row[2])?; + let rint = i32::decode(&row[3])?; + let rbig = i64::decode(&row[4])?; + let rfloat = f32::decode(&row[5])?; + let rdouble = f64::decode(&row[6])?; + let rutiny = u8::decode(&row[7])?; + let rusmall = u16::decode(&row[8])?; + let rumedium = u32::decode(&row[9])?; + let ruint = u32::decode(&row[10])?; + let rubig = u64::decode(&row[11])?; + let rtinyint1 = bool::decode(&row[12])?; + let rbool = bool::decode(&row[13])?; + + let row = NumericRow { + rtiny, + rsmall, + rmedium, + rint, + rbig, + rfloat, + rdouble, + rutiny, + rusmall, + rumedium, + ruint, + rubig, + rtinyint1, + rbool, + }; + + response_lines.push(format!("row: {:#?}", row)); + } + + let response = format!( + "Found {} rows(s) as follows:\n{}\n\n(Column info: {})\n", + response_lines.len(), + response_lines.join("\n"), + column_summary, + ); + + Ok(http::Response::builder() + .status(200) + .body(Some(response.into()))?) +} + +fn test_character_types(_req: Request) -> Result { + let address = std::env::var(DB_URL_ENV)?; + + let create_table_sql = r#" + CREATE TEMPORARY TABLE test_character_types ( + rvarchar varchar(40) NOT NULL, + rtext text NOT NULL, + rchar char(10) NOT NULL, + rbinary binary(10) NOT NULL, + rvarbinary varbinary(10) NOT NULL, + rblob BLOB NOT NULL + ); + "#; + + mysql::execute(&address, create_table_sql, &[]) + .map_err(|e| anyhow!("Error executing MySQL command: {:?}", e))?; + + let insert_sql = r#" + INSERT INTO test_character_types + (rvarchar, rtext, rchar, rbinary, rvarbinary, rblob) + VALUES + ('rvarchar', 'rtext', 'rchar', 'a', 'a', 'a'); + "#; + + mysql::execute(&address, insert_sql, &[])?; + + let sql = r#" + SELECT + rvarchar, rtext, rchar, rbinary, rvarbinary, rblob + FROM test_character_types; + "#; + + let rowset = mysql::query(&address, sql, &[])?; + + let column_summary = rowset + .columns + .iter() + .map(format_col) + .collect::>() + .join(", "); + + let mut response_lines = vec![]; + + for row in rowset.rows { + let rvarchar = String::decode(&row[0])?; + let rtext = String::decode(&row[1])?; + let rchar = String::decode(&row[2])?; + let rbinary = Vec::::decode(&row[3])?; + let rvarbinary = Vec::::decode(&row[4])?; + let rblob = Vec::::decode(&row[5])?; + + let row = CharacterRow { + rvarchar, + rtext, + rchar, + rbinary, + rvarbinary, + rblob, + }; + + response_lines.push(format!("row: {:#?}", row)); + } + + let response = format!( + "Found {} rows(s) as follows:\n{}\n\n(Column info: {})\n", + response_lines.len(), + response_lines.join("\n"), + column_summary, + ); + + Ok(http::Response::builder() + .status(200) + .body(Some(response.into()))?) +} + +fn format_col(column: &mysql::Column) -> String { + format!("{}: {:?}", column.name, column.data_type) +} diff --git a/wit/ephemeral/mysql-types.wit b/wit/ephemeral/mysql-types.wit new file mode 100644 index 0000000000..a18f5d7375 --- /dev/null +++ b/wit/ephemeral/mysql-types.wit @@ -0,0 +1,10 @@ +// General purpose error. +// TODO: We can provide richer info than this: https://docs.rs/mysql/latest/mysql/error/enum.Error.html +variant mysql-error { + success, + connection-failed(string), + bad-parameter(string), + query-failed(string), + value-conversion-failed(string), + other-error(string) +} diff --git a/wit/ephemeral/outbound-mysql.wit b/wit/ephemeral/outbound-mysql.wit new file mode 100644 index 0000000000..8cd0727754 --- /dev/null +++ b/wit/ephemeral/outbound-mysql.wit @@ -0,0 +1,8 @@ +use * from mysql-types +use * from rdbms-types + +// query the database: select +query: func(address: string, statement: string, params: list) -> expected + +// execute command to the database: insert, update, delete +execute: func(address: string, statement: string, params: list) -> expected