From 69f76e177a676dd262cdabc6e40321ea7197cc41 Mon Sep 17 00:00:00 2001 From: Norberto Lopes Date: Sun, 27 Aug 2023 15:44:11 +0100 Subject: [PATCH] init --- .github/dependabot.yml | 6 + .github/workflows/ci.yml | 79 ++++ .gitignore | 2 + Cargo.toml | 49 +++ LICENSE | 25 ++ README.md | 132 +++++++ README.tpl | 8 + examples/mysql-diesel/Cargo.toml | 9 + examples/mysql-diesel/README.md | 30 ++ examples/mysql-diesel/certs/client-cert.pem | 19 + examples/mysql-diesel/certs/client-key.pem | 27 ++ examples/mysql-diesel/certs/client-req.pem | 16 + examples/mysql-diesel/certs/root-ca-key.pem | 27 ++ examples/mysql-diesel/certs/root-ca.pem | 19 + examples/mysql-diesel/certs/server-cert.pem | 19 + examples/mysql-diesel/certs/server-key.pem | 27 ++ examples/mysql-diesel/certs/server-req.pem | 16 + examples/mysql-diesel/docker-compose.yml | 20 ++ .../2023-08-28-162356_add_user/down.sql | 1 + .../2023-08-28-162356_add_user/up.sql | 5 + .../mysql-diesel/scripts/generate-certs.sh | 12 + examples/mysql-diesel/src/main.rs | 21 ++ examples/mysql-diesel/structure.sql | 40 +++ examples/mysql-sqlx/Cargo.toml | 9 + examples/mysql-sqlx/README.md | 30 ++ examples/mysql-sqlx/certs/client-cert.pem | 19 + examples/mysql-sqlx/certs/client-key.pem | 27 ++ examples/mysql-sqlx/certs/client-req.pem | 16 + examples/mysql-sqlx/certs/root-ca-key.pem | 27 ++ examples/mysql-sqlx/certs/root-ca.pem | 19 + examples/mysql-sqlx/certs/server-cert.pem | 19 + examples/mysql-sqlx/certs/server-key.pem | 27 ++ examples/mysql-sqlx/certs/server-req.pem | 16 + examples/mysql-sqlx/docker-compose.yml | 22 ++ .../migrations/20230828202611_add_users.sql | 5 + examples/mysql-sqlx/scripts/generate-certs.sh | 12 + examples/mysql-sqlx/src/main.rs | 21 ++ examples/mysql-sqlx/structure.sql | 44 +++ examples/sqlite-sqlx/Cargo.toml | 8 + examples/sqlite-sqlx/README.md | 5 + .../migrations/20230827152633_add_users.sql | 5 + examples/sqlite-sqlx/src/main.rs | 9 + examples/sqlite-sqlx/structure.sql | 20 ++ .../2023-08-27-215620_add_users/down.sql | 1 + .../2023-08-27-215620_add_users/up.sql | 5 + .../migrations/20230827160610_add_users.sql | 5 + src/error.rs | 41 +++ src/lib.rs | 340 ++++++++++++++++++ src/macros.rs | 60 ++++ src/mysql/mod.rs | 161 +++++++++ src/mysql/options.rs | 220 ++++++++++++ src/process.rs | 64 ++++ src/sqlite/diesel.rs | 71 ++++ src/sqlite/mod.rs | 37 ++ src/sqlite/sqlx.rs | 49 +++ 55 files changed, 2023 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.tpl create mode 100644 examples/mysql-diesel/Cargo.toml create mode 100644 examples/mysql-diesel/README.md create mode 100644 examples/mysql-diesel/certs/client-cert.pem create mode 100644 examples/mysql-diesel/certs/client-key.pem create mode 100644 examples/mysql-diesel/certs/client-req.pem create mode 100644 examples/mysql-diesel/certs/root-ca-key.pem create mode 100644 examples/mysql-diesel/certs/root-ca.pem create mode 100644 examples/mysql-diesel/certs/server-cert.pem create mode 100644 examples/mysql-diesel/certs/server-key.pem create mode 100644 examples/mysql-diesel/certs/server-req.pem create mode 100644 examples/mysql-diesel/docker-compose.yml create mode 100644 examples/mysql-diesel/migrations/2023-08-28-162356_add_user/down.sql create mode 100644 examples/mysql-diesel/migrations/2023-08-28-162356_add_user/up.sql create mode 100755 examples/mysql-diesel/scripts/generate-certs.sh create mode 100644 examples/mysql-diesel/src/main.rs create mode 100644 examples/mysql-diesel/structure.sql create mode 100644 examples/mysql-sqlx/Cargo.toml create mode 100644 examples/mysql-sqlx/README.md create mode 100644 examples/mysql-sqlx/certs/client-cert.pem create mode 100644 examples/mysql-sqlx/certs/client-key.pem create mode 100644 examples/mysql-sqlx/certs/client-req.pem create mode 100644 examples/mysql-sqlx/certs/root-ca-key.pem create mode 100644 examples/mysql-sqlx/certs/root-ca.pem create mode 100644 examples/mysql-sqlx/certs/server-cert.pem create mode 100644 examples/mysql-sqlx/certs/server-key.pem create mode 100644 examples/mysql-sqlx/certs/server-req.pem create mode 100644 examples/mysql-sqlx/docker-compose.yml create mode 100644 examples/mysql-sqlx/migrations/20230828202611_add_users.sql create mode 100755 examples/mysql-sqlx/scripts/generate-certs.sh create mode 100644 examples/mysql-sqlx/src/main.rs create mode 100644 examples/mysql-sqlx/structure.sql create mode 100644 examples/sqlite-sqlx/Cargo.toml create mode 100644 examples/sqlite-sqlx/README.md create mode 100644 examples/sqlite-sqlx/migrations/20230827152633_add_users.sql create mode 100644 examples/sqlite-sqlx/src/main.rs create mode 100644 examples/sqlite-sqlx/structure.sql create mode 100644 fixtures/diesel/sqlite/migrations/2023-08-27-215620_add_users/down.sql create mode 100644 fixtures/diesel/sqlite/migrations/2023-08-27-215620_add_users/up.sql create mode 100644 fixtures/sqlx/sqlite/migrations/20230827160610_add_users.sql create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/macros.rs create mode 100644 src/mysql/mod.rs create mode 100644 src/mysql/options.rs create mode 100644 src/process.rs create mode 100644 src/sqlite/diesel.rs create mode 100644 src/sqlite/mod.rs create mode 100644 src/sqlite/sqlx.rs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..df4f01e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: +- package-ecosystem: cargo + directory: "/" + schedule: + interval: daily diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..07812e7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,79 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + branches: '*' + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.nightly }} + + strategy: + fail-fast: false + matrix: + toolchain: [ 'stable' ] + nightly: [false] + include: + - toolchain: 'nightly' + nightly: true + + steps: + - uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.toolchain }} + override: true + components: clippy, rustfmt + + - name: Cache cargo registry + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: rust_${{ matrix.toolchain }}-cargo-${{ hashFiles('**/Cargo.toml') }} + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Run test - sqlite | sqlx | runtime-async-std | macros + uses: actions-rs/cargo@v1 + with: + command: test + args: --features sqlite,sqlx,runtime-async-std,macros --all-targets --verbose + + - name: Run test - sqlite | diesel | runtime-async-std | macros + uses: actions-rs/cargo@v1 + with: + command: test + args: --features sqlite,diesel,runtime-async-std,macros --all-targets --verbose + + - name: Run test - sqlite | diesel + uses: actions-rs/cargo@v1 + with: + command: test + args: --features sqlite,diesel --all-targets --verbose + + - name: Run doc tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --features sqlite,sqlx,runtime-async-std,macros --doc --verbose + + - name: Run clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --features sqlite,sqlx,runtime-async-std,macros diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b8661d8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "database-schema" +version = "0.1.0" +edition = "2021" +description = "A library to dump a database schema into a file in SQL format" +documentation = "https://docs.rs/database-schema" +authors = ["Norberto Lopes "] +repository = "https://github.com/nlopes/database-schema.git" +homepage = "https://github.com/nlopes/database-schema.git" +license = "MIT OR Apache-2.0" +keywords = ["database", "schema", "structuresql"] +categories = ["database", "web-programming"] +exclude = [".gitignore", ".github/", "README.tpl"] + +[workspace] +members = [".", "examples/*"] +default-members = ["."] +resolver = "2" + +[features] +default = [] +sqlx = ["dep:sqlx", "sqlx?/tls-native-tls"] +diesel = ["dep:diesel", "dep:diesel_migrations"] +macros = [] +runtime-async-std = ["sqlx?/runtime-async-std", "dep:async-std"] +runtime-tokio = ["sqlx?/runtime-tokio", "dep:tokio"] +sqlite = ["sqlx?/sqlite", "diesel?/sqlite", "diesel_migrations?/sqlite"] +mysql = ["sqlx?/mysql", "diesel?/mysql", "diesel_migrations?/mysql", "url", "percent-encoding"] +postgres = ["sqlx?/postgres", "url", "percent-encoding"] + +[dependencies] +async-std = { version = "1", optional = true } # this has to include default due to task::block_on usage +chrono = { version = "0.4", features = ["clock"], default-features = false } +diesel = { version = "2.1", optional = true, default-features = false } +diesel_migrations = { version = "2.1", optional = true, default-features = false} +http = "0.2.9" +percent-encoding = { version = "2.3", optional = true } +sqlx = { version = "0.7", features = ["migrate", "macros"], optional = true, default-features = false } +thiserror = "1" +tokio = { version = "1", features = ["rt-multi-thread"], optional = true, default-features = false } +tracing = { version = "0.1", default-features = false } +url = { version = "2.1", optional = true } + +[dev-dependencies] +tokio = { version = "1", features = ["rt", "macros"], default-features = false } + +[package.metadata.docs.rs] +features = ["sqlite", "sqlx", "macros", "runtime-async-std"] +rustdoc-args = ["--cfg", "docsrs"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..13f9fb1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2023 Norberto Lopes + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..38eb8c8 --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# database-schema + +[![CI Status](https://github.com/nlopes/database-schema/workflows/Test/badge.svg)](https://github.com/nlopes/database-schema/actions) +[![docs.rs](https://docs.rs/database-schema/badge.svg)](https://docs.rs/database-schema) +[![crates.io](https://img.shields.io/crates/v/database-schema.svg)](https://crates.io/crates/database-schema) +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nlopes/database-schema/blob/master/LICENSE) + +This crate provides a simple way to dump a database structure to a file, in SQL +format. + +It takes inspiration by the ruby on rails [schema dump]. + +## Usage + +```rust +use std::path::PathBuf; + +database_structure::generate_structure_sql(PathBuf::from("./structure.sql")); +``` + + +## Feature flags + +`database-schema` uses a set of [feature flags] to reduce the size of the libray and +therefore your binary. The way one should use this package is to pick the right +combination of feature flags for their use case. Below is a list of the available +feature flags and the combinations that are recommended for each use case. + +- `sqlite`: Enables SQLite support. +- `postgres`: Enables PostgreSQL support. +- `mysql`: Enables MySQL support. +- `sqlx`: Enables [sqlx] support. +- `diesel`: Enables [diesel] support. + +### Feature flag matrix +| Database | Query builder | Runtime | +|----------|---------------|---------| +| `sqlite` | `sqlx` | `runtime-async-std` | +| `sqlite` | `sqlx` | `runtime-tokio` | +| `sqlite` | `diesel` | | +| `mysql` | `sqlx` | `runtime-async-std` | +| `mysql` | `sqlx` | `runtime-tokio` | +| `mysql` | `diesel` | | +| `postgres` | `sqlx` | `runtime-async-std` | +| `postgres` | `sqlx` | `runtime-tokio` | +| `postgres` | `diesel` | | + +### Combining feature flags + +The following are the recommended feature flag combinations for each use case. + +First pick one of the following database feature flags: + +* `sqlite` +* `mysql` +* `postgres` + +Then pick one of the following database query building feature flags: + +* `sqlx` +* `diesel` + +If you're using `sqlx`, you also have to pick one of the following runtime feature flags: + +* `runtime-async-std` +* `runtime-tokio` + +### Example + +```toml +[dependencies] +database-schema = { version = "0.1", features = ["sqlite", "sqlx", "runtime-async-std"] } +``` + +alternatively, if you're using `diesel`: +```toml +[dependencies] +database-schema = { version = "0.1", features = ["sqlite", "diesel"] } +``` + +### Macros + +This crate also provides a set of macros that can be used to generate the SQL +structure of a database at compile time. This is useful for generating the SQL from +`build.rs`. + + +```toml +[dependencies] +database-schema = { version = "0.1", features = ["sqlite", "diesel", "macros"] } +``` + +```rust +use database_schema::macros::generate_without_runtime; + +let sql = generate_without_runtime!("./migrations", "structure.sql"); +``` + +The above is strictly equivalent to calling: + +```rust +use database_schema::macros::generate_without_runtime_using_defaults; + +let sql = generate_without_runtime!(); +``` + +## Customization + +```rust +use database_schema::DatabaseSchemaBuilder; + +let migrations_path = "db/migrations"; +let destination_path = "db/structure.sql"; + +// This assumes you're using SQLite in memory. +// +// If you need to set up a `connection_url` you can use +// `DatabaseSchemaBuilder::connection_url` before calling +// `build()`. + +DatabaseSchemaBuilder::new() + .migrations_dir(migrations_path)? + .destination_path(destination_path) + .build() + .dump() + .await +``` + +[feature flags]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section +[sqlx]: https://docs.rs/sqlx/latest/sqlx/ +[diesel]: https://docs.rs/diesel/latest/diesel/ +[schema dump]: https://guides.rubyonrails.org/active_record_migrations.html#schema-dumping-and-you diff --git a/README.tpl b/README.tpl new file mode 100644 index 0000000..a345239 --- /dev/null +++ b/README.tpl @@ -0,0 +1,8 @@ +# {{crate}} + +[![CI Status](https://github.com/nlopes/database-schema/workflows/Test/badge.svg)](https://github.com/nlopes/database-schema/actions) +[![docs.rs](https://docs.rs/database-schema/badge.svg)](https://docs.rs/database-schema) +[![crates.io](https://img.shields.io/crates/v/database-schema.svg)](https://crates.io/crates/database-schema) +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nlopes/database-schema/blob/master/LICENSE) + +{{readme}} diff --git a/examples/mysql-diesel/Cargo.toml b/examples/mysql-diesel/Cargo.toml new file mode 100644 index 0000000..9e8eeb5 --- /dev/null +++ b/examples/mysql-diesel/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mysql-diesel" +version = "0.1.0" +edition = "2021" +authors = ["Norberto Lopes "] + +[dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros"], default-features = false } +database-schema = { path = "../../", features = ["diesel", "mysql"] } diff --git a/examples/mysql-diesel/README.md b/examples/mysql-diesel/README.md new file mode 100644 index 0000000..bb29b00 --- /dev/null +++ b/examples/mysql-diesel/README.md @@ -0,0 +1,30 @@ +# mysql-diesel + +This simple example will use `diesel` to run the migrations found in +[`./migrations`](./migrations) and then generate a [`./structure.sql`](./structure.sql) +file with the database schema; + +# Running + +```shell +# this is an example in my MacOS laptop, using mysql from homebrew, adjust as you see fit! +$ docker compose up +$ export PATH=/opt/homebrew/opt/mysql-client@8.0/bin:$PATH +$ export RUSTFLAGS="-L/opt/homebrew/opt/mysql-client@8.0/lib" +$ cargo run +``` + +## Dependencies + +- `libmysqlclient` +- `mysqldump` +- `docker compose` - if you want to run the example against a docker instance running + `mysql` server. + +If they are not in your default paths you will have to set them like so: + +```shell +export PATH=/path/to/your/mysql-client/install-folder/bin:$PATH +export RUSTFLAGS="-L/path/to/your/lib-mysql-folder/lib" +``` + diff --git a/examples/mysql-diesel/certs/client-cert.pem b/examples/mysql-diesel/certs/client-cert.pem new file mode 100644 index 0000000..376b149 --- /dev/null +++ b/examples/mysql-diesel/certs/client-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBzCCAe8CAQEwDQYJKoZIhvcNAQELBQAwRjELMAkGA1UEBhMCR0IxDzANBgNV +BAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9uZG9uMRUwEwYDVQQDDAxteXNxbC1kaWVz +ZWwwHhcNMjMwODI4MTk0MTM1WhcNMzMwNzA2MTk0MTM1WjBNMQswCQYDVQQGEwJH +QjEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZMb25kb24xHDAaBgNVBAMME215 +c3FsLWRpZXNlbC1jbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCtEfRGJilyVwRC7rAs7VzLkPoXg6gOHeZz9eblUx8lc16uwu/SsS2POZ8QUOdX +fupMj79vK9WSHy59DWBCPfHUSQonJzkP7EehZlhJ5IZG3vH6jtk9RQvCpZYudGev +bFyMPNT5ZqsOnF9u0DnQPNcjts1jgzpQj2tFYXJYtf9YP8GfeudgTcU4Y6+a/uYs +WlidQMPOurOTpdtQW6aS5rOsE2zovcWPboWYahgWn/bl17lX3MprJ7suDIr8zuf+ +Jyiw1jywNi5ZdBoeBeHh9fXZtGynX8X2WicL6kfPOTFRw5/39TLGhRrLXjK8DlVq +F3/R6VAs4gxkOr8jzCLUohsrAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIlZ3Skj +gtqPk/NwzbDBRHivmeOTSBXx9/KP+zPwR5psuJGqRNqo8V4X4l2rd7Cf0g249PLf +FUziwXAUsp6U8k8JfOvW2l70wRhFOZlVR54Pz4g4JH/lSTasbo3JfE57wVQz3XQK +qJi1ZkaQB5mbOG1SJRTf5X32L9ADCkaGV2rwPZbKEqId2RO9ExfL2MG8OnDfSBK2 +ciVTVIH/5J6+I4YgvFYy6qyGyHZcgc5hYynokD2Kfnqnl8pmVITh7xreM7VfWIhW +Tz1vmk58cbDpnDoqahuSf5e8rKcUavrV1x7tYUmnMdO/baOY7jc3CXsHgbBhWZZY +W70jJgfB6jr9Xs4= +-----END CERTIFICATE----- diff --git a/examples/mysql-diesel/certs/client-key.pem b/examples/mysql-diesel/certs/client-key.pem new file mode 100644 index 0000000..454766f --- /dev/null +++ b/examples/mysql-diesel/certs/client-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEArRH0RiYpclcEQu6wLO1cy5D6F4OoDh3mc/Xm5VMfJXNersLv +0rEtjzmfEFDnV37qTI+/byvVkh8ufQ1gQj3x1EkKJyc5D+xHoWZYSeSGRt7x+o7Z +PUULwqWWLnRnr2xcjDzU+WarDpxfbtA50DzXI7bNY4M6UI9rRWFyWLX/WD/Bn3rn +YE3FOGOvmv7mLFpYnUDDzrqzk6XbUFumkuazrBNs6L3Fj26FmGoYFp/25de5V9zK +aye7LgyK/M7n/icosNY8sDYuWXQaHgXh4fX12bRsp1/F9lonC+pHzzkxUcOf9/Uy +xoUay14yvA5Vahd/0elQLOIMZDq/I8wi1KIbKwIDAQABAoIBAH9tMbqYjHmoQfX6 +AfMCTQmA0/KOOCU0tKH6kqeUXOFZIYRw+NzbIR1MIqaDuuF8C4yVZjC3SIdOuA7Q +02fSbgSMRpJvWZ80q8TVMvos7QSvT+DYXnCzLqaA/qNzh4fss/N5MqHyis22KrnP +TFHbCdg81tqHG1+HSUcLKYLRdZEGIQxUIN7Pr7DMKR2ykNN/xulqaN1aJ7D5kScj +rPJVd225UQNXWWh8RYVA9iN25oYOBBOrsJFxnULP3KRkDSdruok9F9GRourL1ELp +Gemxo1CIV+iM+RmsHpYxsh1qgkqVxowaz4KJMQ9PT6oOyljLDuA1g/dJc+rXSjBf +bHpacuECgYEA5pjtVTlSj+o0JV9ABC3XOBv4rYVDB0UcAJvutrqPzj5Oj+m+83sm +N+jAD2zThjwbIQtCEnmeI55iLcZWYlHhGdxJyM74fafVZe7lsKxRWoaecNPaJfT6 +JJHOji9NA3WA4KO4jErEmEB/HOf4bUK91caiUFLKiltGUYSF41j26dsCgYEAwCKz +zj8bbi0pGuKRwgH9RTODhuUMHflA2LC3zknfRaFKcXlV1ddz6IPrOr0HxSVew3G0 +wC6Z7yPVTE2gAy9T2MjeQX8dofFOk5g2bDN3Ac1a8MfyP78i5qt9OxiqBy2uj/lV +M0fgrw/95iRPmxyBAVvAnmjEVEu7fabHO6CUHPECgYBDw68C+2xqxF18mgga8kmr +wHSMsXuoGEQJXcmBw0NdTWwS2JL3xDnP9kLyhX2HlgQ26rMI8NprBzE82GssS3mF ++vln3IKjkn2gjdrL12e03ZiT+X3C58HWm06C9B2CpbYwzYv/Fj29rD5uhTC5EwLs +Xon2Zs4EaJw6emJKFCvDPwKBgQCGkwfbqun8lpcW5KDxAVGzOayjPCTrjZy06bok +PCutapZounK7j+f4cQW+o44gsNcaD7dpcHqTPEb25dvwvyJ8Ud0ShQVtW0YNLOzZ +hoaRdZN/2Jw9uBOq+2yAivr0gjOlVh8uBudB1vKgUsiLPUDCgdB9Y6Y34L+W98zO +X9++EQKBgHtQWPYXwoe0I7UAumPfktbBMQDUtleCEdwGISn3z1zPHSM/9uJL9hvk +mGFaJhGt9Ts1dASxdjkaZ3FEwO1yn+/ToxzODkY6u7LIpkFE0lR7rPfmWN579Q1R +rm4+saCQDNWH8vJS7ToRAz0oheO/k2x9Om3Z1L1fTsYY4U5AIHVJ +-----END RSA PRIVATE KEY----- diff --git a/examples/mysql-diesel/certs/client-req.pem b/examples/mysql-diesel/certs/client-req.pem new file mode 100644 index 0000000..7422915 --- /dev/null +++ b/examples/mysql-diesel/certs/client-req.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICkjCCAXoCAQAwTTELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0G +A1UEBwwGTG9uZG9uMRwwGgYDVQQDDBNteXNxbC1kaWVzZWwtY2xpZW50MIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArRH0RiYpclcEQu6wLO1cy5D6F4Oo +Dh3mc/Xm5VMfJXNersLv0rEtjzmfEFDnV37qTI+/byvVkh8ufQ1gQj3x1EkKJyc5 +D+xHoWZYSeSGRt7x+o7ZPUULwqWWLnRnr2xcjDzU+WarDpxfbtA50DzXI7bNY4M6 +UI9rRWFyWLX/WD/Bn3rnYE3FOGOvmv7mLFpYnUDDzrqzk6XbUFumkuazrBNs6L3F +j26FmGoYFp/25de5V9zKaye7LgyK/M7n/icosNY8sDYuWXQaHgXh4fX12bRsp1/F +9lonC+pHzzkxUcOf9/UyxoUay14yvA5Vahd/0elQLOIMZDq/I8wi1KIbKwIDAQAB +oAAwDQYJKoZIhvcNAQELBQADggEBAJ/kwUYFLLqqkjnNddSwODoQkh8dyNE+gfkW +6pRYF1p/ACtQbIPddJS/xG7rb+DCEHPqQY09N0mgyBu0LiQyG14hqFsZf0fjEBv0 +Of9ZaZsxQovGAGOgQ/GtO9knePpv3ZzzkNFuqKG/5X+RFke9lxqfTY4m6W9vOkeL +dfx+SBz4ZBcJ2FgOwZWLOzmpTXRWrHocsLWsO58Yb2V4tUE5S0ojQrhQbZDEFKZK +hbbwjgAvXR7YJGdOoQq6SkEqGx/gLTTEwkceSn2nRXiuJYLMdXKmoUvY8DQRNH/L +qlGC68jyQdxie1DpRToomx3URg5ceUKJeAdvNC3zb76Wg99Hsqk= +-----END CERTIFICATE REQUEST----- diff --git a/examples/mysql-diesel/certs/root-ca-key.pem b/examples/mysql-diesel/certs/root-ca-key.pem new file mode 100644 index 0000000..5076e6e --- /dev/null +++ b/examples/mysql-diesel/certs/root-ca-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuBdM/TyViYfnYB2DADqg1+6AbxO8a1ZcP3NPxVgBtprhOTPT +H8Yvg0/RKQrnp6/8eNa01wobcKmfDRbpF3WrHPjOq/3FVxNbcCnezrvPxjWmFSEI +kedlokFVPSlSM8wwhHoWOGNU0OS+fiT1ixPQBEPb33xIRIg1gfGqMMlZq4I0qIWk +oVh+qQcaL9clH4RgcRVT6XPCu1fWDZRAO8MdbJr5iqfvanhAHanNIaR2Q3Ja5JsO +zB8j+CFRpf1NwSN8erOAji4ZEDRO1i6+zuveWgOpBn3tay53L8jm0zYMB2noJPqF +Ac/tH2CD/HVcGMcyLBAqIzbHK/ZTWyfBP0TloQIDAQABAoIBAD29stiL5StHJVab +D0CYaTGCkBiw6dSmFjJBnlN1fL2dnEPvGbPiaDUGJAQ74A3hfC+a8vPcM+/JM8rU +EEEJ+eWxnL8aUYEuwNAReuSjIMA9ZgJYHeOxU+jjOI9WuHce2HbV63Xl+qBE146/ +HosSjgWgLLH7oNE7TZbTUl0iaqyWYIXfCP1BmsJfRzluqQlBJN/TuBGYcuSZWx+n +dW/lJNO9WVzFrD2+9lae9Qjnb4jUgt2L8Yi7w+RPoLW/h4dklggKpgyXO0sPXOp6 +CVqPszw1hyoTDoyrQPjVEXlKMnTrLGGLdX9dZPBift7Q4iCiDTgHq4tCEhePrCdi +gB2SAnECgYEA7p8cdsCfri27LDNvfwzC/HHR+a3YWnra5ySiV8/7wOknE9MgDNQV +6e1XwWPvs94suXJsoMAbrCrQxfsiHqQMSDIUtlY5TY/6NpnouOEaW7J6BykqFyxb +xOHmHKjw6vFKw/psDXYcwOqyRihPK2DGiz+LdrdTFp9LskYBfYk+Jn8CgYEAxX+E +Hsesj8V8vCzhfmGmWErSVOgRsB/rlR+b5ONLApa2+I9uEEv+IBT1VxM8XxE6IIN6 +tG7nggzpnwgrNP8KSGb804uFFCazaNuF2bXOvaK67fJz1eA6kXV2NpgU0hVzT3Ad +LTSMCQ7Ps0LkKhXr5h6LFp6O4lU028tSzuoiI98CgYEA4J1Rc88aACD4AUFhgJyI +poyVdItaDsF6cP2g+zvB5PMTX6vqjWjOP+a0JkxmBE/slZvJ+P8cjVG4N8SPd3xA +O2045fH/+qy+gMsbr3vlDc/Q4hCzmCCfOZLSwsOcE+uRzyxYrcsygb3qlfO3okN0 +YPst0k/6nF7SKDuRh5O6tw0CgYAL4FgslatN1f1jP3ur4uli49T0ICR4J+M9y3HP +eM+Y70E/fziKKFe2zCvYuaJmwR1yuRVW5lhrnKUr2AzpGfEfW3oWqowtIwqk4paQ ++frdsnx1NKA8m0hKWPrr24dc/sc5Xq+SeVd2b/qTeBFKapkN9IY+rPhAqgkMspRf +NvsolwKBgFaDHjVaYdWEqdEucvt1wWe/j3cinGgzRBsz5b5PJ0WeWrn1YtxlIH2S +CShwtUK4I4/3PpqTApaJBFdE51edtJKAIwpI40g7zjy/Pc9FQPjMuz4K5BNGFtE3 +WwOiQYO8iHbNJ3ZzrKTYQmD9sHLkFnqhePmfY9ttLMdgB5Xpox8C +-----END RSA PRIVATE KEY----- diff --git a/examples/mysql-diesel/certs/root-ca.pem b/examples/mysql-diesel/certs/root-ca.pem new file mode 100644 index 0000000..7e178cc --- /dev/null +++ b/examples/mysql-diesel/certs/root-ca.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCDCCAfACCQCgRRoacvClxzANBgkqhkiG9w0BAQsFADBGMQswCQYDVQQGEwJH +QjEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZMb25kb24xFTATBgNVBAMMDG15 +c3FsLWRpZXNlbDAeFw0yMzA4MjgxOTM0MzBaFw0zMzA3MDYxOTM0MzBaMEYxCzAJ +BgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRvbjEVMBMG +A1UEAwwMbXlzcWwtZGllc2VsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAuBdM/TyViYfnYB2DADqg1+6AbxO8a1ZcP3NPxVgBtprhOTPTH8Yvg0/RKQrn +p6/8eNa01wobcKmfDRbpF3WrHPjOq/3FVxNbcCnezrvPxjWmFSEIkedlokFVPSlS +M8wwhHoWOGNU0OS+fiT1ixPQBEPb33xIRIg1gfGqMMlZq4I0qIWkoVh+qQcaL9cl +H4RgcRVT6XPCu1fWDZRAO8MdbJr5iqfvanhAHanNIaR2Q3Ja5JsOzB8j+CFRpf1N +wSN8erOAji4ZEDRO1i6+zuveWgOpBn3tay53L8jm0zYMB2noJPqFAc/tH2CD/HVc +GMcyLBAqIzbHK/ZTWyfBP0TloQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCZQyvu +ztrnkmP+/gTbdHlZMcQjA9U0aRQqesJm/O75ICGTpjxlWXXzKe/WSqP2ZoGLRbY0 +Wi1KnL/V0qc8QpgCcBsNSa0VwtcTUZVTUGecGxk5BEc8dH8hfnkGy+bH2e81LmX5 +cMibbkU6wH5NwgTQIkUlRlDbCiLoszAhgAOFkOFLUEIxHIea6YeRw1479nUxNXDM +ZXDsj+EGNE94HQwSiNkxfuWwpCyAOrLeBmaUaG7SvMioh5lffSJcZQ8nPWA/ZJ8n +49kAh3iwKNMGpvKOohu4e1qpFOxPgHiJ+5XKQM+YHmN1TRVzLXEsF/IgwSvb5UxQ +qXdoeBiEhSrHSkom +-----END CERTIFICATE----- diff --git a/examples/mysql-diesel/certs/server-cert.pem b/examples/mysql-diesel/certs/server-cert.pem new file mode 100644 index 0000000..f059a26 --- /dev/null +++ b/examples/mysql-diesel/certs/server-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBzCCAe8CAQEwDQYJKoZIhvcNAQELBQAwRjELMAkGA1UEBhMCR0IxDzANBgNV +BAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9uZG9uMRUwEwYDVQQDDAxteXNxbC1kaWVz +ZWwwHhcNMjMwODI4MTkzNjM1WhcNMzMwNzA2MTkzNjM1WjBNMQswCQYDVQQGEwJH +QjEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZMb25kb24xHDAaBgNVBAMME215 +c3FsLWRpZXNlbC1zZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQDgRFH/Q1eoZoYRGWtDZ7DAZ0xHWtLk4qB12WA+JwwYfEG9g68JSTsyCAwFINB4 +81S7sNH5+FDFmZFAKQnmIfH11LKN5jSsrZLITFlKX4tj4Hisxt5TTyBHSMBQPwoK +RtGV7dhANq5epJVRUvA5ehOEvn1Ke0x38sDihde7Z5P7PyoL1fjHALrTafu/4WsG +EfAAQFtCP5wtJRwyaI0conXBgPdBYs4acKAUMthkSfYc9VNm6JvpZZrjX0MkRLIj +3Z5PYuWVi6cI53RteIXGF6D+TS0cqQFi0cl4Gf78Au8BKjsxb/udJtzC20Jip0VJ +SdTiVta/VMZ+UQFTZD3CYJNVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAJjHuZej +IyRLMdggq6BkV0F6rVqH2wB2dEklwZJPWmJ/v3DLwBbALsUpzVh1gh4Oa93V8Bdj +2kJWmHdFMsb/gwKzl4kHsWMskEzjDYf1RTE+qii7XPm8w8hmxbsHG+7QxJVU4iLA +9ydrsW6aBEl0O4BNcwwlMMpqGyTOFCSCWCEVe0YVIFTJ/2VJHv47wIod4kDvkegT +2+mFyY29uA3N0/zIV29wWZc2nxt+JRf/J4mO85jOm18hstBa8b7Fn4nB+ttH4Blx +7fotbaia8Kb+EgZuwmKiJAJPLdMAQt0YjHkz6JMgM7Lg3YYkwPo9iajKicPh67Tu +nINADc3fblSgyxs= +-----END CERTIFICATE----- diff --git a/examples/mysql-diesel/certs/server-key.pem b/examples/mysql-diesel/certs/server-key.pem new file mode 100644 index 0000000..e9dc4fc --- /dev/null +++ b/examples/mysql-diesel/certs/server-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA4ERR/0NXqGaGERlrQ2ewwGdMR1rS5OKgddlgPicMGHxBvYOv +CUk7MggMBSDQePNUu7DR+fhQxZmRQCkJ5iHx9dSyjeY0rK2SyExZSl+LY+B4rMbe +U08gR0jAUD8KCkbRle3YQDauXqSVUVLwOXoThL59SntMd/LA4oXXu2eT+z8qC9X4 +xwC602n7v+FrBhHwAEBbQj+cLSUcMmiNHKJ1wYD3QWLOGnCgFDLYZEn2HPVTZuib +6WWa419DJESyI92eT2LllYunCOd0bXiFxheg/k0tHKkBYtHJeBn+/ALvASo7MW/7 +nSbcwttCYqdFSUnU4lbWv1TGflEBU2Q9wmCTVQIDAQABAoIBAQDWFiI4ja7GYWim +Nh/BLuD6KyIUE82zFeyb07EeY+QWE7gmE+kp8jCCKFBhLNwWOiOvMLKh77wbhee0 +2fhLKihyxmLVNucd2GyArPGoH+FOrOBF/2oJGUX1Bqwfbq5E9Snp+C0xz4FRc8fe +5E/SbojndXjheyaacrBTDfXNPqlzcyUErpKeNeTA7IEYumxXrL/QfZ3BramSjtDS +168DVWmDZStFjjaoMHjen5VOJiTBZCScA00fx15pCVRfA1hxzLK/yiPldSyUwP0T +CdxTKtK8NxHZCBcIKQa/rje2E/nYyOAF0UseKQdVL32mw5mVNg7Sdanu/4ef3qAh +jhVEbK2ZAoGBAPiw84BLZFHEjOcFWXjnP6svJOqUJDHtTX3CBeMJqATpLqc32G/h +ncV439NKuDXdeG8XuHz2CeRdHmzr1wGqwiUOMZoXyFUnlDuFlQ2wgLV4XUAn9lhg +hFuCKITbvQBj+pgGtJW9lMuPLHtoYBNSqoJ+hwpXmiGY+/8u8bX1ZFUHAoGBAObb +nFAmRHIyNGL0DKBtsXIC5ncCVoWoA2e7LQLsIK48t/Y/umPkSaeuoNMKSLsNmnoA +BHi7R8BxDt5SMUc1Ee3S6T7KoTnDN+WifEPG5ZV5PquwYQzeISSQlPolM3594Tlr +jzFmh8YhWPMQLE0wFTXkORc2Gq3u7Cw/aZLtwPnDAoGAAkfVI9MNK3bK9/9jj6lH +OzGmfAQ5xL9xW3rhBPD8unbM97maYYRHJx1xqAViZRQP26zzAypact6rvcH36csA +FsjniYs4Shef01/pOwPoWkGtUT5MkwjnjdNGvdkqBYvlK2m8VDc12xwIA2W0o9i5 +OZMhd+XPwwkdHnz2uH10mxcCgYAQ6zlaemN7xwgl1GL0gvWFOIcxY595riWJvaL/ +2tROUK7ewi7j0UBApMCulstbwhR3KncpbfhFN0PysS1hXZdRyapEas9ED3WSQ8U9 +pl7/BGK+bG/AZKsjJeF1L7LFa25NKf/VjAEZWSpOrfnBMmvrhi1U8SclZiQy3fbb +yMbixwKBgQD0N3QgaPivL/3WtuNfksseCQKcz6SzLo68VPpIHgol3zC+gaHDEoiX +zK53Pm5aSo3eyajzthUCIvfC2QpSyBETX5kxreNna71/XWhWMx3ZtT2d9zT4uSUt +wzJ7L4lAvQq7IpCrXMP8oApxVt22358x67pE+wjMivKkKEtokGzFRg== +-----END RSA PRIVATE KEY----- diff --git a/examples/mysql-diesel/certs/server-req.pem b/examples/mysql-diesel/certs/server-req.pem new file mode 100644 index 0000000..12f5f5d --- /dev/null +++ b/examples/mysql-diesel/certs/server-req.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICkjCCAXoCAQAwTTELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0G +A1UEBwwGTG9uZG9uMRwwGgYDVQQDDBNteXNxbC1kaWVzZWwtc2VydmVyMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4ERR/0NXqGaGERlrQ2ewwGdMR1rS +5OKgddlgPicMGHxBvYOvCUk7MggMBSDQePNUu7DR+fhQxZmRQCkJ5iHx9dSyjeY0 +rK2SyExZSl+LY+B4rMbeU08gR0jAUD8KCkbRle3YQDauXqSVUVLwOXoThL59SntM +d/LA4oXXu2eT+z8qC9X4xwC602n7v+FrBhHwAEBbQj+cLSUcMmiNHKJ1wYD3QWLO +GnCgFDLYZEn2HPVTZuib6WWa419DJESyI92eT2LllYunCOd0bXiFxheg/k0tHKkB +YtHJeBn+/ALvASo7MW/7nSbcwttCYqdFSUnU4lbWv1TGflEBU2Q9wmCTVQIDAQAB +oAAwDQYJKoZIhvcNAQELBQADggEBAAP0fQjlLa5AlCMCjpoUMVr/vUwCJWnah3vN +nAbJbk1Pq1OgBG1+1iUf41OAFHeVmze/vGyJHYujgUBsgS8tBZmUDon9fVH8ny3Z +aBBbhvOgTMM5Z47zwGN1fZUqvZxth3SEs02jeECqiY1ZOtaluK13ui6Y6187arXH +o5qBx2gJSeerXCgitTMgqGRU9TgvQXZxQfbXNmdrwnI9Y4i763eKSFn+23T/9aQE +OBawz4hUCJn2vmfllMUlIX2AP0+XzuN2GoLTrBxAuYNKCT1xP5V90xl1uw9xIvKf +ebtgVTCUGGbdKTQEgYshoC1aOPIMTb3oBqSZHPNf+8IIkrSuRu4= +-----END CERTIFICATE REQUEST----- diff --git a/examples/mysql-diesel/docker-compose.yml b/examples/mysql-diesel/docker-compose.yml new file mode 100644 index 0000000..0d7d63b --- /dev/null +++ b/examples/mysql-diesel/docker-compose.yml @@ -0,0 +1,20 @@ +services: + mysql-8: + image: mysql:8 + command: [ "mysqld", + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_unicode_ci", + "--bind-address=0.0.0.0", + "--require_secure_transport=ON", + "--ssl-ca=/etc/certs/root-ca.pem", + "--ssl-cert=/etc/certs/server-cert.pem", + "--ssl-key=/etc/certs/server-key.pem"] + ports: + - "127.0.0.1:3306:3306" + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=true + - MYSQL_DATABASE=example + volumes: + - type: bind + source: ./certs + target: /etc/certs/ diff --git a/examples/mysql-diesel/migrations/2023-08-28-162356_add_user/down.sql b/examples/mysql-diesel/migrations/2023-08-28-162356_add_user/down.sql new file mode 100644 index 0000000..291a97c --- /dev/null +++ b/examples/mysql-diesel/migrations/2023-08-28-162356_add_user/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` \ No newline at end of file diff --git a/examples/mysql-diesel/migrations/2023-08-28-162356_add_user/up.sql b/examples/mysql-diesel/migrations/2023-08-28-162356_add_user/up.sql new file mode 100644 index 0000000..7164e32 --- /dev/null +++ b/examples/mysql-diesel/migrations/2023-08-28-162356_add_user/up.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id VARCHAR(64) PRIMARY KEY NOT NULL, + email TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/examples/mysql-diesel/scripts/generate-certs.sh b/examples/mysql-diesel/scripts/generate-certs.sh new file mode 100755 index 0000000..b501a36 --- /dev/null +++ b/examples/mysql-diesel/scripts/generate-certs.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +openssl genrsa 2048 > certs/root-ca-key.pem +openssl req -new -x509 -nodes -days 3600 -subj "/C=GB/ST=London/L=London/CN=mysql-diesel" -key certs/root-ca-key.pem -out certs/root-ca.pem +openssl req -newkey rsa:2048 -days 3600 -nodes -subj "/C=GB/ST=London/L=London/CN=mysql-diesel-server" -keyout certs/server-key.pem -out certs/server-req.pem +openssl rsa -in certs/server-key.pem -out certs/server-key.pem +openssl x509 -req -in certs/server-req.pem -days 3600 -CA certs/root-ca.pem -CAkey certs/root-ca-key.pem -set_serial 01 -out certs/server-cert.pem +openssl verify -CAfile certs/root-ca.pem certs/server-cert.pem +openssl req -newkey rsa:2048 -days 3600 -nodes -subj "/C=GB/ST=London/L=London/CN=mysql-diesel-client" -keyout certs/client-key.pem -out certs/client-req.pem +openssl rsa -in certs/client-key.pem -out certs/client-key.pem +openssl x509 -req -in certs/client-req.pem -days 3600 -CA certs/root-ca.pem -CAkey certs/root-ca-key.pem -set_serial 01 -out certs/client-cert.pem +openssl verify -CAfile certs/root-ca.pem certs/client-cert.pem diff --git a/examples/mysql-diesel/src/main.rs b/examples/mysql-diesel/src/main.rs new file mode 100644 index 0000000..cf0eb5b --- /dev/null +++ b/examples/mysql-diesel/src/main.rs @@ -0,0 +1,21 @@ +use database_schema::DatabaseSchemaBuilder; + +#[tokio::main] +async fn main() -> Result<(), database_schema::Error> { + DatabaseSchemaBuilder::new() + .connection_url("mysql://root:@127.0.0.1:3306/example?ssl_mode=verify_ca&ssl_key=certs/client-key.pem&ssl_ca=certs/root-ca.pem&ssl_cert=certs/client-cert.pem") + .migrations_dir("./migrations")? + .destination_path("./structure.sql") + .build() + .dump() + .await + + // You can also opt to dump all the databases by not specifying a database name + // DatabaseSchemaBuilder::new() + // .connection_url("mysql://root:@127.0.0.1:3306") + // .migrations_dir("./migrations")? + // .destination_path("./structure.sql") + // .build() + // .dump() + // .await +} diff --git a/examples/mysql-diesel/structure.sql b/examples/mysql-diesel/structure.sql new file mode 100644 index 0000000..5512b04 --- /dev/null +++ b/examples/mysql-diesel/structure.sql @@ -0,0 +1,40 @@ + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +DROP TABLE IF EXISTS `__diesel_schema_migrations`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `__diesel_schema_migrations` ( + `version` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `run_on` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`version`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `users` ( + `id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` text COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + diff --git a/examples/mysql-sqlx/Cargo.toml b/examples/mysql-sqlx/Cargo.toml new file mode 100644 index 0000000..887fca8 --- /dev/null +++ b/examples/mysql-sqlx/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mysql-sqlx" +version = "0.1.0" +edition = "2021" +authors = ["Norberto Lopes "] + +[dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros"], default-features = false } +database-schema = { path = "../../", features = ["sqlx", "mysql", "runtime-async-std"] } diff --git a/examples/mysql-sqlx/README.md b/examples/mysql-sqlx/README.md new file mode 100644 index 0000000..c8b5c39 --- /dev/null +++ b/examples/mysql-sqlx/README.md @@ -0,0 +1,30 @@ +# mysql-sqlx + +This simple example will use `sqlx` to run the migrations found in +[`./migrations`](./migrations) and then generate a [`./structure.sql`](./structure.sql) +file with the database schema; + +# Running + +```shell +# this is an example in my MacOS laptop, using mysql from homebrew, adjust as you see fit! +$ docker compose up +$ export PATH=/opt/homebrew/opt/mysql-client@8.0/bin:$PATH +$ export RUSTFLAGS="-L/opt/homebrew/opt/mysql-client@8.0/lib" +$ cargo run +``` + +## Dependencies + +- `libmysqlclient` +- `mysqldump` +- `docker compose` - if you want to run the example against a docker instance running + `mysql` server. + +If they are not in your default paths you will have to set them like so: + +```shell +export PATH=/path/to/your/mysql-client/install-folder/bin:$PATH +export RUSTFLAGS="-L/path/to/your/lib-mysql-folder/lib" +``` + diff --git a/examples/mysql-sqlx/certs/client-cert.pem b/examples/mysql-sqlx/certs/client-cert.pem new file mode 100644 index 0000000..376b149 --- /dev/null +++ b/examples/mysql-sqlx/certs/client-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBzCCAe8CAQEwDQYJKoZIhvcNAQELBQAwRjELMAkGA1UEBhMCR0IxDzANBgNV +BAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9uZG9uMRUwEwYDVQQDDAxteXNxbC1kaWVz +ZWwwHhcNMjMwODI4MTk0MTM1WhcNMzMwNzA2MTk0MTM1WjBNMQswCQYDVQQGEwJH +QjEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZMb25kb24xHDAaBgNVBAMME215 +c3FsLWRpZXNlbC1jbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCtEfRGJilyVwRC7rAs7VzLkPoXg6gOHeZz9eblUx8lc16uwu/SsS2POZ8QUOdX +fupMj79vK9WSHy59DWBCPfHUSQonJzkP7EehZlhJ5IZG3vH6jtk9RQvCpZYudGev +bFyMPNT5ZqsOnF9u0DnQPNcjts1jgzpQj2tFYXJYtf9YP8GfeudgTcU4Y6+a/uYs +WlidQMPOurOTpdtQW6aS5rOsE2zovcWPboWYahgWn/bl17lX3MprJ7suDIr8zuf+ +Jyiw1jywNi5ZdBoeBeHh9fXZtGynX8X2WicL6kfPOTFRw5/39TLGhRrLXjK8DlVq +F3/R6VAs4gxkOr8jzCLUohsrAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIlZ3Skj +gtqPk/NwzbDBRHivmeOTSBXx9/KP+zPwR5psuJGqRNqo8V4X4l2rd7Cf0g249PLf +FUziwXAUsp6U8k8JfOvW2l70wRhFOZlVR54Pz4g4JH/lSTasbo3JfE57wVQz3XQK +qJi1ZkaQB5mbOG1SJRTf5X32L9ADCkaGV2rwPZbKEqId2RO9ExfL2MG8OnDfSBK2 +ciVTVIH/5J6+I4YgvFYy6qyGyHZcgc5hYynokD2Kfnqnl8pmVITh7xreM7VfWIhW +Tz1vmk58cbDpnDoqahuSf5e8rKcUavrV1x7tYUmnMdO/baOY7jc3CXsHgbBhWZZY +W70jJgfB6jr9Xs4= +-----END CERTIFICATE----- diff --git a/examples/mysql-sqlx/certs/client-key.pem b/examples/mysql-sqlx/certs/client-key.pem new file mode 100644 index 0000000..454766f --- /dev/null +++ b/examples/mysql-sqlx/certs/client-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEArRH0RiYpclcEQu6wLO1cy5D6F4OoDh3mc/Xm5VMfJXNersLv +0rEtjzmfEFDnV37qTI+/byvVkh8ufQ1gQj3x1EkKJyc5D+xHoWZYSeSGRt7x+o7Z +PUULwqWWLnRnr2xcjDzU+WarDpxfbtA50DzXI7bNY4M6UI9rRWFyWLX/WD/Bn3rn +YE3FOGOvmv7mLFpYnUDDzrqzk6XbUFumkuazrBNs6L3Fj26FmGoYFp/25de5V9zK +aye7LgyK/M7n/icosNY8sDYuWXQaHgXh4fX12bRsp1/F9lonC+pHzzkxUcOf9/Uy +xoUay14yvA5Vahd/0elQLOIMZDq/I8wi1KIbKwIDAQABAoIBAH9tMbqYjHmoQfX6 +AfMCTQmA0/KOOCU0tKH6kqeUXOFZIYRw+NzbIR1MIqaDuuF8C4yVZjC3SIdOuA7Q +02fSbgSMRpJvWZ80q8TVMvos7QSvT+DYXnCzLqaA/qNzh4fss/N5MqHyis22KrnP +TFHbCdg81tqHG1+HSUcLKYLRdZEGIQxUIN7Pr7DMKR2ykNN/xulqaN1aJ7D5kScj +rPJVd225UQNXWWh8RYVA9iN25oYOBBOrsJFxnULP3KRkDSdruok9F9GRourL1ELp +Gemxo1CIV+iM+RmsHpYxsh1qgkqVxowaz4KJMQ9PT6oOyljLDuA1g/dJc+rXSjBf +bHpacuECgYEA5pjtVTlSj+o0JV9ABC3XOBv4rYVDB0UcAJvutrqPzj5Oj+m+83sm +N+jAD2zThjwbIQtCEnmeI55iLcZWYlHhGdxJyM74fafVZe7lsKxRWoaecNPaJfT6 +JJHOji9NA3WA4KO4jErEmEB/HOf4bUK91caiUFLKiltGUYSF41j26dsCgYEAwCKz +zj8bbi0pGuKRwgH9RTODhuUMHflA2LC3zknfRaFKcXlV1ddz6IPrOr0HxSVew3G0 +wC6Z7yPVTE2gAy9T2MjeQX8dofFOk5g2bDN3Ac1a8MfyP78i5qt9OxiqBy2uj/lV +M0fgrw/95iRPmxyBAVvAnmjEVEu7fabHO6CUHPECgYBDw68C+2xqxF18mgga8kmr +wHSMsXuoGEQJXcmBw0NdTWwS2JL3xDnP9kLyhX2HlgQ26rMI8NprBzE82GssS3mF ++vln3IKjkn2gjdrL12e03ZiT+X3C58HWm06C9B2CpbYwzYv/Fj29rD5uhTC5EwLs +Xon2Zs4EaJw6emJKFCvDPwKBgQCGkwfbqun8lpcW5KDxAVGzOayjPCTrjZy06bok +PCutapZounK7j+f4cQW+o44gsNcaD7dpcHqTPEb25dvwvyJ8Ud0ShQVtW0YNLOzZ +hoaRdZN/2Jw9uBOq+2yAivr0gjOlVh8uBudB1vKgUsiLPUDCgdB9Y6Y34L+W98zO +X9++EQKBgHtQWPYXwoe0I7UAumPfktbBMQDUtleCEdwGISn3z1zPHSM/9uJL9hvk +mGFaJhGt9Ts1dASxdjkaZ3FEwO1yn+/ToxzODkY6u7LIpkFE0lR7rPfmWN579Q1R +rm4+saCQDNWH8vJS7ToRAz0oheO/k2x9Om3Z1L1fTsYY4U5AIHVJ +-----END RSA PRIVATE KEY----- diff --git a/examples/mysql-sqlx/certs/client-req.pem b/examples/mysql-sqlx/certs/client-req.pem new file mode 100644 index 0000000..7422915 --- /dev/null +++ b/examples/mysql-sqlx/certs/client-req.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICkjCCAXoCAQAwTTELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0G +A1UEBwwGTG9uZG9uMRwwGgYDVQQDDBNteXNxbC1kaWVzZWwtY2xpZW50MIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArRH0RiYpclcEQu6wLO1cy5D6F4Oo +Dh3mc/Xm5VMfJXNersLv0rEtjzmfEFDnV37qTI+/byvVkh8ufQ1gQj3x1EkKJyc5 +D+xHoWZYSeSGRt7x+o7ZPUULwqWWLnRnr2xcjDzU+WarDpxfbtA50DzXI7bNY4M6 +UI9rRWFyWLX/WD/Bn3rnYE3FOGOvmv7mLFpYnUDDzrqzk6XbUFumkuazrBNs6L3F +j26FmGoYFp/25de5V9zKaye7LgyK/M7n/icosNY8sDYuWXQaHgXh4fX12bRsp1/F +9lonC+pHzzkxUcOf9/UyxoUay14yvA5Vahd/0elQLOIMZDq/I8wi1KIbKwIDAQAB +oAAwDQYJKoZIhvcNAQELBQADggEBAJ/kwUYFLLqqkjnNddSwODoQkh8dyNE+gfkW +6pRYF1p/ACtQbIPddJS/xG7rb+DCEHPqQY09N0mgyBu0LiQyG14hqFsZf0fjEBv0 +Of9ZaZsxQovGAGOgQ/GtO9knePpv3ZzzkNFuqKG/5X+RFke9lxqfTY4m6W9vOkeL +dfx+SBz4ZBcJ2FgOwZWLOzmpTXRWrHocsLWsO58Yb2V4tUE5S0ojQrhQbZDEFKZK +hbbwjgAvXR7YJGdOoQq6SkEqGx/gLTTEwkceSn2nRXiuJYLMdXKmoUvY8DQRNH/L +qlGC68jyQdxie1DpRToomx3URg5ceUKJeAdvNC3zb76Wg99Hsqk= +-----END CERTIFICATE REQUEST----- diff --git a/examples/mysql-sqlx/certs/root-ca-key.pem b/examples/mysql-sqlx/certs/root-ca-key.pem new file mode 100644 index 0000000..5076e6e --- /dev/null +++ b/examples/mysql-sqlx/certs/root-ca-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuBdM/TyViYfnYB2DADqg1+6AbxO8a1ZcP3NPxVgBtprhOTPT +H8Yvg0/RKQrnp6/8eNa01wobcKmfDRbpF3WrHPjOq/3FVxNbcCnezrvPxjWmFSEI +kedlokFVPSlSM8wwhHoWOGNU0OS+fiT1ixPQBEPb33xIRIg1gfGqMMlZq4I0qIWk +oVh+qQcaL9clH4RgcRVT6XPCu1fWDZRAO8MdbJr5iqfvanhAHanNIaR2Q3Ja5JsO +zB8j+CFRpf1NwSN8erOAji4ZEDRO1i6+zuveWgOpBn3tay53L8jm0zYMB2noJPqF +Ac/tH2CD/HVcGMcyLBAqIzbHK/ZTWyfBP0TloQIDAQABAoIBAD29stiL5StHJVab +D0CYaTGCkBiw6dSmFjJBnlN1fL2dnEPvGbPiaDUGJAQ74A3hfC+a8vPcM+/JM8rU +EEEJ+eWxnL8aUYEuwNAReuSjIMA9ZgJYHeOxU+jjOI9WuHce2HbV63Xl+qBE146/ +HosSjgWgLLH7oNE7TZbTUl0iaqyWYIXfCP1BmsJfRzluqQlBJN/TuBGYcuSZWx+n +dW/lJNO9WVzFrD2+9lae9Qjnb4jUgt2L8Yi7w+RPoLW/h4dklggKpgyXO0sPXOp6 +CVqPszw1hyoTDoyrQPjVEXlKMnTrLGGLdX9dZPBift7Q4iCiDTgHq4tCEhePrCdi +gB2SAnECgYEA7p8cdsCfri27LDNvfwzC/HHR+a3YWnra5ySiV8/7wOknE9MgDNQV +6e1XwWPvs94suXJsoMAbrCrQxfsiHqQMSDIUtlY5TY/6NpnouOEaW7J6BykqFyxb +xOHmHKjw6vFKw/psDXYcwOqyRihPK2DGiz+LdrdTFp9LskYBfYk+Jn8CgYEAxX+E +Hsesj8V8vCzhfmGmWErSVOgRsB/rlR+b5ONLApa2+I9uEEv+IBT1VxM8XxE6IIN6 +tG7nggzpnwgrNP8KSGb804uFFCazaNuF2bXOvaK67fJz1eA6kXV2NpgU0hVzT3Ad +LTSMCQ7Ps0LkKhXr5h6LFp6O4lU028tSzuoiI98CgYEA4J1Rc88aACD4AUFhgJyI +poyVdItaDsF6cP2g+zvB5PMTX6vqjWjOP+a0JkxmBE/slZvJ+P8cjVG4N8SPd3xA +O2045fH/+qy+gMsbr3vlDc/Q4hCzmCCfOZLSwsOcE+uRzyxYrcsygb3qlfO3okN0 +YPst0k/6nF7SKDuRh5O6tw0CgYAL4FgslatN1f1jP3ur4uli49T0ICR4J+M9y3HP +eM+Y70E/fziKKFe2zCvYuaJmwR1yuRVW5lhrnKUr2AzpGfEfW3oWqowtIwqk4paQ ++frdsnx1NKA8m0hKWPrr24dc/sc5Xq+SeVd2b/qTeBFKapkN9IY+rPhAqgkMspRf +NvsolwKBgFaDHjVaYdWEqdEucvt1wWe/j3cinGgzRBsz5b5PJ0WeWrn1YtxlIH2S +CShwtUK4I4/3PpqTApaJBFdE51edtJKAIwpI40g7zjy/Pc9FQPjMuz4K5BNGFtE3 +WwOiQYO8iHbNJ3ZzrKTYQmD9sHLkFnqhePmfY9ttLMdgB5Xpox8C +-----END RSA PRIVATE KEY----- diff --git a/examples/mysql-sqlx/certs/root-ca.pem b/examples/mysql-sqlx/certs/root-ca.pem new file mode 100644 index 0000000..7e178cc --- /dev/null +++ b/examples/mysql-sqlx/certs/root-ca.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCDCCAfACCQCgRRoacvClxzANBgkqhkiG9w0BAQsFADBGMQswCQYDVQQGEwJH +QjEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZMb25kb24xFTATBgNVBAMMDG15 +c3FsLWRpZXNlbDAeFw0yMzA4MjgxOTM0MzBaFw0zMzA3MDYxOTM0MzBaMEYxCzAJ +BgNVBAYTAkdCMQ8wDQYDVQQIDAZMb25kb24xDzANBgNVBAcMBkxvbmRvbjEVMBMG +A1UEAwwMbXlzcWwtZGllc2VsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAuBdM/TyViYfnYB2DADqg1+6AbxO8a1ZcP3NPxVgBtprhOTPTH8Yvg0/RKQrn +p6/8eNa01wobcKmfDRbpF3WrHPjOq/3FVxNbcCnezrvPxjWmFSEIkedlokFVPSlS +M8wwhHoWOGNU0OS+fiT1ixPQBEPb33xIRIg1gfGqMMlZq4I0qIWkoVh+qQcaL9cl +H4RgcRVT6XPCu1fWDZRAO8MdbJr5iqfvanhAHanNIaR2Q3Ja5JsOzB8j+CFRpf1N +wSN8erOAji4ZEDRO1i6+zuveWgOpBn3tay53L8jm0zYMB2noJPqFAc/tH2CD/HVc +GMcyLBAqIzbHK/ZTWyfBP0TloQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCZQyvu +ztrnkmP+/gTbdHlZMcQjA9U0aRQqesJm/O75ICGTpjxlWXXzKe/WSqP2ZoGLRbY0 +Wi1KnL/V0qc8QpgCcBsNSa0VwtcTUZVTUGecGxk5BEc8dH8hfnkGy+bH2e81LmX5 +cMibbkU6wH5NwgTQIkUlRlDbCiLoszAhgAOFkOFLUEIxHIea6YeRw1479nUxNXDM +ZXDsj+EGNE94HQwSiNkxfuWwpCyAOrLeBmaUaG7SvMioh5lffSJcZQ8nPWA/ZJ8n +49kAh3iwKNMGpvKOohu4e1qpFOxPgHiJ+5XKQM+YHmN1TRVzLXEsF/IgwSvb5UxQ +qXdoeBiEhSrHSkom +-----END CERTIFICATE----- diff --git a/examples/mysql-sqlx/certs/server-cert.pem b/examples/mysql-sqlx/certs/server-cert.pem new file mode 100644 index 0000000..f059a26 --- /dev/null +++ b/examples/mysql-sqlx/certs/server-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBzCCAe8CAQEwDQYJKoZIhvcNAQELBQAwRjELMAkGA1UEBhMCR0IxDzANBgNV +BAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9uZG9uMRUwEwYDVQQDDAxteXNxbC1kaWVz +ZWwwHhcNMjMwODI4MTkzNjM1WhcNMzMwNzA2MTkzNjM1WjBNMQswCQYDVQQGEwJH +QjEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZMb25kb24xHDAaBgNVBAMME215 +c3FsLWRpZXNlbC1zZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQDgRFH/Q1eoZoYRGWtDZ7DAZ0xHWtLk4qB12WA+JwwYfEG9g68JSTsyCAwFINB4 +81S7sNH5+FDFmZFAKQnmIfH11LKN5jSsrZLITFlKX4tj4Hisxt5TTyBHSMBQPwoK +RtGV7dhANq5epJVRUvA5ehOEvn1Ke0x38sDihde7Z5P7PyoL1fjHALrTafu/4WsG +EfAAQFtCP5wtJRwyaI0conXBgPdBYs4acKAUMthkSfYc9VNm6JvpZZrjX0MkRLIj +3Z5PYuWVi6cI53RteIXGF6D+TS0cqQFi0cl4Gf78Au8BKjsxb/udJtzC20Jip0VJ +SdTiVta/VMZ+UQFTZD3CYJNVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAJjHuZej +IyRLMdggq6BkV0F6rVqH2wB2dEklwZJPWmJ/v3DLwBbALsUpzVh1gh4Oa93V8Bdj +2kJWmHdFMsb/gwKzl4kHsWMskEzjDYf1RTE+qii7XPm8w8hmxbsHG+7QxJVU4iLA +9ydrsW6aBEl0O4BNcwwlMMpqGyTOFCSCWCEVe0YVIFTJ/2VJHv47wIod4kDvkegT +2+mFyY29uA3N0/zIV29wWZc2nxt+JRf/J4mO85jOm18hstBa8b7Fn4nB+ttH4Blx +7fotbaia8Kb+EgZuwmKiJAJPLdMAQt0YjHkz6JMgM7Lg3YYkwPo9iajKicPh67Tu +nINADc3fblSgyxs= +-----END CERTIFICATE----- diff --git a/examples/mysql-sqlx/certs/server-key.pem b/examples/mysql-sqlx/certs/server-key.pem new file mode 100644 index 0000000..e9dc4fc --- /dev/null +++ b/examples/mysql-sqlx/certs/server-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA4ERR/0NXqGaGERlrQ2ewwGdMR1rS5OKgddlgPicMGHxBvYOv +CUk7MggMBSDQePNUu7DR+fhQxZmRQCkJ5iHx9dSyjeY0rK2SyExZSl+LY+B4rMbe +U08gR0jAUD8KCkbRle3YQDauXqSVUVLwOXoThL59SntMd/LA4oXXu2eT+z8qC9X4 +xwC602n7v+FrBhHwAEBbQj+cLSUcMmiNHKJ1wYD3QWLOGnCgFDLYZEn2HPVTZuib +6WWa419DJESyI92eT2LllYunCOd0bXiFxheg/k0tHKkBYtHJeBn+/ALvASo7MW/7 +nSbcwttCYqdFSUnU4lbWv1TGflEBU2Q9wmCTVQIDAQABAoIBAQDWFiI4ja7GYWim +Nh/BLuD6KyIUE82zFeyb07EeY+QWE7gmE+kp8jCCKFBhLNwWOiOvMLKh77wbhee0 +2fhLKihyxmLVNucd2GyArPGoH+FOrOBF/2oJGUX1Bqwfbq5E9Snp+C0xz4FRc8fe +5E/SbojndXjheyaacrBTDfXNPqlzcyUErpKeNeTA7IEYumxXrL/QfZ3BramSjtDS +168DVWmDZStFjjaoMHjen5VOJiTBZCScA00fx15pCVRfA1hxzLK/yiPldSyUwP0T +CdxTKtK8NxHZCBcIKQa/rje2E/nYyOAF0UseKQdVL32mw5mVNg7Sdanu/4ef3qAh +jhVEbK2ZAoGBAPiw84BLZFHEjOcFWXjnP6svJOqUJDHtTX3CBeMJqATpLqc32G/h +ncV439NKuDXdeG8XuHz2CeRdHmzr1wGqwiUOMZoXyFUnlDuFlQ2wgLV4XUAn9lhg +hFuCKITbvQBj+pgGtJW9lMuPLHtoYBNSqoJ+hwpXmiGY+/8u8bX1ZFUHAoGBAObb +nFAmRHIyNGL0DKBtsXIC5ncCVoWoA2e7LQLsIK48t/Y/umPkSaeuoNMKSLsNmnoA +BHi7R8BxDt5SMUc1Ee3S6T7KoTnDN+WifEPG5ZV5PquwYQzeISSQlPolM3594Tlr +jzFmh8YhWPMQLE0wFTXkORc2Gq3u7Cw/aZLtwPnDAoGAAkfVI9MNK3bK9/9jj6lH +OzGmfAQ5xL9xW3rhBPD8unbM97maYYRHJx1xqAViZRQP26zzAypact6rvcH36csA +FsjniYs4Shef01/pOwPoWkGtUT5MkwjnjdNGvdkqBYvlK2m8VDc12xwIA2W0o9i5 +OZMhd+XPwwkdHnz2uH10mxcCgYAQ6zlaemN7xwgl1GL0gvWFOIcxY595riWJvaL/ +2tROUK7ewi7j0UBApMCulstbwhR3KncpbfhFN0PysS1hXZdRyapEas9ED3WSQ8U9 +pl7/BGK+bG/AZKsjJeF1L7LFa25NKf/VjAEZWSpOrfnBMmvrhi1U8SclZiQy3fbb +yMbixwKBgQD0N3QgaPivL/3WtuNfksseCQKcz6SzLo68VPpIHgol3zC+gaHDEoiX +zK53Pm5aSo3eyajzthUCIvfC2QpSyBETX5kxreNna71/XWhWMx3ZtT2d9zT4uSUt +wzJ7L4lAvQq7IpCrXMP8oApxVt22358x67pE+wjMivKkKEtokGzFRg== +-----END RSA PRIVATE KEY----- diff --git a/examples/mysql-sqlx/certs/server-req.pem b/examples/mysql-sqlx/certs/server-req.pem new file mode 100644 index 0000000..12f5f5d --- /dev/null +++ b/examples/mysql-sqlx/certs/server-req.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICkjCCAXoCAQAwTTELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0G +A1UEBwwGTG9uZG9uMRwwGgYDVQQDDBNteXNxbC1kaWVzZWwtc2VydmVyMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4ERR/0NXqGaGERlrQ2ewwGdMR1rS +5OKgddlgPicMGHxBvYOvCUk7MggMBSDQePNUu7DR+fhQxZmRQCkJ5iHx9dSyjeY0 +rK2SyExZSl+LY+B4rMbeU08gR0jAUD8KCkbRle3YQDauXqSVUVLwOXoThL59SntM +d/LA4oXXu2eT+z8qC9X4xwC602n7v+FrBhHwAEBbQj+cLSUcMmiNHKJ1wYD3QWLO +GnCgFDLYZEn2HPVTZuib6WWa419DJESyI92eT2LllYunCOd0bXiFxheg/k0tHKkB +YtHJeBn+/ALvASo7MW/7nSbcwttCYqdFSUnU4lbWv1TGflEBU2Q9wmCTVQIDAQAB +oAAwDQYJKoZIhvcNAQELBQADggEBAAP0fQjlLa5AlCMCjpoUMVr/vUwCJWnah3vN +nAbJbk1Pq1OgBG1+1iUf41OAFHeVmze/vGyJHYujgUBsgS8tBZmUDon9fVH8ny3Z +aBBbhvOgTMM5Z47zwGN1fZUqvZxth3SEs02jeECqiY1ZOtaluK13ui6Y6187arXH +o5qBx2gJSeerXCgitTMgqGRU9TgvQXZxQfbXNmdrwnI9Y4i763eKSFn+23T/9aQE +OBawz4hUCJn2vmfllMUlIX2AP0+XzuN2GoLTrBxAuYNKCT1xP5V90xl1uw9xIvKf +ebtgVTCUGGbdKTQEgYshoC1aOPIMTb3oBqSZHPNf+8IIkrSuRu4= +-----END CERTIFICATE REQUEST----- diff --git a/examples/mysql-sqlx/docker-compose.yml b/examples/mysql-sqlx/docker-compose.yml new file mode 100644 index 0000000..7e2861f --- /dev/null +++ b/examples/mysql-sqlx/docker-compose.yml @@ -0,0 +1,22 @@ +services: + mysql-8: + image: mysql:8 + command: [ + "mysqld", + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_unicode_ci", + "--bind-address=0.0.0.0", + "--require_secure_transport=ON", + "--ssl-ca=/etc/certs/root-ca.pem", + "--ssl-cert=/etc/certs/server-cert.pem", + "--ssl-key=/etc/certs/server-key.pem" + ] + ports: + - "127.0.0.1:3306:3306" + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=true + - MYSQL_DATABASE=example + volumes: + - type: bind + source: ./certs + target: /etc/certs/ diff --git a/examples/mysql-sqlx/migrations/20230828202611_add_users.sql b/examples/mysql-sqlx/migrations/20230828202611_add_users.sql new file mode 100644 index 0000000..7164e32 --- /dev/null +++ b/examples/mysql-sqlx/migrations/20230828202611_add_users.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id VARCHAR(64) PRIMARY KEY NOT NULL, + email TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/examples/mysql-sqlx/scripts/generate-certs.sh b/examples/mysql-sqlx/scripts/generate-certs.sh new file mode 100755 index 0000000..b501a36 --- /dev/null +++ b/examples/mysql-sqlx/scripts/generate-certs.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +openssl genrsa 2048 > certs/root-ca-key.pem +openssl req -new -x509 -nodes -days 3600 -subj "/C=GB/ST=London/L=London/CN=mysql-diesel" -key certs/root-ca-key.pem -out certs/root-ca.pem +openssl req -newkey rsa:2048 -days 3600 -nodes -subj "/C=GB/ST=London/L=London/CN=mysql-diesel-server" -keyout certs/server-key.pem -out certs/server-req.pem +openssl rsa -in certs/server-key.pem -out certs/server-key.pem +openssl x509 -req -in certs/server-req.pem -days 3600 -CA certs/root-ca.pem -CAkey certs/root-ca-key.pem -set_serial 01 -out certs/server-cert.pem +openssl verify -CAfile certs/root-ca.pem certs/server-cert.pem +openssl req -newkey rsa:2048 -days 3600 -nodes -subj "/C=GB/ST=London/L=London/CN=mysql-diesel-client" -keyout certs/client-key.pem -out certs/client-req.pem +openssl rsa -in certs/client-key.pem -out certs/client-key.pem +openssl x509 -req -in certs/client-req.pem -days 3600 -CA certs/root-ca.pem -CAkey certs/root-ca-key.pem -set_serial 01 -out certs/client-cert.pem +openssl verify -CAfile certs/root-ca.pem certs/client-cert.pem diff --git a/examples/mysql-sqlx/src/main.rs b/examples/mysql-sqlx/src/main.rs new file mode 100644 index 0000000..bc2ec3e --- /dev/null +++ b/examples/mysql-sqlx/src/main.rs @@ -0,0 +1,21 @@ +use database_schema::DatabaseSchemaBuilder; + +#[tokio::main] +async fn main() -> Result<(), database_schema::Error> { + DatabaseSchemaBuilder::new() + .connection_url("mysql://root:@127.0.0.1:3306/example?ssl_mode=verify_ca&ssl_key=certs/client-key.pem&ssl_ca=certs/root-ca.pem&ssl_cert=certs/client-cert.pem") + .migrations_dir("./migrations")? + .destination_path("./structure.sql") + .build() + .dump() + .await + + // You can also opt to dump all the databases by not specifying a database name + // DatabaseSchemaBuilder::new() + // .connection_url("mysql://root:@127.0.0.1:3306/") + // .migrations_dir("./migrations")? + // .destination_path("./structure.sql") + // .build() + // .dump() + // .await +} diff --git a/examples/mysql-sqlx/structure.sql b/examples/mysql-sqlx/structure.sql new file mode 100644 index 0000000..78707ce --- /dev/null +++ b/examples/mysql-sqlx/structure.sql @@ -0,0 +1,44 @@ + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +DROP TABLE IF EXISTS `_sqlx_migrations`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `_sqlx_migrations` ( + `version` bigint NOT NULL, + `description` text COLLATE utf8mb4_unicode_ci NOT NULL, + `installed_on` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `success` tinyint(1) NOT NULL, + `checksum` blob NOT NULL, + `execution_time` bigint NOT NULL, + PRIMARY KEY (`version`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `users` ( + `id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` text COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + diff --git a/examples/sqlite-sqlx/Cargo.toml b/examples/sqlite-sqlx/Cargo.toml new file mode 100644 index 0000000..ac6dc0d --- /dev/null +++ b/examples/sqlite-sqlx/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "sqlite-sqlx" +version = "0.1.0" +edition = "2021" +authors = ["Norberto Lopes "] + +[dependencies] +database-schema = { path = "../../", features = ["sqlx", "sqlite", "runtime-async-std", "macros"] } diff --git a/examples/sqlite-sqlx/README.md b/examples/sqlite-sqlx/README.md new file mode 100644 index 0000000..71680a7 --- /dev/null +++ b/examples/sqlite-sqlx/README.md @@ -0,0 +1,5 @@ +# sqlite-sqlx + +This simple example will use `sqlx` to run the migrations found in +[`./migrations`](./migrations) and then generate a [`./structure.sql`](./structure.sql) +file with the database schema; diff --git a/examples/sqlite-sqlx/migrations/20230827152633_add_users.sql b/examples/sqlite-sqlx/migrations/20230827152633_add_users.sql new file mode 100644 index 0000000..c4e2689 --- /dev/null +++ b/examples/sqlite-sqlx/migrations/20230827152633_add_users.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id TEXT PRIMARY KEY NOT NULL, + email TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT(datetime('now', 'utc')) +); diff --git a/examples/sqlite-sqlx/src/main.rs b/examples/sqlite-sqlx/src/main.rs new file mode 100644 index 0000000..4db0d4a --- /dev/null +++ b/examples/sqlite-sqlx/src/main.rs @@ -0,0 +1,9 @@ +fn main() { + database_schema::generate_without_runtime_using_defaults!(); + // The above is equivalent to: + // + // use std::path::PathBuf; + // let migrations_path = PathBuf::from("./migrations").canonicalize().unwrap(); + // let destination_path = PathBuf::from("./structure.sql"); + // database_schema::generate_without_runtime!(migrations_path, destination_path); +} diff --git a/examples/sqlite-sqlx/structure.sql b/examples/sqlite-sqlx/structure.sql new file mode 100644 index 0000000..6886619 --- /dev/null +++ b/examples/sqlite-sqlx/structure.sql @@ -0,0 +1,20 @@ +-- +-- Name: _sqlx_migrations; Type: table +-- +CREATE TABLE _sqlx_migrations ( + version BIGINT PRIMARY KEY, + description TEXT NOT NULL, + installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + success BOOLEAN NOT NULL, + checksum BLOB NOT NULL, + execution_time BIGINT NOT NULL +); + +-- +-- Name: users; Type: table +-- +CREATE TABLE users ( + id TEXT PRIMARY KEY NOT NULL, + email TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT(datetime('now', 'utc')) +); diff --git a/fixtures/diesel/sqlite/migrations/2023-08-27-215620_add_users/down.sql b/fixtures/diesel/sqlite/migrations/2023-08-27-215620_add_users/down.sql new file mode 100644 index 0000000..291a97c --- /dev/null +++ b/fixtures/diesel/sqlite/migrations/2023-08-27-215620_add_users/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` \ No newline at end of file diff --git a/fixtures/diesel/sqlite/migrations/2023-08-27-215620_add_users/up.sql b/fixtures/diesel/sqlite/migrations/2023-08-27-215620_add_users/up.sql new file mode 100644 index 0000000..c4e2689 --- /dev/null +++ b/fixtures/diesel/sqlite/migrations/2023-08-27-215620_add_users/up.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id TEXT PRIMARY KEY NOT NULL, + email TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT(datetime('now', 'utc')) +); diff --git a/fixtures/sqlx/sqlite/migrations/20230827160610_add_users.sql b/fixtures/sqlx/sqlite/migrations/20230827160610_add_users.sql new file mode 100644 index 0000000..c4e2689 --- /dev/null +++ b/fixtures/sqlx/sqlite/migrations/20230827160610_add_users.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id TEXT PRIMARY KEY NOT NULL, + email TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT(datetime('now', 'utc')) +); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..10466f4 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,41 @@ +//! Error types for the library. + +/// Error types for the library. +#[non_exhaustive] +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("IO error: {0}")] + /// Any kind of IO error + IOError(#[from] std::io::Error), + #[error("Command run error: {0}")] + /// Any kind of error when running a command + CommandRunError(String), + #[cfg(feature = "sqlx")] + #[error("DB error: {0}")] + /// Any kind of database engine related error + DBError(#[from] sqlx::Error), + #[cfg(feature = "diesel")] + #[error("DB error: {0}")] + /// Any kind of database engine related error + DBError(#[from] diesel::result::Error), + #[cfg(feature = "diesel")] + #[error("DB error: {0}")] + /// Any connection error when connecting to the database in `diesel` + DBConnectionError(#[from] diesel::ConnectionError), + #[cfg(feature = "diesel")] + #[error("DB error: {0}")] + /// Any connection error when running migrations in `diesel` + MigrationError(#[from] diesel_migrations::MigrationError), + #[cfg(any(feature = "mysql", feature = "postgres"))] + #[error("Unable to extract database name from connection string")] + /// Extracting the database name from the connection string failed + ExtractDatabaseNameError, + #[cfg(any(feature = "mysql", feature = "postgres"))] + #[error("Uri configuration error: {0}")] + /// Error when parsing the connection string + UriConfiguration(String), + #[cfg(any(feature = "mysql", feature = "postgres"))] + #[error("Uri configuration encoding error: {0}")] + /// Error when decoding parts of the connection string + UriConfigurationDecoding(#[from] core::str::Utf8Error), +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7256b0e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,340 @@ +#![feature(doc_cfg)] +#![deny( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unused_import_braces, + unused_qualifications, + unused_extern_crates, + unused_results, + variant_size_differences +)] +#![cfg_attr(docsrs, feature(doc_cfg), doc = include_str!("../README.md"), allow(unused_attributes))] + +//! This crate provides a simple way to dump a database structure to a file, in SQL +//! format. +//! +//! It takes inspiration by the ruby on rails [schema dump]. +//! +//! # Usage +//! +//! ```rust,ignore +//! use std::path::PathBuf; +//! +//! database_structure::generate_structure_sql(PathBuf::from("./structure.sql")); +//! ``` +//! +//! +//! # Feature flags +//! +//! `database-schema` uses a set of [feature flags] to reduce the size of the libray and +//! therefore your binary. The way one should use this package is to pick the right +//! combination of feature flags for their use case. Below is a list of the available +//! feature flags and the combinations that are recommended for each use case. +//! +//! - `sqlite`: Enables SQLite support. +//! - `postgres`: Enables PostgreSQL support. +//! - `mysql`: Enables MySQL support. +//! - `sqlx`: Enables [sqlx] support. +//! - `diesel`: Enables [diesel] support. +//! +//! ## Feature flag matrix +//! | Database | Query builder | Runtime | +//! |----------|---------------|---------| +//! | `sqlite` | `sqlx` | `runtime-async-std` | +//! | `sqlite` | `sqlx` | `runtime-tokio` | +//! | `sqlite` | `diesel` | | +//! | `mysql` | `sqlx` | `runtime-async-std` | +//! | `mysql` | `sqlx` | `runtime-tokio` | +//! | `mysql` | `diesel` | | +//! | `postgres` | `sqlx` | `runtime-async-std` | +//! | `postgres` | `sqlx` | `runtime-tokio` | +//! | `postgres` | `diesel` | | +//! +//! ## Combining feature flags +//! +//! The following are the recommended feature flag combinations for each use case. +//! +//! First pick one of the following database feature flags: +//! +//! * `sqlite` +//! * `mysql` +//! * `postgres` +//! +//! Then pick one of the following database query building feature flags: +//! +//! * `sqlx` +//! * `diesel` +//! +//! If you're using `sqlx`, you also have to pick one of the following runtime feature flags: +//! +//! * `runtime-async-std` +//! * `runtime-tokio` +//! +//! ## Example +//! +//! ```toml +//! [dependencies] +//! database-schema = { version = "0.1", features = ["sqlite", "sqlx", "runtime-async-std"] } +//! ``` +//! +//! alternatively, if you're using `diesel`: +//! ```toml +//! [dependencies] +//! database-schema = { version = "0.1", features = ["sqlite", "diesel"] } +//! ``` +//! +//! ## Macros +//! +//! This crate also provides a set of macros that can be used to generate the SQL +//! structure of a database at compile time. This is useful for generating the SQL from +//! `build.rs`. +//! +//! +//! ```toml +//! [dependencies] +//! database-schema = { version = "0.1", features = ["sqlite", "diesel", "macros"] } +//! ``` +//! +//! ```rust,ignore +//! use database_schema::macros::generate_without_runtime; +//! +//! let sql = generate_without_runtime!("./migrations", "structure.sql"); +//! ``` +//! +//! The above is strictly equivalent to calling: +//! +//! ```rust,ignore +//! use database_schema::macros::generate_without_runtime_using_defaults; +//! +//! let sql = generate_without_runtime!(); +//! ``` +//! +//! # Customization +//! +//! ```rust,ignore +//! use database_schema::DatabaseSchemaBuilder; +//! +//! let migrations_path = "db/migrations"; +//! let destination_path = "db/structure.sql"; +//! +//! // This assumes you're using SQLite in memory. +//! // +//! // If you need to set up a `connection_url` you can use +//! // `DatabaseSchemaBuilder::connection_url` before calling +//! // `build()`. +//! +//! DatabaseSchemaBuilder::new() +//! .migrations_dir(migrations_path)? +//! .destination_path(destination_path) +//! .build() +//! .dump() +//! .await +//! ``` +//! +//! [feature flags]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section +//! [sqlx]: https://docs.rs/sqlx/latest/sqlx/ +//! [diesel]: https://docs.rs/diesel/latest/diesel/ +//! [schema dump]: https://guides.rubyonrails.org/active_record_migrations.html#schema-dumping-and-you + +#[cfg(not(any(feature = "sqlite", feature = "postgres", feature = "mysql"),))] +compile_error!( + "At least one of the following features must be enabled: sqlite, postgres or mysql." +); +#[cfg(not(any(feature = "sqlx", feature = "diesel"),))] +compile_error!("At least one of the following features must be enabled: sqlx or diesel."); + +#[cfg(feature = "sqlite")] +#[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] +mod sqlite; + +#[cfg(feature = "mysql")] +#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] +mod mysql; + +//#[cfg(feature = "postgres")] +//mod postgres; + +#[cfg(all( + feature = "macros", + not(any(feature = "runtime-async-std", feature = "runtime-tokio")) +))] +compile_error!( + "At least one of the following features must be enabled: runtime-async-std or runtime-tokio." +); + +#[cfg(all( + feature = "macros", + any(feature = "runtime-async-std", feature = "runtime-tokio") +))] +pub mod macros; + +pub(crate) mod process; + +pub mod error; +pub use error::Error; + +/// Entry point for using the crate and the result of calling [`DatabaseSchemaBuilder::build`]. +/// +/// ```rust,ignore +/// DatabaseSchemaBuilder::new().build().dump().await?; +/// ``` +#[cfg(all( + any(feature = "sqlite", feature = "postgres", feature = "mysql"), + any(feature = "sqlx", feature = "diesel") +))] +#[derive(Debug, Default)] +pub struct DatabaseSchema(DatabaseSchemaInner); + +#[derive(Debug, Clone)] +struct ConnectionUrl(String); + +impl Default for ConnectionUrl { + fn default() -> Self { + #[cfg(feature = "sqlite")] + let conn = ConnectionUrl(String::from(sqlite::DEFAULT_CONNECTION_URL)); + #[cfg(feature = "mysql")] + let conn = ConnectionUrl(String::from(mysql::DEFAULT_CONNECTION_URL)); + //#[cfg(feature = "postgres")] + //let conn = ConnectionUrl(String::from(postgres::DEFAULT_CONNECTION_URL)); + + conn + } +} + +#[derive(Debug, Default, Clone)] +struct DatabaseSchemaInner { + connection_url: ConnectionUrl, + migrations_path: std::path::PathBuf, + destination_path: std::path::PathBuf, +} + +/// Builder for `DatabaseSchema` +#[cfg(all( + any(feature = "sqlite", feature = "postgres", feature = "mysql"), + any(feature = "sqlx", feature = "diesel") +))] +#[derive(Debug, Default)] +pub struct DatabaseSchemaBuilder(DatabaseSchemaInner); + +#[cfg(all( + any(feature = "sqlite", feature = "postgres", feature = "mysql"), + any(feature = "sqlx", feature = "diesel") +))] +#[allow(dead_code)] +impl DatabaseSchemaBuilder { + /// Create a new `DatabaseSchemaBuilder` + pub fn new() -> Self { + Self::default() + } + + /// This is the connection URL used to connect to the database. + /// + /// For `mysql` and `postgres` this is the same URL you would pass to the `connect` method of the client. + /// + /// * `mysql`: `mysql://[user[:password]@]host/database_name[?unix_socket=socket-path&ssl_mode=SSL_MODE*&ssl_ca=/etc/ssl/certs/ca-certificates.crt&ssl_cert=/etc/ssl/certs/client-cert.crt&ssl_key=/etc/ssl/certs/client-key.crt]` + /// + /// * `postgres`: `postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...]` - you can read more at [libpq docs](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) + /// + /// * `sqlite`: `sqlite::memory:` in the case of `sqlx` and `:memory:` in the case of + /// `diesel` - you don't need to set this for `sqlite` as we auto-detect it as long as + /// you enable the `sqlite` feature. + pub fn connection_url>(&mut self, connection_url: S) -> &mut Self { + self.0.connection_url = ConnectionUrl(connection_url.into()); + self + } + + /// Set `migrations_dir` - this is the directory path where your migrations are stored. + /// + /// By default we assume that the migrations are stored in the `migrations` directory + /// starting at the root of your project. + /// + /// We call `canonicalize()` on the path, so you can pass in a relative path. The + /// downside is that this call can fail. + pub fn migrations_dir>( + &mut self, + migrations_dir: P, + ) -> Result<&mut Self, Error> { + self.0.migrations_path = migrations_dir.as_ref().to_path_buf().canonicalize()?; + Ok(self) + } + + /// Set `destination_path` - this is the path to the file where we'll store the SQL dump. + /// + /// By default we assume `structure.sql` in the root of your project. + pub fn destination_path>( + &mut self, + destination_path: P, + ) -> &mut Self { + self.0.destination_path = destination_path.as_ref().to_path_buf(); + self + } + + /// Build `DatabaseSchema` from `DatabaseSchemaBuilder` + pub fn build(&self) -> DatabaseSchema { + DatabaseSchema(self.0.clone()) + } +} + +impl DatabaseSchema { + /// Dump the database schema. + pub async fn dump(&self) -> Result<(), Error> { + #[cfg(all(feature = "mysql", any(feature = "sqlx", feature = "diesel")))] + use crate::mysql::write_structure_sql; + #[cfg(all(feature = "sqlite", any(feature = "sqlx", feature = "diesel")))] + use crate::sqlite::write_structure_sql; + //#[cfg(all(feature = "postgres", any(feature = "sqlx", feature = "diesel")))] + //use crate::postgres::write_structure_sql; + + write_structure_sql( + &self.0.connection_url.0, + self.0.migrations_path.clone(), + self.0.destination_path.clone(), + ) + .await?; + Ok(()) + } +} + +/// Generate a `destination_path` SQL file using migrations from the `migrations_path` +/// folder. +/// +/// Calling this function is strictly equivalent to: +/// +/// ```rust,ignore +/// // This assumes you're using SQLite in memory. +/// // +/// // If you need to set up a `connection_url` you can use `DatabaseSchemaBuilder::connection_url` +/// // before calling `build()`. +/// +/// DatabaseSchemaBuilder::new() +/// .migrations_dir(migrations_path)? +/// .destination_path(destination_path) +/// .build() +/// .dump() +/// .await +/// ``` +/// Requires an executor to be available. +#[cfg(all( + any(feature = "sqlite", feature = "postgres", feature = "mysql"), + any(feature = "sqlx", feature = "diesel") +))] +pub async fn generate, Q: AsRef>( + connection_url: Option<&str>, + migrations_path: P, + destination_path: Q, +) -> Result<(), Error> { + let mut builder = DatabaseSchemaBuilder::new(); + if let Some(connection_url) = connection_url { + let _ = builder.connection_url(connection_url); + } + builder + .migrations_dir(migrations_path)? + .destination_path(destination_path) + .build() + .dump() + .await +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..0bf0f3f --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,60 @@ +//! This module contains a few useful macros in order to not need the builder. +//! +//! It provides the following macros: +//! +//! * `generate!` - Generate a `destination_path` file using migrations from the provided +//! `migrations_path` folder. +//! +//! * `generate_using_defaults!` - Generate a `./structure.sql` file using migrations +//! from the `./migrations` folder. + +/// Generate a `destination_path` file using migrations from the provided +/// `migrations_path` folder. +#[macro_export] +macro_rules! generate_without_runtime { + ($migrations_path:expr, $structure_path:expr) => { + let migrations_path: ::std::path::PathBuf = $migrations_path.into(); + let destination_path: ::std::path::PathBuf = $structure_path.into(); + $crate::macros::__generate_within_runtime(migrations_path, destination_path) + }; +} + +/// Generate a `./structure.sql` file using migrations from the `./migrations` folder. +#[macro_export] +macro_rules! generate_without_runtime_using_defaults { + () => { + $crate::macros::__generate_within_runtime( + ::std::path::PathBuf::from("./migrations"), + ::std::path::PathBuf::from("./structure.sql"), + ); + }; +} + +/// Generate a structure.sql file using migrations from the `migrations_path` folder. +/// DO NOT USE THIS DIRECTLY. Use the `generate!` macro instead. +#[doc(hidden)] +pub fn __generate_within_runtime( + migrations_path: std::path::PathBuf, + destination_path: std::path::PathBuf, +) { + run_runtime(migrations_path, destination_path); +} + +fn run_runtime(migrations_path: std::path::PathBuf, destination_path: std::path::PathBuf) { + #[cfg(feature = "runtime-tokio")] + { + let rt = tokio::runtime::Runtime::new().expect("could not create tokio runtime"); + let local = tokio::task::LocalSet::new(); + local + .block_on(&rt, async { + crate::generate(None, migrations_path, destination_path).await + }) + .expect("could not run tokio runtime"); + } + + #[cfg(feature = "runtime-async-std")] + async_std::task::block_on(async { + crate::generate(None, migrations_path, destination_path).await + }) + .expect("could not run async-std runtime"); +} diff --git a/src/mysql/mod.rs b/src/mysql/mod.rs new file mode 100644 index 0000000..7150088 --- /dev/null +++ b/src/mysql/mod.rs @@ -0,0 +1,161 @@ +//! Implementation of the `mysql` feature + +pub(crate) const DEFAULT_CONNECTION_URL: &str = "mysql://root:@localhost:3306"; + +use percent_encoding::percent_decode_str; + +use crate::error::Error; + +mod options; + +use options::MySqlConnectOptions; + +#[allow(unused_results)] +#[cfg(any(feature = "sqlx", feature = "diesel"))] +pub(crate) async fn write_structure_sql, Q: AsRef>( + connection_url: &str, + migrations_path: P, + destination_path: Q, +) -> Result<(), Error> { + let options = extract_connect_options(connection_url)?; + + migrate(connection_url, migrations_path).await?; + + let mut cmd = std::process::Command::new("mysqldump"); + cmd.arg("--no-data") + .arg("--routines") + .arg("--skip-comments") + .arg("--result-file") + .arg(destination_path.as_ref()) + .arg("--host") + .arg(options.host.clone()) + .arg("--port") + .arg(options.port.to_string()) + .arg("--user") + .arg(options.username.clone()) + .arg("--ssl-mode") + .arg(format!("{}", options.ssl_mode)); + + if let Some(ref password) = options.password { + cmd.arg("--password").arg(password.clone()); + } + + if let Some(ref ssl_ca) = options.ssl_ca { + cmd.arg("--ssl-ca").arg(ssl_ca.clone()); + } + if let Some(ref ssl_cert) = options.ssl_client_cert { + cmd.arg("--ssl-cert").arg(ssl_cert.clone()); + } + if let Some(ref ssl_key) = options.ssl_client_key { + cmd.arg("--ssl-key").arg(ssl_key.clone()); + } + // This must come last because mysqldump expects the database name to be the last + // argument + if let Some(ref database) = options.database { + cmd.arg(database.clone()); + } else { + cmd.arg("--all-databases"); + } + + crate::process::run(&mut cmd).await?; + Ok(()) +} + +fn extract_connect_options(connection_url: &str) -> Result { + let url = url::Url::parse(connection_url).unwrap(); + + let mut options = MySqlConnectOptions::new(); + + if let Some(host) = url.host_str() { + options = options.host(host); + } + + if let Some(port) = url.port() { + options = options.port(port); + } + + let username = url.username(); + if !username.is_empty() { + options = options.username( + &*percent_decode_str(username) + .decode_utf8() + .map_err(Error::UriConfigurationDecoding)?, + ); + } + + if let Some(password) = url.password() { + options = options.password( + &*percent_decode_str(password) + .decode_utf8() + .map_err(Error::UriConfigurationDecoding)?, + ); + } + + let path = url.path().trim_start_matches('/'); + if !path.is_empty() { + options = options.database(path); + } + + for (key, value) in url.query_pairs().into_iter() { + match &*key { + "sslmode" | "ssl-mode" | "ssl_mode" => { + options = options.ssl_mode(value.parse().map_err(Error::UriConfiguration)?); + } + + "sslca" | "ssl-ca" | "ssl_ca" => { + options = options.ssl_ca(&*value); + } + + "sslcert" | "ssl-cert" | "ssl_cert" => options = options.ssl_client_cert(&*value), + + "sslkey" | "ssl-key" | "ssl_key" => options = options.ssl_client_key(&*value), + + "socket" => { + options = options.socket(&*value); + } + + _ => {} + } + } + + Ok(options) +} + +#[cfg(feature = "sqlx")] +async fn migrate>( + connection_url: &str, + migrations_path: P, +) -> Result<(), sqlx::Error> { + use sqlx::{ + migrate::{Migrate, Migrator}, + mysql::MySqlConnectOptions, + ConnectOptions, + }; + use std::str::FromStr; + + let mut conn = MySqlConnectOptions::from_str(connection_url)? + .connect() + .await?; + + // Ensure the migrations table exists before we run the migrations + conn.ensure_migrations_table().await?; + + let migrator = Migrator::new(migrations_path.as_ref()).await?; + migrator.run_direct(&mut conn).await?; + Ok(()) +} + +#[cfg(feature = "diesel")] +async fn migrate>( + connection_url: &str, + migrations_path: P, +) -> Result<(), Error> { + use diesel::Connection; + use diesel_migrations::{FileBasedMigrations, HarnessWithOutput, MigrationHarness}; + let mut conn = diesel::MysqlConnection::establish(connection_url)?; + let migrations = FileBasedMigrations::from_path(migrations_path)?; + let _ = HarnessWithOutput::write_to_stdout(&mut conn) + .run_pending_migrations(migrations) + .map(|_| ()); + Ok(()) +} diff --git a/src/mysql/options.rs b/src/mysql/options.rs new file mode 100644 index 0000000..e064a3e --- /dev/null +++ b/src/mysql/options.rs @@ -0,0 +1,220 @@ +// All of the code in this module was taken from sqlx's mysql implementation. +// See https://github.com/launchbadge/sqlx/tree/main/sqlx-mysql/src/options. +// +// I did this so that I didn't need to bring `sqlx` as a dependency when using `diesel` - +// this is because `diesel` doesn't expose the MysqlConnectOptions struct. +// +// If they ever do, we can remove this module and use their implementation instead. + +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +/// Options for controlling the desired security state of the connection to the MySQL server. +/// +/// It is used by the [`ssl_mode`](super::MySqlConnectOptions::ssl_mode) method. +#[derive(Debug, Clone, Copy)] +pub enum MySqlSslMode { + /// Establish an unencrypted connection. + Disabled, + + /// Establish an encrypted connection if the server supports encrypted connections, falling + /// back to an unencrypted connection if an encrypted connection cannot be established. + /// + /// This is the default if `ssl_mode` is not specified. + Preferred, + + /// Establish an encrypted connection if the server supports encrypted connections. + /// The connection attempt fails if an encrypted connection cannot be established. + Required, + + /// Like `Required`, but additionally verify the server Certificate Authority (CA) + /// certificate against the configured CA certificates. The connection attempt fails + /// if no valid matching CA certificates are found. + VerifyCa, + + /// Like `VerifyCa`, but additionally perform host name identity verification by + /// checking the host name the client uses for connecting to the server against the + /// identity in the certificate that the server sends to the client. + VerifyIdentity, +} + +impl std::fmt::Display for MySqlSslMode { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + MySqlSslMode::Disabled => write!(f, "disabled"), + MySqlSslMode::Preferred => write!(f, "preferred"), + MySqlSslMode::Required => write!(f, "required"), + MySqlSslMode::VerifyCa => write!(f, "verify_ca"), + MySqlSslMode::VerifyIdentity => write!(f, "verify_identity"), + } + } +} + +impl Default for MySqlSslMode { + fn default() -> Self { + MySqlSslMode::Preferred + } +} + +impl FromStr for MySqlSslMode { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(match &*s.to_ascii_lowercase() { + "disabled" => MySqlSslMode::Disabled, + "preferred" => MySqlSslMode::Preferred, + "required" => MySqlSslMode::Required, + "verify_ca" => MySqlSslMode::VerifyCa, + "verify_identity" => MySqlSslMode::VerifyIdentity, + + _ => { + return Err(format!("unknown value {s:?} for `ssl_mode`").into()); + } + }) + } +} + +#[derive(Debug, Clone)] +pub struct MySqlConnectOptions { + pub(crate) host: String, + pub(crate) port: u16, + pub(crate) socket: Option, + pub(crate) username: String, + pub(crate) password: Option, + pub(crate) database: Option, + pub(crate) ssl_mode: MySqlSslMode, + pub(crate) ssl_ca: Option, + pub(crate) ssl_client_cert: Option, + pub(crate) ssl_client_key: Option, +} + +impl Default for MySqlConnectOptions { + fn default() -> Self { + Self::new() + } +} + +impl MySqlConnectOptions { + /// Creates a new, default set of options ready for configuration + pub fn new() -> Self { + Self { + port: 3306, + host: String::from("localhost"), + socket: None, + username: String::from("root"), + password: None, + database: None, + ssl_mode: MySqlSslMode::Preferred, + ssl_ca: None, + ssl_client_cert: None, + ssl_client_key: None, + } + } + + /// Sets the name of the host to connect to. + /// + /// The default behavior when the host is not specified, + /// is to connect to localhost. + pub fn host(mut self, host: &str) -> Self { + self.host = host.to_owned(); + self + } + + /// Sets the port to connect to at the server host. + /// + /// The default port for MySQL is `3306`. + pub fn port(mut self, port: u16) -> Self { + self.port = port; + self + } + + /// Pass a path to a Unix socket. This changes the connection stream from + /// TCP to UDS. + /// + /// By default set to `None`. + pub fn socket(mut self, path: impl AsRef) -> Self { + self.socket = Some(path.as_ref().to_path_buf()); + self + } + + /// Sets the username to connect as. + pub fn username(mut self, username: &str) -> Self { + self.username = username.to_owned(); + self + } + + /// Sets the password to connect with. + pub fn password(mut self, password: &str) -> Self { + self.password = Some(password.to_owned()); + self + } + + /// Sets the database name. + pub fn database(mut self, database: &str) -> Self { + self.database = Some(database.to_owned()); + self + } + + /// Sets whether or with what priority a secure SSL TCP/IP connection will be negotiated + /// with the server. + /// + /// By default, the SSL mode is [`Preferred`](MySqlSslMode::Preferred), and the client will + /// first attempt an SSL connection but fallback to a non-SSL connection on failure. + /// + /// # Example + /// + /// ```rust + /// # use sqlx_core::mysql::{MySqlSslMode, MySqlConnectOptions}; + /// let options = MySqlConnectOptions::new() + /// .ssl_mode(MySqlSslMode::Required); + /// ``` + pub fn ssl_mode(mut self, mode: MySqlSslMode) -> Self { + self.ssl_mode = mode; + self + } + + /// Sets the name of a file containing a list of trusted SSL Certificate Authorities. + /// + /// # Example + /// + /// ```rust + /// # use sqlx_core::mysql::{MySqlSslMode, MySqlConnectOptions}; + /// let options = MySqlConnectOptions::new() + /// .ssl_mode(MySqlSslMode::VerifyCa) + /// .ssl_ca("path/to/ca.crt"); + /// ``` + pub fn ssl_ca(mut self, file_name: impl AsRef) -> Self { + self.ssl_ca = Some(file_name.as_ref().to_owned()); + self + } + + /// Sets the name of a file containing SSL client certificate. + /// + /// # Example + /// + /// ```rust + /// # use sqlx_core::mysql::{MySqlSslMode, MySqlConnectOptions}; + /// let options = MySqlConnectOptions::new() + /// .ssl_mode(MySqlSslMode::VerifyCa) + /// .ssl_client_cert("path/to/client.crt"); + /// ``` + pub fn ssl_client_cert(mut self, cert: impl AsRef) -> Self { + self.ssl_client_cert = Some(cert.as_ref().to_path_buf()); + self + } + + /// Sets the name of a file containing SSL client key. + /// + /// # Example + /// + /// ```rust + /// # use sqlx_core::mysql::{MySqlSslMode, MySqlConnectOptions}; + /// let options = MySqlConnectOptions::new() + /// .ssl_mode(MySqlSslMode::VerifyCa) + /// .ssl_client_key("path/to/client.key"); + /// ``` + pub fn ssl_client_key(mut self, key: impl AsRef) -> Self { + self.ssl_client_key = Some(key.as_ref().to_path_buf()); + self + } +} diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 0000000..36962a6 --- /dev/null +++ b/src/process.rs @@ -0,0 +1,64 @@ +pub(crate) use std::process::Command; + +/// Run a command line program +#[allow(dead_code)] +pub(crate) async fn run(command: &mut Command) -> Result<(), crate::error::Error> { + let prog = command.get_program().to_string_lossy().into_owned(); + let args = command + .get_args() + .map(|a| a.to_string_lossy().into_owned()) + .collect::>(); + + match command.output() { + Err(error) => { + tracing::error!(?error, ?prog, ?args, "Command failed to run"); + Err(error.into()) + } + Ok(output) => { + if !output.status.success() { + tracing::error!(?output, ?prog, ?args, "Command failed"); + Err(crate::error::Error::CommandRunError(format!( + "output: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout).into_owned(), + String::from_utf8_lossy(&output.stderr).into_owned(), + ))) + } else { + tracing::trace!(?prog, ?args, "Command succeeded"); + Ok(()) + } + } + } +} + +#[cfg(test)] +mod tests { + #[tokio::test] + async fn test_run_not_found() -> Result<(), crate::Error> { + use super::{run, Command}; + let result = run(&mut Command::new("mysqldump-nonexistent")).await; + assert!( + matches!(result, Err(crate::Error::IOError(t)) if t.kind() == std::io::ErrorKind::NotFound) + ); + Ok(()) + } + + // IMPORTANT: The tests below this notice will fail if mysqldump is not found in the + // PATH. + #[tokio::test] + async fn test_run_found() -> Result<(), crate::Error> { + use super::{run, Command}; + let result = run(Command::new("mysqldump").arg("--version")).await; + assert!(matches!(result, Ok(()))); + Ok(()) + } + + #[tokio::test] + async fn test_run_invalid_arguments() -> Result<(), crate::Error> { + use super::{run, Command}; + let result = run(Command::new("mysqldump").arg("--norberto")).await; + assert!(matches!( + result, + Err(crate::Error::CommandRunError(error_message)) if error_message == "output: \nstderr: mysqldump: [ERROR] unknown option '--norberto'.\n")); + Ok(()) + } +} diff --git a/src/sqlite/diesel.rs b/src/sqlite/diesel.rs new file mode 100644 index 0000000..db8a290 --- /dev/null +++ b/src/sqlite/diesel.rs @@ -0,0 +1,71 @@ +//! Implementation of the `sqlite` feature for the `diesel` backend + +pub(crate) const DEFAULT_CONNECTION_URL: &str = ":memory:"; + +pub(crate) async fn fetch_structure_sql>( + connection_url: &str, + migrations_path: P, +) -> Result { + use diesel::Connection; + use diesel_migrations::{FileBasedMigrations, HarnessWithOutput, MigrationHarness}; + let mut conn = diesel::SqliteConnection::establish(connection_url)?; + let migrations = FileBasedMigrations::from_path(migrations_path)?; + let _ = HarnessWithOutput::write_to_stdout(&mut conn) + .run_pending_migrations(migrations) + .map(|_| ()); + + Ok(fetch_structure(&mut conn).await?) +} + +async fn fetch_structure( + conn: &mut diesel::SqliteConnection, +) -> Result { + use diesel::{sql_query, QueryableByName, RunQueryDsl}; + + #[allow(unused_qualifications)] + mod schema { + diesel::table! { + sqlite_schema(name) { + name -> Text, + mytype -> Text, + sql -> Text, + } + } + } + + use schema::sqlite_schema; + + #[derive(Debug, QueryableByName)] + #[diesel(table_name = sqlite_schema)] + struct SqliteSchema { + name: String, + mytype: String, + sql: String, + } + + let results: Vec = sql_query(super::SQLITE_SCHEMA_QUERY).load(conn)?; + Ok(results + .iter() + .map(|r| { + format!( + "--\n-- Name: {}; Type: {}\n--\n{};\n", + r.name, r.mytype, r.sql + ) + }) + .collect::>() + .join("\n")) +} + +#[cfg(test)] +mod tests { + #[cfg(all(feature = "diesel", feature = "sqlite"))] + #[tokio::test] + async fn test_fetch_structure_sql() -> Result<(), crate::error::Error> { + let migrations_path = std::path::PathBuf::from("./fixtures/diesel/sqlite/migrations"); + let structure = + super::fetch_structure_sql(super::DEFAULT_CONNECTION_URL, migrations_path).await?; + + assert!(structure.contains("--\n-- Name: __diesel_schema_migrations; Type: table\n--\nCREATE TABLE __diesel_schema_migrations (\n version VARCHAR(50) PRIMARY KEY NOT NULL,\n run_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\n--\n-- Name: users; Type: table\n--\nCREATE TABLE users (\n id TEXT PRIMARY KEY NOT NULL,\n email TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT(datetime('now', 'utc'))\n)")); + Ok(()) + } +} diff --git a/src/sqlite/mod.rs b/src/sqlite/mod.rs new file mode 100644 index 0000000..bf47e1a --- /dev/null +++ b/src/sqlite/mod.rs @@ -0,0 +1,37 @@ +//! Implementation of the `sqlite` feature + +// The below select query was taken almost verbatim out of sqlite3 source code. +// See https://sqlite.org/src/file?ci=trunk&name=src/shell.c.in&ln=10008 and also +// the generated shell.c in this mirror +// https://github.com/smparkes/sqlite/blob/8caf9219240123fbe6cff67b1e0da778c62d7621/src/shell.c#L2063 +// (they are somewhat out of sync but the query is the same) +const SQLITE_SCHEMA_QUERY: &str = "SELECT name, type as mytype, sql +FROM sqlite_schema +WHERE + sql NOTNULL AND + name NOT LIKE 'sqlite_%' +ORDER BY tbl_name, type DESC, name"; + +#[cfg(any(feature = "sqlx", feature = "diesel"))] +pub(crate) async fn write_structure_sql, Q: AsRef>( + connection_url: &str, + migrations_path: P, + destination_path: Q, +) -> Result<(), crate::error::Error> { + let structure_sql = fetch_structure_sql(connection_url, migrations_path).await?; + Ok(std::fs::write(destination_path, structure_sql)?) +} + +#[cfg(feature = "sqlx")] +mod sqlx; +#[cfg(feature = "sqlx")] +use sqlx::fetch_structure_sql; +#[cfg(feature = "sqlx")] +pub(crate) use sqlx::DEFAULT_CONNECTION_URL; + +#[cfg(feature = "diesel")] +mod diesel; +#[cfg(feature = "diesel")] +use diesel::fetch_structure_sql; +#[cfg(feature = "diesel")] +pub(crate) use diesel::DEFAULT_CONNECTION_URL; diff --git a/src/sqlite/sqlx.rs b/src/sqlite/sqlx.rs new file mode 100644 index 0000000..16318b8 --- /dev/null +++ b/src/sqlite/sqlx.rs @@ -0,0 +1,49 @@ +//! Implementation of the `sqlite` feature for the `sqlx` backend + +pub(crate) const DEFAULT_CONNECTION_URL: &str = "sqlite::memory:"; + +pub(crate) async fn fetch_structure_sql>( + connection_url: &str, + migrations_path: P, +) -> Result { + use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, ConnectOptions}; + use std::str::FromStr; + + let mut conn = SqliteConnectOptions::from_str(connection_url)? + .connect() + .await?; + + let migrator = Migrator::new(migrations_path.as_ref()).await?; + migrator.run(&mut conn).await?; + fetch_structure(&mut conn).await +} + +async fn fetch_structure(conn: &mut sqlx::sqlite::SqliteConnection) -> Result { + use sqlx::Row; + let structure_dump = sqlx::query(super::SQLITE_SCHEMA_QUERY) + .fetch_all(conn) + .await? + .iter() + .map(|row| { + let name = row.get::(0); + let r#type = row.get::(1); + let sql = row.get::(2); + format!("--\n-- Name: {name}; Type: {type}\n--\n{sql};\n") + }) + .collect::>() + .join("\n"); + Ok(structure_dump) +} + +#[cfg(test)] +mod tests { + #[cfg(all(feature = "sqlx", feature = "sqlite"))] + #[tokio::test] + async fn test_fetch_structure_sql() -> Result<(), crate::error::Error> { + let migrations_path = std::path::PathBuf::from("./fixtures/sqlx/sqlite/migrations"); + let structure = + super::fetch_structure_sql(super::DEFAULT_CONNECTION_URL, migrations_path).await?; + assert!(structure.contains("--\n-- Name: _sqlx_migrations; Type: table\n--\nCREATE TABLE _sqlx_migrations (\n version BIGINT PRIMARY KEY,\n description TEXT NOT NULL,\n installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n success BOOLEAN NOT NULL,\n checksum BLOB NOT NULL,\n execution_time BIGINT NOT NULL\n);\n\n--\n-- Name: users; Type: table\n--\nCREATE TABLE users (\n id TEXT PRIMARY KEY NOT NULL,\n email TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT(datetime('now', 'utc'))\n);\n")); + Ok(()) + } +}