Repository: fermyon/spin
Pull request #864 (2022-10-27 18:06:21)

Add/enhance MySQL support
Rebased https://github.com/fermyon/spin/pull/777 against current master, fixed a few conflicts/incompatibilities.
@itowlson, will be happy to rebase against your branch if you prefer to rebase/sign it yourself.

- Extract outbound-mysql into separate crate
- Add integration test
- Add connections reusing during the request
- Improve MySQL types conversion

etehtsea: @itowlson this one is also ready for review!
itowlson: @etehtsea I tagged you in on the kick-off draft of `wasi-sql`.  At the moment, what they have there is close enough to what you have here that we could consider pushing ahead on this basis.  Or we could try to align more closely, though I'm not sure if the `resource` stuff is properly available yet, and the draft will certainly change.  What do you reckon?
etehtsea: @itowlson thanks! I left a comment there. As you said earlier, if there is somebody interested in MySQL support, it should be good to go (for now).

I was curious about the differences between pg and MySQL implementations. And my conclusion is if the goal is to provide more or less full support for specific DB, it's hard to do this using a shared wit interface between them.

P.S. Also, I've checked wasi preview2 today and it seems that sockets support is somewhere near. So, if there be an ability to use the "native" library from the guest module, it probably be the solution for those who needs full DB support.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index a1add0b4..6e195dbe 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 35561812..8d128af0 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 f434c61f..703a9717 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 00000000..4ea40cfe
--- /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 00000000..a139e9e5
--- /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<String, mysql_async::Conn>,
+}
+
+impl HostComponent for OutboundMysql {
+    type Data = Self;
+
+    fn add_to_linker<T: Send>(
+        linker: &mut spin_core::Linker<T>,
+        get: impl Fn(&mut spin_core::Data<T>) -> &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<ParameterValue<'_>>,
+    ) -> Result<(), MysqlError> {
+        let db_params = params
+            .iter()
+            .map(to_sql_parameter)
+            .collect::<anyhow::Result<Vec<_>>>()
+            .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<ParameterValue<'_>>,
+    ) -> Result<RowSet, MysqlError> {
+        let db_params = params
+            .iter()
+            .map(to_sql_parameter)
+            .collect::<anyhow::Result<Vec<_>>>()
+            .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::<mysql_async::Row>().await {
+            Err(e) => Err(MysqlError::OtherError(format!("{:?}", e))),
+            Ok(result_set) => {
+                let rows = result_set
+                    .into_iter()
+                    .map(|row| convert_row(row, &columns))
+                    .collect::<Result<Vec<_>, _>>()
+                    .map_err(|e| MysqlError::QueryFailed(format!("{:?}", e)))?;
+
+                Ok(RowSet { columns, rows })
+            }
+        }
+    }
+}
+
+fn to_sql_parameter(value: &ParameterValue) -> anyhow::Result<mysql_async::Value> {
+    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<Arc<[mysql_async::Column]>>) -> Vec<Column> {
+    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<Vec<DbValue>, 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<DbValue, MysqlError> {
+    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<DbValue, MysqlError> {
+    match column.data_type {
+        DbDataType::Binary => convert_value_to::<Vec<u8>>(value).map(DbValue::Binary),
+        DbDataType::Boolean => convert_value_to::<bool>(value).map(DbValue::Boolean),
+        DbDataType::Floating32 => convert_value_to::<f32>(value).map(DbValue::Floating32),
+        DbDataType::Floating64 => convert_value_to::<f64>(value).map(DbValue::Floating64),
+        DbDataType::Int8 => convert_value_to::<i8>(value).map(DbValue::Int8),
+        DbDataType::Int16 => convert_value_to::<i16>(value).map(DbValue::Int16),
+        DbDataType::Int32 => convert_value_to::<i32>(value).map(DbValue::Int32),
+        DbDataType::Int64 => convert_value_to::<i64>(value).map(DbValue::Int64),
+        DbDataType::Str => convert_value_to::<String>(value).map(DbValue::Str),
+        DbDataType::Uint8 => convert_value_to::<u8>(value).map(DbValue::Uint8),
+        DbDataType::Uint16 => convert_value_to::<u16>(value).map(DbValue::Uint16),
+        DbDataType::Uint32 => convert_value_to::<u32>(value).map(DbValue::Uint32),
+        DbDataType::Uint64 => convert_value_to::<u64>(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<mysql_async::Conn, mysql_async::Error> {
+    tracing::log::debug!("Build new connection: {}", address);
+
+    let connection_pool = mysql_async::Pool::new(address);
+
+    connection_pool.get_conn().await
+}
+
+fn convert_value_to<T: FromValue>(value: mysql_async::Value) -> Result<T, MysqlError> {
+    from_value_opt::<T>(value).map_err(|e| MysqlError::ValueConversionFailed(format!("{}", e)))
+}
diff --git a/crates/trigger/Cargo.toml b/crates/trigger/Cargo.toml
index 0b29002d..8b7a1013 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 89676679..3a89705f 100644
--- a/crates/trigger/src/lib.rs
+++ b/crates/trigger/src/lib.rs
@@ -94,6 +94,7 @@ impl<Executor: TriggerExecutor> TriggerExecutorBuilder<Executor> {
             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 00000000..6b77899c
--- /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 00000000..2f7896d1
--- /dev/null
+++ b/examples/rust-outbound-mysql/.gitignore
@@ -0,0 +1,1 @@
+target/
diff --git a/examples/rust-outbound-mysql/Cargo.toml b/examples/rust-outbound-mysql/Cargo.toml
new file mode 100644
index 00000000..4620fd21
--- /dev/null
+++ b/examples/rust-outbound-mysql/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "rust-outbound-mysql"
+authors = ["itowlson <ivan.towlson@fermyon.com>"]
+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 00000000..bbf91d68
--- /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 00000000..86f2efc1
--- /dev/null
+++ b/examples/rust-outbound-mysql/spin.toml
@@ -0,0 +1,15 @@
+spin_version = "1"
+authors = ["itowlson <ivan.towlson@fermyon.com>"]
+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 00000000..2e735aa4
--- /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<String>, bool),
+    Error(u16),
+}
+
+#[http_component]
+fn rust_outbound_mysql(req: Request) -> Result<Response> {
+    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<Option<i32>, ()> {
+    match header_val.to_str() {
+        Ok(path) => {
+            let path_parts = &(path.split('/').skip(1).collect::<Vec<_>>()[..]);
+            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<HashMap<String, String>> {
+    // TODO: easier way?
+    let body = match req.body().as_ref() {
+        Some(bytes) => bytes.slice(..),
+        None => bytes::Bytes::default(),
+    };
+    Ok(serde_json::from_slice::<HashMap<String, String>>(&body)?)
+}
+
+fn list() -> Result<Response> {
+    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::<Vec<_>>()
+        .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<Response> {
+    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, &params)?;
+
+    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<String>, is_finicky: bool) -> Result<Response> {
+    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, &params)?;
+
+    let location_url = format!("/{}", id);
+
+    Ok(http::Response::builder()
+        .status(201)
+        .header("Location", location_url)
+        .body(None)?)
+}
+
+fn error(status: u16) -> Result<Response> {
+    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<i32> {
+    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 00000000..18dcea0c
--- /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<String>,
+    is_finicky: bool,
+}
+
+pub(crate) fn as_pet(row: &mysql::Row) -> Result<Pet> {
+    let id = i32::decode(&row[0])?;
+    let name = String::decode(&row[1])?;
+    let prey = Option::<String>::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 7b46adb5..aa995a51 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 00000000..965df5fc
--- /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<u8>` | binary(list<u8>)    | 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<Self, Error>;
+}
+
+impl<T> Decode for Option<T>
+where
+    T: Decode,
+{
+    fn decode(value: &DbValue) -> Result<Self, Error> {
+        match value {
+            DbValue::DbNull => Ok(None),
+            v => Ok(Some(T::decode(v)?)),
+        }
+    }
+}
+
+impl Decode for bool {
+    fn decode(value: &DbValue) -> Result<Self, Error> {
+        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<Self, Error> {
+        match value {
+            DbValue::Int8(n) => Ok(*n),
+            _ => Err(Error::Decode(format_decode_err("TINYINT", value))),
+        }
+    }
+}
+
+impl Decode for i16 {
+    fn decode(value: &DbValue) -> Result<Self, Error> {
+        match value {
+            DbValue::Int16(n) => Ok(*n),
+            _ => Err(Error::Decode(format_decode_err("SMALLINT", value))),
+        }
+    }
+}
+
+impl Decode for i32 {
+    fn decode(value: &DbValue) -> Result<Self, Error> {
+        match value {
+            DbValue::Int32(n) => Ok(*n),
+            _ => Err(Error::Decode(format_decode_err("INT", value))),
+        }
+    }
+}
+
+impl Decode for i64 {
+    fn decode(value: &DbValue) -> Result<Self, Error> {
+        match value {
+            DbValue::Int64(n) => Ok(*n),
+            _ => Err(Error::Decode(format_decode_err("BIGINT", value))),
+        }
+    }
+}
+
+impl Decode for u8 {
+    fn decode(value: &DbValue) -> Result<Self, Error> {
+        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<Self, Error> {
+        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<Self, Error> {
+        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<Self, Error> {
+        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<Self, Error> {
+        match value {
+            DbValue::Floating32(n) => Ok(*n),
+            _ => Err(Error::Decode(format_decode_err("FLOAT", value))),
+        }
+    }
+}
+
+impl Decode for f64 {
+    fn decode(value: &DbValue) -> Result<Self, Error> {
+        match value {
+            DbValue::Floating64(n) => Ok(*n),
+            _ => Err(Error::Decode(format_decode_err("DOUBLE", value))),
+        }
+    }
+}
+
+impl Decode for Vec<u8> {
+    fn decode(value: &DbValue) -> Result<Self, Error> {
+        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<Self, Error> {
+        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::<bool>::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::<i8>::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::<i16>::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::<i32>::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::<i64>::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::<u16>::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::<u16>::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::<u32>::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::<u64>::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::<f32>::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::<f64>::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::<String>::decode(&DbValue::DbNull)
+            .unwrap()
+            .is_none());
+    }
+}
diff --git a/wit/ephemeral/mysql-types.wit b/wit/ephemeral/mysql-types.wit
new file mode 100644
index 00000000..a18f5d73
--- /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 00000000..8cd07277
--- /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<parameter-value>) -> expected<row-set, mysql-error>
+
+// execute command to the database: insert, update, delete
+execute: func(address: string, statement: string, params: list<parameter-value>) -> expected<unit, mysql-error>


Test patch:
diff --git a/tests/integration.rs b/tests/integration.rs
index d191e60f..76e24cc8 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 00000000..6b77899c
--- /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 00000000..cd56000c
--- /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 00000000..c8513259
--- /dev/null
+++ b/tests/outbound-mysql/http-rust-outbound-mysql/spin.toml
@@ -0,0 +1,14 @@
+spin_version = "1"
+authors = ["Fermyon Engineering <engineering@fermyon.com>"]
+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 00000000..623d9c1d
--- /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<u8>,
+    rvarbinary: Vec<u8>,
+    rblob: Vec<u8>
+}
+
+#[http_component]
+fn process(req: Request) -> Result<Response> {
+    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<Response> {
+    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::<Vec<_>>()
+        .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<Response> {
+    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::<Vec<_>>()
+        .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::<u8>::decode(&row[3])?;
+        let rvarbinary = Vec::<u8>::decode(&row[4])?;
+        let rblob = Vec::<u8>::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)
+}