Repository: shuttle-hq/synth (language: rust)

Issue:
Support for mysql/mariadb
**Required Functionality**
It would be really useful for my work to be able to populate directly MySQL/MariaDb databases.

Something like:

```sh
synth generate tpch --to mysql://user:pass@localhost:3066/mydbname
```

**Proposed Solution**


**Use case**
PostgreSQL or MongoDb are widely used but so are MySQL/MariaDB.



Discussion / hints:
Hey thanks for this. 

I will write a plan for implementation shortly. There are a few pre-requisites that need to be completed first. The APIs which define the integrations (PG, Mongo and now MySQL/MariaDB) need to change in a breaking way. We have an internal issue for this and all this will be made public as soon as we have a little time to open source our dev process.

Fix patch:
diff --git a/.github/workflows/scripts/validate_mysql_gen_count.sh b/.github/workflows/scripts/validate_mysql_gen_count.sh
new file mode 100644
index 00000000..5f43aa0a
--- /dev/null
+++ b/.github/workflows/scripts/validate_mysql_gen_count.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+sum_rows_query="SELECT (SELECT count(*) FROM hospitals) +  (SELECT count(*) FROM doctors) + (SELECT count(*) FROM patients)"
+sum=`mysql -h $MYSQL_HOST -u root --password=$MYSQL_ROOT_PASSWORD -P $MYSQL_PORT $MYSQL_DATABASE -e "$sum_rows_query" | grep -o '[[:digit:]]*'`
+if [ "$sum" -lt "30" ]
+then
+    exit 1
+fi
\ No newline at end of file
diff --git a/.github/workflows/synth-mysql.yml b/.github/workflows/synth-mysql.yml
new file mode 100644
index 00000000..5ba417d7
--- /dev/null
+++ b/.github/workflows/synth-mysql.yml
@@ -0,0 +1,66 @@
+name: synth-mysql
+
+on:
+  push:
+    branches: [ master ]
+    paths: [ '**/*.rs' ]
+  pull_request:
+    branches: [ master ]
+    paths: [ '**/*.rs' ]
+
+  workflow_dispatch:
+
+jobs:
+  e2e_tests_mysql:
+    runs-on: ubuntu-latest
+    services:
+      mysql:
+        image: mysql
+        env:
+          MYSQL_ROOT_PASSWORD: mysecretpassword
+          MYSQL_DATABASE: test_db
+        ports:
+          - 3306:3306
+        options: >-
+          --health-cmd="mysqladmin ping"
+          --health-interval=10s
+          --health-timeout=5s
+          --health-retries=3
+    steps:
+      - uses: actions/checkout@v2
+      ## Set env. variables for this job
+      - run: |
+          echo "MYSQL_HOST=127.0.0.1" >> $GITHUB_ENV
+          echo "MYSQL_USER=root" >> $GITHUB_ENV
+          echo "MYSQL_PORT=3306" >> $GITHUB_ENV
+          echo "MYSQL_ROOT_PASSWORD=mysecretpassword" >> $GITHUB_ENV
+          echo "MYSQL_DATABASE=test_db" >> $GITHUB_ENV
+      - run: |
+          sudo apt-get update
+          sudo apt-get install --yes --no-install-recommends mysql-client jq
+      - run: >
+          mysql -h "${{ env.MYSQL_HOST }}" -u "${{ env.MYSQL_USER }}" --password="${{ env.MYSQL_ROOT_PASSWORD }}"
+          -P "${{ env.MYSQL_PORT }}" "${{ env.MYSQL_DATABASE }}" < synth/testing_harness/mysql/0_hospital_schema.sql
+      - uses: actions-rs/toolchain@v1
+        with:
+          toolchain: nightly-2021-05-17
+      - run: cargo +nightly-2021-05-17 install --debug --path=synth
+      - run: |
+          echo "Running generate test"
+          cd synth/testing_harness/mysql
+          synth init || true
+          synth generate hospital_master --to mysql://${{ env.MYSQL_USER }}:${{ env.MYSQL_ROOT_PASSWORD }}@${{ env.MYSQL_HOST }}:${{ env.MYSQL_PORT }}/${{ env.MYSQL_DATABASE }} --size 30
+          bash "${GITHUB_WORKSPACE}/.github/workflows/scripts/validate_mysql_gen_count.sh"
+      ## Clear out and repopulate DB
+      - run: >
+          mysql -h "${{ env.MYSQL_HOST }}" -u "${{ env.MYSQL_USER }}" --password="${{ env.MYSQL_ROOT_PASSWORD }}"
+          -P "${{ env.MYSQL_PORT }}" "${{ env.MYSQL_DATABASE }}" < synth/testing_harness/mysql/0_hospital_schema.sql
+      - run: >
+          mysql -h "${{ env.MYSQL_HOST }}" -u "${{ env.MYSQL_USER }}" --password="${{ env.MYSQL_ROOT_PASSWORD }}"
+          -P "${{ env.MYSQL_PORT }}" "${{ env.MYSQL_DATABASE }}" < synth/testing_harness/mysql/1_hospital_data.sql
+      - run: |
+          echo "Testing import"
+          cd synth/testing_harness/mysql
+          synth init || true
+          synth import --from mysql://${{ env.MYSQL_USER }}:${{ env.MYSQL_ROOT_PASSWORD }}@${{ env.MYSQL_HOST }}:${{ env.MYSQL_PORT }}/${{ env.MYSQL_DATABASE }} hospital_import
+          diff <(jq --sort-keys . hospital_import/*) <(jq --sort-keys . hospital_master/*)
diff --git a/Cargo.lock b/Cargo.lock
index 1f853ba3..0ea16fe3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -71,6 +71,17 @@ dependencies = [
  "opaque-debug 0.3.0",
 ]
 
+[[package]]
+name = "ahash"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
+dependencies = [
+ "getrandom 0.2.3",
+ "once_cell",
+ "version_check",
+]
+
 [[package]]
 name = "aho-corasick"
 version = "0.7.18"
@@ -224,6 +235,18 @@ dependencies = [
  "event-listener",
 ]
 
+[[package]]
+name = "async-native-tls"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e9e7a929bd34c68a82d58a4de7f86fffdaf97fb2af850162a7bb19dd7269b33"
+dependencies = [
+ "async-std",
+ "native-tls",
+ "thiserror",
+ "url",
+]
+
 [[package]]
 name = "async-process"
 version = "1.1.0"
@@ -322,6 +345,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "atoi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "atomic-waker"
 version = "1.0.0"
@@ -339,6 +371,12 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "autocfg"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
+
 [[package]]
 name = "autocfg"
 version = "1.0.1"
@@ -384,6 +422,16 @@ version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
 
+[[package]]
+name = "beau_collector"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33a143066d3cbd3d32c15b51c39449e50e32a68293d31589fb941d6a9df0df4f"
+dependencies = [
+ "anyhow",
+ "itertools",
+]
+
 [[package]]
 name = "bimap"
 version = "0.6.1"
@@ -411,6 +459,18 @@ version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
 
+[[package]]
+name = "bitvec"
+version = "0.19.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
 [[package]]
 name = "blake3"
 version = "0.3.8"
@@ -498,6 +558,12 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "build_const"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7"
+
 [[package]]
 name = "bumpalo"
 version = "3.7.0"
@@ -698,6 +764,15 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba"
 
+[[package]]
+name = "crc"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
+dependencies = [
+ "build_const",
+]
+
 [[package]]
 name = "crossbeam-channel"
 version = "0.5.1"
@@ -956,6 +1031,12 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
 
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
 [[package]]
 name = "dynfmt"
 version = "0.1.5"
@@ -1060,12 +1141,6 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
 
-[[package]]
-name = "fallible-iterator"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
-
 [[package]]
 name = "fastrand"
 version = "1.4.1"
@@ -1148,6 +1223,12 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
 
+[[package]]
+name = "funty"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
+
 [[package]]
 name = "futures"
 version = "0.3.15"
@@ -1217,7 +1298,7 @@ version = "0.3.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121"
 dependencies = [
- "autocfg",
+ "autocfg 1.0.1",
  "proc-macro-hack",
  "proc-macro2",
  "quote",
@@ -1242,7 +1323,7 @@ version = "0.3.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967"
 dependencies = [
- "autocfg",
+ "autocfg 1.0.1",
  "futures-channel",
  "futures-core",
  "futures-io",
@@ -1387,6 +1468,24 @@ version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
 
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
+dependencies = [
+ "hashbrown 0.11.2",
+]
+
 [[package]]
 name = "heck"
 version = "0.3.3"
@@ -1675,8 +1774,8 @@ version = "1.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
 dependencies = [
- "autocfg",
- "hashbrown",
+ "autocfg 1.0.1",
+ "hashbrown 0.9.1",
 ]
 
 [[package]]
@@ -1733,6 +1832,15 @@ version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
 
+[[package]]
+name = "itertools"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "0.4.7"
@@ -1781,6 +1889,22 @@ name = "lazy_static"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+dependencies = [
+ "spin",
+]
+
+[[package]]
+name = "lexical-core"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
+dependencies = [
+ "arrayvec",
+ "bitflags",
+ "cfg-if 1.0.0",
+ "ryu",
+ "static_assertions",
+]
 
 [[package]]
 name = "libc"
@@ -1802,6 +1926,12 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "libm"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
+
 [[package]]
 name = "libsqlite3-sys"
 version = "0.22.2"
@@ -1872,6 +2002,12 @@ dependencies = [
  "linked-hash-map",
 ]
 
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
 [[package]]
 name = "match_cfg"
 version = "0.1.0"
@@ -1918,7 +2054,7 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9"
 dependencies = [
- "autocfg",
+ "autocfg 1.0.1",
 ]
 
 [[package]]
@@ -1965,7 +2101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
 dependencies = [
  "adler",
- "autocfg",
+ "autocfg 1.0.1",
 ]
 
 [[package]]
@@ -2047,7 +2183,7 @@ dependencies = [
  "rustls",
  "serde",
  "serde_with",
- "sha-1",
+ "sha-1 0.8.2",
  "sha2 0.8.2",
  "socket2 0.3.19",
  "stringprep",
@@ -2106,6 +2242,19 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "nom"
+version = "6.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
+dependencies = [
+ "bitvec",
+ "funty",
+ "lexical-core",
+ "memchr",
+ "version_check",
+]
+
 [[package]]
 name = "ntapi"
 version = "0.3.6"
@@ -2121,7 +2270,7 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b7a8e9be5e039e2ff869df49155f1c06bd01ade2117ec783e56ab0932b67a8f"
 dependencies = [
- "num-bigint",
+ "num-bigint 0.3.2",
  "num-complex",
  "num-integer",
  "num-iter",
@@ -2135,9 +2284,38 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7d0a3d5e207573f948a9e5376662aa743a2ea13f7c50a554d7af443a73fbfeba"
 dependencies = [
- "autocfg",
+ "autocfg 1.0.1",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512"
+dependencies = [
+ "autocfg 1.0.1",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4547ee5541c18742396ae2c895d0717d0f886d8823b8399cdaf7b07d63ad0480"
+dependencies = [
+ "autocfg 0.1.7",
+ "byteorder",
+ "lazy_static",
+ "libm",
  "num-integer",
+ "num-iter",
  "num-traits",
+ "rand 0.8.3",
+ "smallvec",
+ "zeroize",
 ]
 
 [[package]]
@@ -2155,7 +2333,7 @@ version = "0.1.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
 dependencies = [
- "autocfg",
+ "autocfg 1.0.1",
  "num-traits",
 ]
 
@@ -2165,7 +2343,7 @@ version = "0.1.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
 dependencies = [
- "autocfg",
+ "autocfg 1.0.1",
  "num-integer",
  "num-traits",
 ]
@@ -2176,8 +2354,8 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
 dependencies = [
- "autocfg",
- "num-bigint",
+ "autocfg 1.0.1",
+ "num-bigint 0.3.2",
  "num-integer",
  "num-traits",
 ]
@@ -2188,7 +2366,8 @@ version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
 dependencies = [
- "autocfg",
+ "autocfg 1.0.1",
+ "libm",
 ]
 
 [[package]]
@@ -2260,7 +2439,7 @@ version = "0.9.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6b0d6fb7d80f877617dfcb014e605e2b5ab2fb0afdf27935219bb6bd984cb98"
 dependencies = [
- "autocfg",
+ "autocfg 1.0.1",
  "cc",
  "libc",
  "pkg-config",
@@ -2330,28 +2509,21 @@ dependencies = [
 ]
 
 [[package]]
-name = "percent-encoding"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
-
-[[package]]
-name = "phf"
-version = "0.8.0"
+name = "pem"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb"
 dependencies = [
- "phf_shared",
+ "base64 0.13.0",
+ "once_cell",
+ "regex",
 ]
 
 [[package]]
-name = "phf_shared"
-version = "0.8.0"
+name = "percent-encoding"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
-dependencies = [
- "siphasher",
-]
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
 
 [[package]]
 name = "pin-project"
@@ -2421,50 +2593,6 @@ dependencies = [
  "universal-hash",
 ]
 
-[[package]]
-name = "postgres"
-version = "0.19.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7871ee579860d8183f542e387b176a25f2656b9fb5211e045397f745a68d1c2"
-dependencies = [
- "bytes 1.0.1",
- "fallible-iterator",
- "futures",
- "log",
- "tokio 1.6.2",
- "tokio-postgres",
-]
-
-[[package]]
-name = "postgres-protocol"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff3e0f70d32e20923cabf2df02913be7c1842d4c772db8065c00fcfdd1d1bff3"
-dependencies = [
- "base64 0.13.0",
- "byteorder",
- "bytes 1.0.1",
- "fallible-iterator",
- "hmac 0.10.1",
- "md-5 0.9.1",
- "memchr",
- "rand 0.8.3",
- "sha2 0.9.5",
- "stringprep",
-]
-
-[[package]]
-name = "postgres-types"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "430f4131e1b7657b0cd9a2b0c3408d77c9a43a042d300b8c77f981dffcc43a2f"
-dependencies = [
- "bytes 1.0.1",
- "chrono",
- "fallible-iterator",
- "postgres-protocol",
-]
-
 [[package]]
 name = "posthog-rs"
 version = "0.1.3"
@@ -2553,6 +2681,12 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "radium"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
+
 [[package]]
 name = "rand"
 version = "0.7.3"
@@ -2660,7 +2794,7 @@ version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90"
 dependencies = [
- "autocfg",
+ "autocfg 1.0.1",
  "crossbeam-deque",
  "either",
  "rayon-core",
@@ -2825,6 +2959,26 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e"
 
+[[package]]
+name = "rsa"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ef841a26fc5d040ced0417c6c6a64ee851f42489df11cdf0218e545b6f8d28"
+dependencies = [
+ "byteorder",
+ "digest 0.9.0",
+ "lazy_static",
+ "num-bigint-dig",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "pem",
+ "rand 0.8.3",
+ "simple_asn1",
+ "subtle 2.4.0",
+ "zeroize",
+]
+
 [[package]]
 name = "rust_decimal"
 version = "1.14.2"
@@ -2832,10 +2986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9787e62372fc0c5a0f3af64c392652db72d3ec1cc0cff1becc175d2c11e6fbcc"
 dependencies = [
  "arrayvec",
- "byteorder",
- "bytes 1.0.1",
  "num-traits",
- "postgres",
  "serde",
 ]
 
@@ -3034,6 +3185,19 @@ dependencies = [
  "opaque-debug 0.2.3",
 ]
 
+[[package]]
+name = "sha-1"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug 0.3.0",
+]
+
 [[package]]
 name = "sha1"
 version = "0.6.0"
@@ -3093,6 +3257,18 @@ dependencies = [
  "event-listener",
 ]
 
+[[package]]
+name = "simple_asn1"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eb4ea60fb301dc81dfc113df680571045d375ab7345d171c5dc7d7e13107a80"
+dependencies = [
+ "chrono",
+ "num-bigint 0.4.0",
+ "num-traits",
+ "thiserror",
+]
+
 [[package]]
 name = "siphasher"
 version = "0.3.5"
@@ -3138,6 +3314,112 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
 
+[[package]]
+name = "sqlformat"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d86e3c77ff882a828346ba401a7ef4b8e440df804491c6064fe8295765de71c"
+dependencies = [
+ "lazy_static",
+ "maplit",
+ "nom",
+ "regex",
+ "unicode_categories",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba82f79b31f30acebf19905bcd8b978f46891b9d0723f578447361a8910b6584"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f23af36748ec8ea8d49ef8499839907be41b0b1178a4e82b8cb45d29f531dc9"
+dependencies = [
+ "ahash",
+ "atoi",
+ "base64 0.13.0",
+ "bitflags",
+ "byteorder",
+ "bytes 1.0.1",
+ "chrono",
+ "crc",
+ "crossbeam-channel",
+ "crossbeam-queue",
+ "crossbeam-utils",
+ "digest 0.9.0",
+ "dirs",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "generic-array 0.14.4",
+ "hashlink",
+ "hex",
+ "hmac 0.10.1",
+ "itoa",
+ "libc",
+ "log",
+ "md-5 0.9.1",
+ "memchr",
+ "num-bigint 0.3.2",
+ "once_cell",
+ "parking_lot",
+ "percent-encoding",
+ "rand 0.8.3",
+ "rsa",
+ "rust_decimal",
+ "serde",
+ "serde_json",
+ "sha-1 0.9.6",
+ "sha2 0.9.5",
+ "smallvec",
+ "sqlformat",
+ "sqlx-rt",
+ "stringprep",
+ "thiserror",
+ "url",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e4a2349d1ffd60a03ca0de3f116ba55d7f406e55a0d84c64a5590866d94c06"
+dependencies = [
+ "dotenv",
+ "either",
+ "futures",
+ "heck",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "sha2 0.9.5",
+ "sqlx-core",
+ "sqlx-rt",
+ "syn",
+ "url",
+]
+
+[[package]]
+name = "sqlx-rt"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8199b421ecf3493ee9ef3e7bc90c904844cfb2ea7ea2f57347a93f52bfd3e057"
+dependencies = [
+ "async-native-tls",
+ "async-std",
+ "native-tls",
+]
+
 [[package]]
 name = "stable_deref_trait"
 version = "1.2.0"
@@ -3153,6 +3435,12 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
 [[package]]
 name = "stdweb"
 version = "0.4.20"
@@ -3291,10 +3579,12 @@ dependencies = [
 
 [[package]]
 name = "synth"
-version = "0.5.0"
+version = "0.5.2"
 dependencies = [
  "anyhow",
  "async-std",
+ "async-trait",
+ "beau_collector",
  "chrono",
  "colored",
  "ctrlc",
@@ -3303,6 +3593,7 @@ dependencies = [
  "dirs",
  "env_logger",
  "fs2",
+ "futures",
  "git2",
  "iai",
  "indicatif",
@@ -3310,7 +3601,6 @@ dependencies = [
  "log",
  "mongodb",
  "num_cpus",
- "postgres",
  "posthog-rs",
  "rand 0.8.3",
  "regex",
@@ -3318,6 +3608,7 @@ dependencies = [
  "rust_decimal",
  "serde",
  "serde_json",
+ "sqlx",
  "strsim 0.10.0",
  "structopt",
  "synth-core",
@@ -3409,6 +3700,12 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
 
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
 [[package]]
 name = "tempfile"
 version = "3.2.0"
@@ -3604,7 +3901,7 @@ version = "1.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aea337f72e96efe29acc234d803a5981cd9a2b6ed21655cd7fc21cfe021e8ec7"
 dependencies = [
- "autocfg",
+ "autocfg 1.0.1",
  "bytes 1.0.1",
  "libc",
  "memchr",
@@ -3634,29 +3931,6 @@ dependencies = [
  "tokio 1.6.2",
 ]
 
-[[package]]
-name = "tokio-postgres"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d2b1383c7e4fb9a09e292c7c6afb7da54418d53b045f1c1fac7a911411a2b8b"
-dependencies = [
- "async-trait",
- "byteorder",
- "bytes 1.0.1",
- "fallible-iterator",
- "futures",
- "log",
- "parking_lot",
- "percent-encoding",
- "phf",
- "pin-project-lite 0.2.6",
- "postgres-protocol",
- "postgres-types",
- "socket2 0.4.0",
- "tokio 1.6.2",
- "tokio-util 0.6.7",
-]
-
 [[package]]
 name = "tokio-rustls"
 version = "0.13.1"
@@ -3852,6 +4126,12 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
 
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
 [[package]]
 name = "universal-hash"
 version = "0.4.0"
@@ -4053,6 +4333,16 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "whoami"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4abacf325c958dfeaf1046931d37f2a901b6dfe0968ee965a29e94c6766b2af6"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
 [[package]]
 name = "widestring"
 version = "0.4.3"
@@ -4129,3 +4419,30 @@ dependencies = [
  "winapi 0.2.8",
  "winapi-build",
 ]
+
+[[package]]
+name = "wyz"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
+
+[[package]]
+name = "zeroize"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2c1e130bebaeab2f23886bf9acbaca14b092408c452543c857f66399cd6dab1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
diff --git a/synth/Cargo.toml b/synth/Cargo.toml
index 931226a2..b03870bb 100644
--- a/synth/Cargo.toml
+++ b/synth/Cargo.toml
@@ -43,6 +43,8 @@ sysinfo = "0.15.2"
 strsim = "0.10.0"
 
 async-std = { version = "1.6.4", features = [ "attributes", "unstable" ] }
+async-trait = "0.1.50"
+futures = "0.3.15"
 tide = { version = "0.13.0", optional = true }
 
 reqwest = { version = "0.10", features = ["blocking", "json"], optional = true } # only used by `rlog`
@@ -59,10 +61,13 @@ ctrlc = { version = "3.0", features = ["termination"] }
 synth-core = { path = "../core" }
 synth-gen = { path = "../gen" }
 
-postgres = { version = "0.19.0", features = ["with-chrono-0_4"] }
-rust_decimal = { version = "1.10.3", features = ["db-postgres"] }
+rust_decimal = "1.10.3"
 indicatif = "0.15.0"
 
 dirs = "3.0.2"
 uuid = { version = "0.8.2", features = ["v4"] }
 mongodb = {version = "1.2.1", features = ["sync"] , default-features = false}
+
+sqlx = { version = "0.5.5", features = [ "postgres", "mysql", "runtime-async-std-native-tls", "decimal", "chrono" ] }
+
+beau_collector = "0.2.1"
\ No newline at end of file
diff --git a/synth/src/cli/export.rs b/synth/src/cli/export.rs
index 36c077f3..67bbf3c6 100644
--- a/synth/src/cli/export.rs
+++ b/synth/src/cli/export.rs
@@ -1,11 +1,17 @@
 use crate::cli::postgres::PostgresExportStrategy;
 use crate::cli::stdf::StdoutExportStrategy;
-use anyhow::Result;
+use anyhow::{Result, Context};
 
 use std::str::FromStr;
+use std::convert::TryFrom;
 
 use crate::cli::mongo::MongoExportStrategy;
 use synth_core::{Name, Namespace};
+use serde_json::Value;
+use async_std::task;
+use crate::datasource::DataSource;
+use crate::sampler::Sampler;
+use crate::cli::mysql::MySqlExportStrategy;
 
 pub trait ExportStrategy {
     fn export(self, params: ExportParams) -> Result<()>;
@@ -23,6 +29,7 @@ pub enum SomeExportStrategy {
     StdoutExportStrategy(StdoutExportStrategy),
     FromPostgres(PostgresExportStrategy),
     FromMongo(MongoExportStrategy),
+    FromMySql(MySqlExportStrategy)
 }
 
 impl ExportStrategy for SomeExportStrategy {
@@ -31,6 +38,7 @@ impl ExportStrategy for SomeExportStrategy {
             SomeExportStrategy::StdoutExportStrategy(ses) => ses.export(params),
             SomeExportStrategy::FromPostgres(pes) => pes.export(params),
             SomeExportStrategy::FromMongo(mes) => mes.export(params),
+            SomeExportStrategy::FromMySql(mes) => mes.export(params)
         }
     }
 }
@@ -58,9 +66,50 @@ impl FromStr for SomeExportStrategy {
             return Ok(SomeExportStrategy::FromMongo(MongoExportStrategy {
                 uri: s.to_string(),
             }));
+        } else if s.starts_with("mysql://") {
+            return Ok(SomeExportStrategy::FromMySql(MySqlExportStrategy {
+                uri: s.to_string(),
+            }));
         }
         Err(anyhow!(
             "Data sink not recognized. Was expecting one of 'mongodb' or 'postgres'"
         ))
     }
 }
+
+pub(crate) fn create_and_insert_values<T: DataSource>(params: ExportParams, datasource: &T)
+    -> Result<()> {
+    let sampler = Sampler::try_from(&params.namespace)?;
+    let values =
+        sampler.sample_seeded(params.collection_name.clone(), params.target, params.seed)?;
+
+    match values {
+        Value::Array(collection_json) => {
+            insert_data(datasource, params.collection_name.unwrap().to_string(), &collection_json)
+        }
+        Value::Object(namespace_json) => {
+            for (collection_name, collection_json) in namespace_json {
+                let collection_json = &collection_json
+                    .as_array()
+                    .expect("This is always a collection (sampler contract)");
+
+                insert_data(datasource, collection_name.clone(), &collection_json)?;
+            }
+
+            Ok(())
+        }
+        _ => bail!(
+            "The sampler will never generate a value which is not an array or object (sampler contract)"
+        ),
+    }
+}
+
+fn insert_data<T: DataSource>(datasource: &T, collection_name: String, collection_json: &Vec<Value>)
+    -> Result<()> {
+    task::block_on(
+        datasource.insert_data(
+            collection_name.clone(),
+            collection_json
+        )
+    ).context(format!("Failed to insert data for collection {}", collection_name))
+}
diff --git a/synth/src/cli/import.rs b/synth/src/cli/import.rs
index ae36fd33..9c032090 100644
--- a/synth/src/cli/import.rs
+++ b/synth/src/cli/import.rs
@@ -8,6 +8,7 @@ use std::str::FromStr;
 use synth_core::graph::prelude::{MergeStrategy, OptionalMergeStrategy};
 use synth_core::schema::Namespace;
 use synth_core::{Content, Name};
+use crate::cli::mysql::MySqlImportStrategy;
 
 pub trait ImportStrategy: Sized {
     fn import(self) -> Result<Namespace> {
@@ -26,6 +27,7 @@ pub enum SomeImportStrategy {
     #[allow(unused)]
     FromPostgres(PostgresImportStrategy),
     FromMongo(MongoImportStrategy),
+    FromMySql(MySqlImportStrategy)
 }
 
 impl Default for SomeImportStrategy {
@@ -51,6 +53,10 @@ impl FromStr for SomeImportStrategy {
             return Ok(SomeImportStrategy::FromMongo(MongoImportStrategy {
                 uri: s.to_string(),
             }));
+        } else if s.starts_with("mysql://") {
+            return Ok(SomeImportStrategy::FromMySql(MySqlImportStrategy {
+                uri: s.to_string(),
+            }));
         }
 
         if let Ok(path) = PathBuf::from_str(s) {
@@ -71,6 +77,7 @@ impl ImportStrategy for SomeImportStrategy {
             SomeImportStrategy::FromPostgres(pis) => pis.import(),
             SomeImportStrategy::StdinImportStrategy(sis) => sis.import(),
             SomeImportStrategy::FromMongo(mis) => mis.import(),
+            SomeImportStrategy::FromMySql(mis) => mis.import()
         }
     }
     fn import_collection(self, name: &Name) -> Result<Content> {
@@ -79,6 +86,7 @@ impl ImportStrategy for SomeImportStrategy {
             SomeImportStrategy::FromPostgres(pis) => pis.import_collection(name),
             SomeImportStrategy::StdinImportStrategy(sis) => sis.import_collection(name),
             SomeImportStrategy::FromMongo(mis) => mis.import_collection(name),
+            SomeImportStrategy::FromMySql(mis) => mis.import_collection(name),
         }
     }
     fn into_value(self) -> Result<Value> {
@@ -87,6 +95,7 @@ impl ImportStrategy for SomeImportStrategy {
             SomeImportStrategy::FromPostgres(pis) => pis.into_value(),
             SomeImportStrategy::StdinImportStrategy(sis) => sis.into_value(),
             SomeImportStrategy::FromMongo(mis) => mis.into_value(),
+            SomeImportStrategy::FromMySql(mis) => mis.into_value(),
         }
     }
 }
diff --git a/synth/src/cli/import_utils.rs b/synth/src/cli/import_utils.rs
new file mode 100644
index 00000000..7e88135b
--- /dev/null
+++ b/synth/src/cli/import_utils.rs
@@ -0,0 +1,167 @@
+use crate::datasource::relational_datasource::{ColumnInfo, RelationalDataSource};
+use synth_core::{Namespace, Name, Content};
+use crate::datasource::DataSource;
+use async_std::task;
+use std::str::FromStr;
+use anyhow::{Result, Context};
+use log::debug;
+use synth_core::schema::{FieldRef, NumberContent, Id, SameAsContent, OptionalMergeStrategy, ObjectContent, ArrayContent, RangeStep, OneOfContent, VariantContent, FieldContent};
+use synth_core::schema::content::number_content::U64;
+use serde_json::Value;
+use std::convert::TryFrom;
+
+#[derive(Debug)]
+pub(crate) struct Collection {
+    pub(crate) collection: Content,
+}
+
+/// Wrapper around `FieldContent` since we cant' impl `TryFrom` on a struct in a non-owned crate
+struct FieldContentWrapper(FieldContent);
+
+pub(crate) fn build_namespace_import<T: DataSource + RelationalDataSource>(datasource: &T)
+    -> Result<Namespace> {
+    let table_names = task::block_on(datasource.get_table_names())
+        .context(format!("Failed to get table names"))?;
+
+    let mut namespace = Namespace::default();
+
+    info!("Building namespace collections...");
+    populate_namespace_collections(&mut namespace, &table_names, datasource)?;
+
+    info!("Building namespace primary keys...");
+    populate_namespace_primary_keys(&mut namespace, &table_names, datasource)?;
+
+    info!("Building namespace foreign keys...");
+    populate_namespace_foreign_keys(&mut namespace, datasource)?;
+
+    info!("Building namespace values...");
+    populate_namespace_values(&mut namespace, &table_names, datasource)?;
+
+    Ok(namespace)
+}
+
+fn populate_namespace_collections<T: DataSource + RelationalDataSource>(
+    namespace: &mut Namespace, table_names: &Vec<String>, datasource: &T) -> Result<()> {
+    for table_name in table_names.iter() {
+        info!("Building {} collection...", table_name);
+
+        let column_infos = task::block_on(datasource.get_columns_infos(table_name))?;
+
+        namespace.put_collection(
+            &Name::from_str(&table_name)?,
+            Collection::try_from((datasource, column_infos))?.collection,
+        )?;
+    }
+
+    Ok(())
+}
+
+fn populate_namespace_primary_keys<T: DataSource + RelationalDataSource>(
+    namespace: &mut Namespace, table_names: &Vec<String>, datasource: &T) -> Result<()> {
+    for table_name in table_names.iter() {
+        let primary_keys = task::block_on(datasource.get_primary_keys(table_name))?;
+
+        if primary_keys.len() > 1 {
+            bail!("{} primary keys found at collection {}. Synth does not currently support \
+            composite primary keys.", primary_keys.len(), table_name)
+        }
+
+        if let Some(primary_key) = primary_keys.get(0) {
+            let field = FieldRef::new(&format!(
+                "{}.content.{}",
+                table_name, primary_key.column_name
+            ))?;
+            let node = namespace.get_s_node_mut(&field)?;
+            *node = Content::Number(NumberContent::U64(U64::Id(Id::default())));
+        }
+    }
+
+    Ok(())
+}
+
+fn populate_namespace_foreign_keys<T: DataSource + RelationalDataSource>(
+    namespace: &mut Namespace, datasource: &T) -> Result<()> {
+    let foreign_keys = task::block_on(datasource.get_foreign_keys())?;
+
+    debug!("{} foreign keys found.", foreign_keys.len());
+
+    for fk in foreign_keys {
+        let from_field =
+            FieldRef::new(&format!("{}.content.{}", fk.from_table, fk.from_column))?;
+        let to_field = FieldRef::new(&format!("{}.content.{}", fk.to_table, fk.to_column))?;
+        let node = namespace.get_s_node_mut(&from_field)?;
+        *node = Content::SameAs(SameAsContent { ref_: to_field });
+    }
+
+    Ok(())
+}
+
+fn populate_namespace_values<T: DataSource + RelationalDataSource>(
+    namespace: &mut Namespace, table_names: &Vec<String>, datasource: &T) -> Result<()> {
+    task::block_on(datasource.set_seed())?;
+
+    for table in table_names {
+        let values = task::block_on(datasource.get_deterministic_samples(&table))?;
+
+        namespace.try_update(
+            OptionalMergeStrategy,
+            &Name::from_str(&table).unwrap(),
+            &Value::from(values),
+        )?;
+    }
+
+    Ok(())
+}
+
+impl<T: RelationalDataSource + DataSource> TryFrom<(&T, Vec<ColumnInfo>)> for Collection {
+    type Error = anyhow::Error;
+
+    fn try_from(columns_meta: (&T, Vec<ColumnInfo>)) -> Result<Self> {
+        let mut collection = ObjectContent::default();
+
+        for column_info in columns_meta.1 {
+            let content = FieldContentWrapper::try_from((columns_meta.0, &column_info))?.0;
+
+            collection
+                .fields
+                .insert(column_info.column_name.clone(), content);
+        }
+
+        Ok(Collection {
+            collection: Content::Array(ArrayContent {
+                length: Box::new(Content::Number(NumberContent::U64(U64::Range(RangeStep {
+                    low: 1,
+                    high: 2,
+                    step: 1,
+                })))),
+                content: Box::new(Content::Object(collection)),
+            }),
+        })
+    }
+}
+
+impl<T: RelationalDataSource + DataSource> TryFrom<(&T, &ColumnInfo)> for FieldContentWrapper {
+    type Error = anyhow::Error;
+
+    fn try_from(column_meta: (&T, &ColumnInfo)) -> Result<Self> {
+        let data_type = &column_meta.1.data_type;
+        let mut content= column_meta.0.decode_to_content(data_type, column_meta.1.character_maximum_length)?;
+
+        // This happens because an `optional` field in a Synth schema
+        // won't show up as a key during generation. Whereas what we
+        // want instead is a null field.
+        if column_meta.1.is_nullable {
+            content = Content::OneOf(OneOfContent {
+                variants: vec![
+                    VariantContent::new(content),
+                    VariantContent::new(Content::Null),
+                ],
+            })
+        }
+
+        Ok(FieldContentWrapper(FieldContent {
+            optional: false,
+            content: Box::new(content),
+        }))
+    }
+}
diff --git a/synth/src/cli/mod.rs b/synth/src/cli/mod.rs
index d6d73e73..41df5bd4 100644
--- a/synth/src/cli/mod.rs
+++ b/synth/src/cli/mod.rs
@@ -5,6 +5,8 @@ mod postgres;
 mod stdf;
 mod store;
 mod telemetry;
+mod mysql;
+mod import_utils;
 
 use crate::cli::export::SomeExportStrategy;
 use crate::cli::export::{ExportParams, ExportStrategy};
diff --git a/synth/src/cli/mysql.rs b/synth/src/cli/mysql.rs
new file mode 100644
index 00000000..70cdb56f
--- /dev/null
+++ b/synth/src/cli/mysql.rs
@@ -0,0 +1,46 @@
+use crate::cli::export::{ExportStrategy, ExportParams, create_and_insert_values};
+use crate::datasource::mysql_datasource::MySqlDataSource;
+use crate::datasource::DataSource;
+use anyhow::Result;
+use crate::cli::import::ImportStrategy;
+use synth_core::schema::Namespace;
+use synth_core::{Content, Name};
+use crate::cli::import_utils::build_namespace_import;
+use serde_json::Value;
+
+#[derive(Clone, Debug)]
+pub struct MySqlExportStrategy {
+    pub uri: String,
+}
+
+impl ExportStrategy for MySqlExportStrategy {
+    fn export(self, params: ExportParams) -> Result<()> {
+        let datasource = MySqlDataSource::new(&self.uri)?;
+
+        create_and_insert_values(params, &datasource)
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct MySqlImportStrategy {
+    pub uri: String,
+}
+
+impl ImportStrategy for MySqlImportStrategy {
+    fn import(self) -> Result<Namespace> {
+        let datasource = MySqlDataSource::new(&self.uri)?;
+
+        build_namespace_import(&datasource)
+    }
+
+    fn import_collection(self, name: &Name) -> Result<Content> {
+        self.import()?
+            .collections
+            .remove(name)
+            .ok_or_else(|| anyhow!("Could not find table '{}' in Postgres database.", name))
+    }
+
+    fn into_value(self) -> Result<Value> {
+        bail!("MySql import doesn't support conversion into value")
+    }
+}
diff --git a/synth/src/cli/postgres.rs b/synth/src/cli/postgres.rs
index d5a34830..9cee1d9e 100644
--- a/synth/src/cli/postgres.rs
+++ b/synth/src/cli/postgres.rs
@@ -1,22 +1,12 @@
-use crate::cli::export::{ExportParams, ExportStrategy};
+use crate::cli::export::{ExportParams, ExportStrategy, create_and_insert_values};
 use crate::cli::import::ImportStrategy;
-use anyhow::{Context, Result};
-
-use postgres::{Client, Column, Row};
-use rust_decimal::prelude::ToPrimitive;
-use serde_json::{Map, Number, Value};
-use std::convert::TryFrom;
-
-use crate::sampler::Sampler;
-use std::str::FromStr;
-use synth_core::graph::prelude::{Uuid, VariantContent};
-use synth_core::schema::number_content::*;
-use synth_core::schema::{
-    ArrayContent, BoolContent, ChronoValueType, DateTimeContent, FieldContent, FieldRef, Id,
-    Namespace, NumberContent, ObjectContent, OneOfContent, OptionalMergeStrategy, RangeStep,
-    RegexContent, SameAsContent, StringContent,
-};
+use anyhow::Result;
+use serde_json::Value;
+use synth_core::schema::Namespace;
 use synth_core::{Content, Name};
+use crate::datasource::postgres_datasource::PostgresDataSource;
+use crate::datasource::DataSource;
+use crate::cli::import_utils::build_namespace_import;
 
 #[derive(Clone, Debug)]
 pub struct PostgresExportStrategy {
@@ -25,101 +15,9 @@ pub struct PostgresExportStrategy {
 
 impl ExportStrategy for PostgresExportStrategy {
     fn export(self, params: ExportParams) -> Result<()> {
-        let mut client =
-            Client::connect(&self.uri, postgres::tls::NoTls).expect("Failed to connect");
-
-        let sampler = Sampler::try_from(&params.namespace)?;
-        let values =
-            sampler.sample_seeded(params.collection_name.clone(), params.target, params.seed)?;
-
-        match values {
-            Value::Array(collection_json) => {
-                self.insert_data(params.collection_name.unwrap().to_string(), &collection_json, &mut client)
-            }
-            Value::Object(namespace_json) => {
-                for (collection_name, collection_json) in namespace_json {
-                    self.insert_data(
-                        collection_name,
-                        &collection_json
-                            .as_array()
-                            .expect("This is always a collection (sampler contract)"),
-                        &mut client,
-                    )?;
-                }
-                Ok(())
-            }
-            _ => unreachable!(
-                "The sampler will never generate a value which is not an array or object (sampler contract)"
-            ),
-        }
-    }
-}
-
-impl PostgresExportStrategy {
-    fn insert_data(
-        &self,
-        collection_name: String,
-        collection: &[Value],
-        client: &mut Client,
-    ) -> Result<()> {
-        // how to to ordering here?
-        // If we have foreign key constraints we need to traverse the tree and
-        // figure out insertion order
-        // We basically need something like an InsertionStrategy where we have a DAG of insertions
-        let batch_size = 1000;
-
-        if collection.is_empty() {
-            println!(
-                "Collection {} generated 0 values. Skipping insertion...",
-                collection_name
-            );
-            return Ok(());
-        }
-
-        let column_names = collection
-            .get(0)
-            .expect("Explicit check is done above that this collection is non-empty")
-            .as_object()
-            .expect("This is always an object (sampler contract)")
-            .keys()
-            .cloned()
-            .collect::<Vec<String>>()
-            .join(",");
+        let datasource = PostgresDataSource::new(&self.uri)?;
 
-        for rows in collection.chunks(batch_size) {
-            let mut query = format!(
-                "INSERT INTO {} ({}) VALUES \n",
-                collection_name, column_names
-            );
-
-            for (i, row) in rows.iter().enumerate() {
-                let row_obj = row
-                    .as_object()
-                    .expect("This is always an object (sampler contract)");
-
-                let values = row_obj
-                    .values()
-                    .map(|v| v.to_string().replace("'", "''")) // two single quotes are the standard way to escape double quotes
-                    .map(|v| v.replace("\"", "'")) // values in the object have quotes around them by default.
-                    .collect::<Vec<String>>()
-                    .join(",");
-
-                // We should be using some form of a prepared statement here.
-                // It is not clear how this would work for our batch inserts...
-                if i == rows.len() - 1 {
-                    query.push_str(&format!("({});\n", values));
-                } else {
-                    query.push_str(&format!("({}),\n", values));
-                }
-            }
-
-            if let Err(err) = client.query(query.as_str(), &[]) {
-                println!("Error at: {}", query);
-                return Err(err.into());
-            }
-        }
-        println!("Inserted {} rows...", collection.len());
-        Ok(())
+        create_and_insert_values(params, &datasource)
     }
 }
 
@@ -130,139 +28,9 @@ pub struct PostgresImportStrategy {
 
 impl ImportStrategy for PostgresImportStrategy {
     fn import(self) -> Result<Namespace> {
-        let mut client =
-            Client::connect(&self.uri, postgres::tls::NoTls).expect("Failed to connect");
-
-        let catalog = self
-            .uri
-            .split('/')
-            .last()
-            .ok_or_else(|| anyhow!("Cannot import data. No catalog specified in the uri"))?;
-
-        let query ="SELECT table_name FROM information_schema.tables where table_catalog = $1 and table_schema = 'public' and table_type = 'BASE TABLE'";
-
-        let table_names: Vec<String> = client
-            .query(query, &[&catalog])
-            .expect("Failed to get tables")
-            .iter()
-            .map(|row| row.get(0))
-            .collect();
-
-        let mut namespace = Namespace::default();
-
-        // First pass - build naive Collections
-        // Now we can execute a simple statement that just returns its parameter.
-        for table_name in table_names.iter() {
-            println!("Building {} collection...", table_name);
-            // Get columns
-            let col_info_query = r"select column_name, ordinal_position, is_nullable, udt_name, character_maximum_length
-                 from information_schema.columns 
-                 where table_name = $1 and table_catalog = $2;";
+        let datasource = PostgresDataSource::new(&self.uri)?;
 
-            let column_info: Vec<ColumnInfo> = client
-                .query(col_info_query, &[&table_name, &catalog])
-                .unwrap_or_else(|_| panic!("Failed to get columns for {}", table_name))
-                .into_iter()
-                .map(ColumnInfo::try_from)
-                .collect::<Result<Vec<ColumnInfo>>>()?;
-
-            namespace.put_collection(
-                &Name::from_str(&table_name)?,
-                Collection::from(column_info).collection,
-            )?;
-        }
-
-        // Second pass - build primary keys
-        println!("Building primary keys...");
-        for table_name in table_names.iter() {
-            // Unfortunately cannot use parameterised queries here
-            let pk_query: &str = &format!(
-                r"SELECT a.attname, format_type(a.atttypid, a.atttypmod) AS data_type
-                    FROM   pg_index i
-                    JOIN   pg_attribute a ON a.attrelid = i.indrelid
-                                         AND a.attnum = ANY(i.indkey)
-                    WHERE  i.indrelid = '{}'::regclass
-                    AND    i.indisprimary;",
-                &table_name
-            );
-
-            let primary_keys = client
-                .query(pk_query, &[])
-                .unwrap_or_else(|_| panic!("Failed to get primary keys for {}", table_name))
-                .into_iter()
-                .map(PrimaryKey::try_from)
-                .collect::<Result<Vec<PrimaryKey>>>()?;
-
-            if primary_keys.len() > 1 {
-                unimplemented!("Synth does not support composite primary keys")
-            }
-
-            if let Some(primary_key) = primary_keys.get(0) {
-                let field = FieldRef::new(&format!(
-                    "{}.content.{}",
-                    table_name, primary_key.column_name
-                ))?;
-                let node = namespace.get_s_node_mut(&field)?;
-                *node = Content::Number(NumberContent::U64(U64::Id(Id::default())));
-            }
-        }
-
-        // Third pass - foreign keys
-        println!("Building foreign keys...");
-        let fk_query: &str = r"SELECT
-                    tc.table_name, kcu.column_name,
-                    ccu.table_name AS foreign_table_name,
-                    ccu.column_name AS foreign_column_name
-                FROM information_schema.table_constraints AS tc
-                    JOIN information_schema.key_column_usage 
-                        AS kcu ON tc.constraint_name = kcu.constraint_name
-                    JOIN information_schema.constraint_column_usage 
-                        AS ccu ON ccu.constraint_name = tc.constraint_name
-                WHERE constraint_type = 'FOREIGN KEY';";
-
-        let foreign_keys = client
-            .query(fk_query, &[])
-            .expect("Failed to get foreign keys")
-            .into_iter()
-            .map(ForeignKey::try_from)
-            .collect::<Result<Vec<ForeignKey>>>()?;
-
-        println!("{} foreign keys found.", foreign_keys.len());
-
-        for fk in foreign_keys {
-            let from_field =
-                FieldRef::new(&format!("{}.content.{}", fk.from_table, fk.from_column))?;
-            let to_field = FieldRef::new(&format!("{}.content.{}", fk.to_table, fk.to_column))?;
-            let node = namespace.get_s_node_mut(&from_field)?;
-            *node = Content::SameAs(SameAsContent { ref_: to_field });
-        }
-
-        // Set the RNG to get a deterministic sample
-        let _ = client
-            .query("select setseed(0.5);", &[])
-            .expect("Failed to set seed for sampling");
-
-        // Fourth pass - ingest
-        for table in table_names {
-            print!("Ingesting data for table {}... ", table);
-
-            // Again parameterised queries don't work here
-            let data_query: &str = &format!("select * from {} order by random() limit 10;", table);
-            let data = client
-                .query(data_query, &[])
-                .expect("Failed to retrieve data");
-            println!(" {} rows done.", data.len());
-
-            let values: Values = Values::try_from(data).context(format!("at table {}", table))?;
-
-            namespace.try_update(
-                OptionalMergeStrategy,
-                &Name::from_str(&table).unwrap(),
-                &Value::from(values.0),
-            )?;
-        }
-
-        Ok(namespace)
+        build_namespace_import(&datasource)
     }
 
     fn import_collection(self, name: &Name) -> Result<Content> {
@@ -273,255 +41,7 @@ impl ImportStrategy for PostgresImportStrategy {
     }
 
     fn into_value(self) -> Result<Value> {
-        unreachable!()
+        bail!("Postgres import doesn't support conversion into value")
     }
 }
 
-// Wrapper around content
-#[derive(Debug)]
-struct Collection {
-    collection: Content,
-}
-
-// Wrapper around rows
-#[derive(Debug)]
-struct Values(Vec<Value>);
-
-#[derive(Debug)]
-struct ColumnInfo {
-    column_name: String,
-    ordinal_position: i32,
-    is_nullable: bool,
-    udt_name: String,
-    character_maximum_length: Option<i32>,
-}
-
-#[derive(Debug)]
-struct PrimaryKey {
-    column_name: String,
-    type_name: String,
-}
-
-#[derive(Debug)]
-struct ForeignKey {
-    from_table: String,
-    from_column: String,
-    to_table: String,
-    to_column: String,
-}
-
-impl TryFrom<Vec<Row>> for Values {
-    type Error = anyhow::Error;
-
-    // Need to check for nulls here.
-    // Not sure how we're going to do this. We need some macro to try_get, and if it fails
-    // go for content::null
-    // https://kotiri.com/2018/01/31/postgresql-diesel-rust-types.html
-    fn try_from(rows: Vec<Row>) -> Result<Self, Self::Error> {
-        let mut values = Vec::new();
-        for row in rows {
-            let mut obj_content = Map::new();
-            for i in 0..row.columns().len() {
-                let column = row
-                    .columns()
-                    .get(i)
-                    .expect("Cannot go out of range here since iterator is bound by length");
-                let value = try_match_value(&row, i, column).unwrap_or(Value::Null);
-                obj_content.insert(column.name().to_string(), value);
-            }
-            values.push(Value::Object(obj_content));
-        }
-        Ok(Values(values))
-    }
-}
-
-fn try_match_value(row: &Row, i: usize, column: &Column) -> Result<Value> {
-    let value = match column.type_().name() {
-        "bool" => Value::Bool(row.try_get(i)?),
-        "oid" => {
-            unimplemented!()
-        }
-        "char" | "varchar" | "text" | "bpchar" | "name" | "unknown" => {
-            Value::String(row.try_get(i)?)
-        }
-        "int2" => Value::Number(Number::from(row.try_get::<_, i16>(i)?)),
-        "int4" => Value::Number(Number::from(row.try_get::<_, i32>(i)?)),
-        "int8" => Value::Number(Number::from(row.try_get::<_, i64>(i)?)),
-        "float4" => Value::Number(
-            Number::from_f64(row.try_get(i)?).expect("Cloud not convert to f64. Value was NaN."),
-        ),
-        "float8" => Value::Number(
-            Number::from_f64(row.try_get(i)?).expect("Cloud not convert to f64. Value was NaN."),
-        ),
-        "numeric" => {
-            let as_decimal: rust_decimal::Decimal = row.try_get(i)?;
-            Value::Number(
-                Number::from_f64(
-                    as_decimal
-                        .to_f64()
-                        .expect("Could not convert decimal to f64 for reasons todo"),
-                )
-                .expect("Cloud not convert to f64. Value was NaN."),
-            )
-        }
-        "timestampz" => Value::String(row.try_get(i)?),
-        "timestamp" => Value::String(row.try_get(i)?),
-        "date" => Value::String(format!("{}", row.try_get::<_, chrono::NaiveDate>(i)?)),
-        _ => {
-            return Err(anyhow!(
-                "Could not convert value. Converter not implemented for {}",
-                column.type_().name()
-            ));
-        }
-    };
-    Ok(value)
-}
-
-impl TryFrom<postgres::Row> for ForeignKey {
-    type Error = anyhow::Error;
-
-    fn try_from(value: Row) -> Result<Self, Self::Error> {
-        Ok(ForeignKey {
-            from_table: value.try_get(0)?,
-            from_column: value.try_get(1)?,
-            to_table: value.try_get(2)?,
-            to_column: value.try_get(3)?,
-        })
-    }
-}
-
-impl TryFrom<postgres::Row> for PrimaryKey {
-    type Error = anyhow::Error;
-
-    fn try_from(row: Row) -> Result<Self, Self::Error> {
-        Ok(PrimaryKey {
-            column_name: row.try_get(0)?,
-            type_name: row.try_get(1)?,
-        })
-    }
-}
-
-impl TryFrom<postgres::Row> for ColumnInfo {
-    type Error = anyhow::Error;
-
-    fn try_from(row: Row) -> Result<Self, Self::Error> {
-        Ok(ColumnInfo {
-            column_name: row.try_get(0)?,
-            ordinal_position: row.try_get(1)?,
-            is_nullable: row.try_get::<_, String>(2)? == *"YES",
-            udt_name: row.try_get(3)?,
-            character_maximum_length: row.try_get(4)?,
-        })
-    }
-}
-
-impl From<Vec<ColumnInfo>> for Collection {
-    fn from(columns: Vec<ColumnInfo>) -> Self {
-        let mut collection = ObjectContent::default();
-
-        for column in columns {
-            collection
-                .fields
-                .insert(column.column_name.clone(), column.into());
-        }
-
-        Collection {
-            collection: Content::Array(ArrayContent {
-                length: Box::new(Content::Number(NumberContent::U64(U64::Range(RangeStep {
-                    low: 1,
-                    high: 2,
-                    step: 1,
-                })))),
-                content: Box::new(Content::Object(collection)),
-            }),
-        }
-    }
-}
-
-// Type conversions: https://docs.rs/postgres-types/0.2.0/src/postgres_types/lib.rs.html#360
-impl From<ColumnInfo> for FieldContent {
-    fn from(column: ColumnInfo) -> Self {
-        let mut content = match column.udt_name.as_ref() {
-            "bool" => Content::Bool(BoolContent::default()),
-            "oid" => {
-                unimplemented!()
-            }
-            "char" | "varchar" | "text" | "bpchar" | "name" | "unknown" => {
-                let pattern = "[a-zA-Z0-9]{0, {}}".replace(
-                    "{}",
-                    &format!("{}", column.character_maximum_length.unwrap_or(1)),
-                );
-                Content::String(StringContent::Pattern(
-                    RegexContent::pattern(pattern).expect("pattern will always compile"),
-                ))
-            }
-            "int2" => Content::Number(NumberContent::I64(I64::Range(RangeStep {
-                low: 0,
-                high: 1,
-                step: 1,
-            }))),
-            "int4" => Content::Number(NumberContent::I64(I64::Range(RangeStep {
-                low: 0,
-                high: 1,
-                step: 1,
-            }))),
-            "int8" => Content::Number(NumberContent::I64(I64::Range(RangeStep {
-                low: 1,
-                high: 1,
-                step: 1,
-            }))),
-            "float4" => Content::Number(NumberContent::F64(F64::Range(RangeStep {
-                low: 0.0,
-                high: 1.0,
-                step: 0.1, //todo
-            }))),
-            "float8" => Content::Number(NumberContent::F64(F64::Range(RangeStep {
-                low: 0.0,
-                high: 1.0,
-                step: 0.1, //todo
-            }))),
-            "numeric" => Content::Number(NumberContent::F64(F64::Range(RangeStep {
-                low: 0.0,
-                high: 1.0,
-                step: 0.1, //todo
-            }))),
-            "timestampz" => Content::String(StringContent::DateTime(DateTimeContent {
-                format: "".to_string(), // todo
-                type_: ChronoValueType::DateTime,
-                begin: None,
-                end: None,
-            })),
-            "timestamp" => Content::String(StringContent::DateTime(DateTimeContent {
-                format: "".to_string(), // todo
-                type_: ChronoValueType::NaiveDateTime,
-                begin: None,
-                end: None,
-            })),
-            "date" => Content::String(StringContent::DateTime(DateTimeContent {
-                format: "%Y-%m-%d".to_string(),
-                type_: ChronoValueType::NaiveDate,
-                begin: None,
-                end: None,
-            })),
-            "uuid" => Content::String(StringContent::Uuid(Uuid)),
-            _ => unimplemented!("We haven't implemented a converter for {}", column.udt_name),
-        };
-
-        // This happens because an `optional` field in a Synth schema
-        // won't show up as a key during generation. Whereas what we
-        // want instead is a null field.
-        if column.is_nullable {
-            content = Content::OneOf(OneOfContent {
-                variants: vec![
-                    VariantContent::new(content),
-                    VariantContent::new(Content::Null),
-                ],
-            })
-        }
-
-        FieldContent {
-            optional: false,
-            content: Box::new(content),
-        }
-    }
-}
diff --git a/synth/src/datasource/mod.rs b/synth/src/datasource/mod.rs
new file mode 100644
index 00000000..f301938c
--- /dev/null
+++ b/synth/src/datasource/mod.rs
@@ -0,0 +1,23 @@
+use serde_json::Value;
+use anyhow::Result;
+use async_trait::async_trait;
+
+pub(crate) mod relational_datasource;
+pub(crate) mod postgres_datasource;
+pub(crate) mod mysql_datasource;
+
+/// This trait encompasses all data source types, whether it's SQL or No-SQL. APIs should be defined
+/// async when possible, delegating to the caller on how to handle it. Data source specific
+/// implementations should be defined within the implementing struct.
+#[async_trait]
+pub trait DataSource {
+    type ConnectParams;
+
+    fn new(connect_params: &Self::ConnectParams) -> Result<Self> where Self: Sized;
+
+    async fn insert_data(
+        &self,
+        collection_name: String,
+        collection: &[Value],
+    ) -> Result<()>;
+}
\ No newline at end of file
diff --git a/synth/src/datasource/mysql_datasource.rs b/synth/src/datasource/mysql_datasource.rs
new file mode 100644
index 00000000..4f2d605a
--- /dev/null
+++ b/synth/src/datasource/mysql_datasource.rs
@@ -0,0 +1,319 @@
+use sqlx::{Pool, MySql, Row, Column, TypeInfo};
+use anyhow::{Result, Context};
+use crate::datasource::DataSource;
+use async_std::task;
+use sqlx::mysql::{MySqlPoolOptions, MySqlQueryResult, MySqlRow, MySqlColumn};
+use serde_json::{Value, Map, Number};
+use async_trait::async_trait;
+use crate::datasource::relational_datasource::{RelationalDataSource, ColumnInfo, PrimaryKey, ForeignKey, ValueWrapper};
+use std::prelude::rust_2015::Result::Ok;
+use std::convert::TryFrom;
+use synth_core::Content;
+use rust_decimal::Decimal;
+use rust_decimal::prelude::ToPrimitive;
+use synth_core::schema::{StringContent, RegexContent, NumberContent, RangeStep, DateTimeContent, ChronoValueType};
+use synth_core::schema::number_content::{I64, F64, U64};
+
+/// TODO
+/// Known issues:
+/// - MySql aliases bool and boolean data types as tinyint. We currently define all tinyint as i8.
+///   Ideally, the user can define a way to force certain fields as bool rather than i8.
+
+pub struct MySqlDataSource {
+    pool: Pool<MySql>,
+    connect_params: String
+}
+
+#[async_trait]
+impl DataSource for MySqlDataSource {
+    type ConnectParams = String;
+
+    fn new(connect_params: &Self::ConnectParams) -> Result<Self> {
+        task::block_on(async {
+            let pool = MySqlPoolOptions::new()
+                .max_connections(3) //TODO expose this as a user config?
+                .connect(connect_params.as_str())
+                .await?;
+
+            Ok::<Self, anyhow::Error>(MySqlDataSource {
+                pool,
+                connect_params: connect_params.to_string()
+            })
+        })
+    }
+
+    async fn insert_data(&self, collection_name: String, collection: &[Value]) -> Result<()> {
+        self.insert_relational_data(collection_name, collection).await.unwrap();
+        Ok(())
+    }
+}
+
+#[async_trait]
+impl RelationalDataSource for MySqlDataSource {
+    type QueryResult = MySqlQueryResult;
+
+    async fn execute_query(&self, query: String, query_params: Vec<&str>) -> Result<MySqlQueryResult> {
+        let mut query = sqlx::query(query.as_str());
+
+        for param in query_params {
+            query = query.bind(param);
+        }
+
+        let result = query
+            .execute(&self.pool)
+            .await?;
+
+        Ok(result)
+    }
+
+    fn get_catalog(&self) -> Result<&str> {
+        self.connect_params
+            .split('/')
+            .last()
+            .ok_or_else(|| anyhow!("No catalog specified in the uri"))
+    }
+
+    async fn get_table_names(&self) -> Result<Vec<String>> {
+        let query =
+            r"SELECT table_name FROM information_schema.tables
+            WHERE table_schema = ? and table_type = 'BASE TABLE'";
+
+        let table_names: Vec<String> = sqlx::query(query)
+            .bind(self.get_catalog()?)
+            .fetch_all(&self.pool)
+            .await?
+            .iter()
+            .map(|row| row.get::<String, usize>(0))
+            .collect();
+
+        Ok(table_names)
+    }
+
+    async fn get_columns_infos(&self, table_name: &str) -> Result<Vec<ColumnInfo>> {
+        let query =
+            r"SELECT column_name, ordinal_position, is_nullable, data_type,
+            character_maximum_length
+            FROM information_schema.columns
+            WHERE table_name = ? AND table_schema = ?";
+
+        let column_infos = sqlx::query(query)
+            .bind(table_name)
+            .bind(self.get_catalog()?)
+            .fetch_all(&self.pool)
+            .await?
+            .into_iter()
+            .map(ColumnInfo::try_from)
+            .collect::<Result<Vec<ColumnInfo>>>()?;
+
+        Ok(column_infos)
+    }
+
+    async fn get_primary_keys(&self, table_name: &str) -> Result<Vec<PrimaryKey>> {
+        let query: &str =
+            r"SELECT column_name, data_type
+            FROM information_schema.columns
+            WHERE table_schema = ? AND table_name = ? AND column_key = 'PRI'";
+
+        sqlx::query(query)
+            .bind(self.get_catalog()?)
+            .bind(table_name)
+            .fetch_all(&self.pool)
+            .await?
+            .into_iter()
+            .map(PrimaryKey::try_from)
+            .collect::<Result<Vec<PrimaryKey>>>()
+    }
+
+    async fn get_foreign_keys(&self) -> Result<Vec<ForeignKey>> {
+        let query: &str =
+            r"SELECT table_name, column_name, referenced_table_name, referenced_column_name
+            FROM information_schema.key_column_usage
+            WHERE referenced_table_schema = ?";
+
+        sqlx::query(query)
+            .bind(self.get_catalog()?)
+            .fetch_all(&self.pool)
+            .await?
+            .into_iter()
+            .map(ForeignKey::try_from)
+            .collect::<Result<Vec<ForeignKey>>>()
+    }
+
+    async fn set_seed(&self) -> Result<()> {
+        // MySql doesn't set seed in a separate query
+        Ok(())
+    }
+
+    async fn get_deterministic_samples(&self, table_name: &str) -> Result<Vec<Value>> {
+        let query: &str = &format!("SELECT * FROM {} ORDER BY rand(0.5) LIMIT 10", table_name);
+
+        let values = sqlx::query(query)
+            .fetch_all(&self.pool)
+            .await?
+            .into_iter()
+            .map(ValueWrapper::try_from)
+            .map(|v| {
+                match v {
+                    Ok(wrapper) => Ok(wrapper.0),
+                    Err(e) => bail!("Failed to convert to value wrapper from query results: {:?}", e)
+                }
+            })
+            .collect::<Result<Vec<Value>>>()?;
+
+        Ok(values)
+    }
+
+    fn decode_to_content(&self, data_type: &str, char_max_len: Option<i32>) -> Result<Content> {
+        let content = match data_type.to_lowercase().as_str() {
+            "char" | "varchar" | "text" | "binary" | "varbinary" | "enum" | "set" => {
+                let pattern = "[a-zA-Z0-9]{0, {}}".replace(
+                    "{}",
+                    &format!("{}", char_max_len.unwrap_or(1)),
+                );
+                Content::String(StringContent::Pattern(
+                    RegexContent::pattern(pattern).context("pattern will always compile")?,
+                ))
+            },
+            "int" | "integer" | "tinyint" | "smallint" | "mediumint" | "bigint" =>
+                Content::Number(NumberContent::I64(I64::Range(RangeStep {
+                    low: 0,
+                    high: 1,
+                    step: 1,
+                }))),
+            "serial" => Content::Number(NumberContent::U64(U64::Range(RangeStep {
+                low: 0,
+                high: 1,
+                step: 1,
+            }))),
+            "float" | "double" | "numeric" | "decimal" =>
+                Content::Number(NumberContent::F64(F64::Range(RangeStep {
+                low: 0.0,
+                high: 1.0,
+                step: 0.1, //todo
+            }))),
+            "timestamp" => Content::String(StringContent::DateTime(DateTimeContent {
+                format: "".to_string(), // todo
+                type_: ChronoValueType::NaiveDateTime,
+                begin: None,
+                end: None,
+            })),
+            "date" => Content::String(StringContent::DateTime(DateTimeContent {
+                format: "%Y-%m-%d".to_string(),
+                type_: ChronoValueType::NaiveDate,
+                begin: None,
+                end: None,
+            })),
+            "datetime" => Content::String(StringContent::DateTime(DateTimeContent {
+                format: "%Y-%m-%d %H:%M:%S".to_string(),
+                type_: ChronoValueType::NaiveDateTime,
+                begin: None,
+                end: None,
+            })),
+            "time" => Content::String(StringContent::DateTime(DateTimeContent {
+                format: "%H:%M:%S".to_string(),
+                type_: ChronoValueType::NaiveTime,
+                begin: None,
+                end: None,
+            })),
+            _ => bail!("We haven't implemented a converter for {}", data_type),
+        };
+
+        Ok(content)
+    }
+}
+
+impl TryFrom<MySqlRow> for ColumnInfo {
+    type Error = anyhow::Error;
+
+    fn try_from(row: MySqlRow) -> Result<Self, Self::Error> {
+        Ok(ColumnInfo {
+            column_name: row.try_get::<String, usize>(0)?,
+            ordinal_position: row.try_get::<u32, usize>(1)? as i32,
+            is_nullable: row.try_get::<String, usize>(2)? == *"YES",
+            data_type: row.try_get::<String, usize>(3)?,
+            character_maximum_length: row.try_get::<Option<i32>, usize>(4)?,
+        })
+    }
+}
+
+impl TryFrom<MySqlRow> for PrimaryKey {
+    type Error = anyhow::Error;
+
+    fn try_from(row: MySqlRow) -> Result<Self, Self::Error> {
+        Ok(PrimaryKey {
+            column_name: row.try_get::<String, usize>(0)?,
+            type_name: row.try_get::<String, usize>(1)?,
+        })
+    }
+}
+
+impl TryFrom<MySqlRow> for ForeignKey {
+    type Error = anyhow::Error;
+
+    fn try_from(row: MySqlRow) -> Result<Self, Self::Error> {
+        Ok(ForeignKey {
+            from_table: row.try_get::<String, usize>(0)?,
+            from_column: row.try_get::<String, usize>(1)?,
+            to_table: row.try_get::<String, usize>(2)?,
+            to_column: row.try_get::<String, usize>(3)?
+        })
+    }
+}
+
+impl TryFrom<MySqlRow> for ValueWrapper {
+    type Error = anyhow::Error;
+
+    fn try_from(row: MySqlRow) -> Result<Self, Self::Error> {
+        let mut json_kv = Map::new();
+
+        for column in row.columns() {
+            let value = try_match_value(&row, column).unwrap_or(Value::Null);
+            json_kv.insert(column.name().to_string(), value);
+        }
+
+        Ok(ValueWrapper(Value::Object(json_kv)))
+    }
+}
+
+fn try_match_value(row: &MySqlRow, column: &MySqlColumn) -> Result<Value> {
+    let value = match column.type_info().name().to_lowercase().as_str() {
+        "char" | "varchar" | "text" | "binary" | "varbinary" | "enum" | "set" => {
+            Value::String(row.try_get::<String, &str>(column.name())?)
+        }
+        "tinyint" => Value::Number(Number::from(row.try_get::<i8, &str>(column.name())?)),
+        "smallint" => Value::Number(Number::from(row.try_get::<i16, &str>(column.name())?)),
+        "mediumint" | "int" | "integer" => Value::Number(Number::from(row.try_get::<i32, &str>(column.name())?)),
+        "bigint" => Value::Number(Number::from(row.try_get::<i64, &str>(column.name())?)),
+        "serial" => Value::Number(Number::from(row.try_get::<u64, &str>(column.name())?)),
+        "float" => Value::Number(Number::from_f64(row.try_get::<f32, &str>(column.name())? as f64)
+            .ok_or(anyhow!("Failed to convert float data type"))?),
+        "double" => Value::Number(Number::from_f64(row.try_get::<f64, &str>(column.name())?)
+            .ok_or(anyhow!("Failed to convert double data type"))?),
+        "numeric" | "decimal" => {
+            let as_decimal = row.try_get::<Decimal, &str>(column.name())?;
+
+            if let Some(truncated) = as_decimal.to_f64() {
+                if let Some(json_number) =  Number::from_f64(truncated) {
+                    return Ok(Value::Number(json_number));
+                }
+            }
+
+            bail!("Failed to convert Postgresql numeric data type to 64 bit float")
+        }
+        "timestamp" => Value::String(
+            row.try_get::<String, &str>(column.name())?),
+        "date" => Value::String(format!("{}", row.try_get::<chrono::NaiveDate, &str>(column.name())?)),
+        "datetime" => Value::String(
+            format!("{}", row.try_get::<chrono::NaiveDateTime, &str>(column.name())?)),
+        "time" => Value::String(
+            format!("{}", row.try_get::<chrono::NaiveTime, &str>(column.name())?)),
+        _ => {
+            bail!(
+                "Could not convert value. Converter not implemented for {}",
+                column.type_info().name()
+            );
+        }
+    };
+
+    Ok(value)
+}
\ No newline at end of file
diff --git a/synth/src/datasource/postgres_datasource.rs b/synth/src/datasource/postgres_datasource.rs
new file mode 100644
index 00000000..cce0cf06
--- /dev/null
+++ b/synth/src/datasource/postgres_datasource.rs
@@ -0,0 +1,336 @@
+use crate::datasource::DataSource;
+use anyhow::{Result, Context};
+use crate::datasource::relational_datasource::{RelationalDataSource, ColumnInfo, PrimaryKey, ForeignKey, ValueWrapper};
+use sqlx::{Pool, Postgres, Row, Column, TypeInfo};
+use sqlx::postgres::{PgPoolOptions, PgQueryResult, PgRow, PgColumn};
+use serde_json::{Value, Map, Number};
+use async_std::task;
+use async_trait::async_trait;
+use std::convert::TryFrom;
+use rust_decimal::Decimal;
+use rust_decimal::prelude::ToPrimitive;
+use synth_core::Content;
+use synth_core::schema::{BoolContent, StringContent, RegexContent, NumberContent, RangeStep, DateTimeContent, ChronoValueType, Uuid};
+use synth_core::schema::number_content::{I64, F64};
+
+pub struct PostgresDataSource {
+    pool: Pool<Postgres>,
+    single_thread_pool: Pool<Postgres>,
+    connect_params: String
+}
+
+#[async_trait]
+impl DataSource for PostgresDataSource {
+    type ConnectParams = String;
+
+    fn new(connect_params: &Self::ConnectParams) -> Result<Self> {
+        task::block_on(async {
+            let pool = PgPoolOptions::new()
+                .max_connections(3) //TODO expose this as a user config?
+                .connect(connect_params.as_str())
+                .await?;
+
+            // Needed for queries that require explicit synchronous order, i.e. setseed + random
+            let single_thread_pool = PgPoolOptions::new()
+                .max_connections(1)
+                .connect(connect_params.as_str())
+                .await?;
+
+            Ok::<Self, anyhow::Error>(PostgresDataSource {
+                pool,
+                single_thread_pool,
+                connect_params: connect_params.to_string()
+            })
+        })
+    }
+
+    async fn insert_data(&self, collection_name: String, collection: &[Value]) -> Result<()> {
+        self.insert_relational_data(collection_name, collection).await.unwrap();
+        Ok(())
+    }
+}
+
+#[async_trait]
+impl RelationalDataSource for PostgresDataSource {
+    type QueryResult = PgQueryResult;
+
+    async fn execute_query(&self, query: String, query_params: Vec<&str>) -> Result<PgQueryResult> {
+        let mut query = sqlx::query(query.as_str());
+
+        for param in query_params {
+            query = query.bind(param);
+        }
+
+        let result = query
+            .execute(&self.pool)
+            .await?;
+
+        Ok(result)
+    }
+
+    fn get_catalog(&self) -> Result<&str> {
+        self.connect_params
+            .split('/')
+            .last()
+            .ok_or_else(|| anyhow!("No catalog specified in the uri"))
+    }
+
+    async fn get_table_names(&self) -> Result<Vec<String>> {
+        let query = r"SELECT table_name
+        FROM information_schema.tables
+        WHERE table_catalog = $1 AND table_schema = 'public' AND table_type = 'BASE TABLE'";
+
+        sqlx::query(query)
+            .bind(self.get_catalog()?)
+            .fetch_all(&self.pool)
+            .await?
+            .iter()
+            .map(|row| row.try_get::<String, usize>(0).map_err(|e| anyhow!("{:?}", e)))
+            .collect()
+    }
+
+    async fn get_columns_infos(&self, table_name: &str) -> Result<Vec<ColumnInfo>> {
+        let query = r"SELECT column_name, ordinal_position, is_nullable, udt_name,
+        character_maximum_length
+        FROM information_schema.columns
+        WHERE table_name = $1 AND table_catalog = $2";
+
+        sqlx::query(query)
+            .bind(table_name)
+            .bind(self.get_catalog()?)
+            .fetch_all(&self.pool)
+            .await?
+            .into_iter()
+            .map(ColumnInfo::try_from)
+            .collect::<Result<Vec<ColumnInfo>>>()
+    }
+
+    async fn get_primary_keys(&self, table_name: &str) -> Result<Vec<PrimaryKey>> {
+        // Unfortunately cannot use parameterised queries here
+        let query: &str = &format!(
+            r"SELECT a.attname, format_type(a.atttypid, a.atttypmod) AS data_type
+            FROM pg_index i
+            JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
+            WHERE  i.indrelid = '{}'::regclass AND i.indisprimary",
+            &table_name
+        );
+
+        sqlx::query(query)
+            .fetch_all(&self.pool)
+            .await?
+            .into_iter()
+            .map(PrimaryKey::try_from)
+            .collect::<Result<Vec<PrimaryKey>>>()
+    }
+
+    async fn get_foreign_keys(&self) -> Result<Vec<ForeignKey>> {
+        let query: &str = 
+            r"SELECT tc.table_name, kcu.column_name, ccu.table_name AS foreign_table_name, 
+            ccu.column_name AS foreign_column_name 
+            FROM information_schema.table_constraints AS tc 
+            JOIN information_schema.key_column_usage AS kcu 
+            ON tc.constraint_name = kcu.constraint_name
+            JOIN information_schema.constraint_column_usage AS ccu 
+            ON ccu.constraint_name = tc.constraint_name
+            WHERE constraint_type = 'FOREIGN KEY'";
+
+        sqlx::query(query)
+            .fetch_all(&self.pool)
+            .await?
+            .into_iter()
+            .map(ForeignKey::try_from)
+            .collect::<Result<Vec<ForeignKey>>>()
+    }
+
+    /// Must use the singled threaded pool when setting this in conjunction with random, called by
+    /// [get_deterministic_samples]. Otherwise, expect endless facepalms (-_Q)
+    async fn set_seed(&self) -> Result<()> {
+        sqlx::query("SELECT setseed(0.5)").execute(&self.single_thread_pool).await?;
+        Ok(())
+    }
+
+    /// Must use the singled threaded pool when setting this in conjunction with setseed, called by
+    /// [set_seed]. Otherwise, expect big regrets :(
+    async fn get_deterministic_samples(&self, table_name: &str) -> Result<Vec<Value>> {
+        let query: &str = &format!("SELECT * FROM {} ORDER BY random() LIMIT 10", table_name);
+
+        let values = sqlx::query(query)
+            .fetch_all(&self.single_thread_pool)
+            .await?
+            .into_iter()
+            .map(ValueWrapper::try_from)
+            .map(|v| {
+                match v {
+                    Ok(wrapper) => Ok(wrapper.0),
+                    Err(e) => bail!("Failed to convert to value wrapper from query results: {:?}", e)
+                }
+            })
+            .collect::<Result<Vec<Value>>>()?;
+
+        Ok(values)
+    }
+
+    fn decode_to_content(&self, data_type: &str, char_max_len: Option<i32>) -> Result<Content> {
+        let content = match data_type.to_lowercase().as_str() {
+            "bool" => Content::Bool(BoolContent::default()),
+            "oid" => {
+                bail!("OID data type not supported")
+            }
+            "char" | "varchar" | "text" | "bpchar" | "name" | "unknown" => {
+                let pattern = "[a-zA-Z0-9]{0, {}}".replace(
+                    "{}",
+                    &format!("{}", char_max_len.unwrap_or(1)),
+                );
+                Content::String(StringContent::Pattern(
+                    RegexContent::pattern(pattern).context("pattern will always compile")?,
+                ))
+            }
+            "int2" => Content::Number(NumberContent::I64(I64::Range(RangeStep {
+                low: 0,
+                high: 1,
+                step: 1,
+            }))),
+            "int4" => Content::Number(NumberContent::I64(I64::Range(RangeStep {
+                low: 0,
+                high: 1,
+                step: 1,
+            }))),
+            "int8" => Content::Number(NumberContent::I64(I64::Range(RangeStep {
+                low: 0,
+                high: 1,
+                step: 1,
+            }))),
+            "float4" => Content::Number(NumberContent::F64(F64::Range(RangeStep {
+                low: 0.0,
+                high: 1.0,
+                step: 0.1, //todo
+            }))),
+            "float8" => Content::Number(NumberContent::F64(F64::Range(RangeStep {
+                low: 0.0,
+                high: 1.0,
+                step: 0.1, //todo
+            }))),
+            "numeric" => Content::Number(NumberContent::F64(F64::Range(RangeStep {
+                low: 0.0,
+                high: 1.0,
+                step: 0.1, //todo
+            }))),
+            "timestampz" => Content::String(StringContent::DateTime(DateTimeContent {
+                format: "".to_string(), // todo
+                type_: ChronoValueType::DateTime,
+                begin: None,
+                end: None,
+            })),
+            "timestamp" => Content::String(StringContent::DateTime(DateTimeContent {
+                format: "".to_string(), // todo
+                type_: ChronoValueType::NaiveDateTime,
+                begin: None,
+                end: None,
+            })),
+            "date" => Content::String(StringContent::DateTime(DateTimeContent {
+                format: "%Y-%m-%d".to_string(),
+                type_: ChronoValueType::NaiveDate,
+                begin: None,
+                end: None,
+            })),
+            "uuid" => Content::String(StringContent::Uuid(Uuid)),
+            _ => bail!("We haven't implemented a converter for {}", data_type),
+        };
+
+        Ok(content)
+    }
+}
+
+impl TryFrom<PgRow> for ColumnInfo {
+    type Error = anyhow::Error;
+
+    fn try_from(row: PgRow) -> Result<Self, Self::Error> {
+        Ok(ColumnInfo {
+            column_name: row.try_get::<String, usize>(0)?,
+            ordinal_position: row.try_get::<i32, usize>(1)?,
+            is_nullable: row.try_get::<String, usize>(2)? == *"YES",
+            data_type: row.try_get::<String, usize>(3)?,
+            character_maximum_length: row.try_get::<Option<i32>, usize>(4)?,
+        })
+    }
+}
+
+impl TryFrom<PgRow> for PrimaryKey {
+    type Error = anyhow::Error;
+
+    fn try_from(row: PgRow) -> Result<Self, Self::Error> {
+        Ok(PrimaryKey {
+            column_name: row.try_get::<String, usize>(0)?,
+            type_name: row.try_get::<String, usize>(1)?,
+        })
+    }
+}
+
+impl TryFrom<PgRow> for ForeignKey {
+    type Error = anyhow::Error;
+
+    fn try_from(row: PgRow) -> Result<Self, Self::Error> {
+        Ok(ForeignKey {
+            from_table: row.try_get::<String, usize>(0)?,
+            from_column: row.try_get::<String, usize>(1)?,
+            to_table: row.try_get::<String, usize>(2)?,
+            to_column: row.try_get::<String, usize>(3)?
+        })
+    }
+}
+
+impl TryFrom<PgRow> for ValueWrapper {
+    type Error = anyhow::Error;
+
+    fn try_from(row: PgRow) -> Result<Self, Self::Error> {
+        let mut json_kv = Map::new();
+
+        for column in row.columns() {
+            let value = try_match_value(&row, column).unwrap_or(Value::Null);
+            json_kv.insert(column.name().to_string(), value);
+        }
+
+        Ok(ValueWrapper(Value::Object(json_kv)))
+    }
+}
+
+fn try_match_value(row: &PgRow, column: &PgColumn) -> Result<Value> {
+    let value = match column.type_info().name().to_lowercase().as_str() {
+        "bool" => Value::Bool(row.try_get::<bool, &str>(column.name())?),
+        "oid" => {
+            bail!("OID data type not supported for Postgresql")
+        }
+        "char" | "varchar" | "text" | "bpchar" | "name" | "unknown" => {
+            Value::String(row.try_get::<String, &str>(column.name())?)
+        }
+        "int2" => Value::Number(Number::from(row.try_get::<i16, &str>(column.name())?)),
+        "int4" => Value::Number(Number::from(row.try_get::<i32, &str>(column.name())?)),
+        "int8" => Value::Number(Number::from(row.try_get::<i64, &str>(column.name())?)),
+        "float4" => Value::Number(Number::from_f64(row.try_get::<f32, &str>(column.name())? as f64)
+            .ok_or(anyhow!("Failed to convert float4 data type"))?), // TODO test f32, f64
+        "float8" => Value::Number(Number::from_f64(row.try_get::<f64, &str>(column.name())?)
+            .ok_or(anyhow!("Failed to convert float8 data type"))?),
+        "numeric" => {
+            let as_decimal = row.try_get::<Decimal, &str>(column.name())?;
+
+            if let Some(truncated) = as_decimal.to_f64() {
+                if let Some(json_number) =  Number::from_f64(truncated) {
+                    return Ok(Value::Number(json_number));
+                }
+            }
+
+            bail!("Failed to convert Postgresql numeric data type to 64 bit float")
+        }
+        "timestampz" => Value::String(row.try_get::<String, &str>(column.name())?),
+        "timestamp" => Value::String(row.try_get::<String, &str>(column.name())?),
+        "date" => Value::String(format!("{}", row.try_get::<chrono::NaiveDate, &str>(column.name())?)),
+        _ => {
+            bail!(
+                "Could not convert value. Converter not implemented for {}",
+                column.type_info().name()
+            );
+        }
+    };
+
+    Ok(value)
+}
\ No newline at end of file
diff --git a/synth/src/datasource/relational_datasource.rs b/synth/src/datasource/relational_datasource.rs
new file mode 100644
index 00000000..a350fc31
--- /dev/null
+++ b/synth/src/datasource/relational_datasource.rs
@@ -0,0 +1,130 @@
+use crate::datasource::DataSource;
+use serde_json::Value;
+use anyhow::{Result};
+use async_trait::async_trait;
+use futures::future::join_all;
+use beau_collector::BeauCollector;
+use synth_core::Content;
+
+const DEFAULT_INSERT_BATCH_SIZE: usize = 1000;
+
+#[derive(Debug)]
+pub struct ColumnInfo {
+    pub(crate) column_name: String,
+    pub(crate) ordinal_position: i32,
+    pub(crate) is_nullable: bool,
+    pub(crate) data_type: String,
+    pub(crate) character_maximum_length: Option<i32>,
+}
+
+#[derive(Debug)]
+pub struct PrimaryKey {
+    pub(crate) column_name: String,
+    pub(crate) type_name: String,
+}
+
+#[derive(Debug)]
+pub struct ForeignKey {
+    pub(crate) from_table: String,
+    pub(crate) from_column: String,
+    pub(crate) to_table: String,
+    pub(crate) to_column: String,
+}
+
+/// Wrapper around `Value` since we can't impl `TryFrom` on a struct in a non-owned crate
+#[derive(Debug)]
+pub struct ValueWrapper(pub(crate) Value);
+
+/// All relational databases should define this trait and implement database specific queries in
+/// their own impl. APIs should be defined async when possible, delegating to the caller on how to
+/// handle it.
+#[async_trait]
+pub trait RelationalDataSource : DataSource {
+    type QueryResult: Send + Sync;
+
+    async fn insert_relational_data(&self, collection_name: String, collection: &[Value]) -> Result<()> {
+        // how to to ordering here?
+        // If we have foreign key constraints we need to traverse the tree and
+        // figure out insertion order
+        // We basically need something like an InsertionStrategy where we have a DAG of insertions
+        let batch_size = DEFAULT_INSERT_BATCH_SIZE;
+
+        if collection.is_empty() {
+            println!(
+                "Collection {} generated 0 values. Skipping insertion...",
+                collection_name
+            );
+            return Ok(());
+        }
+
+        let column_names = collection
+            .get(0)
+            .expect("Explicit check is done above that this collection is non-empty")
+            .as_object()
+            .expect("This is always an object (sampler contract)")
+            .keys()
+            .cloned()
+            .collect::<Vec<String>>()
+            .join(",");
+
+        let mut futures = vec![];
+
+        for rows in collection.chunks(batch_size) {
+            let mut query = format!(
+                "INSERT INTO {} ({}) VALUES \n",
+                collection_name, column_names
+            );
+
+            for (i, row) in rows.iter().enumerate() {
+                let row_obj = row
+                    .as_object()
+                    .expect("This is always an object (sampler contract)");
+
+                let values = row_obj
+                    .values()
+                    .map(|v| v.to_string().replace("'", "''")) // two single quotes are the standard way to escape double quotes
+                    .map(|v| v.replace("\"", "'")) // values in the object have quotes around them by default.
+                    .collect::<Vec<String>>()
+                    .join(",");
+
+                // We should be using some form of a prepared statement here.
+                // It is not clear how this would work for our batch inserts...
+                if i == rows.len() - 1 {
+                    query.push_str(&format!("({});\n", values));
+                } else {
+                    query.push_str(&format!("({}),\n", values));
+                }
+            }
+
+            let future = self.execute_query(query, vec![]);
+            futures.push(future);
+        }
+
+        let results: Vec<Result<Self::QueryResult>> = join_all(futures).await;
+
+        if let Err(e) = results.into_iter().bcollect::<Vec<Self::QueryResult>>() {
+            bail!("One or more database inserts failed: {:?}", e)
+        }
+
+        println!("Inserted {} rows...", collection.len());
+        Ok(())
+    }
+
+    async fn execute_query(&self, query: String, query_params: Vec<&str>) -> Result<Self::QueryResult>;
+
+    fn get_catalog(&self) -> Result<&str>;
+
+    async fn get_table_names(&self) -> Result<Vec<String>>;
+
+    async fn get_columns_infos(&self, table_name: &str) -> Result<Vec<ColumnInfo>>;
+
+    async fn get_primary_keys(&self, table_name: &str) -> Result<Vec<PrimaryKey>>;
+
+    async fn get_foreign_keys(&self) -> Result<Vec<ForeignKey>>;
+
+    async fn set_seed(&self) -> Result<()>;
+
+    async fn get_deterministic_samples(&self, table_name: &str) -> Result<Vec<Value>>;
+
+    fn decode_to_content(&self, data_type: &str, _char_max_len: Option<i32>) -> Result<Content>;
+}
\ No newline at end of file
diff --git a/synth/src/lib.rs b/synth/src/lib.rs
index f6897cde..931ec94a 100644
--- a/synth/src/lib.rs
+++ b/synth/src/lib.rs
@@ -61,6 +61,7 @@ use crate::cli::CliArgs;
 
 mod sampler;
 pub mod store;
+mod datasource;
 
 include!(concat!(env!("OUT_DIR"), "/meta.rs"));
 
diff --git a/tools/vagrant/README.md b/tools/vagrant/README.md
new file mode 100644
index 00000000..a1e64163
--- /dev/null
+++ b/tools/vagrant/README.md
@@ -0,0 +1,32 @@
+Sanbox Dev and Testing with Vagrant
+====================================
+
+This directory contains `Vagrantfile`s that define VMs that can be used to set up a development or testing environment 
+for synth on different platforms. Vagrant is very simple to spin up and great for running in a repeatable OS 
+environment.
+
+The `Vagrantfile` will install all rust dependencies to build and install `synth` from source. Docker is also installed, 
+as it's convenient for running database services. Note: switch user to root after `vagrant ssh` since the rust and 
+synth tools are installed under root.
+
+Two source environments are available on the VM: 
+
+- `/synth` is a synced folder to your local synth source and modifications there will reflect in your local source.
+- `/synth-sandbox` is a copy of the synth source and modifications are not persisted after the VM is destroyed.
+
+# Requirements
+
+- Virtual box
+- Vagrant
+
+# Common Commands
+
+Make sure you are in the directory with the `Vagrantfile` before executing vagrant. These command need to be run in the 
+vagrant directory:
+
+- Init/start the vagrant VM: `vagrant up`
+- Stop the VM: `vagrant suspend`
+- Destroy the VM completely: `vagrant destroy`
+- Open a ssh session to the VM: `vagrant ssh`
+- Start a remote desktop session: `vagrant rdp` (windows)
+- Send a file to the VM: `vagrant upload`
diff --git a/tools/vagrant/linux/ubuntu/Vagrantfile b/tools/vagrant/linux/ubuntu/Vagrantfile
new file mode 100644
index 00000000..3a8eb2fc
--- /dev/null
+++ b/tools/vagrant/linux/ubuntu/Vagrantfile
@@ -0,0 +1,44 @@
+PROJECT_SRC_DIR = "/synth"
+PROJECT_SRC_SANDBOX_DIR = "/synth-sandbox"
+
+Vagrant.configure("2") do |config|
+  config.vm.box = "ubuntu/focal64"
+
+  config.vm.provider :virtualbox do |v, override|
+    v.customize ["modifyvm", :id, "--memory", "4096"]
+    v.customize ["modifyvm", :id, "--cpus", "2"]
+    v.customize ["modifyvm", :id, "--vram", 32]
+    v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
+    v.customize ["modifyvm", :id, "--audio", "none"]
+    v.customize ["modifyvm", :id, "--clipboard", "bidirectional"]
+    v.customize ["modifyvm", :id, "--draganddrop", "hosttoguest"]
+    v.customize ["modifyvm", :id, "--usb", "off"]
+    v.linked_clone = true if Vagrant::VERSION >= '1.8.0'
+  end
+
+  config.vm.network "private_network", type: "dhcp"
+
+  config.vm.synced_folder "../../../../", PROJECT_SRC_DIR
+  config.vm.provision "shell", :args => [PROJECT_SRC_DIR, PROJECT_SRC_SANDBOX_DIR], privileged: true, inline: <<-SHELL
+    cp -r $1 $2
+    chmod -R a+rw $2
+    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
+    apt-get update
+    apt-get install -y build-essential pkg-config libssl-dev jq
+    source ~/.profile
+    rustup toolchain install nightly-2021-05-17
+    cd $2
+    /root/.cargo/bin/cargo +nightly-2021-05-17 install --debug --path=synth
+  SHELL
+
+#   Install Docker
+  config.vm.provision "shell", privileged: true, inline: <<-SHELL
+    apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
+    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
+    echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
+      $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
+    apt-get update
+    apt-get install -y docker-ce docker-ce-cli containerd.io
+  SHELL
+
+end


Test patch:
diff --git a/synth/testing_harness/mysql/0_hospital_schema.sql b/synth/testing_harness/mysql/0_hospital_schema.sql
new file mode 100644
index 00000000..a8fbda41
--- /dev/null
+++ b/synth/testing_harness/mysql/0_hospital_schema.sql
@@ -0,0 +1,31 @@
+drop table if exists hospitals;
+
+create table hospitals
+(
+    id            int primary key,
+    hospital_name varchar(255),
+    address       varchar(255)
+);
+
+drop table if exists doctors;
+
+create table doctors
+(
+    id          int primary key,
+    hospital_id int,
+    name        varchar(255),
+    date_joined date
+);
+
+drop table if exists patients;
+
+create table patients
+(
+    id          int primary key,
+    doctor_id   int,
+    name        varchar(255),
+    date_joined date,
+    address     varchar(255),
+    phone       varchar(20),
+    ssn         varchar(12)
+);
\ No newline at end of file
diff --git a/synth/testing_harness/mysql/1_hospital_data.sql b/synth/testing_harness/mysql/1_hospital_data.sql
new file mode 100644
index 00000000..9e933e4f
--- /dev/null
+++ b/synth/testing_harness/mysql/1_hospital_data.sql
@@ -0,0 +1,122 @@
+-- Hospitals
+
+INSERT INTO hospitals (id,hospital_name,address) VALUES
+(1,'Garcia-Washington','194 Davis Ferry Suite 232\nJenningsmouth, NV 83701'),
+(2,'Cruz, Bowman and Martinez','1938 Key Wall\nMartinshire, OR 24041'),
+(3,'Bishop, Hartman and Zuniga','574 Snyder Crossing\nPort Christineland, VT 37567'),
+(4,'Maxwell-Garcia','328 Williams Coves\nSmithside, HI 71878'),
+(5,'Potter-Lindsey','5737 Carmen Trace Suite 312\nSouth Evelyn, WY 40089'),
+(6,'Nielsen-Sanchez','70964 Carrillo Burg\nSouth Karichester, ID 67549'),
+(7,'Burch-Daniels','Unit 4839 Box 1083\nDPO AA 25986'),
+(8,'Marshall, Anderson and Jarvis','51322 Joseph Park\nMelissaton, AZ 67575'),
+(9,'Nelson-Jones','8068 David Turnpike\nDelgadoside, FL 82542'),
+(10,'Hall, Wells and Salas','5280 Kelley Crossroad Apt. 574\nLake Davidfort, CT 94005'),
+(11,'Hardy-Obrien','19920 Brian Curve Suite 711\nThompsonville, KY 89805'),
+(12,'Ayala LLC','0079 Michelle Skyway Suite 179\nPort Tony, CA 48596'),
+(13,'Hale-Padilla','19876 Carroll Flats\nClaytonbury, IA 94229'),
+(14,'Jones Inc','82451 Anita Rue Suite 317\nJustintown, WI 30269');
+
+-- Doctors
+
+INSERT INTO doctors (id,hospital_id,name,date_joined) VALUES
+(1,1,'Bonnie Johnson','2011-05-25'),
+(2,2,'Ian Garrett','2019-03-30'),
+(3,1,'Brittney Rowe','2015-01-12'),
+(4,3,'Mary Pierce','2019-07-05'),
+(5,4,'Cynthia Mendoza','2011-05-16'),
+(6,3,'Raymond Bates','2015-01-06'),
+(7,5,'Connie Johnson','2019-09-14'),
+(8,6,'Maria Rowland','2011-09-04'),
+(9,5,'Nicole Taylor','2016-01-27'),
+(10,7,'Kenneth Sweeney','2015-11-22'),
+(11,8,'Patrick Adams','2017-06-27'),
+(12,7,'Shannon Smith','2017-06-13'),
+(13,9,'Sydney Walker','2014-07-23'),
+(14,10,'Tracy Simon','2015-06-18'),
+(15,9,'Mark Harris','2010-11-07'),
+(16,11,'Mary Wilson','2012-01-29'),
+(17,12,'Danny White','2012-08-12'),
+(18,11,'Ashley Manning','2017-01-08'),
+(19,13,'Kristine Montgomery','2017-05-22'),
+(20,14,'Joshua Ford','2018-07-29'),
+(21,13,'Jason Payne','2016-03-03');
+
+-- Patients
+
+INSERT INTO patients (id,doctor_id,name,date_joined,address,phone,ssn) VALUES
+(1,1,'Thomas Dixon','2010-02-24','Studio 6\nCross lock\nEmilyside\nS2E 5RY','(0114)4960562','145-94-5022'),
+(2,2,'Anthony Morales','2013-07-06','Flat 52D\nAkhtar overpass\nNew Bernardshire\nLN8 1RE','+441154960433','812-01-4209'),
+(3,3,'Curtis West','2010-05-06','6 Fox roads\nSouth Martynchester\nHS3W 1TT','(0116) 496 0076','548-03-1303'),
+(4,1,'Nicholas Ryan','2010-10-31','90 Grace radial\nEast Terenceside\nRH87 6GH','(028) 9018 0291','504-87-6136'),
+(5,2,'Kristy Rodriguez','2017-08-27','035 Oliver forks\nThompsonborough\nB6 2NE','(0808)1570377','689-22-2477'),
+(6,3,'Tracy Pacheco','2011-12-28','Studio 52\nDorothy tunnel\nMartinstad\nRH69 3AA','(0121)4960038','431-83-7953'),
+(7,1,'Tracy Stevens','2010-10-17','Flat 74\nAndrew manor\nDianaland\nMK5A 2ZT','01632 960167','552-39-8225'),
+(8,2,'Thomas Scott','2012-01-04','Flat 26q\nPowell manor\nBarrymouth\nM2 0QU','01184960712','151-56-5875'),
+(9,3,'Tammy Charles','2013-10-23','798 Jenkins harbor\nNorth Jeremy\nIG49 7YS','+44161 496 0324','474-29-4412'),
+(10,1,'Jeanette Nichols','2010-04-06','3 Valerie tunnel\nNorth Colin\nSA2N 2QR','+44113 496 0050','378-64-1750');
+INSERT INTO patients (id,doctor_id,name,date_joined,address,phone,ssn) VALUES
+(11,4,'Brett White','2010-11-13','Studio 31\nHughes mountains\nEast Norman\nTS23 7JW','+44909 8790083','548-42-7713'),
+(12,5,'Raymond Rodriguez','2017-06-10','Studio 23a\nSam turnpike\nEmmaview\nOX3P 1DN','+44(0)909 8790659','079-84-1510'),
+(13,6,'Lisa Johnson','2018-12-12','Flat 7\nRachel hill\nJenniferfurt\nG2 0QD','(0161)4960608','865-55-3656'),
+(14,4,'Christopher Gilbert','2010-12-29','Studio 0\nJulie motorway\nWatersville\nEN98 4SJ','(0808)1570048','121-28-3435'),
+(15,5,'Dr. Evan Garcia','2012-02-11','688 Glenn cliff\nAmberton\nN0 4PZ','(0161) 4960368','159-49-5330'),
+(16,6,'Christine Phillips','2012-07-06','2 Damian lights\nReecemouth\nMK15 1JQ','+44(0)117 4960500','725-75-1763'),
+(17,4,'Joseph Stone','2011-12-10','203 Rachael union\nJeanmouth\nG07 8DA','+44121 4960898','839-95-0445'),
+(18,5,'Donald Ford','2013-12-29','064 Glover forges\nNorth Philipport\nL88 8QU','01144960306','197-85-6864'),
+(19,6,'Robert Mayer','2017-03-30','Studio 91n\nLambert squares\nBrownside\nB6 9YP','01174960906','696-18-2664'),
+(20,4,'Deborah Watkins','2011-01-19','0 Paul port\nNorth Benjamin\nL7 0ZX','+44117 4960736','862-01-7079');
+INSERT INTO patients (id,doctor_id,name,date_joined,address,phone,ssn) VALUES
+(21,7,'Hannah Kelly','2018-01-17','66 Billy turnpike\nNorth Melissa\nAL34 2RG','(0113) 4960268','388-46-1305'),
+(22,8,'Jesus Davis','2014-11-28','0 Taylor path\nNorth Judithville\nSY6 8SA','+44116 496 0687','020-44-4656'),
+(23,9,'Tammy Green','2015-08-06','5 Thomas mountain\nSouth Callumland\nE9C 0LQ','+44306 999 0527','086-87-2967'),
+(24,7,'Gina Hunt','2014-09-15','59 Gordon bridge\nPort Sandraberg\nWS8A 2AS','0151 496 0045','449-82-8506'),
+(25,8,'Brittany Matthews','2012-09-06','1 Walker station\nEast Alanfurt\nIP7 1QN','0141 496 0206','310-40-7955'),
+(26,9,'Jason Miller','2011-05-16','Studio 91e\nSheila field\nParkerbury\nLU1 3BP','+44(0)115 496 0265','197-90-4619'),
+(27,7,'Victoria Phillips','2014-11-13','47 Mason highway\nLake Andreaview\nTS1E 3XQ','+44(0)114 4960023','276-80-3097'),
+(28,8,'Robin Fowler','2019-04-19','Flat 8\nHeather ridge\nJessicahaven\nGL9R 7SX','+44(0)131 496 0765','544-59-7500'),
+(29,9,'Gregory Valencia','2016-02-19','090 Dunn shoal\nEast Katyville\nL9J 9GQ','+44(0)118 4960683','477-72-8246'),
+(30,7,'Maria Fletcher','2017-09-12','Flat 53\nRachael station\nNew Jemma\nE0 1JD','+4428 9018615','591-54-6181');
+INSERT INTO patients (id,doctor_id,name,date_joined,address,phone,ssn) VALUES
+(31,10,'Jennifer Medina','2017-07-12','0 Murphy track\nJohnsonmouth\nW8W 8GZ','01164960054','172-54-0780'),
+(32,11,'Mr. Brian Porter','2016-10-29','Flat 4\nRice camp\nGoodwinview\nW20 5XZ','01164960868','179-79-1760'),
+(33,12,'Mark Young','2011-03-16','Flat 75\nCarol radial\nNorth Valerie\nHX62 6QA','(0114) 4960210','579-70-1181'),
+(34,10,'Michael Nicholson','2017-11-16','53 Nicole camp\nGeoffreyside\nNN3H 1FH','(0141) 4960237','155-92-3714'),
+(35,11,'George Mcguire','2013-11-07','189 Gould overpass\nCookefurt\nHD9 9RF','+44141 4960493','113-40-5786'),
+(36,12,'Karen Watkins','2019-11-21','76 Carol view\nNew Stephanie\nRH7H 0FQ','+441314960135','567-67-7368'),
+(37,10,'Stephanie Gamble','2014-08-09','97 Ann flats\nEast Charleneside\nWC7 1HT','+44121 4960213','250-03-1755'),
+(38,11,'Kim Meyers','2018-11-26','409 Butcher light\nLake Victoriafort\nB65 5FE','(0117)4960932','739-57-9547'),
+(39,12,'Ashley Harris','2012-05-19','06 Matthews mews\nWoodshire\nUB82 1JY','+44(0)121 4960110','723-90-8396'),
+(40,10,'Justin Kim','2010-11-20','Flat 25\nLisa inlet\nPort Ritafurt\nHS41 5ER','+44(0)118 496 0663','823-58-6236');
+INSERT INTO patients (id,doctor_id,name,date_joined,address,phone,ssn) VALUES
+(41,13,'Mrs. Kristin Lewis DVM','2010-12-25','00 Barnes wells\nNew Sian\nS00 8YR','(020) 74960924','156-29-6887'),
+(42,14,'Janet Fernandez','2016-11-29','Flat 79\nNorman run\nSharonmouth\nB4U 7BL','+44(0)115 496 0126','460-18-0819'),
+(43,15,'Leslie Casey','2018-05-19','66 Smith expressway\nNormantown\nL1 5WR','+44(0)306 999 0764','067-93-6633'),
+(44,13,'Samantha Wright','2015-02-03','629 Burton gateway\nHughville\nS9 3LJ','0118 4960244','738-95-0807'),
+(45,14,'Brett Miller','2018-04-11','436 Graham hill\nShirleyborough\nPL5Y 3LD','0141 496 0793','726-38-6007'),
+(46,15,'Jason Crane','2017-04-26','Flat 99\nYoung fall\nAliceton\nB0S 2AT','+44(0)1174960479','833-04-0115'),
+(47,13,'Madison Yoder','2015-03-23','062 Katy landing\nPhillipsview\nSS91 9YP','01184960060','006-10-0679'),
+(48,14,'Michael Sellers','2019-10-28','142 Stephen junction\nBennettfurt\nW2W 9BD','+44(0)8081570284','472-17-6148'),
+(49,15,'Rebecca Brown','2011-10-20','Flat 64\nDennis throughway\nEast Benjaminland\nDT37 3FL','(0117) 4960069','233-17-3594'),
+(50,13,'Cody Bryant','2015-08-23','Studio 5\nLewis hollow\nEdwardsland\nS96 4TG','+44115 496 0334','649-23-3293');
+INSERT INTO patients (id,doctor_id,name,date_joined,address,phone,ssn) VALUES
+(51,16,'Kevin Payne','2019-03-24','Studio 8\nBethan stream\nNew Elliottshire\nSL7H 0SS','+441314960539','585-82-2591'),
+(52,17,'Megan Ryan','2016-04-21','Flat 6\nHenry inlet\nWest Samantha\nHS7N 9FT','+44(0)116 4960007','698-18-1758'),
+(53,18,'Matthew Henry','2018-10-20','743 Shaw knoll\nLeeside\nM01 3HY','01154960297','775-07-6124'),
+(54,16,'Noah Chandler','2013-03-31','Studio 73\nBell loaf\nAlitown\nCA0 8AY','+44121 4960245','761-56-1725'),
+(55,17,'Garrett Fowler','2011-07-17','Flat 23A\nMitchell squares\nHoltchester\nE23 9WJ','+441632 960 338','859-35-9693'),
+(56,18,'Spencer Campbell','2013-06-16','915 Mason bridge\nSouth Patriciachester\nDA61 0ZN','+44(0)808 157 0553','363-69-2489'),
+(57,16,'Robert Becker','2018-11-27','900 Boyle forest\nPort Andrewburgh\nM6 2HT','(020) 7496 0080','788-71-1402'),
+(58,17,'Matthew Murillo','2015-03-01','Studio 30\nHughes river\nWhitemouth\nS10 0LN','02074960325','330-99-9498'),
+(59,18,'Brandy Hernandez','2018-05-27','Flat 3\nAntony forges\nLake Eric\nPO0 8UP','09098790239','803-29-7495'),
+(60,16,'Jamie Sutton','2010-11-19','927 Gibbs cliff\nGregoryshire\nUB6H 9XQ','+44(0)29 2018479','645-23-9803');
+INSERT INTO patients (id,doctor_id,name,date_joined,address,phone,ssn) VALUES
+(61,19,'Renee Robertson','2019-07-24','0 Jodie pike\nEast Alan\nBH7Y 2WN','(0115) 4960107','787-06-1801'),
+(62,20,'Rachel Nelson','2012-01-11','Studio 6\nAdam lodge\nNew Maureen\nG4W 3UN','0131 4960314','829-19-7394'),
+(63,21,'Melissa Silva','2019-05-09','Studio 58\nGreen shore\nNorth Shauntown\nS54 5NY','(0121) 4960580','321-13-7363'),
+(64,19,'Frank Bradley','2012-09-08','9 Sandra avenue\nJaniceville\nM1 0JP','(0161) 496 0548','187-91-0569'),
+(65,20,'Stephen Briggs','2010-03-29','Flat 82\nQuinn parkways\nEast Joyceland\nG2 2TX','029 2018 0186','231-59-8966'),
+(66,21,'Emily Camacho','2016-12-26','093 Marshall plains\nEast Mathewfort\nKY3 5WU','+44(0)909 8790988','534-62-0149'),
+(67,19,'Ryan Cain','2017-11-18','8 Dawn glens\nLouiseburgh\nE1W 0LH','(0141)4960552','518-81-2366'),
+(68,20,'Daniel Thornton','2017-01-16','01 Lisa cliff\nSmithmouth\nOX92 5ZQ','01414960391','511-66-0104'),
+(69,21,'Kyle Pacheco','2018-06-26','754 Thompson lodge\nThomashaven\nDH41 3JW','(028) 9018567','008-70-2086'),
+(70,19,'Scott Nielsen','2016-10-15','Studio 74y\nBeth path\nMooreburgh\nE2 9ZB','(0808)1570400','823-56-1410');
\ No newline at end of file
diff --git a/synth/testing_harness/mysql/README.md b/synth/testing_harness/mysql/README.md
new file mode 100644
index 00000000..2303a300
--- /dev/null
+++ b/synth/testing_harness/mysql/README.md
@@ -0,0 +1,15 @@
+Integration Tests for MySql
+====================================
+
+This is an integration test that validates the synth generate and synth import commands for MySql on a Debian flavored 
+OS. The models in hospital_master are used as a known "golden" set to validate against. The *.sql scripts generate the
+schema and test data within the database.
+
+# Requirements:
+- Docker
+- jq
+
+# Instructions
+
+To run this, execute the `e2e.sh` script from the current directory. A non-zero return code denotes failure. 
+Note: run this with all dependencies installed in Vagrant with the [Vagrantfile](tools/vagrant/linux/ubuntu/Vagrantfile)
diff --git a/synth/testing_harness/mysql/e2e.sh b/synth/testing_harness/mysql/e2e.sh
new file mode 100644
index 00000000..c92a8d47
--- /dev/null
+++ b/synth/testing_harness/mysql/e2e.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+
+DB_HOST=127.0.0.1
+DB_PORT=3306
+DB_USER=root
+DB_PASSWORD="mysecretpassword"
+DB_NAME=test_db
+CONTAINER_NAME=mysql-synth-harness
+
+######### Initialization #########
+
+# Install dependencies
+apt-get install -y mysql-client
+
+cd "$( dirname "${BASH_SOURCE[0]}" )"
+
+# delete leftover config
+rm -f .synth/config.toml
+
+# 0. init workspace
+synth init || exit 1
+
+# 1. Init DB service
+container_id=$(docker run --name $CONTAINER_NAME -e MYSQL_ROOT_PASSWORD=$DB_PASSWORD -e MYSQL_DATABASE=$DB_NAME -p $DB_PORT:3306 -d mysql:latest)
+
+# Waits til DB is ready
+while ! mysql -h $DB_HOST -u root --password=$DB_PASSWORD -P $DB_PORT $DB_NAME -e "SELECT 1" > /dev/null 2>&1; do
+    sleep 1
+done
+
+######### Export Test #########
+
+# 2. Populate DB schema
+mysql -h $DB_HOST -u root --password=$DB_PASSWORD -P $DB_PORT $DB_NAME < 0_hospital_schema.sql || exit 1
+
+result=0
+
+# 3. Verify gen to DB crates min. expected rows
+synth generate hospital_master --to mysql://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME --size 30 || result=1
+sum_rows_query="SELECT (SELECT count(*) FROM hospitals) +  (SELECT count(*) FROM doctors) + (SELECT count(*) FROM patients)"
+sum=`mysql -h $DB_HOST -u root --password=$DB_PASSWORD -P $DB_PORT $DB_NAME -e "$sum_rows_query" | grep -o '[[:digit:]]*'`
+[ "$sum" -gt "30" ] || result=1
+
+######### Import Test #########
+
+# 4. Clear out export data and insert known golden master data
+mysql -h $DB_HOST -u root --password=$DB_PASSWORD -P $DB_PORT $DB_NAME < 0_hospital_schema.sql || exit 1
+mysql -h $DB_HOST -u root --password=$DB_PASSWORD -P $DB_PORT $DB_NAME < 1_hospital_data.sql || exit 1
+
+# 5. Import with synth and diff
+synth import --from mysql://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME hospital_import || result=1
+diff <(jq --sort-keys . hospital_import/*) <(jq --sort-keys . hospital_master/*) || result=1
+
+######### Cleanup #########
+
+docker rm -f "${container_id}"
+rm -rf hospital_import
+rm -rf  .synth
+
+# fail if any of the commands failed
+if [ $result -ne 0 ]
+then
+    exit 1
+fi
diff --git a/synth/testing_harness/mysql/hospital_master/doctors.json b/synth/testing_harness/mysql/hospital_master/doctors.json
new file mode 100644
index 00000000..6e67eeb3
--- /dev/null
+++ b/synth/testing_harness/mysql/hospital_master/doctors.json
@@ -0,0 +1,72 @@
+{
+  "type": "array",
+  "length": {
+    "type": "number",
+    "range": {
+      "low": 1,
+      "high": 10,
+      "step": 1
+    },
+    "subtype": "u64"
+  },
+  "content": {
+    "type": "object",
+    "date_joined": {
+      "type": "one_of",
+      "variants": [
+        {
+          "weight": 1.0,
+          "type": "string",
+          "date_time": {
+            "format": "%Y-%m-%d",
+            "subtype": "naive_date",
+            "begin": "2010-11-07",
+            "end": "2019-07-05"
+          }
+        },
+        {
+          "weight": 1.0,
+          "type": "null"
+        }
+      ]
+    },
+    "hospital_id": {
+      "type": "one_of",
+      "variants": [
+        {
+          "weight": 1.0,
+          "type": "number",
+          "range": {
+            "low": 0,
+            "high": 14,
+            "step": 1
+          },
+          "subtype": "i64"
+        },
+        {
+          "weight": 1.0,
+          "type": "null"
+        }
+      ]
+    },
+    "id": {
+      "type": "number",
+      "id": {},
+      "subtype": "u64"
+    },
+    "name": {
+      "type": "one_of",
+      "variants": [
+        {
+          "weight": 1.0,
+          "type": "string",
+          "pattern": "[a-zA-Z0-9]{0, 255}"
+        },
+        {
+          "weight": 1.0,
+          "type": "null"
+        }
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/synth/testing_harness/mysql/hospital_master/hospitals.json b/synth/testing_harness/mysql/hospital_master/hospitals.json
new file mode 100644
index 00000000..60886cde
--- /dev/null
+++ b/synth/testing_harness/mysql/hospital_master/hospitals.json
@@ -0,0 +1,48 @@
+{
+  "type": "array",
+  "length": {
+    "type": "number",
+    "range": {
+      "low": 1,
+      "high": 10,
+      "step": 1
+    },
+    "subtype": "u64"
+  },
+  "content": {
+    "type": "object",
+    "address": {
+      "type": "one_of",
+      "variants": [
+        {
+          "weight": 1.0,
+          "type": "string",
+          "pattern": "[a-zA-Z0-9]{0, 255}"
+        },
+        {
+          "weight": 1.0,
+          "type": "null"
+        }
+      ]
+    },
+    "hospital_name": {
+      "type": "one_of",
+      "variants": [
+        {
+          "weight": 1.0,
+          "type": "string",
+          "pattern": "[a-zA-Z0-9]{0, 255}"
+        },
+        {
+          "weight": 1.0,
+          "type": "null"
+        }
+      ]
+    },
+    "id": {
+      "type": "number",
+      "id": {},
+      "subtype": "u64"
+    }
+  }
+}
\ No newline at end of file
diff --git a/synth/testing_harness/mysql/hospital_master/patients.json b/synth/testing_harness/mysql/hospital_master/patients.json
new file mode 100644
index 00000000..1619a732
--- /dev/null
+++ b/synth/testing_harness/mysql/hospital_master/patients.json
@@ -0,0 +1,114 @@
+{
+  "type": "array",
+  "length": {
+    "type": "number",
+    "range": {
+      "low": 1,
+      "high": 10,
+      "step": 1
+    },
+    "subtype": "u64"
+  },
+  "content": {
+    "type": "object",
+    "address": {
+      "type": "one_of",
+      "variants": [
+        {
+          "weight": 1.0,
+          "type": "string",
+          "pattern": "[a-zA-Z0-9]{0, 255}"
+        },
+        {
+          "weight": 1.0,
+          "type": "null"
+        }
+      ]
+    },
+    "date_joined": {
+      "type": "one_of",
+      "variants": [
+        {
+          "weight": 1.0,
+          "type": "string",
+          "date_time": {
+            "format": "%Y-%m-%d",
+            "subtype": "naive_date",
+            "begin": "2010-10-31",
+            "end": "2019-04-19"
+          }
+        },
+        {
+          "weight": 1.0,
+          "type": "null"
+        }
+      ]
+    },
+    "doctor_id": {
+      "type": "one_of",
+      "variants": [
+        {
+          "weight": 1.0,
+          "type": "number",
+          "range": {
+            "low": 0,
+            "high": 20,
+            "step": 1
+          },
+          "subtype": "i64"
+        },
+        {
+          "weight": 1.0,
+          "type": "null"
+        }
+      ]
+    },
+    "id": {
+      "type": "number",
+      "id": {},
+      "subtype": "u64"
+    },
+    "name": {
+      "type": "one_of",
+      "variants": [
+        {
+          "weight": 1.0,
+          "type": "string",
+          "pattern": "[a-zA-Z0-9]{0, 255}"
+        },
+        {
+          "weight": 1.0,
+          "type": "null"
+        }
+      ]
+    },
+    "phone": {
+      "type": "one_of",
+      "variants": [
+        {
+          "weight": 1.0,
+          "type": "string",
+          "pattern": "[a-zA-Z0-9]{0, 20}"
+        },
+        {
+          "weight": 1.0,
+          "type": "null"
+        }
+      ]
+    },
+    "ssn": {
+      "type": "one_of",
+      "variants": [
+        {
+          "weight": 1.0,
+          "type": "string",
+          "pattern": "[a-zA-Z0-9]{0, 12}"
+        },
+        {
+          "weight": 1.0,
+          "type": "null"
+        }
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/synth/testing_harness/postgres/e2e.sh b/synth/testing_harness/postgres/e2e.sh
index 695478e3..d44f643f 100755
--- a/synth/testing_harness/postgres/e2e.sh
+++ b/synth/testing_harness/postgres/e2e.sh
@@ -38,7 +38,7 @@ echo "stopping container"
 docker stop "${CONTAINER}" > /dev/null
 
 # check by diff against golden master
-diff hospital_import hospital_master || RESULT=1
+diff <(jq --sort-keys . hospital_import/*) <(jq --sort-keys . hospital_master/*) || RESULT=1
 
 # removing generated namespace files
 rm -f hospital_import/doctors.json hospital_import/hospitals.json hospital_import/patients.json || RESULT=1