├── .github ├── .well-known │ └── funding-manifest-urls ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── feature-request.md └── workflows │ ├── release-bot.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── build-tools ├── rustclippy.sh └── rustfmt.sh ├── docs └── SeaQL logo dual.png ├── sea-schema-derive ├── Cargo.toml └── src │ └── lib.rs ├── src ├── lib.rs ├── mysql │ ├── def │ │ ├── char_set.rs │ │ ├── column.rs │ │ ├── foreign_key.rs │ │ ├── index.rs │ │ ├── mod.rs │ │ ├── schema.rs │ │ ├── storage_engine.rs │ │ ├── system.rs │ │ ├── table.rs │ │ └── types.rs │ ├── discovery │ │ ├── executor │ │ │ ├── mock.rs │ │ │ ├── mod.rs │ │ │ └── real.rs │ │ └── mod.rs │ ├── mod.rs │ ├── parser │ │ ├── column.rs │ │ ├── foreign_key.rs │ │ ├── index.rs │ │ ├── mod.rs │ │ ├── system.rs │ │ └── table.rs │ ├── probe.rs │ ├── query │ │ ├── char_set.rs │ │ ├── column.rs │ │ ├── foreign_key.rs │ │ ├── index.rs │ │ ├── mod.rs │ │ ├── schema.rs │ │ ├── table.rs │ │ └── version.rs │ └── writer │ │ ├── column.rs │ │ ├── foreign_key.rs │ │ ├── index.rs │ │ ├── mod.rs │ │ ├── table.rs │ │ └── types.rs ├── name.rs ├── parser.rs ├── postgres │ ├── def │ │ ├── column.rs │ │ ├── constraints.rs │ │ ├── mod.rs │ │ ├── schema.rs │ │ ├── table.rs │ │ └── types.rs │ ├── discovery │ │ ├── executor │ │ │ ├── mock.rs │ │ │ ├── mod.rs │ │ │ └── real.rs │ │ └── mod.rs │ ├── mod.rs │ ├── parser │ │ ├── column.rs │ │ ├── mod.rs │ │ ├── pg_indexes.rs │ │ ├── table.rs │ │ └── table_constraints.rs │ ├── probe.rs │ ├── query │ │ ├── char_set.rs │ │ ├── column.rs │ │ ├── constraints │ │ │ ├── check_constraints.rs │ │ │ ├── key_column_usage.rs │ │ │ ├── mod.rs │ │ │ ├── referential_constraints.rs │ │ │ └── table_constraints.rs │ │ ├── enumeration.rs │ │ ├── mod.rs │ │ ├── pg_indexes.rs │ │ ├── schema.rs │ │ └── table.rs │ └── writer │ │ ├── column.rs │ │ ├── constraints.rs │ │ ├── enumeration.rs │ │ ├── mod.rs │ │ ├── schema.rs │ │ ├── table.rs │ │ └── types.rs ├── probe.rs ├── sqlite │ ├── def │ │ ├── column.rs │ │ ├── mod.rs │ │ ├── schema.rs │ │ ├── table.rs │ │ └── types.rs │ ├── discovery.rs │ ├── error.rs │ ├── executor │ │ ├── mock.rs │ │ ├── mod.rs │ │ └── real.rs │ ├── mod.rs │ ├── probe.rs │ └── query.rs ├── sqlx_types │ ├── mock.rs │ ├── mod.rs │ └── real.rs └── util.rs └── tests ├── discovery ├── mysql │ ├── Cargo.toml │ ├── Readme.md │ ├── schema.rs │ └── src │ │ └── main.rs ├── postgres │ ├── Cargo.toml │ ├── Readme.md │ ├── schema.rs │ └── src │ │ └── main.rs └── sqlite │ ├── Cargo.toml │ ├── Readme.md │ ├── schema.rs │ └── src │ └── main.rs ├── live ├── mysql │ ├── Cargo.toml │ ├── Readme.md │ └── src │ │ └── main.rs ├── postgres │ ├── Cargo.toml │ ├── Readme.md │ └── src │ │ └── main.rs └── sqlite │ ├── Cargo.toml │ └── src │ └── main.rs ├── sakila ├── .gitattributes ├── .gitignore ├── Readme.md ├── composer.json ├── composer.lock ├── index.php ├── mysql │ ├── sakila-data.sql │ └── sakila-schema.sql ├── phpinfo.php ├── postgres │ ├── sakila-data.sql │ └── sakila-schema.sql ├── schema-mysql.json ├── schema-pgsql.json └── sqlite │ ├── sakila.db │ └── sqlite-sakila-schema.sql └── writer ├── mysql ├── Cargo.toml ├── Readme.md └── src │ └── main.rs ├── postgres ├── Cargo.toml ├── Readme.md ├── schema.sql └── src │ └── main.rs └── sqlite ├── Cargo.toml ├── Readme.md └── src └── main.rs /.github/.well-known/funding-manifest-urls: -------------------------------------------------------------------------------- 1 | https://www.sea-ql.org/funding.json 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: SeaQL -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug or feature flaw 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 20 | 21 | ## Description 22 | 23 | 24 | 25 | ## Steps to Reproduce 26 | 27 | 1. 28 | 2. 29 | 3. 30 | 31 | ### Expected Behavior 32 | 33 | 34 | 35 | ### Actual Behavior 36 | 37 | 38 | 39 | ### Reproduces How Often 40 | 41 | 42 | 43 | ## Versions 44 | 45 | 46 | 47 | ## Additional Information 48 | 49 | 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Q & A 4 | url: https://github.com/SeaQL/sea-schema/discussions/new?category=q-a 5 | about: Ask a question or look for help. Try to provide sufficient context, snippets to reproduce and error messages. 6 | - name: SeaQL Discord Server 7 | url: https://discord.com/invite/uCPdDXzbdv 8 | about: Join our Discord server to chat with others in the SeaQL community! 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest a new feature for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 22 | 23 | ## Motivation 24 | 25 | 26 | 27 | ## Proposed Solutions 28 | 29 | 30 | 31 | ## Additional Information 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/release-bot.yml: -------------------------------------------------------------------------------- 1 | name: Release Bot 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | comment: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - name: Commenting on `${{ github.event.release.tag_name }}` release 15 | uses: billy1624/release-comment-on-pr@master 16 | with: 17 | release-tag: ${{ github.event.release.tag_name }} 18 | token: ${{ github.token }} 19 | message: | 20 | ### :tada: Released In [${releaseTag}](${releaseUrl}) :tada: 21 | 22 | Thank you everyone for the contribution! 23 | This feature is now available in the latest release. Now is a good time to upgrade! 24 | Your participation is what makes us unique; your adoption is what drives us forward. 25 | You can support SeaQL 🌊 by starring our repos, sharing our libraries and becoming a sponsor ⭐. 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | firedbg/ 3 | Cargo.lock 4 | *.sublime* 5 | .vscode 6 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## 0.16.2 - 2025-05-07 9 | 10 | ### Enhancements 11 | 12 | * Map `postgres-vector` type in discovery https://github.com/SeaQL/sea-schema/pull/146 13 | 14 | ## 0.16.1 - 2024-12-26 15 | 16 | ### Features 17 | 18 | * Added `IndexType::Vector` to MySQL (behind feature `planetscale`) https://github.com/SeaQL/sea-schema/pull/139 19 | * Implement postgres vector extension https://github.com/SeaQL/sea-schema/pull/137 20 | 21 | ### Bug fixes 22 | 23 | * Fix enum values ordering https://github.com/SeaQL/sea-schema/pull/138 24 | 25 | ## 0.16.0 - 2024-10-17 26 | 27 | ### Versions 28 | 29 | + `sea-schema`/`0.16.0-rc.1`: 2024-08-09 30 | 31 | ### Upgrades 32 | 33 | * Upgrade `sea-query` to `0.32.0` https://github.com/SeaQL/sea-schema/pull/136 34 | * Upgrade `sea-query-binder` to `0.7.0` https://github.com/SeaQL/sea-schema/pull/136 35 | * Upgrade `sqlx` to `0.8` https://github.com/SeaQL/sea-schema/pull/136 36 | 37 | ## 0.15.0 - 2024-08-02 38 | 39 | ### Versions 40 | 41 | + `sea-schema`/`0.15.0-rc.1`: 2024-01-31 42 | + `sea-schema`/`0.15.0-rc.2`: 2024-02-02 43 | + `sea-schema`/`0.15.0-rc.3`: 2024-03-15 44 | + `sea-schema`/`0.15.0-rc.4`: 2024-03-24 45 | + `sea-schema`/`0.15.0-rc.5`: 2024-05-02 46 | + `sea-schema`/`0.15.0-rc.6`: 2024-05-03 47 | + `sea-schema`/`0.15.0-rc.7`: 2024-06-19 48 | + `sea-schema-derive`/`0.3.0`: 2024-08-02 49 | 50 | ### Features 51 | 52 | * Rework SQLite data type mapping https://github.com/SeaQL/sea-schema/pull/117 53 | * Update binary and bit data types mapping https://github.com/SeaQL/sea-schema/pull/122 54 | 55 | ### Bug fixes 56 | 57 | * Fix constraint query when table is partitioned https://github.com/SeaQL/sea-schema/pull/125 58 | * Fix Postgres foreign key column without unique constraint https://github.com/SeaQL/sea-schema/pull/131 59 | * Fix discovery of MySQL, SQLite and PostgreSQL unique indexes https://github.com/SeaQL/sea-schema/pull/133 60 | 61 | ### Breaking changes 62 | 63 | * `SchemaProbe::query_tables(..)` changed to `SchemaProbe::query_tables(&self, ..)` https://github.com/SeaQL/sea-schema/pull/127 64 | * `SchemaProbe::has_table(..)` changed to `SchemaProbe::has_table(&self, ..)` https://github.com/SeaQL/sea-schema/pull/126 65 | * `SchemaProbe::has_column(..)` changed to `SchemaProbe::has_column(&self, ..)` https://github.com/SeaQL/sea-schema/pull/126 66 | * `SchemaProbe::has_index(..)` changed to `SchemaProbe::has_index(&self, ..)` https://github.com/SeaQL/sea-schema/pull/126 67 | 68 | ### Enhancements 69 | 70 | * Added non-TLS runtime https://github.com/SeaQL/sea-schema/pull/134 71 | 72 | ### Upgrades 73 | 74 | * Upgrade `syn` to `2` https://github.com/SeaQL/sea-schema/pull/129 75 | 76 | ## 0.14.2 - 2024-01-18 77 | 78 | ### Bug fixes 79 | 80 | * Fix composite foreign key discovery for Postgres https://github.com/SeaQL/sea-schema/pull/118 81 | * Fix composite foreign key discovery for SQLite https://github.com/SeaQL/sea-schema/pull/119 82 | 83 | ### House keeping 84 | 85 | * Fix clippy warnings on 1.75 https://github.com/SeaQL/sea-schema/pull/121 86 | 87 | ## 0.14.1 - 2023-09-14 88 | 89 | * Added `has_index` to `SchemaProbe` https://github.com/SeaQL/sea-schema/pull/115 90 | 91 | ## 0.14.0 - 2023-07-21 92 | 93 | ### Upgrades 94 | 95 | * Upgrade `sea-query` to `0.30` https://github.com/SeaQL/sea-schema/pull/114 96 | * Upgrade `sea-query-binder` to `0.5` https://github.com/SeaQL/sea-schema/pull/114 97 | * Upgrade `sqlx` to `0.7` https://github.com/SeaQL/sea-schema/pull/114 98 | 99 | ### Bug fixes 100 | 101 | * Fix PostgreSQL enum arrays and case-sensitive types https://github.com/SeaQL/sea-schema/pull/108 102 | 103 | ## 0.13.0 - Skipped 104 | 105 | ## 0.12.0 - 2023-07-20 106 | 107 | + 2023-03-22: `0.12.0-rc.1` 108 | + 2023-05-18: `0.12.0-rc.2` 109 | 110 | ### Features and upgrades 111 | 112 | * Skip parsing partitioned Postgres tables https://github.com/SeaQL/sea-schema/pull/105 113 | * Upgrade `heck` dependency in `sea-schema-derive` to 0.4 https://github.com/SeaQL/sea-schema/pull/103 114 | * Upgrade `sea-query` to `0.29` https://github.com/SeaQL/sea-schema/pull/104 115 | * Upgrade `sea-query-binder` to `0.4` https://github.com/SeaQL/sea-schema/pull/104 116 | * Replace the use of `SeaRc` where `T` isn't `dyn Iden` with `RcOrArc` https://github.com/SeaQL/sea-schema/pull/107 117 | * Customized parsing logic for MySQL and MariaDB column default https://github.com/SeaQL/sea-schema/pull/110 118 | * Properly distinguish between Value and Expression, and the very special CURRENT_TIMESTAMP 119 | * Improve SQLite's column default parsing logic https://github.com/SeaQL/sea-schema/pull/112 120 | 121 | ### Breaking changes 122 | 123 | * API now returns `Result` instead of panic on errors https://github.com/SeaQL/sea-schema/pull/109 124 | * `ColumnDefault` changed from a struct into an enum https://github.com/SeaQL/sea-schema/pull/110 125 | * Added `CurrentTimestamp` variant to SQLite's `DefaultType` https://github.com/SeaQL/sea-schema/pull/112 126 | 127 | ## 0.11.0 - 2023-01-05 128 | 129 | * Upgrade SeaQuery to 0.28 https://github.com/SeaQL/sea-schema/pull/90 130 | * Changed all version = "^x.y.z" into version = "x.y.z" and disabled default features and enable only the needed ones https://github.com/SeaQL/sea-schema/pull/93 131 | * Skip parsing Postgres check constraints when check expression is empty https://github.com/SeaQL/sea-schema/pull/96 132 | * Parsing Postgres citext column type https://github.com/SeaQL/sea-schema/pull/94 133 | 134 | ## 0.10.3 - 2022-11-16 135 | 136 | * Backward compatible schema discovery for MySQL 5.6 https://github.com/SeaQL/sea-schema/pull/86 137 | 138 | ## 0.10.2 - 2022-10-26 139 | 140 | * Fix parsing of Postgres user-defined types https://github.com/SeaQL/sea-schema/pull/84 141 | 142 | ## 0.10.1 - 2022-10-23 143 | 144 | * Parse & write Postgres array datatypes https://github.com/SeaQL/sea-schema/pull/83 145 | 146 | ## 0.10.0 - 2022-10-18 147 | 148 | * Upgrade SeaQuery to 0.27 https://github.com/SeaQL/sea-schema/pull/81 149 | 150 | ## 0.9.4 - 2022-09-16 151 | 152 | * Parsing SQLite integer column types without space in it https://github.com/SeaQL/sea-schema/pull/77 153 | 154 | ## 0.9.3 - 2022-07-17 155 | 156 | * SQLite real datatype maps to double https://github.com/SeaQL/sea-schema/pull/75 157 | * Discover SYSTEM VERSIONED tables for MariaDB https://github.com/SeaQL/sea-schema/pull/76 158 | 159 | ## 0.9.2 - 2022-07-04 160 | 161 | * PostgreSQL datetime and timestamp datatype are equivalent https://github.com/SeaQL/sea-schema/pull/69 162 | * MySQL VarBinary column type mapping https://github.com/SeaQL/sea-schema/pull/67 163 | * Upgrade `sqlx` to 0.6 164 | * Upgrade `sea-query` to 0.26 165 | 166 | ## 0.8.1 - 2022-06-17 167 | 168 | * Fix SQLx version to ^0.5 https://github.com/SeaQL/sea-schema/pull/70 169 | * PostgreSQL query non-key foreign key info https://github.com/SeaQL/sea-schema/pull/65 170 | 171 | ## 0.8.0 - 2022-05-09 172 | 173 | * Dropping `migration` entirely; introducing `SchemaProbe` 174 | 175 | ## 0.7.1 - 2022-03-26 176 | 177 | * Support SeaORM 0.7.0 178 | * Support Postgres jsonb in entity generation https://github.com/SeaQL/sea-schema/pull/51 179 | 180 | ## 0.6.0 - 2022-03-14 181 | 182 | * Write MySQL unsigned integer types https://github.com/SeaQL/sea-schema/pull/37 183 | * Fix Sqlite BLOB type https://github.com/SeaQL/sea-schema/pull/44 184 | * Migrate with `sea_orm::DbConn` https://github.com/SeaQL/sea-schema/pull/49 185 | 186 | ## 0.5.1 - 2022-02-07 187 | 188 | * Add `migration::prelude` to replace wildcard imports #43 189 | 190 | ## 0.5.0 - 2022-02-07 191 | 192 | * Fix Postgres discover duplicated foreign keys by @billy1624 in https://github.com/SeaQL/sea-schema/pull/30 193 | * Schema Manager by @billy1624 in https://github.com/SeaQL/sea-schema/pull/26 194 | 195 | **Full Changelog**: https://github.com/SeaQL/sea-schema/compare/0.4.0...0.5.0 196 | 197 | ## 0.4.0 - 2021-12-25 198 | 199 | * SQLite schema discovery https://github.com/SeaQL/sea-schema/pull/34 200 | 201 | ## 0.3.1 - 2021-12-12 202 | 203 | * Add support for the Postgres interval type by @autarch in https://github.com/SeaQL/sea-schema/pull/20 204 | * CI: Clippy, MySQL & Postgres by @billy1624 in https://github.com/SeaQL/sea-schema/pull/21 205 | * Write MySQL & Postgres Enum Columns by @billy1624 in https://github.com/SeaQL/sea-schema/pull/29 206 | 207 | **Full Changelog**: https://github.com/SeaQL/sea-schema/compare/0.2.9...0.3.1 208 | 209 | ## 0.2.9 - 2021-09-24 210 | 211 | + [[#18]] MySQL: handle panic upon unique constraint 212 | 213 | [#18]: https://github.com/SeaQL/sea-schema/issues/18 214 | 215 | ## 0.2.8 - 2021-09-17 216 | 217 | + Fix Postgres `TimestampWithTimeZone` 218 | 219 | ## 0.2.7 - 2021-08-23 220 | 221 | + Use SeaRc to support SeaQuery's `thread-safe` 222 | 223 | ## 0.2.6 - 2021-08-21 224 | 225 | + Use sea-query to 0.15 226 | + [[#13]] Added `is_identity` to Postgres `ColumnInfo` 227 | 228 | [#13]: https://github.com/SeaQL/sea-schema/issues/13 229 | 230 | ## 0.2.5 - 2021-08-14 231 | 232 | + improve Postgres schema discovery 233 | 234 | ## 0.2.4 - 2021-08-07 235 | 236 | + improve Postgres schema discovery 237 | 238 | ## 0.2.3 - 2021-06-19 239 | 240 | + Improve `ColumnType` output of MySQL writer 241 | 242 | ## 0.2.2 - 2021-06-19 243 | 244 | + Added `ColumnExpression` to MySQL ColumnInfo output 245 | + Postgres type definitions 246 | 247 | ## 0.2.1 - 2021-04-30 248 | 249 | + Foreign key writer 250 | + Index prefix and order 251 | 252 | ## 0.2.0 - 2021-04-25 253 | 254 | + `Writer` 255 | + changed `StringAttr` definition 256 | + added `IndexPart` definition 257 | 258 | ## 0.1.4 - 2021-04-13 259 | 260 | + serde support on types 261 | + Query table's `char_set` from information_schema 262 | 263 | ## 0.1.3 - 2021-04-11 264 | 265 | + `TableInfo` includes `char_set` 266 | 267 | ## 0.1.2 - 2021-04-11 268 | 269 | + Restructure dependencies 270 | 271 | ## 0.1.1 - 2021-04-08 272 | 273 | + Fix docs.rs 274 | 275 | ## 0.1.0 - 2021-04-08 276 | 277 | + Initial release 278 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | ".", 4 | "tests/discovery/mysql", 5 | "tests/discovery/postgres", 6 | "tests/discovery/sqlite", 7 | "tests/writer/mysql", 8 | "tests/writer/postgres", 9 | "tests/writer/sqlite", 10 | "tests/live/mysql", 11 | "tests/live/postgres", 12 | "tests/live/sqlite", 13 | ] 14 | 15 | [package] 16 | name = "sea-schema" 17 | version = "0.17.0-rc.1" 18 | authors = [ "Chris Tsang " ] 19 | edition = "2024" 20 | description = "🌿 SQL schema definition and discovery" 21 | license = "MIT OR Apache-2.0" 22 | documentation = "https://docs.rs/sea-schema" 23 | repository = "https://github.com/SeaQL/sea-schema" 24 | categories = ["database"] 25 | keywords = ["database", "sql", "mysql", "postgres"] 26 | rust-version = "1.85.0" 27 | 28 | [package.metadata.docs.rs] 29 | features = ["default"] 30 | rustdoc-args = ["--cfg", "docsrs"] 31 | 32 | [lib] 33 | name = "sea_schema" 34 | path = "src/lib.rs" 35 | 36 | [dependencies] 37 | futures = { version = "0.3", default-features = false, optional = true, features = ["alloc"] } 38 | sea-schema-derive = { version = "0.3.0", path = "sea-schema-derive", default-features = false } 39 | sea-query = { version = "1.0.0-rc.1", default-features = false, features = ["derive"] } 40 | sea-query-binder = { version = "0.8.0-rc.1", default-features = false, optional = true } 41 | serde = { version = "1", default-features = false, optional = true, features = ["derive"] } 42 | sqlx = { version = "0.8", default-features = false, optional = true } 43 | log = { version = "0.4", default-features = false, optional = true } 44 | 45 | [features] 46 | default = ["mysql", "postgres", "sqlite", "discovery", "writer", "probe"] 47 | debug-print = ["log"] 48 | mysql = ["sea-query/backend-mysql"] 49 | postgres = ["sea-query/backend-postgres"] 50 | postgres-vector = ["sea-query/postgres-vector", "sea-query-binder/postgres-vector"] 51 | sqlite = ["sea-query/backend-sqlite"] 52 | def = [] 53 | discovery = ["futures", "parser"] 54 | parser = ["query"] 55 | query = ["def"] 56 | writer = ["def"] 57 | planetscale = [] 58 | probe = ["query"] 59 | sqlx-dep = ["sqlx"] 60 | sqlx-all = ["sqlx-mysql", "sqlx-postgres", "sqlx-sqlite"] 61 | sqlx-mysql = [ 62 | "mysql", 63 | "futures", 64 | "sqlx-dep", 65 | "sea-query-binder/sqlx-mysql", 66 | "sqlx/mysql", 67 | ] 68 | sqlx-postgres = [ 69 | "postgres", 70 | "futures", 71 | "sqlx-dep", 72 | "sea-query-binder/sqlx-postgres", 73 | "sqlx/postgres", 74 | ] 75 | sqlx-sqlite = [ 76 | "sqlite", 77 | "futures", 78 | "sqlx-dep", 79 | "sea-query-binder/sqlx-sqlite", 80 | "sqlx/sqlite", 81 | ] 82 | runtime-actix = [ 83 | "sqlx?/runtime-tokio", 84 | "sea-query-binder?/runtime-actix", 85 | ] 86 | runtime-async-std = [ 87 | "sqlx?/runtime-async-std", 88 | "sea-query-binder?/runtime-async-std", 89 | ] 90 | runtime-tokio = [ 91 | "sqlx?/runtime-tokio", 92 | "sea-query-binder?/runtime-tokio", 93 | ] 94 | runtime-actix-native-tls = [ 95 | "sqlx?/runtime-tokio-native-tls", 96 | "sea-query-binder?/runtime-actix-native-tls", 97 | ] 98 | runtime-async-std-native-tls = [ 99 | "sqlx?/runtime-async-std-native-tls", 100 | "sea-query-binder?/runtime-async-std-native-tls", 101 | ] 102 | runtime-tokio-native-tls = [ 103 | "sqlx?/runtime-tokio-native-tls", 104 | "sea-query-binder?/runtime-tokio-native-tls", 105 | ] 106 | runtime-actix-rustls = [ 107 | "sqlx?/runtime-tokio-rustls", 108 | "sea-query-binder?/runtime-actix-rustls", 109 | ] 110 | runtime-async-std-rustls = [ 111 | "sqlx?/runtime-async-std-rustls", 112 | "sea-query-binder?/runtime-async-std-rustls", 113 | ] 114 | runtime-tokio-rustls = [ 115 | "sqlx?/runtime-tokio-rustls", 116 | "sea-query-binder?/runtime-tokio-rustls", 117 | ] 118 | with-serde = ["serde"] 119 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Tsang Hao Fung 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |

SeaSchema

6 | 7 |

8 | 🌿 SQL schema definition and discovery 9 |

10 | 11 | [![crate](https://img.shields.io/crates/v/sea-schema.svg)](https://crates.io/crates/sea-schema) 12 | [![docs](https://docs.rs/sea-schema/badge.svg)](https://docs.rs/sea-schema) 13 | [![build status](https://github.com/SeaQL/sea-schema/actions/workflows/rust.yml/badge.svg)](https://github.com/SeaQL/sea-schema/actions/workflows/rust.yml) 14 | 15 |
16 | 17 | ## About 18 | 19 | SeaSchema is a library to help you manage database schema for MySQL, Postgres and SQLite. It provides 1) type definitions for representing database schema mapping each database closely and 2) utilities to discover them. 20 | 21 | [![GitHub stars](https://img.shields.io/github/stars/SeaQL/sea-schema.svg?style=social&label=Star&maxAge=1)](https://github.com/SeaQL/sea-schema/stargazers/) 22 | If you like what we do, consider starring, commenting, sharing and contributing! 23 | 24 | [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) 25 | Join our Discord server to chat with others in the SeaQL community! 26 | 27 | ## Architecture 28 | 29 | The crate is divided into different modules: 30 | 31 | + `def`: type definitions 32 | + `query`, `parser`: for querying and parsing information_schema 33 | + `discovery`: connect to a live database and discover a `Schema` 34 | + `writer`: for exporting `Schema` into SeaQuery and SQL statements 35 | 36 | JSON de/serialize on type definitions can be enabled with `with-serde`. 37 | 38 | ## Schema Discovery 39 | 40 | Take the MySQL [Sakila Sample Database](tests/sakila/mysql/sakila-schema.sql) as example, given the following table: 41 | 42 | ```SQL 43 | CREATE TABLE film_actor ( 44 | actor_id SMALLINT UNSIGNED NOT NULL, 45 | film_id SMALLINT UNSIGNED NOT NULL, 46 | last_update TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 47 | PRIMARY KEY (actor_id,film_id), 48 | KEY idx_fk_film_id (`film_id`), 49 | CONSTRAINT fk_film_actor_actor FOREIGN KEY (actor_id) REFERENCES actor (actor_id) ON DELETE RESTRICT ON UPDATE CASCADE, 50 | CONSTRAINT fk_film_actor_film FOREIGN KEY (film_id) REFERENCES film (film_id) ON DELETE RESTRICT ON UPDATE CASCADE 51 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 52 | 53 | ``` 54 | 55 | The [discovered schema result](tests/discovery/mysql/schema.rs): 56 | 57 | ```rust 58 | TableDef { 59 | info: TableInfo { 60 | name: "film_actor", 61 | engine: InnoDb, 62 | auto_increment: None, 63 | char_set: Utf8Mb4, 64 | collation: Utf8Mb40900AiCi, 65 | comment: "", 66 | }, 67 | columns: [ 68 | ColumnInfo { 69 | name: "actor_id", 70 | col_type: SmallInt( 71 | NumericAttr { 72 | maximum: None, 73 | decimal: None, 74 | unsigned: Some(true), 75 | zero_fill: None, 76 | }, 77 | ), 78 | null: false, 79 | key: Primary, 80 | default: None, 81 | extra: ColumnExtra { 82 | auto_increment: false, 83 | on_update_current_timestamp: false, 84 | generated: false, 85 | default_generated: false, 86 | }, 87 | expression: None, 88 | comment: "", 89 | }, 90 | ColumnInfo { 91 | name: "film_id", 92 | col_type: SmallInt( 93 | NumericAttr { 94 | maximum: None, 95 | decimal: None, 96 | unsigned: Some(true), 97 | zero_fill: None, 98 | }, 99 | ), 100 | null: false, 101 | key: Primary, 102 | default: None, 103 | extra: ColumnExtra { 104 | auto_increment: false, 105 | on_update_current_timestamp: false, 106 | generated: false, 107 | default_generated: false, 108 | }, 109 | expression: None, 110 | comment: "", 111 | }, 112 | ColumnInfo { 113 | name: "last_update", 114 | col_type: Timestamp(TimeAttr { fractional: None }), 115 | null: false, 116 | key: NotKey, 117 | default: Some(ColumnDefault::CurrentTimestamp), 118 | extra: ColumnExtra { 119 | auto_increment: false, 120 | on_update_current_timestamp: true, 121 | generated: false, 122 | default_generated: true, 123 | }, 124 | expression: None, 125 | comment: "", 126 | }, 127 | ], 128 | indexes: [ 129 | IndexInfo { 130 | unique: false, 131 | name: "idx_fk_film_id", 132 | parts: [ 133 | IndexPart { 134 | column: "film_id", 135 | order: Ascending, 136 | sub_part: None, 137 | }, 138 | ], 139 | nullable: false, 140 | idx_type: BTree, 141 | comment: "", 142 | functional: false, 143 | }, 144 | IndexInfo { 145 | unique: true, 146 | name: "PRIMARY", 147 | parts: [ 148 | IndexPart { 149 | column: "actor_id", 150 | order: Ascending, 151 | sub_part: None, 152 | }, 153 | IndexPart { 154 | column: "film_id", 155 | order: Ascending, 156 | sub_part: None, 157 | }, 158 | ], 159 | nullable: false, 160 | idx_type: BTree, 161 | comment: "", 162 | functional: false, 163 | }, 164 | ], 165 | foreign_keys: [ 166 | ForeignKeyInfo { 167 | name: "fk_film_actor_actor", 168 | columns: [ "actor_id" ], 169 | referenced_table: "actor", 170 | referenced_columns: [ "actor_id" ], 171 | on_update: Cascade, 172 | on_delete: Restrict, 173 | }, 174 | ForeignKeyInfo { 175 | name: "fk_film_actor_film", 176 | columns: [ "film_id" ], 177 | referenced_table: "film", 178 | referenced_columns: [ "film_id" ], 179 | on_update: Cascade, 180 | on_delete: Restrict, 181 | }, 182 | ], 183 | } 184 | ``` 185 | 186 | ## License 187 | 188 | Licensed under either of 189 | 190 | - Apache License, Version 2.0 191 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 192 | - MIT license 193 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 194 | 195 | at your option. 196 | 197 | ## Contribution 198 | 199 | Unless you explicitly state otherwise, any contribution intentionally submitted 200 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 201 | dual licensed as above, without any additional terms or conditions. 202 | 203 | SeaSchema is a community driven project. We welcome you to participate, contribute and together build for Rust's future. 204 | 205 | A big shout out to our contributors: 206 | 207 | [![Contributors](https://opencollective.com/sea-schema/contributors.svg?width=1000&button=false)](https://github.com/SeaQL/sea-schema/graphs/contributors) 208 | -------------------------------------------------------------------------------- /build-tools/rustclippy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | if [ -d ./build-tools ]; then 4 | targets=( 5 | "Cargo.toml" 6 | "sea-schema-derive/Cargo.toml" 7 | ) 8 | 9 | for target in "${targets[@]}"; do 10 | echo "cargo clippy --manifest-path ${target} --fix --allow-dirty --allow-staged" 11 | cargo clippy --manifest-path "${target}" --fix --allow-dirty --allow-staged 12 | done 13 | 14 | tests=(`find tests -type f -name 'Cargo.toml'`) 15 | for example in "${tests[@]}"; do 16 | echo "cargo clippy --manifest-path ${example} --fix --allow-dirty --allow-staged" 17 | cargo clippy --manifest-path "${example}" --fix --allow-dirty --allow-staged 18 | done 19 | else 20 | echo "Please execute this script from the repository root." 21 | fi 22 | -------------------------------------------------------------------------------- /build-tools/rustfmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | if [ -d ./build-tools ]; then 4 | targets=( 5 | "Cargo.toml" 6 | "sea-schema-derive/Cargo.toml" 7 | ) 8 | 9 | for target in "${targets[@]}"; do 10 | echo "cargo +nightly fmt --manifest-path ${target} --all" 11 | cargo +nightly fmt --manifest-path "${target}" --all 12 | done 13 | 14 | tests=(`find tests -type f -name 'Cargo.toml'`) 15 | for example in "${tests[@]}"; do 16 | echo "cargo +nightly fmt --manifest-path ${example} --all" 17 | cargo +nightly fmt --manifest-path "${example}" --all 18 | done 19 | else 20 | echo "Please execute this script from the repository root." 21 | fi 22 | -------------------------------------------------------------------------------- /docs/SeaQL logo dual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeaQL/sea-schema/9bee1b1674b1ae31cbd90b3f5f2d32ab65009bca/docs/SeaQL logo dual.png -------------------------------------------------------------------------------- /sea-schema-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sea-schema-derive" 3 | version = "0.3.0" 4 | authors = [ "Chris Tsang " ] 5 | edition = "2024" 6 | description = "Derive macro for sea-schema's Name trait" 7 | license = "MIT OR Apache-2.0" 8 | documentation = "https://docs.rs/sea-schema" 9 | repository = "https://github.com/SeaQL/sea-schema" 10 | categories = [ "database" ] 11 | keywords = [ "database", "sql", "mysql", "postgres", "sqlite" ] 12 | rust-version = "1.85.0" 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | syn = { version = "2", default-features = false, features = [ "derive", "parsing", "proc-macro", "printing" ] } 19 | quote = { version = "1", default-features = false } 20 | heck = { version = "0.4", default-features = false } 21 | proc-macro2 = { version = "1", default-features = false } 22 | -------------------------------------------------------------------------------- /sea-schema-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use heck::ToSnakeCase; 2 | use proc_macro::{self, TokenStream}; 3 | use proc_macro2::Span; 4 | use quote::{quote, quote_spanned}; 5 | use syn::{ 6 | Attribute, DataEnum, DataStruct, DeriveInput, Expr, ExprLit, Fields, Ident, Lit, Meta, Variant, 7 | parse_macro_input, 8 | }; 9 | 10 | fn get_iden_attr(attrs: &[Attribute]) -> Option<&syn::Expr> { 11 | for attr in attrs { 12 | let name_value = match &attr.meta { 13 | Meta::NameValue(nv) => nv, 14 | _ => continue, 15 | }; 16 | if name_value.path.is_ident("iden") || // interoperate with sea_query_derive Iden 17 | name_value.path.is_ident("name") 18 | { 19 | return Some(&name_value.value); 20 | } 21 | } 22 | None 23 | } 24 | 25 | fn get_catch_attr(attrs: &[Attribute]) -> Option<&syn::Expr> { 26 | for attr in attrs { 27 | let name_value = match &attr.meta { 28 | Meta::NameValue(nv) => nv, 29 | _ => continue, 30 | }; 31 | if name_value.path.is_ident("catch") { 32 | return Some(&name_value.value); 33 | } 34 | } 35 | None 36 | } 37 | 38 | #[proc_macro_derive(Name, attributes(iden, name, catch))] 39 | pub fn derive_iden(input: TokenStream) -> TokenStream { 40 | let DeriveInput { 41 | ident, data, attrs, .. 42 | } = parse_macro_input!(input); 43 | 44 | let table_name = match get_iden_attr(&attrs) { 45 | Some(iden) => quote! { #iden }, 46 | None => { 47 | let normalized = ident.to_string().to_snake_case(); 48 | quote! { #normalized } 49 | } 50 | }; 51 | 52 | let catch = match get_catch_attr(&attrs) { 53 | Some(lit) => { 54 | let name: String = match lit { 55 | Expr::Lit(ExprLit { 56 | lit: Lit::Str(name), 57 | .. 58 | }) => name.value(), 59 | _ => panic!("expected string for `catch`"), 60 | }; 61 | let method = Ident::new(name.as_str(), Span::call_site()); 62 | 63 | quote! { #ident::#method(string) } 64 | } 65 | None => { 66 | quote! { None } 67 | } 68 | }; 69 | 70 | // Currently we only support enums and unit structs 71 | let variants = 72 | match data { 73 | syn::Data::Enum(DataEnum { variants, .. }) => variants, 74 | syn::Data::Struct(DataStruct { 75 | fields: Fields::Unit, 76 | .. 77 | }) => { 78 | return quote! { 79 | impl sea_schema::Name for #ident { 80 | fn from_str(string: &str) -> Option { 81 | if string == #table_name { 82 | Some(Self) 83 | } else { 84 | None 85 | } 86 | } 87 | } 88 | } 89 | .into(); 90 | } 91 | _ => return quote_spanned! { 92 | ident.span() => compile_error!("you can only derive Name on enums or unit structs"); 93 | } 94 | .into(), 95 | }; 96 | 97 | if variants.is_empty() { 98 | return TokenStream::new(); 99 | } 100 | 101 | let variant = variants 102 | .iter() 103 | .filter(|v| get_catch_attr(&v.attrs).is_none() && matches!(v.fields, Fields::Unit)) 104 | .map(|Variant { ident, fields, .. }| match fields { 105 | Fields::Unit => quote! { #ident }, 106 | _ => panic!(), 107 | }); 108 | 109 | let name = variants.iter().map(|v| { 110 | if let Some(iden) = get_iden_attr(&v.attrs) { 111 | // If the user supplied a name, just use it 112 | quote! { #iden } 113 | } else if v.ident == "Table" { 114 | table_name.clone() 115 | } else { 116 | let ident = v.ident.to_string().to_snake_case(); 117 | quote! { #ident } 118 | } 119 | }); 120 | 121 | let output = quote! { 122 | impl sea_schema::Name for #ident { 123 | fn from_str(string: &str) -> Option { 124 | let result = match string { 125 | #(#name => Some(Self::#variant),)* 126 | _ => None, 127 | }; 128 | if result.is_some() { 129 | result 130 | } else { 131 | #catch 132 | } 133 | } 134 | } 135 | }; 136 | 137 | output.into() 138 | } 139 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | 3 | #[cfg(feature = "mysql")] 4 | #[cfg_attr(docsrs, doc(cfg(feature = "mysql")))] 5 | pub mod mysql; 6 | 7 | #[cfg(feature = "postgres")] 8 | #[cfg_attr(docsrs, doc(cfg(feature = "postgres")))] 9 | pub mod postgres; 10 | 11 | #[cfg(feature = "sqlite")] 12 | #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))] 13 | pub mod sqlite; 14 | 15 | pub use sea_query; 16 | 17 | pub(crate) mod parser; 18 | pub(crate) mod sqlx_types; 19 | pub(crate) mod util; 20 | 21 | pub mod name; 22 | pub use name::*; 23 | 24 | #[cfg(feature = "probe")] 25 | pub mod probe; 26 | -------------------------------------------------------------------------------- /src/mysql/def/column.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::Type; 5 | 6 | #[derive(Clone, Debug, PartialEq)] 7 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 8 | pub struct ColumnInfo { 9 | /// The name of the column 10 | pub name: String, 11 | /// The type of the column with additional definitions, e.g. precision, length 12 | pub col_type: ColumnType, 13 | /// Can this column contains null 14 | pub null: bool, 15 | /// Is this column indexed 16 | pub key: ColumnKey, 17 | /// Default value expression for this column, if any 18 | pub default: Option, 19 | /// Extra definitions for this column, e.g. auto_increment 20 | pub extra: ColumnExtra, 21 | /// The generation expression if this is a generated column 22 | pub expression: Option, 23 | /// User comments 24 | pub comment: String, 25 | } 26 | 27 | pub type ColumnType = Type; 28 | 29 | #[derive(Clone, Debug, PartialEq)] 30 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 31 | pub enum ColumnKey { 32 | /// This column is not the first column of any key 33 | NotKey, 34 | /// This column is part of the primary key 35 | Primary, 36 | /// This column is the first column of a unique key 37 | Unique, 38 | /// This column is the first column of a non-unique key 39 | Multiple, 40 | } 41 | 42 | #[derive(Clone, Debug, PartialEq)] 43 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 44 | pub enum ColumnDefault { 45 | Null, 46 | Int(i64), 47 | Real(f64), 48 | String(String), 49 | CustomExpr(String), 50 | CurrentTimestamp, 51 | } 52 | 53 | #[derive(Clone, Debug, PartialEq)] 54 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 55 | pub struct ColumnExpression { 56 | /// generation expression 57 | pub expr: String, 58 | } 59 | 60 | #[derive(Clone, Debug, Default, PartialEq)] 61 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 62 | pub struct ColumnExtra { 63 | /// Auto increment 64 | pub auto_increment: bool, 65 | /// Only applies to timestamp or datetime 66 | pub on_update_current_timestamp: bool, 67 | /// This is a generated column 68 | pub generated: bool, 69 | /// This column has a default value expression 70 | pub default_generated: bool, 71 | } 72 | -------------------------------------------------------------------------------- /src/mysql/def/foreign_key.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate as sea_schema; 5 | 6 | #[derive(Clone, Debug, PartialEq)] 7 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 8 | pub struct ForeignKeyInfo { 9 | /// The name of the foreign key 10 | pub name: String, 11 | /// The columns composing this foreign key 12 | pub columns: Vec, 13 | /// Referenced table name 14 | pub referenced_table: String, 15 | /// The columns composing the index of the referenced table 16 | pub referenced_columns: Vec, 17 | /// Action on update 18 | pub on_update: ForeignKeyAction, 19 | /// Action on delete 20 | pub on_delete: ForeignKeyAction, 21 | } 22 | 23 | #[derive(Clone, Debug, PartialEq, sea_schema_derive::Name)] 24 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 25 | pub enum ForeignKeyAction { 26 | #[name = "CASCADE"] 27 | Cascade, 28 | #[name = "SET NULL"] 29 | SetNull, 30 | #[name = "SET DEFAULT"] 31 | SetDefault, 32 | #[name = "RESTRICT"] 33 | Restrict, 34 | #[name = "NO ACTION"] 35 | NoAction, 36 | } 37 | -------------------------------------------------------------------------------- /src/mysql/def/index.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate as sea_schema; 5 | 6 | #[derive(Clone, Debug, PartialEq)] 7 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 8 | pub struct IndexInfo { 9 | /// Does this index requires unique values 10 | pub unique: bool, 11 | /// The name of the index 12 | pub name: String, 13 | /// The parts composing this index 14 | pub parts: Vec, 15 | /// Does this index allow null values 16 | pub nullable: bool, 17 | /// BTree (the default), full-text etc 18 | pub idx_type: IndexType, 19 | /// User comments 20 | pub comment: String, 21 | /// True if part of the index is computed 22 | pub functional: bool, 23 | } 24 | 25 | #[derive(Clone, Debug, PartialEq)] 26 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 27 | pub struct IndexPart { 28 | /// Identifier for this column. If functional is true, may contain expression. 29 | pub column: String, 30 | /// Ascending, descending or unordered 31 | pub order: IndexOrder, 32 | /// If the whole column is indexed, this value is null. Otherwise the number indicates number of characters indexed 33 | pub sub_part: Option, 34 | } 35 | 36 | #[derive(Clone, Debug, PartialEq)] 37 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 38 | pub enum IndexOrder { 39 | Ascending, 40 | Descending, 41 | Unordered, 42 | } 43 | 44 | #[derive(Clone, Debug, PartialEq, sea_query::Iden, sea_schema_derive::Name)] 45 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 46 | pub enum IndexType { 47 | #[iden = "BTREE"] 48 | BTree, 49 | #[iden = "FULLTEXT"] 50 | FullText, 51 | #[iden = "HASH"] 52 | Hash, 53 | #[iden = "RTREE"] 54 | RTree, 55 | #[iden = "SPATIAL"] 56 | Spatial, 57 | #[cfg(feature = "planetscale")] 58 | #[iden = "VECTOR"] 59 | Vector, 60 | } 61 | -------------------------------------------------------------------------------- /src/mysql/def/mod.rs: -------------------------------------------------------------------------------- 1 | //! To represent MySQL's schema definitions 2 | 3 | mod char_set; 4 | mod column; 5 | mod foreign_key; 6 | mod index; 7 | mod schema; 8 | mod storage_engine; 9 | mod system; 10 | mod table; 11 | mod types; 12 | 13 | pub use char_set::*; 14 | pub use column::*; 15 | pub use foreign_key::*; 16 | pub use index::*; 17 | pub use schema::*; 18 | pub use storage_engine::*; 19 | pub use system::*; 20 | pub use table::*; 21 | pub use types::*; 22 | -------------------------------------------------------------------------------- /src/mysql/def/schema.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::*; 5 | 6 | #[derive(Clone, Debug, PartialEq)] 7 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 8 | pub struct Schema { 9 | pub schema: String, 10 | pub system: SystemInfo, 11 | pub tables: Vec, 12 | } 13 | 14 | #[derive(Clone, Debug, PartialEq)] 15 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 16 | pub struct TableDef { 17 | pub info: TableInfo, 18 | pub columns: Vec, 19 | pub indexes: Vec, 20 | pub foreign_keys: Vec, 21 | } 22 | -------------------------------------------------------------------------------- /src/mysql/def/storage_engine.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate as sea_schema; 5 | 6 | #[derive(Clone, Debug, PartialEq, sea_query::Iden, sea_schema_derive::Name)] 7 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 8 | #[catch = "string_to_unknown"] 9 | pub enum StorageEngine { 10 | #[iden = "ARCHIVE"] 11 | Archive, 12 | #[iden = "BLACKHOLE"] 13 | Blackhole, 14 | #[iden = "MRG_MYISAM"] 15 | MrgMyIsam, 16 | #[iden = "FEDERATED"] 17 | Federated, 18 | #[iden = "MyISAM"] 19 | MyIsam, 20 | #[iden = "PERFORMANCE_SCHEMA"] 21 | PerformanceSchema, 22 | #[iden = "InnoDB"] 23 | InnoDb, 24 | #[iden = "MEMORY"] 25 | Memory, 26 | #[iden = "CSV"] 27 | Csv, 28 | #[method = "unknown_to_string"] 29 | Unknown(String), 30 | } 31 | 32 | impl StorageEngine { 33 | pub fn unknown_to_string(&self) -> &String { 34 | match self { 35 | Self::Unknown(custom) => custom, 36 | _ => panic!("not Unknown"), 37 | } 38 | } 39 | 40 | pub fn string_to_unknown(string: &str) -> Option { 41 | Some(Self::Unknown(string.to_string())) 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use super::*; 48 | use crate::Name; 49 | 50 | #[test] 51 | fn test_0() { 52 | assert_eq!( 53 | StorageEngine::from_str("ARCHIVE").unwrap(), 54 | StorageEngine::Archive 55 | ); 56 | assert_eq!( 57 | StorageEngine::from_str("InnoDB").unwrap(), 58 | StorageEngine::InnoDb 59 | ); 60 | assert_eq!( 61 | StorageEngine::from_str("MyISAM").unwrap(), 62 | StorageEngine::MyIsam 63 | ); 64 | } 65 | 66 | #[test] 67 | fn test_1() { 68 | assert_eq!( 69 | StorageEngine::from_str("hello").unwrap(), 70 | StorageEngine::Unknown("hello".to_owned()) 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/mysql/def/system.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, Default, PartialEq)] 5 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 6 | pub struct SystemInfo { 7 | /// The version number converted to integer using the following formula: 8 | /// major_version * 10000 + minor_version * 100 + sub_version 9 | pub version: u32, 10 | /// The system string. it may be: `0ubuntu0.*` or `MariaDB` 11 | pub system: String, 12 | /// Additional suffix 13 | pub suffix: Vec, 14 | } 15 | 16 | impl SystemInfo { 17 | /// Return true if the system is MariaDB 18 | pub fn is_maria_db(&self) -> bool { 19 | self.system == "MariaDB" 20 | } 21 | 22 | /// Return true if the system is not MariaDB 23 | pub fn is_mysql(&self) -> bool { 24 | !self.is_maria_db() 25 | } 26 | 27 | /// Return the version version as string. e.g. 8.0.1 28 | pub fn version_string(&self) -> String { 29 | format!( 30 | "{}.{}.{}", 31 | self.version / 10000, 32 | self.version / 100 % 100, 33 | self.version % 100 34 | ) 35 | } 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::*; 41 | 42 | #[test] 43 | fn test_0() { 44 | let system = SystemInfo { 45 | version: 50110, 46 | ..Default::default() 47 | }; 48 | assert_eq!(system.version_string(), "5.1.10".to_owned()); 49 | } 50 | 51 | #[test] 52 | fn test_1() { 53 | let system = SystemInfo { 54 | version: 80023, 55 | ..Default::default() 56 | }; 57 | assert_eq!(system.version_string(), "8.0.23".to_owned()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/mysql/def/table.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::{CharSet, Collation, StorageEngine}; 5 | 6 | #[derive(Clone, Debug, PartialEq)] 7 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 8 | pub struct TableInfo { 9 | /// The name of the table 10 | pub name: String, 11 | pub engine: StorageEngine, 12 | pub auto_increment: Option, 13 | pub char_set: CharSet, 14 | pub collation: Collation, 15 | pub comment: String, 16 | } 17 | -------------------------------------------------------------------------------- /src/mysql/discovery/executor/mock.rs: -------------------------------------------------------------------------------- 1 | use crate::sqlx_types::{MySqlPool, mysql::MySqlRow}; 2 | use sea_query::{MysqlQueryBuilder, SelectStatement}; 3 | 4 | use crate::{debug_print, sqlx_types::SqlxError}; 5 | 6 | #[allow(dead_code)] 7 | pub struct Executor { 8 | pool: MySqlPool, 9 | } 10 | 11 | pub trait IntoExecutor { 12 | fn into_executor(self) -> Executor; 13 | } 14 | 15 | impl IntoExecutor for MySqlPool { 16 | fn into_executor(self) -> Executor { 17 | Executor { pool: self } 18 | } 19 | } 20 | 21 | impl Executor { 22 | pub async fn fetch_all(&self, select: SelectStatement) -> Result, SqlxError> { 23 | let (_sql, _values) = select.build(MysqlQueryBuilder); 24 | debug_print!("{}, {:?}", _sql, _values); 25 | 26 | panic!("This is a mock Executor"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/mysql/discovery/executor/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "sqlx-mysql")] 2 | mod real; 3 | #[cfg(feature = "sqlx-mysql")] 4 | pub use real::*; 5 | 6 | #[cfg(not(feature = "sqlx-mysql"))] 7 | mod mock; 8 | #[cfg(not(feature = "sqlx-mysql"))] 9 | pub use mock::*; 10 | -------------------------------------------------------------------------------- /src/mysql/discovery/executor/real.rs: -------------------------------------------------------------------------------- 1 | use sea_query::{MysqlQueryBuilder, SelectStatement}; 2 | use sea_query_binder::SqlxBinder; 3 | use sqlx::{MySqlPool, Row, mysql::MySqlRow}; 4 | 5 | use crate::{debug_print, sqlx_types::SqlxError}; 6 | 7 | pub struct Executor { 8 | pool: MySqlPool, 9 | } 10 | 11 | pub trait IntoExecutor { 12 | fn into_executor(self) -> Executor; 13 | } 14 | 15 | impl IntoExecutor for MySqlPool { 16 | fn into_executor(self) -> Executor { 17 | Executor { pool: self } 18 | } 19 | } 20 | 21 | impl Executor { 22 | pub async fn fetch_all(&self, select: SelectStatement) -> Result, SqlxError> { 23 | let (sql, values) = select.build_sqlx(MysqlQueryBuilder); 24 | debug_print!("{}, {:?}", sql, values); 25 | 26 | sqlx::query_with(&sql, values) 27 | .fetch_all(&mut *self.pool.acquire().await?) 28 | .await 29 | } 30 | } 31 | 32 | pub trait GetMySqlValue { 33 | fn get_string(&self, idx: usize) -> String; 34 | 35 | fn get_string_opt(&self, idx: usize) -> Option; 36 | } 37 | 38 | impl GetMySqlValue for MySqlRow { 39 | fn get_string(&self, idx: usize) -> String { 40 | String::from_utf8(self.get::, _>(idx)).unwrap() 41 | } 42 | 43 | fn get_string_opt(&self, idx: usize) -> Option { 44 | self.get::>, _>(idx) 45 | .map(|v| String::from_utf8(v).unwrap()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/mysql/discovery/mod.rs: -------------------------------------------------------------------------------- 1 | //! To query & parse MySQL's INFORMATION_SCHEMA and construct a [`Schema`] 2 | 3 | use crate::debug_print; 4 | use crate::mysql::def::*; 5 | use crate::mysql::parser::{parse_foreign_key_query_results, parse_index_query_results}; 6 | use crate::mysql::query::{ 7 | ColumnQueryResult, ForeignKeyQueryResult, IndexQueryResult, SchemaQueryBuilder, 8 | TableQueryResult, VersionQueryResult, 9 | }; 10 | use crate::sqlx_types::SqlxError; 11 | use futures::future; 12 | use sea_query::{Alias, Iden, IntoIden, SeaRc}; 13 | 14 | mod executor; 15 | pub use executor::*; 16 | 17 | pub struct SchemaDiscovery { 18 | pub query: SchemaQueryBuilder, 19 | pub executor: Executor, 20 | pub schema: SeaRc, 21 | } 22 | 23 | impl SchemaDiscovery { 24 | pub fn new(executor: E, schema: &str) -> Self 25 | where 26 | E: IntoExecutor, 27 | { 28 | Self { 29 | query: SchemaQueryBuilder::default(), 30 | executor: executor.into_executor(), 31 | schema: Alias::new(schema).into_iden(), 32 | } 33 | } 34 | 35 | pub async fn discover(mut self) -> Result { 36 | self.query = SchemaQueryBuilder::new(self.discover_system().await?); 37 | let tables = self.discover_tables().await?; 38 | let tables = future::try_join_all( 39 | tables 40 | .into_iter() 41 | .map(|t| (&self, t)) 42 | .map(Self::discover_table_static), 43 | ) 44 | .await?; 45 | 46 | Ok(Schema { 47 | schema: self.schema.to_string(), 48 | system: self.query.system, 49 | tables, 50 | }) 51 | } 52 | 53 | pub async fn discover_system(&mut self) -> Result { 54 | let rows = self.executor.fetch_all(self.query.query_version()).await?; 55 | 56 | #[allow(clippy::never_loop)] 57 | for row in rows.iter() { 58 | let result: VersionQueryResult = row.into(); 59 | debug_print!("{:?}", result); 60 | let version = result.parse(); 61 | debug_print!("{:?}", version); 62 | return Ok(version); 63 | } 64 | Err(SqlxError::RowNotFound) 65 | } 66 | 67 | pub async fn discover_tables(&mut self) -> Result, SqlxError> { 68 | let rows = self 69 | .executor 70 | .fetch_all(self.query.query_tables(self.schema.clone())) 71 | .await?; 72 | 73 | let tables: Vec = rows 74 | .iter() 75 | .map(|row| { 76 | let result: TableQueryResult = row.into(); 77 | debug_print!("{:?}", result); 78 | let table = result.parse(); 79 | debug_print!("{:?}", table); 80 | table 81 | }) 82 | .collect(); 83 | 84 | Ok(tables) 85 | } 86 | 87 | async fn discover_table_static(params: (&Self, TableInfo)) -> Result { 88 | let this = params.0; 89 | let info = params.1; 90 | Self::discover_table(this, info).await 91 | } 92 | 93 | pub async fn discover_table(&self, info: TableInfo) -> Result { 94 | let table = SeaRc::new(Alias::new(info.name.as_str())); 95 | let columns = self 96 | .discover_columns(self.schema.clone(), table.clone(), &self.query.system) 97 | .await?; 98 | let indexes = self 99 | .discover_indexes(self.schema.clone(), table.clone()) 100 | .await?; 101 | let foreign_keys = self 102 | .discover_foreign_keys(self.schema.clone(), table.clone()) 103 | .await?; 104 | 105 | Ok(TableDef { 106 | info, 107 | columns, 108 | indexes, 109 | foreign_keys, 110 | }) 111 | } 112 | 113 | pub async fn discover_columns( 114 | &self, 115 | schema: SeaRc, 116 | table: SeaRc, 117 | system: &SystemInfo, 118 | ) -> Result, SqlxError> { 119 | let rows = self 120 | .executor 121 | .fetch_all(self.query.query_columns(schema.clone(), table.clone())) 122 | .await?; 123 | 124 | let columns = rows 125 | .iter() 126 | .map(|row| { 127 | let result: ColumnQueryResult = row.into(); 128 | debug_print!("{:?}", result); 129 | let column = result.parse(system); 130 | debug_print!("{:?}", column); 131 | column 132 | }) 133 | .collect::>(); 134 | 135 | Ok(columns) 136 | } 137 | 138 | pub async fn discover_indexes( 139 | &self, 140 | schema: SeaRc, 141 | table: SeaRc, 142 | ) -> Result, SqlxError> { 143 | let rows = self 144 | .executor 145 | .fetch_all(self.query.query_indexes(schema.clone(), table.clone())) 146 | .await?; 147 | 148 | let results = rows.into_iter().map(|row| { 149 | let result: IndexQueryResult = (&row).into(); 150 | debug_print!("{:?}", result); 151 | result 152 | }); 153 | 154 | Ok(parse_index_query_results(Box::new(results)) 155 | .map(|index| { 156 | debug_print!("{:?}", index); 157 | index 158 | }) 159 | .collect()) 160 | } 161 | 162 | pub async fn discover_foreign_keys( 163 | &self, 164 | schema: SeaRc, 165 | table: SeaRc, 166 | ) -> Result, SqlxError> { 167 | let rows = self 168 | .executor 169 | .fetch_all(self.query.query_foreign_key(schema.clone(), table.clone())) 170 | .await?; 171 | 172 | let results = rows.into_iter().map(|row| { 173 | let result: ForeignKeyQueryResult = (&row).into(); 174 | debug_print!("{:?}", result); 175 | result 176 | }); 177 | 178 | Ok(parse_foreign_key_query_results(Box::new(results)) 179 | .map(|index| { 180 | debug_print!("{:?}", index); 181 | index 182 | }) 183 | .collect()) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/mysql/mod.rs: -------------------------------------------------------------------------------- 1 | pub struct MySql; 2 | 3 | #[cfg(feature = "def")] 4 | #[cfg_attr(docsrs, doc(cfg(feature = "def")))] 5 | pub mod def; 6 | 7 | #[cfg(feature = "discovery")] 8 | #[cfg_attr(docsrs, doc(cfg(feature = "discovery")))] 9 | pub mod discovery; 10 | 11 | #[cfg(feature = "parser")] 12 | #[cfg_attr(docsrs, doc(cfg(feature = "parser")))] 13 | pub mod parser; 14 | 15 | #[cfg(feature = "query")] 16 | #[cfg_attr(docsrs, doc(cfg(feature = "query")))] 17 | pub mod query; 18 | 19 | #[cfg(feature = "writer")] 20 | #[cfg_attr(docsrs, doc(cfg(feature = "writer")))] 21 | pub mod writer; 22 | 23 | #[cfg(feature = "probe")] 24 | #[cfg_attr(docsrs, doc(cfg(feature = "probe")))] 25 | pub mod probe; 26 | -------------------------------------------------------------------------------- /src/mysql/parser/foreign_key.rs: -------------------------------------------------------------------------------- 1 | use crate::Name; 2 | use crate::mysql::def::*; 3 | use crate::mysql::query::ForeignKeyQueryResult; 4 | 5 | pub struct ForeignKeyQueryResultParser { 6 | curr: Option, 7 | results: Box>, 8 | } 9 | 10 | /// ForeignKeyQueryResult must be sorted by (TableName, ConstraintName, OrdinalPosition) 11 | pub fn parse_foreign_key_query_results( 12 | results: Box>, 13 | ) -> impl Iterator { 14 | ForeignKeyQueryResultParser { 15 | curr: None, 16 | results, 17 | } 18 | } 19 | 20 | impl Iterator for ForeignKeyQueryResultParser { 21 | type Item = ForeignKeyInfo; 22 | 23 | fn next(&mut self) -> Option { 24 | for result in self.results.by_ref() { 25 | let mut foreign_key = parse_foreign_key_query_result(result); 26 | if let Some(curr) = &mut self.curr { 27 | // group by `foreign_key.name` 28 | if curr.name == foreign_key.name { 29 | curr.columns.push(foreign_key.columns.pop().unwrap()); 30 | curr.referenced_columns 31 | .push(foreign_key.referenced_columns.pop().unwrap()); 32 | } else { 33 | let prev = self.curr.take(); 34 | self.curr = Some(foreign_key); 35 | return prev; 36 | } 37 | } else { 38 | self.curr = Some(foreign_key); 39 | } 40 | } 41 | self.curr.take() 42 | } 43 | } 44 | 45 | pub fn parse_foreign_key_query_result(result: ForeignKeyQueryResult) -> ForeignKeyInfo { 46 | ForeignKeyInfo { 47 | name: result.constraint_name, 48 | columns: vec![result.column_name], 49 | referenced_table: result.referenced_table_name, 50 | referenced_columns: vec![result.referenced_column_name], 51 | on_update: parse_foreign_key_action(result.update_rule.as_str()), 52 | on_delete: parse_foreign_key_action(result.delete_rule.as_str()), 53 | } 54 | } 55 | 56 | pub fn parse_foreign_key_action(string: &str) -> ForeignKeyAction { 57 | ForeignKeyAction::from_str(string).unwrap() 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::*; 63 | 64 | #[test] 65 | fn test_1() { 66 | assert_eq!( 67 | parse_foreign_key_query_results(Box::new( 68 | vec![ 69 | ForeignKeyQueryResult { 70 | constraint_name: "fk-cat-dog".to_owned(), 71 | column_name: "d1".to_owned(), 72 | referenced_table_name: "cat".to_owned(), 73 | referenced_column_name: "c1".to_owned(), 74 | update_rule: "CASCADE".to_owned(), 75 | delete_rule: "NO ACTION".to_owned(), 76 | }, 77 | ForeignKeyQueryResult { 78 | constraint_name: "fk-cat-dog".to_owned(), 79 | column_name: "d2".to_owned(), 80 | referenced_table_name: "cat".to_owned(), 81 | referenced_column_name: "c2".to_owned(), 82 | update_rule: "CASCADE".to_owned(), 83 | delete_rule: "NO ACTION".to_owned(), 84 | }, 85 | ] 86 | .into_iter() 87 | )) 88 | .collect::>(), 89 | vec![ForeignKeyInfo { 90 | name: "fk-cat-dog".to_owned(), 91 | columns: vec!["d1".to_owned(), "d2".to_owned()], 92 | referenced_table: "cat".to_owned(), 93 | referenced_columns: vec!["c1".to_owned(), "c2".to_owned()], 94 | on_update: ForeignKeyAction::Cascade, 95 | on_delete: ForeignKeyAction::NoAction, 96 | }] 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/mysql/parser/index.rs: -------------------------------------------------------------------------------- 1 | use crate::Name; 2 | use crate::mysql::def::*; 3 | use crate::mysql::query::IndexQueryResult; 4 | 5 | pub struct IndexQueryResultParser { 6 | curr: Option, 7 | results: Box>, 8 | } 9 | 10 | /// IndexQueryResult must be sorted by (TableName, IndexName, SeqInIndex) 11 | pub fn parse_index_query_results( 12 | results: Box>, 13 | ) -> impl Iterator { 14 | IndexQueryResultParser { 15 | curr: None, 16 | results, 17 | } 18 | } 19 | 20 | impl Iterator for IndexQueryResultParser { 21 | type Item = IndexInfo; 22 | 23 | fn next(&mut self) -> Option { 24 | for result in self.results.by_ref() { 25 | let mut index = parse_index_query_result(result); 26 | if let Some(curr) = &mut self.curr { 27 | // group by `index.name`, consolidate to `index.parts` 28 | if curr.name == index.name { 29 | curr.parts.push(index.parts.pop().unwrap()); 30 | curr.functional |= index.functional; 31 | } else { 32 | let prev = self.curr.take(); 33 | self.curr = Some(index); 34 | return prev; 35 | } 36 | } else { 37 | self.curr = Some(index); 38 | } 39 | } 40 | self.curr.take() 41 | } 42 | } 43 | 44 | pub fn parse_index_query_result(mut result: IndexQueryResult) -> IndexInfo { 45 | IndexInfo { 46 | unique: match result.non_unique { 47 | 0 => true, 48 | 1 => false, 49 | _ => unimplemented!(), 50 | }, 51 | name: result.index_name, 52 | parts: vec![IndexPart { 53 | column: if result.column_name.is_some() { 54 | result.column_name.take().unwrap() 55 | } else if result.expression.is_some() { 56 | result.expression.take().unwrap() 57 | } else { 58 | panic!("index column error") 59 | }, 60 | order: match result.collation { 61 | Some(collation) => match collation.as_str() { 62 | "A" => IndexOrder::Ascending, 63 | "D" => IndexOrder::Descending, 64 | _ => unimplemented!(), 65 | }, 66 | None => IndexOrder::Unordered, 67 | }, 68 | sub_part: result.sub_part.map(|v| v as u32), 69 | }], 70 | nullable: matches!(result.nullable.as_str(), "YES"), 71 | idx_type: IndexType::from_str(result.index_type.as_str()).unwrap(), 72 | comment: result.index_comment, 73 | functional: result.expression.is_some(), 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use super::*; 80 | 81 | #[test] 82 | fn test_1() { 83 | assert_eq!( 84 | parse_index_query_results(Box::new( 85 | vec![IndexQueryResult { 86 | non_unique: 0, 87 | index_name: "PRIMARY".to_owned(), 88 | column_name: Some("film_id".to_owned()), 89 | collation: Some("A".to_owned()), 90 | sub_part: None, 91 | nullable: "".to_owned(), 92 | index_type: "BTREE".to_owned(), 93 | index_comment: "".to_owned(), 94 | expression: None 95 | }] 96 | .into_iter() 97 | )) 98 | .collect::>(), 99 | vec![IndexInfo { 100 | unique: true, 101 | name: "PRIMARY".to_owned(), 102 | parts: vec![IndexPart { 103 | column: "film_id".to_owned(), 104 | order: IndexOrder::Ascending, 105 | sub_part: None, 106 | },], 107 | nullable: false, 108 | idx_type: IndexType::BTree, 109 | comment: "".to_owned(), 110 | functional: false, 111 | }] 112 | ); 113 | } 114 | 115 | #[test] 116 | fn test_2() { 117 | assert_eq!( 118 | parse_index_query_results(Box::new( 119 | vec![IndexQueryResult { 120 | non_unique: 1, 121 | index_name: "idx_title".to_owned(), 122 | column_name: Some("title".to_owned()), 123 | collation: Some("A".to_owned()), 124 | sub_part: None, 125 | nullable: "".to_owned(), 126 | index_type: "BTREE".to_owned(), 127 | index_comment: "".to_owned(), 128 | expression: None 129 | }] 130 | .into_iter() 131 | )) 132 | .collect::>(), 133 | vec![IndexInfo { 134 | unique: false, 135 | name: "idx_title".to_owned(), 136 | parts: vec![IndexPart { 137 | column: "title".to_owned(), 138 | order: IndexOrder::Ascending, 139 | sub_part: None, 140 | },], 141 | nullable: false, 142 | idx_type: IndexType::BTree, 143 | comment: "".to_owned(), 144 | functional: false, 145 | }] 146 | ); 147 | } 148 | 149 | #[test] 150 | fn test_3() { 151 | assert_eq!( 152 | parse_index_query_results(Box::new( 153 | vec![ 154 | IndexQueryResult { 155 | non_unique: 0, 156 | index_name: "rental_date".to_owned(), 157 | column_name: Some("rental_date".to_owned()), 158 | collation: Some("A".to_owned()), 159 | sub_part: None, 160 | nullable: "".to_owned(), 161 | index_type: "BTREE".to_owned(), 162 | index_comment: "".to_owned(), 163 | expression: None 164 | }, 165 | IndexQueryResult { 166 | non_unique: 0, 167 | index_name: "rental_date".to_owned(), 168 | column_name: Some("inventory_id".to_owned()), 169 | collation: Some("D".to_owned()), 170 | sub_part: None, 171 | nullable: "".to_owned(), 172 | index_type: "BTREE".to_owned(), 173 | index_comment: "".to_owned(), 174 | expression: None 175 | }, 176 | IndexQueryResult { 177 | non_unique: 0, 178 | index_name: "rental_date".to_owned(), 179 | column_name: Some("customer_id".to_owned()), 180 | collation: Some("A".to_owned()), 181 | sub_part: None, 182 | nullable: "".to_owned(), 183 | index_type: "BTREE".to_owned(), 184 | index_comment: "".to_owned(), 185 | expression: None 186 | }, 187 | ] 188 | .into_iter() 189 | )) 190 | .collect::>(), 191 | vec![IndexInfo { 192 | unique: true, 193 | name: "rental_date".to_owned(), 194 | parts: vec![ 195 | IndexPart { 196 | column: "rental_date".to_owned(), 197 | order: IndexOrder::Ascending, 198 | sub_part: None, 199 | }, 200 | IndexPart { 201 | column: "inventory_id".to_owned(), 202 | order: IndexOrder::Descending, 203 | sub_part: None, 204 | }, 205 | IndexPart { 206 | column: "customer_id".to_owned(), 207 | order: IndexOrder::Ascending, 208 | sub_part: None, 209 | }, 210 | ], 211 | nullable: false, 212 | idx_type: IndexType::BTree, 213 | comment: "".to_owned(), 214 | functional: false 215 | }] 216 | ); 217 | } 218 | 219 | #[test] 220 | fn test_4() { 221 | assert_eq!( 222 | parse_index_query_results(Box::new( 223 | vec![IndexQueryResult { 224 | non_unique: 1, 225 | index_name: "idx_location".to_owned(), 226 | column_name: Some("location".to_owned()), 227 | collation: Some("A".to_owned()), 228 | sub_part: Some(32), 229 | nullable: "".to_owned(), 230 | index_type: "SPATIAL".to_owned(), 231 | index_comment: "".to_owned(), 232 | expression: None 233 | }] 234 | .into_iter() 235 | )) 236 | .collect::>(), 237 | vec![IndexInfo { 238 | unique: false, 239 | name: "idx_location".to_owned(), 240 | parts: vec![IndexPart { 241 | column: "location".to_owned(), 242 | order: IndexOrder::Ascending, 243 | sub_part: Some(32), 244 | },], 245 | nullable: false, 246 | idx_type: IndexType::Spatial, 247 | comment: "".to_owned(), 248 | functional: false, 249 | }] 250 | ); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/mysql/parser/mod.rs: -------------------------------------------------------------------------------- 1 | //! To parse MySQL's INFORMATION_SCHEMA 2 | 3 | mod column; 4 | mod foreign_key; 5 | mod index; 6 | mod system; 7 | mod table; 8 | 9 | pub use column::*; 10 | pub use foreign_key::*; 11 | pub use index::*; 12 | pub use system::*; 13 | pub use table::*; 14 | -------------------------------------------------------------------------------- /src/mysql/parser/system.rs: -------------------------------------------------------------------------------- 1 | use crate::mysql::def::*; 2 | use crate::mysql::query::VersionQueryResult; 3 | 4 | impl VersionQueryResult { 5 | pub fn parse(self) -> SystemInfo { 6 | parse_version_query_result(self) 7 | } 8 | } 9 | 10 | pub fn parse_version_query_result(result: VersionQueryResult) -> SystemInfo { 11 | parse_version_string(result.version.as_str()) 12 | } 13 | 14 | pub fn parse_version_string(string: &str) -> SystemInfo { 15 | let mut system = SystemInfo::default(); 16 | for (i, part) in string.split('-').enumerate() { 17 | if i == 0 { 18 | system.version = parse_version_number(part); 19 | } else if i == 1 { 20 | system.system = part.to_string(); 21 | } else { 22 | system.suffix.push(part.to_owned()); 23 | } 24 | } 25 | system 26 | } 27 | 28 | pub fn parse_version_number(string: &str) -> u32 { 29 | let mut number: u32 = 0; 30 | let numbers: Vec<&str> = string.split('.').collect(); 31 | #[allow(clippy::len_zero)] 32 | if numbers.len() > 0 { 33 | number += numbers[0].parse::().unwrap() * 10000 34 | } 35 | if numbers.len() > 1 { 36 | number += numbers[1].parse::().unwrap() * 100 37 | } 38 | if numbers.len() > 2 { 39 | number += numbers[2].parse::().unwrap() 40 | } 41 | number 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use super::*; 47 | 48 | #[test] 49 | fn test_0() { 50 | assert_eq!(parse_version_number("5.1.10"), 50110); 51 | } 52 | 53 | #[test] 54 | fn test_1() { 55 | assert_eq!(parse_version_number("8.0.23"), 80023); 56 | } 57 | 58 | #[test] 59 | fn test_2() { 60 | assert_eq!( 61 | parse_version_string("8.0.23-0ubuntu0.20.04.1"), 62 | SystemInfo { 63 | version: 80023, 64 | system: "0ubuntu0.20.04.1".to_owned(), 65 | suffix: vec![], 66 | } 67 | ) 68 | } 69 | 70 | #[test] 71 | fn test_3() { 72 | assert_eq!( 73 | parse_version_string("10.2.31-MariaDB"), 74 | SystemInfo { 75 | version: 100231, 76 | system: "MariaDB".to_owned(), 77 | suffix: vec![], 78 | } 79 | ) 80 | } 81 | 82 | #[test] 83 | fn test_4() { 84 | assert_eq!( 85 | parse_version_string("10.2.31-MariaDB-debug"), 86 | SystemInfo { 87 | version: 100231, 88 | system: "MariaDB".to_owned(), 89 | suffix: vec!["debug".to_owned()], 90 | } 91 | ) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/mysql/parser/table.rs: -------------------------------------------------------------------------------- 1 | use crate::Name; 2 | use crate::mysql::def::*; 3 | use crate::mysql::query::TableQueryResult; 4 | 5 | impl TableQueryResult { 6 | pub fn parse(self) -> TableInfo { 7 | parse_table_query_result(self) 8 | } 9 | } 10 | 11 | pub fn parse_table_query_result(result: TableQueryResult) -> TableInfo { 12 | TableInfo { 13 | name: result.table_name, 14 | engine: StorageEngine::from_str(result.engine.as_str()).unwrap(), 15 | auto_increment: result.auto_increment, 16 | char_set: CharSet::from_str(result.table_char_set.as_str()).unwrap(), 17 | collation: Collation::from_str(result.table_collation.as_str()).unwrap(), 18 | comment: result.table_comment, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/mysql/probe.rs: -------------------------------------------------------------------------------- 1 | use sea_query::{Condition, Expr, Query, SelectStatement, SimpleExpr}; 2 | 3 | use super::MySql; 4 | use super::query::{InformationSchema as Schema, StatisticsFields, TablesFields}; 5 | use crate::probe::{Has, SchemaProbe}; 6 | 7 | impl SchemaProbe for MySql { 8 | fn get_current_schema() -> SimpleExpr { 9 | Expr::cust("DATABASE()") 10 | } 11 | 12 | fn query_tables(&self) -> SelectStatement { 13 | Query::select() 14 | .expr_as(Expr::col(TablesFields::TableName), TablesFields::TableName) 15 | .from((Schema::Schema, Schema::Tables)) 16 | .cond_where( 17 | Condition::all().add( 18 | Expr::expr(Self::get_current_schema()) 19 | .equals((Schema::Tables, TablesFields::TableSchema)), 20 | ), 21 | ) 22 | .take() 23 | } 24 | 25 | fn has_index(&self, table: T, index: C) -> SelectStatement 26 | where 27 | T: AsRef, 28 | C: AsRef, 29 | { 30 | Query::select() 31 | .expr_as(Expr::cust("COUNT(*) > 0"), Has::Index) 32 | .from((Schema::Schema, Schema::Statistics)) 33 | .cond_where( 34 | Condition::all() 35 | .add(Expr::col(StatisticsFields::TableSchema).eq(Self::get_current_schema())) 36 | .add(Expr::col(StatisticsFields::TableName).eq(table.as_ref())) 37 | .add(Expr::col(StatisticsFields::IndexName).eq(index.as_ref())), 38 | ) 39 | .take() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/mysql/query/char_set.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, sea_query::Iden)] 2 | /// Ref: https://dev.mysql.com/doc/refman/8.0/en/information-schema-collation-character-set-applicability-table.html 3 | pub enum CharacterSetFields { 4 | CharacterSetName, 5 | CollationName, 6 | } 7 | -------------------------------------------------------------------------------- /src/mysql/query/column.rs: -------------------------------------------------------------------------------- 1 | use super::{InformationSchema, SchemaQueryBuilder}; 2 | use crate::sqlx_types::mysql::MySqlRow; 3 | use sea_query::{Expr, Iden, Order, Query, SeaRc, SelectStatement, Value}; 4 | 5 | #[derive(Debug, sea_query::Iden)] 6 | /// Ref: https://dev.mysql.com/doc/refman/8.0/en/information-schema-columns-table.html 7 | pub enum ColumnFields { 8 | TableCatalog, 9 | TableSchema, 10 | TableName, 11 | ColumnName, 12 | OrdinalPosition, 13 | ColumnDefault, 14 | IsNullable, 15 | DataType, 16 | CharacterMaximumLength, 17 | CharacterOctetLength, 18 | NumericPrecision, 19 | NumericScale, 20 | DatetimePrecision, 21 | CharacterSetName, 22 | CollationName, 23 | ColumnType, 24 | ColumnKey, 25 | Extra, 26 | Privileges, 27 | ColumnComment, 28 | GenerationExpression, 29 | SrsId, 30 | } 31 | 32 | #[derive(Debug, Default)] 33 | pub struct ColumnQueryResult { 34 | pub column_name: String, 35 | pub column_type: String, 36 | pub is_nullable: String, 37 | pub column_key: String, 38 | pub column_default: Option, 39 | pub extra: String, 40 | pub generation_expression: Option, 41 | pub column_comment: String, 42 | } 43 | 44 | impl SchemaQueryBuilder { 45 | pub fn query_columns( 46 | &self, 47 | schema: SeaRc, 48 | table: SeaRc, 49 | ) -> SelectStatement { 50 | Query::select() 51 | .columns([ 52 | ColumnFields::ColumnName, 53 | ColumnFields::ColumnType, 54 | ColumnFields::IsNullable, 55 | ColumnFields::ColumnKey, 56 | ColumnFields::ColumnDefault, 57 | ColumnFields::Extra, 58 | ]) 59 | .conditions( 60 | self.system.is_mysql() && self.system.version >= 50700, 61 | |q| { 62 | q.column(ColumnFields::GenerationExpression); 63 | }, 64 | |q| { 65 | q.expr(Expr::val(Value::String(None))); 66 | }, 67 | ) 68 | .column(ColumnFields::ColumnComment) 69 | .from((InformationSchema::Schema, InformationSchema::Columns)) 70 | .and_where(Expr::col(ColumnFields::TableSchema).eq(schema.to_string())) 71 | .and_where(Expr::col(ColumnFields::TableName).eq(table.to_string())) 72 | .order_by(ColumnFields::OrdinalPosition, Order::Asc) 73 | .take() 74 | } 75 | } 76 | 77 | #[cfg(feature = "sqlx-mysql")] 78 | impl From<&MySqlRow> for ColumnQueryResult { 79 | fn from(row: &MySqlRow) -> Self { 80 | use crate::mysql::discovery::GetMySqlValue; 81 | Self { 82 | column_name: row.get_string(0), 83 | column_type: row.get_string(1), 84 | is_nullable: row.get_string(2), 85 | column_key: row.get_string(3), 86 | column_default: row.get_string_opt(4), 87 | extra: row.get_string(5), 88 | generation_expression: row.get_string_opt(6), 89 | column_comment: row.get_string(7), 90 | } 91 | } 92 | } 93 | 94 | #[cfg(not(feature = "sqlx-mysql"))] 95 | impl From<&MySqlRow> for ColumnQueryResult { 96 | fn from(_: &MySqlRow) -> Self { 97 | Self::default() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/mysql/query/foreign_key.rs: -------------------------------------------------------------------------------- 1 | use super::{InformationSchema, SchemaQueryBuilder}; 2 | use crate::sqlx_types::mysql::MySqlRow; 3 | use sea_query::{Expr, Iden, Order, Query, SeaRc, SelectStatement}; 4 | 5 | #[derive(Debug, sea_query::Iden)] 6 | /// Ref: https://dev.mysql.com/doc/refman/8.0/en/information-schema-key-column-usage-table.html 7 | pub enum KeyColumnUsageFields { 8 | ConstraintSchema, 9 | ConstraintName, 10 | TableSchema, 11 | TableName, 12 | ColumnName, 13 | OrdinalPosition, 14 | PositionInUniqueConstraint, 15 | ReferencedTableSchema, 16 | ReferencedTableName, 17 | ReferencedColumnName, 18 | } 19 | 20 | #[derive(Debug, sea_query::Iden)] 21 | /// Ref: https://dev.mysql.com/doc/refman/8.0/en/information-schema-referential-constraints-table.html 22 | pub enum ReferentialConstraintsFields { 23 | ConstraintSchema, 24 | ConstraintName, 25 | UniqueConstraintSchema, 26 | UniqueConstraintName, 27 | UpdateRule, 28 | DeleteRule, 29 | TableName, 30 | ReferencedTableName, 31 | } 32 | 33 | #[derive(Debug, Default)] 34 | pub struct ForeignKeyQueryResult { 35 | pub constraint_name: String, 36 | pub column_name: String, 37 | pub referenced_table_name: String, 38 | pub referenced_column_name: String, 39 | pub update_rule: String, 40 | pub delete_rule: String, 41 | } 42 | 43 | impl SchemaQueryBuilder { 44 | pub fn query_foreign_key( 45 | &self, 46 | schema: SeaRc, 47 | table: SeaRc, 48 | ) -> SelectStatement { 49 | type Schema = InformationSchema; 50 | type Key = KeyColumnUsageFields; 51 | type Ref = ReferentialConstraintsFields; 52 | Query::select() 53 | .columns(vec![ 54 | (Schema::KeyColumnUsage, Key::ConstraintName), 55 | (Schema::KeyColumnUsage, Key::ColumnName), 56 | (Schema::KeyColumnUsage, Key::ReferencedTableName), 57 | (Schema::KeyColumnUsage, Key::ReferencedColumnName), 58 | ]) 59 | .columns(vec![ 60 | (Schema::ReferentialConstraints, Ref::UpdateRule), 61 | (Schema::ReferentialConstraints, Ref::DeleteRule), 62 | ]) 63 | .from((Schema::Schema, Schema::KeyColumnUsage)) 64 | .inner_join( 65 | (Schema::Schema, Schema::ReferentialConstraints), 66 | Expr::col((Schema::KeyColumnUsage, Key::ConstraintSchema)) 67 | .equals((Schema::ReferentialConstraints, Ref::ConstraintSchema)) 68 | .and( 69 | Expr::col((Schema::KeyColumnUsage, Key::ConstraintName)) 70 | .equals((Schema::ReferentialConstraints, Ref::ConstraintName)), 71 | ), 72 | ) 73 | .and_where( 74 | Expr::col((Schema::KeyColumnUsage, Key::ConstraintSchema)).eq(schema.to_string()), 75 | ) 76 | .and_where(Expr::col((Schema::KeyColumnUsage, Key::TableName)).eq(table.to_string())) 77 | .and_where(Expr::col((Schema::KeyColumnUsage, Key::ReferencedTableName)).is_not_null()) 78 | .and_where(Expr::col((Schema::KeyColumnUsage, Key::ReferencedColumnName)).is_not_null()) 79 | .order_by(Key::ConstraintName, Order::Asc) 80 | .order_by(Key::OrdinalPosition, Order::Asc) 81 | .take() 82 | } 83 | } 84 | 85 | #[cfg(feature = "sqlx-mysql")] 86 | impl From<&MySqlRow> for ForeignKeyQueryResult { 87 | fn from(row: &MySqlRow) -> Self { 88 | use crate::mysql::discovery::GetMySqlValue; 89 | Self { 90 | constraint_name: row.get_string(0), 91 | column_name: row.get_string(1), 92 | referenced_table_name: row.get_string(2), 93 | referenced_column_name: row.get_string(3), 94 | update_rule: row.get_string(4), 95 | delete_rule: row.get_string(5), 96 | } 97 | } 98 | } 99 | 100 | #[cfg(not(feature = "sqlx-mysql"))] 101 | impl From<&MySqlRow> for ForeignKeyQueryResult { 102 | fn from(_: &MySqlRow) -> Self { 103 | Self::default() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/mysql/query/index.rs: -------------------------------------------------------------------------------- 1 | use super::{InformationSchema, SchemaQueryBuilder}; 2 | use crate::sqlx_types::mysql::MySqlRow; 3 | use sea_query::{Expr, Iden, Order, Query, SeaRc, SelectStatement, Value}; 4 | 5 | #[derive(Debug, sea_query::Iden)] 6 | /// Ref: https://dev.mysql.com/doc/refman/8.0/en/information-schema-statistics-table.html 7 | pub enum StatisticsFields { 8 | TableCatalog, 9 | TableSchema, 10 | TableName, 11 | NonUnique, 12 | IndexSchema, 13 | IndexName, 14 | SeqInIndex, 15 | ColumnName, 16 | Collation, 17 | Cardinality, 18 | SubPart, 19 | Packed, 20 | Nullable, 21 | IndexType, 22 | Comment, 23 | IndexComment, 24 | IsVisible, 25 | Expression, 26 | } 27 | 28 | #[derive(Debug, Default)] 29 | pub struct IndexQueryResult { 30 | pub non_unique: i32, 31 | pub index_name: String, 32 | pub column_name: Option, 33 | pub collation: Option, 34 | pub sub_part: Option, 35 | pub nullable: String, 36 | pub index_type: String, 37 | pub index_comment: String, 38 | pub expression: Option, 39 | } 40 | 41 | impl SchemaQueryBuilder { 42 | pub fn query_indexes( 43 | &self, 44 | schema: SeaRc, 45 | table: SeaRc, 46 | ) -> SelectStatement { 47 | Query::select() 48 | .columns(vec![ 49 | StatisticsFields::NonUnique, 50 | StatisticsFields::IndexName, 51 | StatisticsFields::ColumnName, 52 | StatisticsFields::Collation, 53 | StatisticsFields::SubPart, 54 | StatisticsFields::Nullable, 55 | StatisticsFields::IndexType, 56 | StatisticsFields::IndexComment, 57 | ]) 58 | .conditions( 59 | self.system.is_mysql() && self.system.version >= 80013, 60 | |q| { 61 | q.column(StatisticsFields::Expression); 62 | }, 63 | |q| { 64 | q.expr(Expr::val(Value::String(None))); 65 | }, 66 | ) 67 | .from((InformationSchema::Schema, InformationSchema::Statistics)) 68 | .and_where(Expr::col(StatisticsFields::TableSchema).eq(schema.to_string())) 69 | .and_where(Expr::col(StatisticsFields::TableName).eq(table.to_string())) 70 | .order_by(StatisticsFields::IndexName, Order::Asc) 71 | .order_by(StatisticsFields::SeqInIndex, Order::Asc) 72 | .take() 73 | } 74 | } 75 | 76 | #[cfg(feature = "sqlx-mysql")] 77 | impl From<&MySqlRow> for IndexQueryResult { 78 | fn from(row: &MySqlRow) -> Self { 79 | use crate::mysql::discovery::GetMySqlValue; 80 | use crate::sqlx_types::Row; 81 | Self { 82 | non_unique: row.get(0), 83 | index_name: row.get_string(1), 84 | column_name: row.get_string_opt(2), 85 | collation: row.get_string_opt(3), 86 | sub_part: row.get(4), 87 | nullable: row.get_string(5), 88 | index_type: row.get_string(6), 89 | index_comment: row.get_string(7), 90 | expression: row.get_string_opt(8), 91 | } 92 | } 93 | } 94 | 95 | #[cfg(not(feature = "sqlx-mysql"))] 96 | impl From<&MySqlRow> for IndexQueryResult { 97 | fn from(_: &MySqlRow) -> Self { 98 | Self::default() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/mysql/query/mod.rs: -------------------------------------------------------------------------------- 1 | //! To query MySQL's INFORMATION_SCHEMA 2 | 3 | mod char_set; 4 | mod column; 5 | mod foreign_key; 6 | mod index; 7 | mod schema; 8 | mod table; 9 | mod version; 10 | 11 | pub use char_set::*; 12 | pub use column::*; 13 | pub use foreign_key::*; 14 | pub use index::*; 15 | pub use schema::*; 16 | pub use table::*; 17 | pub use version::*; 18 | -------------------------------------------------------------------------------- /src/mysql/query/schema.rs: -------------------------------------------------------------------------------- 1 | use crate::mysql::def::SystemInfo; 2 | 3 | #[derive(Debug, Default)] 4 | pub struct SchemaQueryBuilder { 5 | pub system: SystemInfo, 6 | } 7 | 8 | impl SchemaQueryBuilder { 9 | pub fn new(system: SystemInfo) -> Self { 10 | Self { system } 11 | } 12 | } 13 | 14 | #[derive(Debug, sea_query::Iden)] 15 | /// Ref: https://dev.mysql.com/doc/refman/8.0/en/information-schema.html 16 | pub enum InformationSchema { 17 | #[iden = "information_schema"] 18 | Schema, 19 | Tables, 20 | Columns, 21 | Statistics, 22 | KeyColumnUsage, 23 | ReferentialConstraints, 24 | #[iden = "collation_character_set_applicability"] 25 | CollationCharacterSet, 26 | } 27 | -------------------------------------------------------------------------------- /src/mysql/query/table.rs: -------------------------------------------------------------------------------- 1 | use super::{CharacterSetFields, InformationSchema, SchemaQueryBuilder}; 2 | use crate::sqlx_types::mysql::MySqlRow; 3 | use sea_query::{Expr, Iden, Order, Query, SeaRc, SelectStatement}; 4 | 5 | #[derive(Debug, sea_query::Iden)] 6 | /// Ref: https://dev.mysql.com/doc/refman/8.0/en/information-schema-tables-table.html 7 | pub enum TablesFields { 8 | TableCatalog, 9 | TableSchema, 10 | TableName, 11 | TableType, 12 | Engine, 13 | Version, 14 | RowFormat, 15 | TableRows, 16 | AvgRowLength, 17 | DataLength, 18 | MaxDataLength, 19 | IndexLength, 20 | DataFree, 21 | AutoIncrement, 22 | CreateTime, 23 | UpdateTime, 24 | CheckTime, 25 | TableCollation, 26 | Checksum, 27 | CreateOptions, 28 | TableComment, 29 | } 30 | 31 | #[derive(Debug, sea_query::Iden)] 32 | pub enum TableType { 33 | #[iden = "BASE TABLE"] 34 | BaseTable, 35 | #[iden = "VIEW"] 36 | View, 37 | #[iden = "SYSTEM VIEW"] 38 | SystemView, 39 | #[iden = "SYSTEM VERSIONED"] 40 | SystemVersioned, 41 | } 42 | 43 | #[derive(Debug, Default)] 44 | pub struct TableQueryResult { 45 | pub table_name: String, 46 | pub engine: String, 47 | pub auto_increment: Option, 48 | pub table_char_set: String, 49 | pub table_collation: String, 50 | pub table_comment: String, 51 | pub create_options: String, 52 | } 53 | 54 | impl SchemaQueryBuilder { 55 | pub fn query_tables(&self, schema: SeaRc) -> SelectStatement { 56 | type Schema = InformationSchema; 57 | Query::select() 58 | .columns(vec![ 59 | TablesFields::TableName, 60 | TablesFields::Engine, 61 | TablesFields::AutoIncrement, 62 | TablesFields::TableCollation, 63 | TablesFields::TableComment, 64 | TablesFields::CreateOptions, 65 | ]) 66 | .column(( 67 | Schema::CollationCharacterSet, 68 | CharacterSetFields::CharacterSetName, 69 | )) 70 | .from((Schema::Schema, Schema::Tables)) 71 | .left_join( 72 | (Schema::Schema, Schema::CollationCharacterSet), 73 | Expr::col(( 74 | Schema::CollationCharacterSet, 75 | CharacterSetFields::CollationName, 76 | )) 77 | .equals((Schema::Tables, TablesFields::TableCollation)), 78 | ) 79 | .and_where(Expr::col(TablesFields::TableSchema).eq(schema.to_string())) 80 | .and_where(Expr::col(TablesFields::TableType).is_in([ 81 | TableType::BaseTable.to_string(), 82 | TableType::SystemVersioned.to_string(), 83 | ])) 84 | .order_by(TablesFields::TableName, Order::Asc) 85 | .take() 86 | } 87 | } 88 | 89 | #[cfg(feature = "sqlx-mysql")] 90 | impl From<&MySqlRow> for TableQueryResult { 91 | fn from(row: &MySqlRow) -> Self { 92 | use crate::mysql::discovery::GetMySqlValue; 93 | use crate::sqlx_types::Row; 94 | Self { 95 | table_name: row.get_string(0), 96 | engine: row.get_string(1), 97 | auto_increment: row.get(2), 98 | table_collation: row.get_string(3), 99 | table_comment: row.get_string(4), 100 | create_options: row.get_string(5), 101 | table_char_set: row.get_string(6), 102 | } 103 | } 104 | } 105 | 106 | #[cfg(not(feature = "sqlx-mysql"))] 107 | impl From<&MySqlRow> for TableQueryResult { 108 | fn from(_: &MySqlRow) -> Self { 109 | Self::default() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/mysql/query/version.rs: -------------------------------------------------------------------------------- 1 | use super::SchemaQueryBuilder; 2 | use crate::sqlx_types::mysql::MySqlRow; 3 | use sea_query::{Func, Query, SelectStatement}; 4 | 5 | #[derive(sea_query::Iden)] 6 | enum MysqlFunc { 7 | Version, 8 | } 9 | 10 | #[derive(Debug, Default)] 11 | pub struct VersionQueryResult { 12 | pub version: String, 13 | } 14 | 15 | impl SchemaQueryBuilder { 16 | pub fn query_version(&self) -> SelectStatement { 17 | Query::select().expr(Func::cust(MysqlFunc::Version)).take() 18 | } 19 | } 20 | 21 | #[cfg(feature = "sqlx-mysql")] 22 | impl From<&MySqlRow> for VersionQueryResult { 23 | fn from(row: &MySqlRow) -> Self { 24 | use crate::mysql::discovery::GetMySqlValue; 25 | Self { 26 | version: row.get_string(0), 27 | } 28 | } 29 | } 30 | 31 | #[cfg(not(feature = "sqlx-mysql"))] 32 | impl From<&MySqlRow> for VersionQueryResult { 33 | fn from(_: &MySqlRow) -> Self { 34 | Self::default() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/mysql/writer/foreign_key.rs: -------------------------------------------------------------------------------- 1 | use crate::mysql::def::{ForeignKeyAction, ForeignKeyInfo}; 2 | use sea_query::{Alias, ForeignKey, ForeignKeyCreateStatement}; 3 | 4 | impl ForeignKeyInfo { 5 | pub fn write(&self) -> ForeignKeyCreateStatement { 6 | let mut key = ForeignKey::create(); 7 | key.name(&self.name) 8 | .to_tbl(Alias::new(&self.referenced_table)); 9 | for column in self.columns.iter() { 10 | key.from_col(Alias::new(column.as_str())); 11 | } 12 | for ref_col in self.referenced_columns.iter() { 13 | key.to_col(Alias::new(ref_col.as_str())); 14 | } 15 | key.on_update(match self.on_update { 16 | ForeignKeyAction::Cascade => sea_query::ForeignKeyAction::Cascade, 17 | ForeignKeyAction::SetNull => sea_query::ForeignKeyAction::SetNull, 18 | ForeignKeyAction::SetDefault => sea_query::ForeignKeyAction::SetDefault, 19 | ForeignKeyAction::Restrict => sea_query::ForeignKeyAction::Restrict, 20 | ForeignKeyAction::NoAction => sea_query::ForeignKeyAction::NoAction, 21 | }); 22 | key.on_delete(match self.on_delete { 23 | ForeignKeyAction::Cascade => sea_query::ForeignKeyAction::Cascade, 24 | ForeignKeyAction::SetNull => sea_query::ForeignKeyAction::SetNull, 25 | ForeignKeyAction::SetDefault => sea_query::ForeignKeyAction::SetDefault, 26 | ForeignKeyAction::Restrict => sea_query::ForeignKeyAction::Restrict, 27 | ForeignKeyAction::NoAction => sea_query::ForeignKeyAction::NoAction, 28 | }); 29 | key.to_owned() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/mysql/writer/index.rs: -------------------------------------------------------------------------------- 1 | use crate::mysql::def::{IndexInfo, IndexOrder, IndexType}; 2 | use sea_query::{Alias, Iden, Index, IndexCreateStatement, SeaRc}; 3 | 4 | impl IndexInfo { 5 | #[allow(clippy::unnecessary_unwrap)] 6 | pub fn write(&self) -> IndexCreateStatement { 7 | let mut index = Index::create(); 8 | if self.name == "PRIMARY" { 9 | index.primary(); 10 | } else { 11 | index.name(&self.name); 12 | if self.unique { 13 | index.unique(); 14 | } 15 | } 16 | for part in self.parts.iter() { 17 | let pre = part.sub_part; 18 | let ord = if self.parts.len() == 1 { 19 | match part.order { 20 | IndexOrder::Ascending => None, 21 | IndexOrder::Descending => Some(sea_query::IndexOrder::Desc), 22 | IndexOrder::Unordered => None, 23 | } 24 | } else { 25 | None 26 | }; 27 | if pre.is_none() && ord.is_none() { 28 | index.col(Alias::new(&part.column)); 29 | } else if pre.is_none() && ord.is_some() { 30 | index.col((Alias::new(&part.column), ord.unwrap())); 31 | } else if pre.is_some() && ord.is_none() { 32 | index.col((Alias::new(&part.column), pre.unwrap())); 33 | } else { 34 | index.col((Alias::new(&part.column), pre.unwrap(), ord.unwrap())); 35 | } 36 | } 37 | match self.idx_type { 38 | IndexType::BTree => {} 39 | IndexType::FullText => { 40 | index.index_type(sea_query::IndexType::FullText); 41 | } 42 | IndexType::Hash => { 43 | index.index_type(sea_query::IndexType::Hash); 44 | } 45 | IndexType::RTree => { 46 | index.index_type(sea_query::IndexType::Custom(SeaRc::new(Alias::new( 47 | self.idx_type.to_string(), 48 | )))); 49 | } 50 | IndexType::Spatial => { 51 | index.index_type(sea_query::IndexType::Custom(SeaRc::new(Alias::new( 52 | self.idx_type.to_string(), 53 | )))); 54 | } 55 | #[cfg(feature = "planetscale")] 56 | IndexType::Vector => { 57 | index.index_type(sea_query::IndexType::Custom(SeaRc::new(Alias::new( 58 | self.idx_type.to_string(), 59 | )))); 60 | } 61 | } 62 | index 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/mysql/writer/mod.rs: -------------------------------------------------------------------------------- 1 | //! To write [`mysql::Schema`] to SQL statements 2 | 3 | mod column; 4 | mod foreign_key; 5 | mod index; 6 | mod table; 7 | mod types; 8 | 9 | use super::def::Schema; 10 | use sea_query::TableCreateStatement; 11 | 12 | impl Schema { 13 | pub fn write(&self) -> Vec { 14 | self.tables.iter().map(|table| table.write()).collect() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/name.rs: -------------------------------------------------------------------------------- 1 | pub trait Name { 2 | fn from_str(string: &str) -> Option 3 | where 4 | Self: Sized; 5 | } 6 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use sea_query::{Token, Tokenizer}; 4 | 5 | pub struct Parser { 6 | pub tokens: Tokenizer, 7 | pub curr: Option, 8 | pub last: Option, 9 | } 10 | 11 | impl Parser { 12 | pub fn new(string: &str) -> Self { 13 | Self { 14 | tokens: Tokenizer::new(string), 15 | curr: None, 16 | last: None, 17 | } 18 | } 19 | 20 | pub fn curr(&mut self) -> Option<&Token> { 21 | if self.curr.is_some() { 22 | self.curr.as_ref() 23 | } else { 24 | self.next() 25 | } 26 | } 27 | 28 | pub fn last(&mut self) -> Option<&Token> { 29 | self.last.as_ref() 30 | } 31 | 32 | #[allow(clippy::should_implement_trait)] 33 | pub fn next(&mut self) -> Option<&Token> { 34 | if self.curr.is_some() { 35 | self.last = std::mem::take(&mut self.curr); 36 | } 37 | 38 | if let Some(tok) = self.tokens.next() { 39 | if tok.is_space() { 40 | if let Some(tok) = self.tokens.next() { 41 | self.curr = Some(tok); 42 | } 43 | } else { 44 | self.curr = Some(tok); 45 | } 46 | } 47 | self.curr.as_ref() 48 | } 49 | 50 | pub fn next_if_unquoted(&mut self, word: &str) -> bool { 51 | if let Some(tok) = self.curr() { 52 | if tok.is_unquoted() && tok.as_str().to_lowercase() == word.to_lowercase() { 53 | self.next(); 54 | return true; 55 | } 56 | } 57 | false 58 | } 59 | 60 | pub fn next_if_quoted_any(&mut self) -> Option<&Token> { 61 | if let Some(tok) = self.curr() { 62 | if tok.is_quoted() { 63 | self.next(); 64 | return self.last(); 65 | } 66 | } 67 | None 68 | } 69 | 70 | pub fn next_if_unquoted_any(&mut self) -> Option<&Token> { 71 | if let Some(tok) = self.curr() { 72 | if tok.is_unquoted() { 73 | self.next(); 74 | return self.last(); 75 | } 76 | } 77 | None 78 | } 79 | 80 | pub fn next_if_punctuation(&mut self, word: &str) -> bool { 81 | if let Some(tok) = self.curr() { 82 | if tok.is_punctuation() && tok.as_str() == word { 83 | self.next(); 84 | return true; 85 | } 86 | } 87 | false 88 | } 89 | 90 | pub fn curr_is_unquoted(&mut self) -> bool { 91 | self.curr().is_some() && self.curr().unwrap().is_unquoted() 92 | } 93 | 94 | pub fn curr_as_str(&mut self) -> &str { 95 | self.curr().unwrap().as_str() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/postgres/def/column.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::{NotNull, Type}; 5 | 6 | #[derive(Clone, Debug, PartialEq)] 7 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 8 | pub struct ColumnInfo { 9 | /// The name of the column 10 | pub name: String, 11 | /// The type of the column with any additional definitions such as the precision or the character 12 | /// set 13 | pub col_type: ColumnType, 14 | /// The default value experssion for this column, if any 15 | pub default: Option, 16 | /// The generation expression for this column, if it is a generated colum 17 | pub generated: Option, 18 | pub not_null: Option, 19 | pub is_identity: bool, 20 | // TODO: 21 | // /// A constraint that ensures the value of a column is unique among all other rows in the table 22 | // pub unique: Option>, 23 | // /// A constraint that states that the column is the unique identifier or part of the unique 24 | // /// identifier of each row for this table 25 | // pub primary_key: Option, 26 | // /// A constraint that ensures that the value of this column must refer to a unique key in another 27 | // /// table 28 | // pub references: Option, 29 | 30 | // FIXME: Include if there's a convenient way to look for this 31 | // /// Comments on the column made by the user 32 | // pub comment: String, 33 | } 34 | 35 | pub type ColumnType = Type; 36 | 37 | #[derive(Clone, Debug, PartialEq)] 38 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 39 | pub struct ColumnExpression(pub String); 40 | 41 | impl ColumnExpression { 42 | pub fn from_option_string(maybe_string: Option) -> Option { 43 | maybe_string.map(ColumnExpression) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/postgres/def/constraints.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate as sea_schema; 5 | 6 | #[derive(Clone, Debug, PartialEq)] 7 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 8 | /// An enum consisting of all constraints 9 | pub enum Constraint { 10 | Check(Check), 11 | NotNull(NotNull), 12 | Unique(Unique), 13 | PrimaryKey(PrimaryKey), 14 | References(References), 15 | Exclusion(Exclusion), 16 | } 17 | 18 | #[derive(Clone, Debug, PartialEq)] 19 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 20 | /// A constraint which states that a value must satisfy the following Boolean expression 21 | pub struct Check { 22 | pub name: String, 23 | /// The Boolean expression that must be satisfied 24 | pub expr: String, 25 | /// If marked with NO INHERIT, the constraint will not propogate to child tables 26 | pub no_inherit: bool, 27 | } 28 | 29 | #[derive(Clone, Debug, PartialEq)] 30 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 31 | /// The constraint that a value must not be null 32 | pub struct NotNull; 33 | 34 | impl NotNull { 35 | pub fn from_bool(boolean: bool) -> Option { 36 | if boolean { Some(NotNull) } else { None } 37 | } 38 | } 39 | 40 | #[derive(Clone, Debug, PartialEq)] 41 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 42 | /// That each set of values for these columns must be unique across the whole table 43 | pub struct Unique { 44 | pub name: String, 45 | pub columns: Vec, 46 | } 47 | 48 | #[derive(Clone, Debug, PartialEq)] 49 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 50 | /// A constraint stating that the given columns act as a unique identifier for rows in the table. 51 | /// This implies that the columns are not null and are unique together 52 | pub struct PrimaryKey { 53 | pub name: String, 54 | pub columns: Vec, 55 | } 56 | 57 | #[derive(Clone, Debug, PartialEq)] 58 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 59 | /// A constraint that column references the values appearing in the row of another table 60 | pub struct References { 61 | pub name: String, 62 | pub columns: Vec, 63 | pub table: String, 64 | pub foreign_columns: Vec, 65 | pub on_update: Option, 66 | pub on_delete: Option, 67 | } 68 | 69 | #[derive(Clone, Debug, PartialEq, sea_schema_derive::Name)] 70 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 71 | pub enum ForeignKeyAction { 72 | #[name = "CASCADE"] 73 | Cascade, 74 | #[name = "SET NULL"] 75 | SetNull, 76 | #[name = "SET DEFAULT"] 77 | SetDefault, 78 | #[name = "RESTRICT"] 79 | Restrict, 80 | #[name = "NO ACTION"] 81 | NoAction, 82 | } 83 | 84 | #[derive(Clone, Debug, PartialEq)] 85 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 86 | /// A constraint that ensures that, if any two rows are compared on the specified columns or 87 | /// expressions using the specified operators, at least one of these operator comparisons returns 88 | /// false or null 89 | pub struct Exclusion { 90 | pub name: String, 91 | pub using: String, 92 | pub columns: Vec, 93 | pub operation: String, 94 | } 95 | -------------------------------------------------------------------------------- /src/postgres/def/mod.rs: -------------------------------------------------------------------------------- 1 | mod column; 2 | mod constraints; 3 | mod schema; 4 | mod table; 5 | mod types; 6 | 7 | pub use column::*; 8 | pub use constraints::*; 9 | pub use schema::*; 10 | pub use table::*; 11 | pub use types::*; 12 | -------------------------------------------------------------------------------- /src/postgres/def/schema.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "with-serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::*; 5 | 6 | #[derive(Clone, Debug, PartialEq)] 7 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 8 | pub struct Schema { 9 | pub schema: String, 10 | pub tables: Vec, 11 | } 12 | 13 | #[derive(Clone, Debug, PartialEq)] 14 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 15 | pub struct TableDef { 16 | pub info: TableInfo, 17 | pub columns: Vec, 18 | 19 | pub check_constraints: Vec, 20 | pub not_null_constraints: Vec, 21 | pub unique_constraints: Vec, 22 | pub primary_key_constraints: Vec, 23 | pub reference_constraints: Vec, 24 | pub exclusion_constraints: Vec, 25 | // FIXME: Duplication? TableInfo also have of_type 26 | // pub of_type: Option, 27 | // TODO: 28 | // pub inherets: Vec, 29 | } 30 | -------------------------------------------------------------------------------- /src/postgres/def/table.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | #[cfg(feature = "with-serde")] 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Clone, Debug, PartialEq)] 6 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 7 | /// Information relating to the table, but not its individual components. For information on a 8 | /// table including its columns and constraints, use [`TableDef`] 9 | pub struct TableInfo { 10 | pub name: String, 11 | pub of_type: Option, 12 | // TODO: 13 | // pub comment: String 14 | } 15 | -------------------------------------------------------------------------------- /src/postgres/discovery/executor/mock.rs: -------------------------------------------------------------------------------- 1 | use crate::sqlx_types::{PgPool, postgres::PgRow}; 2 | use sea_query::{PostgresQueryBuilder, SelectStatement}; 3 | 4 | use crate::{debug_print, sqlx_types::SqlxError}; 5 | 6 | #[allow(dead_code)] 7 | pub struct Executor { 8 | pool: PgPool, 9 | } 10 | 11 | pub trait IntoExecutor { 12 | fn into_executor(self) -> Executor; 13 | } 14 | 15 | impl IntoExecutor for PgPool { 16 | fn into_executor(self) -> Executor { 17 | Executor { pool: self } 18 | } 19 | } 20 | 21 | impl Executor { 22 | pub async fn fetch_all(&self, select: SelectStatement) -> Result, SqlxError> { 23 | let (_sql, _values) = select.build(PostgresQueryBuilder); 24 | debug_print!("{}, {:?}", _sql, _values); 25 | 26 | panic!("This is a mock Executor"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/postgres/discovery/executor/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "sqlx-postgres")] 2 | mod real; 3 | #[cfg(feature = "sqlx-postgres")] 4 | pub use real::*; 5 | 6 | #[cfg(not(feature = "sqlx-postgres"))] 7 | mod mock; 8 | #[cfg(not(feature = "sqlx-postgres"))] 9 | pub use mock::*; 10 | -------------------------------------------------------------------------------- /src/postgres/discovery/executor/real.rs: -------------------------------------------------------------------------------- 1 | use sea_query::{PostgresQueryBuilder, SelectStatement}; 2 | use sea_query_binder::SqlxBinder; 3 | use sqlx::{PgPool, postgres::PgRow}; 4 | 5 | use crate::{debug_print, sqlx_types::SqlxError}; 6 | 7 | pub struct Executor { 8 | pool: PgPool, 9 | } 10 | 11 | pub trait IntoExecutor { 12 | fn into_executor(self) -> Executor; 13 | } 14 | 15 | impl IntoExecutor for PgPool { 16 | fn into_executor(self) -> Executor { 17 | Executor { pool: self } 18 | } 19 | } 20 | 21 | impl Executor { 22 | pub async fn fetch_all(&self, select: SelectStatement) -> Result, SqlxError> { 23 | let (sql, values) = select.build_sqlx(PostgresQueryBuilder); 24 | debug_print!("{}, {:?}", sql, values); 25 | 26 | sqlx::query_with(&sql, values) 27 | .fetch_all(&mut *self.pool.acquire().await?) 28 | .await 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/postgres/discovery/mod.rs: -------------------------------------------------------------------------------- 1 | //! To query & parse MySQL's INFORMATION_SCHEMA and construct a [`Schema`] 2 | 3 | use crate::debug_print; 4 | use crate::postgres::def::*; 5 | use crate::postgres::parser::{ 6 | parse_table_constraint_query_results, parse_unique_index_query_results, 7 | }; 8 | use crate::postgres::query::{ 9 | ColumnQueryResult, EnumQueryResult, SchemaQueryBuilder, TableConstraintsQueryResult, 10 | TableQueryResult, UniqueIndexQueryResult, 11 | }; 12 | use crate::sqlx_types::SqlxError; 13 | use futures::future; 14 | use sea_query::{Alias, Iden, IntoIden, SeaRc}; 15 | use std::collections::HashMap; 16 | 17 | mod executor; 18 | pub use executor::*; 19 | 20 | pub(crate) type EnumVariantMap = HashMap>; 21 | 22 | pub struct SchemaDiscovery { 23 | pub query: SchemaQueryBuilder, 24 | pub executor: Executor, 25 | pub schema: SeaRc, 26 | } 27 | 28 | impl SchemaDiscovery { 29 | pub fn new(executor: E, schema: &str) -> Self 30 | where 31 | E: IntoExecutor, 32 | { 33 | Self { 34 | query: SchemaQueryBuilder::default(), 35 | executor: executor.into_executor(), 36 | schema: Alias::new(schema).into_iden(), 37 | } 38 | } 39 | 40 | pub async fn discover(&self) -> Result { 41 | let enums: EnumVariantMap = self 42 | .discover_enums() 43 | .await? 44 | .into_iter() 45 | .map(|enum_def| (enum_def.typename, enum_def.values)) 46 | .collect(); 47 | let tables = future::try_join_all( 48 | self.discover_tables() 49 | .await? 50 | .into_iter() 51 | .map(|t| (self, t, &enums)) 52 | .map(Self::discover_table_static), 53 | ) 54 | .await?; 55 | 56 | Ok(Schema { 57 | schema: self.schema.to_string(), 58 | tables, 59 | }) 60 | } 61 | 62 | pub async fn discover_tables(&self) -> Result, SqlxError> { 63 | let rows = self 64 | .executor 65 | .fetch_all(self.query.query_tables(self.schema.clone())) 66 | .await?; 67 | 68 | let tables: Vec = rows 69 | .iter() 70 | .map(|row| { 71 | let result: TableQueryResult = row.into(); 72 | debug_print!("{:?}", result); 73 | let table = result.parse(); 74 | debug_print!("{:?}", table); 75 | table 76 | }) 77 | .collect(); 78 | 79 | Ok(tables) 80 | } 81 | 82 | async fn discover_table_static( 83 | params: (&Self, TableInfo, &EnumVariantMap), 84 | ) -> Result { 85 | let this = params.0; 86 | let info = params.1; 87 | let enums = params.2; 88 | Self::discover_table(this, info, enums).await 89 | } 90 | 91 | pub async fn discover_table( 92 | &self, 93 | info: TableInfo, 94 | enums: &EnumVariantMap, 95 | ) -> Result { 96 | let table = SeaRc::new(Alias::new(info.name.as_str())); 97 | let columns = self 98 | .discover_columns(self.schema.clone(), table.clone(), enums) 99 | .await?; 100 | let constraints = self 101 | .discover_constraints(self.schema.clone(), table.clone()) 102 | .await?; 103 | let ( 104 | check_constraints, 105 | not_null_constraints, 106 | primary_key_constraints, 107 | reference_constraints, 108 | exclusion_constraints, 109 | ) = constraints.into_iter().fold( 110 | (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new()), 111 | |mut acc, constraint| { 112 | match constraint { 113 | Constraint::Check(check) => acc.0.push(check), 114 | Constraint::NotNull(not_null) => acc.1.push(not_null), 115 | Constraint::Unique(_) => (), 116 | Constraint::PrimaryKey(primary_key) => acc.2.push(primary_key), 117 | Constraint::References(references) => acc.3.push(references), 118 | Constraint::Exclusion(exclusion) => acc.4.push(exclusion), 119 | } 120 | acc 121 | }, 122 | ); 123 | 124 | let unique_constraints = self 125 | .discover_unique_indexes(self.schema.clone(), table.clone()) 126 | .await?; 127 | 128 | Ok(TableDef { 129 | info, 130 | columns, 131 | check_constraints, 132 | not_null_constraints, 133 | unique_constraints, 134 | primary_key_constraints, 135 | reference_constraints, 136 | exclusion_constraints, 137 | }) 138 | } 139 | 140 | pub async fn discover_columns( 141 | &self, 142 | schema: SeaRc, 143 | table: SeaRc, 144 | enums: &EnumVariantMap, 145 | ) -> Result, SqlxError> { 146 | let rows = self 147 | .executor 148 | .fetch_all(self.query.query_columns(schema.clone(), table.clone())) 149 | .await?; 150 | 151 | Ok(rows 152 | .into_iter() 153 | .map(|row| { 154 | let result: ColumnQueryResult = (&row).into(); 155 | debug_print!("{:?}", result); 156 | let column = result.parse(enums); 157 | debug_print!("{:?}", column); 158 | column 159 | }) 160 | .collect()) 161 | } 162 | 163 | pub async fn discover_constraints( 164 | &self, 165 | schema: SeaRc, 166 | table: SeaRc, 167 | ) -> Result, SqlxError> { 168 | let rows = self 169 | .executor 170 | .fetch_all( 171 | self.query 172 | .query_table_constraints(schema.clone(), table.clone()), 173 | ) 174 | .await?; 175 | 176 | let results = rows.into_iter().map(|row| { 177 | let result: TableConstraintsQueryResult = (&row).into(); 178 | debug_print!("{:?}", result); 179 | result 180 | }); 181 | 182 | Ok(parse_table_constraint_query_results(Box::new(results)) 183 | .map(|index| { 184 | debug_print!("{:?}", index); 185 | index 186 | }) 187 | .collect()) 188 | } 189 | 190 | pub async fn discover_unique_indexes( 191 | &self, 192 | schema: SeaRc, 193 | table: SeaRc, 194 | ) -> Result, SqlxError> { 195 | let rows = self 196 | .executor 197 | .fetch_all( 198 | self.query 199 | .query_table_unique_indexes(schema.clone(), table.clone()), 200 | ) 201 | .await?; 202 | 203 | let results = rows.into_iter().map(|row| { 204 | let result: UniqueIndexQueryResult = (&row).into(); 205 | debug_print!("{:?}", result); 206 | result 207 | }); 208 | 209 | Ok(parse_unique_index_query_results(Box::new(results)) 210 | .map(|index| { 211 | debug_print!("{:?}", index); 212 | index 213 | }) 214 | .collect()) 215 | } 216 | 217 | pub async fn discover_enums(&self) -> Result, SqlxError> { 218 | let rows = self.executor.fetch_all(self.query.query_enums()).await?; 219 | 220 | let enum_rows = rows.into_iter().map(|row| { 221 | let result: EnumQueryResult = (&row).into(); 222 | debug_print!("{:?}", result); 223 | result 224 | }); 225 | 226 | let map = enum_rows.fold( 227 | HashMap::new(), 228 | |mut map: HashMap>, 229 | EnumQueryResult { 230 | typename, 231 | enumlabel, 232 | }| { 233 | if let Some(entry_exists) = map.get_mut(&typename) { 234 | entry_exists.push(enumlabel); 235 | } else { 236 | map.insert(typename, vec![enumlabel]); 237 | } 238 | map 239 | }, 240 | ); 241 | 242 | Ok(map 243 | .into_iter() 244 | .map(|(typename, values)| EnumDef { values, typename }) 245 | .collect()) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/postgres/mod.rs: -------------------------------------------------------------------------------- 1 | pub struct Postgres; 2 | 3 | #[cfg(feature = "def")] 4 | #[cfg_attr(docsrs, doc(cfg(feature = "def")))] 5 | pub mod def; 6 | 7 | #[cfg(feature = "discovery")] 8 | #[cfg_attr(docsrs, doc(cfg(feature = "discovery")))] 9 | pub mod discovery; 10 | 11 | #[cfg(feature = "parser")] 12 | #[cfg_attr(docsrs, doc(cfg(feature = "parser")))] 13 | pub mod parser; 14 | 15 | #[cfg(feature = "query")] 16 | #[cfg_attr(docsrs, doc(cfg(feature = "query")))] 17 | pub mod query; 18 | 19 | #[cfg(feature = "writer")] 20 | #[cfg_attr(docsrs, doc(cfg(feature = "writer")))] 21 | pub mod writer; 22 | 23 | #[cfg(feature = "probe")] 24 | #[cfg_attr(docsrs, doc(cfg(feature = "probe")))] 25 | pub mod probe; 26 | -------------------------------------------------------------------------------- /src/postgres/parser/column.rs: -------------------------------------------------------------------------------- 1 | use crate::postgres::{ 2 | def::*, discovery::EnumVariantMap, parser::yes_or_no_to_bool, query::ColumnQueryResult, 3 | }; 4 | use sea_query::RcOrArc; 5 | 6 | impl ColumnQueryResult { 7 | pub fn parse(self, enums: &EnumVariantMap) -> ColumnInfo { 8 | parse_column_query_result(self, enums) 9 | } 10 | } 11 | 12 | pub fn parse_column_query_result(result: ColumnQueryResult, enums: &EnumVariantMap) -> ColumnInfo { 13 | ColumnInfo { 14 | name: result.column_name.clone(), 15 | col_type: parse_column_type(&result, enums), 16 | default: ColumnExpression::from_option_string(result.column_default), 17 | generated: ColumnExpression::from_option_string(result.column_generated), 18 | not_null: NotNull::from_bool(!yes_or_no_to_bool(&result.is_nullable)), 19 | is_identity: yes_or_no_to_bool(&result.is_identity), 20 | } 21 | } 22 | 23 | pub fn parse_column_type(result: &ColumnQueryResult, enums: &EnumVariantMap) -> ColumnType { 24 | let is_enum = result 25 | .udt_name 26 | .as_ref() 27 | .is_some_and(|udt_name| enums.contains_key(udt_name)); 28 | let mut ctype = Type::from_str( 29 | result.column_type.as_str(), 30 | result.udt_name.as_deref(), 31 | is_enum, 32 | ); 33 | 34 | if ctype.has_numeric_attr() { 35 | ctype = parse_numeric_attributes( 36 | result.numeric_precision, 37 | result.numeric_precision_radix, 38 | result.numeric_scale, 39 | ctype, 40 | ); 41 | } 42 | if ctype.has_string_attr() { 43 | ctype = parse_string_attributes(result.character_maximum_length, ctype); 44 | } 45 | if ctype.has_time_attr() { 46 | ctype = parse_time_attributes(result.datetime_precision, ctype); 47 | } 48 | if ctype.has_interval_attr() { 49 | ctype = parse_interval_attributes(&result.interval_type, result.interval_precision, ctype); 50 | } 51 | if ctype.has_bit_attr() { 52 | ctype = parse_bit_attributes(result.character_maximum_length, ctype); 53 | } 54 | if ctype.has_enum_attr() { 55 | ctype = parse_enum_attributes(result.udt_name.as_deref(), ctype, enums); 56 | } 57 | if ctype.has_array_attr() { 58 | ctype = parse_array_attributes(result.udt_name_regtype.as_deref(), ctype, enums); 59 | } 60 | #[cfg(feature = "postgres-vector")] 61 | if ctype.has_vector_attr() { 62 | ctype = parse_vector_attributes(result.character_maximum_length, ctype); 63 | } 64 | 65 | ctype 66 | } 67 | 68 | pub fn parse_numeric_attributes( 69 | num_precision: Option, 70 | num_precision_radix: Option, 71 | num_scale: Option, 72 | mut ctype: ColumnType, 73 | ) -> ColumnType { 74 | let numeric_precision: Option = match num_precision { 75 | None => None, 76 | Some(num) => u16::try_from(num).ok(), 77 | }; 78 | let _numeric_precision_radix: Option = match num_precision_radix { 79 | None => None, 80 | Some(num) => u16::try_from(num).ok(), 81 | }; 82 | let numeric_scale: Option = match num_scale { 83 | None => None, 84 | Some(num) => u16::try_from(num).ok(), 85 | }; 86 | 87 | match ctype { 88 | Type::Decimal(ref mut attr) | Type::Numeric(ref mut attr) => { 89 | attr.precision = numeric_precision; 90 | attr.scale = numeric_scale; 91 | } 92 | _ => panic!("parse_numeric_attributes(_) received a type other than Decimal or Numeric"), 93 | }; 94 | 95 | ctype 96 | } 97 | 98 | pub fn parse_string_attributes( 99 | character_maximum_length: Option, 100 | mut ctype: ColumnType, 101 | ) -> ColumnType { 102 | match ctype { 103 | Type::Varchar(ref mut attr) | Type::Char(ref mut attr) => { 104 | attr.length = match character_maximum_length { 105 | None => None, 106 | Some(num) => u16::try_from(num).ok(), 107 | }; 108 | } 109 | _ => panic!("parse_string_attributes(_) received a type that does not have StringAttr"), 110 | }; 111 | 112 | ctype 113 | } 114 | 115 | pub fn parse_time_attributes(datetime_precision: Option, mut ctype: ColumnType) -> ColumnType { 116 | match ctype { 117 | Type::Timestamp(ref mut attr) 118 | | Type::TimestampWithTimeZone(ref mut attr) 119 | | Type::Time(ref mut attr) 120 | | Type::TimeWithTimeZone(ref mut attr) => { 121 | attr.precision = match datetime_precision { 122 | None => None, 123 | Some(num) => u16::try_from(num).ok(), 124 | }; 125 | } 126 | _ => panic!("parse_time_attributes(_) received a type that does not have TimeAttr"), 127 | }; 128 | 129 | ctype 130 | } 131 | 132 | pub fn parse_interval_attributes( 133 | interval_type: &Option, 134 | interval_precision: Option, 135 | mut ctype: ColumnType, 136 | ) -> ColumnType { 137 | match ctype { 138 | Type::Interval(ref mut attr) => { 139 | attr.field.clone_from(interval_type); 140 | attr.precision = match interval_precision { 141 | None => None, 142 | Some(num) => u16::try_from(num).ok(), 143 | }; 144 | } 145 | _ => panic!("parse_interval_attributes(_) received a type that does not have IntervalAttr"), 146 | }; 147 | 148 | ctype 149 | } 150 | 151 | pub fn parse_bit_attributes( 152 | character_maximum_length: Option, 153 | mut ctype: ColumnType, 154 | ) -> ColumnType { 155 | match ctype { 156 | Type::Bit(ref mut attr) => { 157 | attr.length = match character_maximum_length { 158 | None => None, 159 | Some(num) => u16::try_from(num).ok(), 160 | }; 161 | } 162 | Type::VarBit(ref mut attr) => { 163 | attr.length = match character_maximum_length { 164 | None => None, 165 | Some(num) => u16::try_from(num).ok(), 166 | }; 167 | } 168 | _ => panic!("parse_bit_attributes(_) received a type that does not have BitAttr"), 169 | }; 170 | 171 | ctype 172 | } 173 | 174 | pub fn parse_enum_attributes( 175 | udt_name: Option<&str>, 176 | mut ctype: ColumnType, 177 | enums: &EnumVariantMap, 178 | ) -> ColumnType { 179 | match ctype { 180 | Type::Enum(ref mut def) => { 181 | def.typename = match udt_name { 182 | None => panic!("parse_enum_attributes(_) received an empty udt_name"), 183 | Some(typename) => typename.to_string(), 184 | }; 185 | if let Some(variants) = enums.get(&def.typename) { 186 | def.values.clone_from(variants); 187 | } 188 | } 189 | _ => panic!("parse_enum_attributes(_) received a type that does not have EnumDef"), 190 | }; 191 | 192 | ctype 193 | } 194 | 195 | pub fn parse_array_attributes( 196 | udt_name_regtype: Option<&str>, 197 | mut ctype: ColumnType, 198 | enums: &EnumVariantMap, 199 | ) -> ColumnType { 200 | match ctype { 201 | Type::Array(ref mut def) => { 202 | def.col_type = match udt_name_regtype { 203 | None => panic!("parse_array_attributes(_) received an empty udt_name_regtype"), 204 | Some(typename) => { 205 | let typename = &typename.replacen('"', "", 2).replacen("[]", "", 1); 206 | let arr_col_type = if let Some(variants) = enums.get(typename) { 207 | Type::Enum(EnumDef { 208 | typename: typename.to_string(), 209 | values: variants.clone(), 210 | }) 211 | } else { 212 | Type::from_str(typename, Some(typename), false) 213 | }; 214 | Some(RcOrArc::new(arr_col_type)) 215 | } 216 | }; 217 | } 218 | _ => panic!("parse_array_attributes(_) received a type that does not have EnumDef"), 219 | }; 220 | 221 | ctype 222 | } 223 | 224 | #[cfg(feature = "postgres-vector")] 225 | pub fn parse_vector_attributes( 226 | character_maximum_length: Option, 227 | mut ctype: ColumnType, 228 | ) -> ColumnType { 229 | match ctype { 230 | Type::Vector(ref mut attr) => { 231 | attr.length = match character_maximum_length { 232 | None => None, 233 | Some(num) => match u32::try_from(num) { 234 | Ok(num) => Some(num), 235 | Err(_) => None, 236 | }, 237 | }; 238 | } 239 | _ => panic!("parse_vector_attributes(_) received a type that does not have StringAttr"), 240 | }; 241 | 242 | ctype 243 | } 244 | -------------------------------------------------------------------------------- /src/postgres/parser/mod.rs: -------------------------------------------------------------------------------- 1 | mod column; 2 | mod pg_indexes; 3 | mod table; 4 | mod table_constraints; 5 | 6 | pub use column::*; 7 | pub use pg_indexes::*; 8 | pub use table::*; 9 | pub use table_constraints::*; 10 | 11 | fn yes_or_no_to_bool(string: &str) -> bool { 12 | matches!(string.to_uppercase().as_str(), "YES") 13 | } 14 | -------------------------------------------------------------------------------- /src/postgres/parser/pg_indexes.rs: -------------------------------------------------------------------------------- 1 | use crate::postgres::{def::*, query::UniqueIndexQueryResult}; 2 | 3 | pub struct UniqueIndexQueryResultParser { 4 | curr: Option, 5 | results: Box>, 6 | } 7 | 8 | pub fn parse_unique_index_query_results( 9 | results: Box>, 10 | ) -> impl Iterator { 11 | UniqueIndexQueryResultParser { 12 | curr: None, 13 | results, 14 | } 15 | } 16 | 17 | impl Iterator for UniqueIndexQueryResultParser { 18 | type Item = Unique; 19 | 20 | fn next(&mut self) -> Option { 21 | let result = if let Some(result) = self.curr.take() { 22 | result 23 | } else { 24 | self.results.next()? 25 | }; 26 | 27 | let index_name = result.index_name; 28 | let mut columns = vec![result.column_name]; 29 | 30 | for result in self.results.by_ref() { 31 | if result.index_name != index_name { 32 | self.curr = Some(result); 33 | return Some(Unique { 34 | name: index_name, 35 | columns, 36 | }); 37 | } 38 | 39 | columns.push(result.column_name); 40 | } 41 | 42 | Some(Unique { 43 | name: index_name, 44 | columns, 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/postgres/parser/table.rs: -------------------------------------------------------------------------------- 1 | use crate::postgres::def::*; 2 | use crate::postgres::query::TableQueryResult; 3 | 4 | impl TableQueryResult { 5 | pub fn parse(self) -> TableInfo { 6 | parse_table_query_result(self) 7 | } 8 | } 9 | 10 | pub fn parse_table_query_result(table_query: TableQueryResult) -> TableInfo { 11 | TableInfo { 12 | name: table_query.table_name, 13 | of_type: table_query 14 | .user_defined_type_name 15 | .map(|type_name| Type::from_str(&type_name, Some(&type_name), false)), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/postgres/parser/table_constraints.rs: -------------------------------------------------------------------------------- 1 | use crate::Name; 2 | use crate::postgres::{def::*, query::TableConstraintsQueryResult}; 3 | 4 | pub struct TableConstraintsQueryResultParser { 5 | curr: Option, 6 | results: Box>, 7 | } 8 | 9 | /// Assumed to be ordered by table name, then constraint name, then ordinal position, then the 10 | /// constraint name of the foreign key, then the ordinal position of the foreign key 11 | pub fn parse_table_constraint_query_results( 12 | results: Box>, 13 | ) -> impl Iterator { 14 | TableConstraintsQueryResultParser { 15 | curr: None, 16 | results, 17 | } 18 | } 19 | 20 | impl Iterator for TableConstraintsQueryResultParser { 21 | type Item = Constraint; 22 | 23 | // FIXME/TODO: How to handle invalid input 24 | fn next(&mut self) -> Option { 25 | let result = if let Some(result) = self.curr.take() { 26 | result 27 | } else { 28 | self.results.next()? 29 | }; 30 | 31 | let constraint_name = result.constraint_name; 32 | match result.constraint_type.as_str() { 33 | "CHECK" => { 34 | match result.check_clause { 35 | Some(check_clause) => { 36 | Some(Constraint::Check(Check { 37 | name: constraint_name, 38 | expr: check_clause, 39 | // TODO: How to find? 40 | no_inherit: false, 41 | })) 42 | } 43 | None => self.next(), 44 | } 45 | } 46 | 47 | "FOREIGN KEY" => { 48 | let mut columns = Vec::new(); 49 | let mut foreign_columns = Vec::new(); 50 | 51 | columns.push(result.column_name.unwrap()); 52 | let table = result.referential_key_table_name.unwrap(); 53 | foreign_columns.push(result.referential_key_column_name.unwrap()); 54 | let on_update = 55 | ForeignKeyAction::from_str(&result.update_rule.clone().unwrap_or_default()); 56 | let on_delete = 57 | ForeignKeyAction::from_str(&result.delete_rule.clone().unwrap_or_default()); 58 | 59 | for result in self.results.by_ref() { 60 | if result.constraint_name != constraint_name { 61 | self.curr = Some(result); 62 | return Some(Constraint::References(References { 63 | name: constraint_name, 64 | columns, 65 | table, 66 | foreign_columns, 67 | on_update, 68 | on_delete, 69 | })); 70 | } 71 | 72 | if result.column_name.is_some() && result.referential_key_column_name.is_some() 73 | { 74 | columns.push(result.column_name.unwrap()); 75 | foreign_columns.push(result.referential_key_column_name.unwrap()); 76 | } 77 | } 78 | 79 | Some(Constraint::References(References { 80 | name: constraint_name, 81 | columns, 82 | table, 83 | foreign_columns, 84 | on_update, 85 | on_delete, 86 | })) 87 | } 88 | 89 | "PRIMARY KEY" => { 90 | let mut columns = vec![result.column_name.unwrap()]; 91 | 92 | for result in self.results.by_ref() { 93 | if result.constraint_name != constraint_name { 94 | self.curr = Some(result); 95 | return Some(Constraint::PrimaryKey(PrimaryKey { 96 | name: constraint_name, 97 | columns, 98 | })); 99 | } 100 | 101 | columns.push(result.column_name.unwrap()); 102 | } 103 | 104 | Some(Constraint::PrimaryKey(PrimaryKey { 105 | name: constraint_name, 106 | columns, 107 | })) 108 | } 109 | 110 | "UNIQUE" => { 111 | let mut columns = vec![result.column_name.unwrap()]; 112 | 113 | for result in self.results.by_ref() { 114 | if result.constraint_name != constraint_name { 115 | self.curr = Some(result); 116 | return Some(Constraint::Unique(Unique { 117 | name: constraint_name, 118 | columns, 119 | })); 120 | } 121 | 122 | columns.push(result.column_name.unwrap()); 123 | } 124 | 125 | Some(Constraint::Unique(Unique { 126 | name: constraint_name, 127 | columns, 128 | })) 129 | } 130 | 131 | _ => { 132 | // FIXME: Invalid input error handling 133 | None 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/postgres/probe.rs: -------------------------------------------------------------------------------- 1 | use sea_query::{Condition, Expr, Query, SelectStatement, SimpleExpr}; 2 | 3 | use super::Postgres; 4 | use super::query::{InformationSchema as Schema, PgIndexes, TablesFields}; 5 | use crate::probe::{Has, SchemaProbe}; 6 | 7 | impl SchemaProbe for Postgres { 8 | fn get_current_schema() -> SimpleExpr { 9 | Expr::cust("CURRENT_SCHEMA()") 10 | } 11 | 12 | fn query_tables(&self) -> SelectStatement { 13 | Query::select() 14 | .expr_as(Expr::col(TablesFields::TableName), TablesFields::TableName) 15 | .from((Schema::Schema, Schema::Tables)) 16 | .cond_where( 17 | Condition::all() 18 | .add( 19 | Expr::expr(Self::get_current_schema()) 20 | .equals((Schema::Tables, TablesFields::TableSchema)), 21 | ) 22 | .add(Expr::col(TablesFields::TableType).eq("BASE TABLE")), 23 | ) 24 | .take() 25 | } 26 | 27 | fn has_index(&self, table: T, index: C) -> SelectStatement 28 | where 29 | T: AsRef, 30 | C: AsRef, 31 | { 32 | Query::select() 33 | .expr_as(Expr::cust("COUNT(*) > 0"), Has::Index) 34 | .from(PgIndexes::Table) 35 | .cond_where( 36 | Condition::all() 37 | .add(Expr::col(PgIndexes::SchemaName).eq(Self::get_current_schema())) 38 | .add(Expr::col(PgIndexes::TableName).eq(table.as_ref())) 39 | .add(Expr::col(PgIndexes::IndexName).eq(index.as_ref())), 40 | ) 41 | .take() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/postgres/query/char_set.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, sea_query::Iden)] 2 | /// Ref: https://www.postgresql.org/docs/13/infoschema-character-sets.html 3 | pub enum CharacterSetFields { 4 | /// This column is null 5 | CharacterSetCatalog, 6 | /// This column is null 7 | CharacterSetSchema, 8 | 9 | CharacterSetName, 10 | ChacterRepetoire, 11 | FormOfUse, 12 | DefaultCollateCatalog, 13 | DefaultCollateSchema, 14 | DefaultCollateName, 15 | } 16 | -------------------------------------------------------------------------------- /src/postgres/query/column.rs: -------------------------------------------------------------------------------- 1 | use super::{InformationSchema, SchemaQueryBuilder}; 2 | use crate::sqlx_types::postgres::PgRow; 3 | use sea_query::{BinOper, Expr, Iden, Query, SeaRc, SelectStatement}; 4 | 5 | #[derive(Debug, sea_query::Iden)] 6 | /// Ref: https://www.postgresql.org/docs/13/infoschema-columns.html 7 | pub enum ColumnsField { 8 | TableCatalog, 9 | TableSchema, 10 | TableName, 11 | ColumnName, 12 | OrdinalPosition, 13 | ColumnDefault, 14 | IsNullable, 15 | DataType, 16 | CharacterMaximumLength, 17 | CharacterOctetLength, 18 | NumericPrecision, 19 | NumericPrecisionRadix, 20 | NumericScale, 21 | DatetimePrecision, 22 | IntervalType, 23 | IntervalPrecision, 24 | CollationCatalog, 25 | CollationSchema, 26 | CollationName, 27 | DomainCatalog, 28 | DomainSchema, 29 | DomainName, 30 | UdtCatalog, 31 | UdtSchema, 32 | UdtName, 33 | DtdIdentifier, 34 | IsIdentity, 35 | IdentityGeneration, 36 | IdentityStart, 37 | IdentityIncrement, 38 | IdentityMaximum, 39 | IdentityMinimum, 40 | IdentityCycle, 41 | IsGenerated, 42 | GenerationExpression, 43 | IsUpdatable, 44 | } 45 | 46 | #[derive(Debug, Default)] 47 | pub struct ColumnQueryResult { 48 | pub column_name: String, 49 | pub column_type: String, 50 | pub column_default: Option, 51 | pub column_generated: Option, 52 | pub is_nullable: String, 53 | pub is_identity: String, 54 | 55 | // Declared or implicit parameters of numeric types; null for other data types 56 | pub numeric_precision: Option, 57 | pub numeric_precision_radix: Option, 58 | pub numeric_scale: Option, 59 | 60 | pub character_maximum_length: Option, 61 | pub character_octet_length: Option, 62 | 63 | pub datetime_precision: Option, 64 | 65 | pub interval_type: Option, 66 | pub interval_precision: Option, 67 | 68 | pub udt_name: Option, 69 | pub udt_name_regtype: Option, 70 | } 71 | 72 | impl SchemaQueryBuilder { 73 | pub fn query_columns( 74 | &self, 75 | schema: SeaRc, 76 | table: SeaRc, 77 | ) -> SelectStatement { 78 | Query::select() 79 | .columns([ 80 | ColumnsField::ColumnName, 81 | ColumnsField::DataType, 82 | ColumnsField::ColumnDefault, 83 | ColumnsField::GenerationExpression, 84 | ColumnsField::IsNullable, 85 | ColumnsField::IsIdentity, 86 | ColumnsField::NumericPrecision, 87 | ColumnsField::NumericPrecisionRadix, 88 | ColumnsField::NumericScale, 89 | ColumnsField::CharacterMaximumLength, 90 | ColumnsField::CharacterOctetLength, 91 | ColumnsField::DatetimePrecision, 92 | ColumnsField::IntervalType, 93 | ColumnsField::IntervalPrecision, 94 | ColumnsField::UdtName, 95 | ]) 96 | .expr( 97 | // The double quotes are required to correctly handle user types containing 98 | // upper case letters. 99 | Expr::expr(Expr::cust("CONCAT('\"', udt_name, '\"')::regtype").cast_as(Text)) 100 | .binary(BinOper::As, Expr::col(UdtNameRegtype)), 101 | ) 102 | .from((InformationSchema::Schema, InformationSchema::Columns)) 103 | .and_where(Expr::col(ColumnsField::TableSchema).eq(schema.to_string())) 104 | .and_where(Expr::col(ColumnsField::TableName).eq(table.to_string())) 105 | .take() 106 | } 107 | } 108 | 109 | #[cfg(feature = "sqlx-postgres")] 110 | impl From<&PgRow> for ColumnQueryResult { 111 | fn from(row: &PgRow) -> Self { 112 | use crate::sqlx_types::Row; 113 | Self { 114 | column_name: row.get(0), 115 | column_type: row.get(1), 116 | column_default: row.get(2), 117 | column_generated: row.get(3), 118 | is_nullable: row.get(4), 119 | is_identity: row.get(5), 120 | numeric_precision: row.get(6), 121 | numeric_precision_radix: row.get(7), 122 | numeric_scale: row.get(8), 123 | character_maximum_length: row.get(9), 124 | character_octet_length: row.get(10), 125 | datetime_precision: row.get(11), 126 | interval_type: row.get(12), 127 | interval_precision: row.get(13), 128 | udt_name: row.get(14), 129 | udt_name_regtype: row.get(15), 130 | } 131 | } 132 | } 133 | 134 | #[cfg(not(feature = "sqlx-postgres"))] 135 | impl From<&PgRow> for ColumnQueryResult { 136 | fn from(_: &PgRow) -> Self { 137 | Self::default() 138 | } 139 | } 140 | 141 | #[derive(Iden)] 142 | struct Text; 143 | #[derive(Iden)] 144 | struct UdtNameRegtype; 145 | -------------------------------------------------------------------------------- /src/postgres/query/constraints/check_constraints.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, sea_query::Iden)] 2 | /// Ref: https://www.postgresql.org/docs/13/infoschema-check-constraints.html 3 | pub enum CheckConstraintsFields { 4 | ConstraintCatalog, 5 | ConstraintSchema, 6 | ConstraintName, 7 | CheckClause, 8 | } 9 | -------------------------------------------------------------------------------- /src/postgres/query/constraints/key_column_usage.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, sea_query::Iden)] 2 | /// Ref: https://www.postgresql.org/docs/13/infoschema-key-column-usage.html 3 | pub enum KeyColumnUsageFields { 4 | ConstraintCatalog, 5 | ConstraintSchema, 6 | ConstraintName, 7 | TableCatalog, 8 | TableSchema, 9 | TableName, 10 | ColumnName, 11 | OrdinalPosition, 12 | PositionInUniqueConstraint, 13 | } 14 | -------------------------------------------------------------------------------- /src/postgres/query/constraints/referential_constraints.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, sea_query::Iden)] 2 | /// Ref: https://www.postgresql.org/docs/13/infoschema-referential-constraints.html 3 | pub enum ReferentialConstraintsFields { 4 | ConstraintName, 5 | UniqueConstraintSchema, 6 | UniqueConstraintName, 7 | MatchOption, 8 | UpdateRule, 9 | DeleteRule, 10 | } 11 | -------------------------------------------------------------------------------- /src/postgres/query/constraints/table_constraints.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, sea_query::Iden)] 2 | /// Ref: https://www.postgresql.org/docs/13/infoschema-table-constraints.html 3 | pub enum TableConstraintsField { 4 | ConstraintCatalog, 5 | ConstraintSchema, 6 | ConstraintName, 7 | TableCatalog, 8 | TableSchema, 9 | TableName, 10 | ConstraintType, 11 | IsDeferrable, 12 | InitiallyDeferred, 13 | } 14 | -------------------------------------------------------------------------------- /src/postgres/query/enumeration.rs: -------------------------------------------------------------------------------- 1 | use super::SchemaQueryBuilder; 2 | use crate::sqlx_types::postgres::PgRow; 3 | use sea_query::{Expr, Order, Query, SelectStatement}; 4 | 5 | #[derive(Debug, sea_query::Iden)] 6 | pub enum PgType { 7 | #[iden = "pg_type"] 8 | Table, 9 | #[iden = "typname"] 10 | TypeName, 11 | #[iden = "oid"] 12 | Oid, 13 | } 14 | 15 | #[derive(Debug, sea_query::Iden)] 16 | pub enum PgEnum { 17 | #[iden = "pg_enum"] 18 | Table, 19 | #[iden = "enumlabel"] 20 | EnumLabel, 21 | #[iden = "enumtypid"] 22 | EnumTypeId, 23 | #[iden = "enumsortorder"] 24 | EnumSortOrder, 25 | } 26 | 27 | #[derive(Debug, Default)] 28 | pub struct EnumQueryResult { 29 | pub typename: String, 30 | pub enumlabel: String, 31 | } 32 | 33 | impl SchemaQueryBuilder { 34 | pub fn query_enums(&self) -> SelectStatement { 35 | Query::select() 36 | .column((PgType::Table, PgType::TypeName)) 37 | .column((PgEnum::Table, PgEnum::EnumLabel)) 38 | .from(PgType::Table) 39 | .inner_join( 40 | PgEnum::Table, 41 | Expr::col((PgEnum::Table, PgEnum::EnumTypeId)).equals((PgType::Table, PgType::Oid)), 42 | ) 43 | .order_by((PgType::Table, PgType::TypeName), Order::Asc) 44 | .order_by((PgEnum::Table, PgEnum::EnumSortOrder), Order::Asc) 45 | .order_by((PgEnum::Table, PgEnum::EnumLabel), Order::Asc) 46 | .take() 47 | } 48 | } 49 | 50 | #[cfg(feature = "sqlx-postgres")] 51 | impl From<&PgRow> for EnumQueryResult { 52 | fn from(row: &PgRow) -> Self { 53 | use crate::sqlx_types::Row; 54 | Self { 55 | typename: row.get(0), 56 | enumlabel: row.get(1), 57 | } 58 | } 59 | } 60 | 61 | #[cfg(not(feature = "sqlx-postgres"))] 62 | impl From<&PgRow> for EnumQueryResult { 63 | fn from(_: &PgRow) -> Self { 64 | Self::default() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/postgres/query/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod char_set; 2 | pub mod column; 3 | pub mod constraints; 4 | pub mod enumeration; 5 | pub mod pg_indexes; 6 | pub mod schema; 7 | pub mod table; 8 | 9 | pub use char_set::*; 10 | pub use column::*; 11 | pub use constraints::*; 12 | pub use enumeration::*; 13 | pub use pg_indexes::*; 14 | pub use schema::*; 15 | pub use table::*; 16 | -------------------------------------------------------------------------------- /src/postgres/query/pg_indexes.rs: -------------------------------------------------------------------------------- 1 | use super::SchemaQueryBuilder; 2 | use crate::sqlx_types::postgres::PgRow; 3 | use sea_query::{Alias, Condition, Expr, Iden, JoinType, Order, Query, SeaRc, SelectStatement}; 4 | 5 | #[derive(Debug, Iden)] 6 | pub enum PgIndexes { 7 | Table, 8 | #[iden = "tablename"] 9 | TableName, 10 | #[iden = "schemaname"] 11 | SchemaName, 12 | #[iden = "indexname"] 13 | IndexName, 14 | } 15 | 16 | #[derive(Debug, Iden)] 17 | pub enum PgIndex { 18 | Table, 19 | #[iden = "indexrelid"] 20 | IndexRelId, 21 | #[iden = "indrelid"] 22 | IndRelId, 23 | #[iden = "indisunique"] 24 | IndIsUnique, 25 | #[iden = "indisprimary"] 26 | IndIsPrimary, 27 | } 28 | 29 | #[derive(Debug, Iden)] 30 | pub enum PgClass { 31 | Table, 32 | Oid, 33 | #[iden = "relnamespace"] 34 | RelNamespace, 35 | #[iden = "relname"] 36 | RelName, 37 | } 38 | 39 | #[derive(Debug, Iden)] 40 | pub enum PgNamespace { 41 | Table, 42 | Oid, 43 | #[iden = "nspname"] 44 | NspName, 45 | } 46 | 47 | #[derive(Debug, Iden)] 48 | pub enum PgAttribute { 49 | Table, 50 | Oid, 51 | #[iden = "attrelid"] 52 | AttRelId, 53 | #[iden = "attname"] 54 | AttName, 55 | } 56 | 57 | #[derive(Debug, Default)] 58 | pub struct UniqueIndexQueryResult { 59 | pub index_name: String, 60 | pub table_schema: String, 61 | pub table_name: String, 62 | pub column_name: String, 63 | } 64 | 65 | impl SchemaQueryBuilder { 66 | pub fn query_table_unique_indexes( 67 | &self, 68 | schema: SeaRc, 69 | table: SeaRc, 70 | ) -> SelectStatement { 71 | let idx = Alias::new("idx"); 72 | let insp = Alias::new("insp"); 73 | let tbl = Alias::new("tbl"); 74 | let tnsp = Alias::new("tnsp"); 75 | let col = Alias::new("col"); 76 | 77 | Query::select() 78 | .column((idx.clone(), PgClass::RelName)) 79 | .column((insp.clone(), PgNamespace::NspName)) 80 | .column((tbl.clone(), PgClass::RelName)) 81 | .column((col.clone(), PgAttribute::AttName)) 82 | .from(PgIndex::Table) 83 | .join_as( 84 | JoinType::Join, 85 | PgClass::Table, 86 | idx.clone(), 87 | Expr::col((idx.clone(), PgClass::Oid)) 88 | .equals((PgIndex::Table, PgIndex::IndexRelId)), 89 | ) 90 | .join_as( 91 | JoinType::Join, 92 | PgNamespace::Table, 93 | insp.clone(), 94 | Expr::col((insp.clone(), PgNamespace::Oid)) 95 | .equals((idx.clone(), PgClass::RelNamespace)), 96 | ) 97 | .join_as( 98 | JoinType::Join, 99 | PgClass::Table, 100 | tbl.clone(), 101 | Expr::col((tbl.clone(), PgClass::Oid)).equals((PgIndex::Table, PgIndex::IndRelId)), 102 | ) 103 | .join_as( 104 | JoinType::Join, 105 | PgNamespace::Table, 106 | tnsp.clone(), 107 | Expr::col((tnsp.clone(), PgNamespace::Oid)) 108 | .equals((tbl.clone(), PgClass::RelNamespace)), 109 | ) 110 | .join_as( 111 | JoinType::Join, 112 | PgAttribute::Table, 113 | col.clone(), 114 | Expr::col((col.clone(), PgAttribute::AttRelId)) 115 | .equals((idx.clone(), PgAttribute::Oid)), 116 | ) 117 | .cond_where( 118 | Condition::all() 119 | .add(Expr::col((PgIndex::Table, PgIndex::IndIsUnique)).eq(true)) 120 | .add(Expr::col((PgIndex::Table, PgIndex::IndIsPrimary)).eq(false)) 121 | .add(Expr::col((tbl.clone(), PgClass::RelName)).eq(table.to_string())) 122 | .add(Expr::col((tnsp.clone(), PgNamespace::NspName)).eq(schema.to_string())), 123 | ) 124 | .order_by((PgIndex::Table, PgIndex::IndexRelId), Order::Asc) 125 | .take() 126 | } 127 | } 128 | 129 | #[cfg(feature = "sqlx-postgres")] 130 | impl From<&PgRow> for UniqueIndexQueryResult { 131 | fn from(row: &PgRow) -> Self { 132 | use crate::sqlx_types::Row; 133 | Self { 134 | index_name: row.get(0), 135 | table_schema: row.get(1), 136 | table_name: row.get(2), 137 | column_name: row.get(3), 138 | } 139 | } 140 | } 141 | 142 | #[cfg(not(feature = "sqlx-postgres"))] 143 | impl From<&PgRow> for UniqueIndexQueryResult { 144 | fn from(_: &PgRow) -> Self { 145 | Self::default() 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/postgres/query/schema.rs: -------------------------------------------------------------------------------- 1 | use sea_query::{Condition, Expr, Iden, JoinType, Query, SelectStatement}; 2 | 3 | #[derive(Debug, Default)] 4 | pub struct SchemaQueryBuilder {} 5 | 6 | #[derive(Debug, Iden)] 7 | /// Ref: https://www.postgresql.org/docs/13/information-schema.html 8 | pub enum InformationSchema { 9 | #[iden = "information_schema"] 10 | Schema, 11 | Columns, 12 | CheckConstraints, 13 | KeyColumnUsage, 14 | ReferentialConstraints, 15 | Tables, 16 | TableConstraints, 17 | ConstraintColumnUsage, 18 | } 19 | 20 | pub(crate) fn select_base_table_and_view() -> SelectStatement { 21 | #[derive(Debug, Iden)] 22 | enum PgClass { 23 | Table, 24 | Relname, 25 | Relkind, 26 | Oid, 27 | } 28 | 29 | #[derive(Debug, Iden)] 30 | enum PgInherits { 31 | Table, 32 | Inhrelid, 33 | } 34 | 35 | Query::select() 36 | .column((PgClass::Table, PgClass::Relname)) 37 | .from(PgInherits::Table) 38 | .join( 39 | JoinType::Join, 40 | PgClass::Table, 41 | Condition::all() 42 | .add( 43 | Expr::col((PgInherits::Table, PgInherits::Inhrelid)) 44 | .equals((PgClass::Table, PgClass::Oid)), 45 | ) 46 | .add( 47 | // List of possible value of the `relkind` column. 48 | // ======== 49 | // r = ordinary table 50 | // i = index 51 | // S = sequence 52 | // t = TOAST table 53 | // v = view 54 | // m = materialized view 55 | // c = composite type 56 | // f = foreign table 57 | // p = partitioned table 58 | // I = partitioned index 59 | // Extracted from https://www.postgresql.org/docs/current/catalog-pg-class.html 60 | // 61 | // We want to select tables and views only. 62 | Expr::col((PgClass::Table, PgClass::Relkind)) 63 | .is_in(["r", "t", "v", "m", "f", "p"]), 64 | ), 65 | ) 66 | .to_owned() 67 | } 68 | -------------------------------------------------------------------------------- /src/postgres/query/table.rs: -------------------------------------------------------------------------------- 1 | use super::{InformationSchema, SchemaQueryBuilder, select_base_table_and_view}; 2 | use crate::sqlx_types::postgres::PgRow; 3 | use sea_query::{Expr, Iden, Query, SeaRc, SelectStatement}; 4 | 5 | #[derive(Debug, sea_query::Iden)] 6 | /// Ref: https://www.postgresql.org/docs/13/infoschema-tables.html 7 | pub enum TablesFields { 8 | TableCatalog, 9 | TableSchema, 10 | TableName, 11 | TableType, 12 | UserDefinedTypeSchema, 13 | UserDefinedTypeName, 14 | // IsInsertableInto is always true for BASE TABLEs 15 | IsInsertableInto, 16 | IsTyped, 17 | } 18 | 19 | #[derive(Debug, sea_query::Iden)] 20 | pub enum TableType { 21 | #[iden = "BASE TABLE"] 22 | BaseTable, 23 | #[iden = "VIEW"] 24 | View, 25 | #[iden = "FOREIGN"] 26 | Foreign, 27 | #[iden = "LOCAL TEMPORARY"] 28 | Temporary, 29 | } 30 | 31 | #[derive(Debug, Default)] 32 | pub struct TableQueryResult { 33 | pub table_name: String, 34 | pub user_defined_type_schema: Option, 35 | pub user_defined_type_name: Option, 36 | } 37 | 38 | impl SchemaQueryBuilder { 39 | pub fn query_tables(&self, schema: SeaRc) -> SelectStatement { 40 | Query::select() 41 | .columns(vec![ 42 | TablesFields::TableName, 43 | TablesFields::UserDefinedTypeSchema, 44 | TablesFields::UserDefinedTypeName, 45 | ]) 46 | .from((InformationSchema::Schema, InformationSchema::Tables)) 47 | .and_where(Expr::col(TablesFields::TableSchema).eq(schema.to_string())) 48 | .and_where(Expr::col(TablesFields::TableType).eq(TableType::BaseTable.to_string())) 49 | .and_where( 50 | Expr::col(TablesFields::TableName).not_in_subquery(select_base_table_and_view()), 51 | ) 52 | .take() 53 | } 54 | } 55 | 56 | #[cfg(feature = "sqlx-postgres")] 57 | impl From<&PgRow> for TableQueryResult { 58 | fn from(row: &PgRow) -> Self { 59 | use crate::sqlx_types::Row; 60 | Self { 61 | table_name: row.get(0), 62 | user_defined_type_schema: row.get(1), 63 | user_defined_type_name: row.get(2), 64 | } 65 | } 66 | } 67 | 68 | #[cfg(not(feature = "sqlx-postgres"))] 69 | impl From<&PgRow> for TableQueryResult { 70 | fn from(_: &PgRow) -> Self { 71 | Self::default() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/postgres/writer/column.rs: -------------------------------------------------------------------------------- 1 | use crate::postgres::def::{ColumnInfo, Type}; 2 | use sea_query::{Alias, ColumnDef, ColumnType, DynIden, IntoIden, PgInterval, RcOrArc, StringLen}; 3 | use std::{convert::TryFrom, fmt::Write}; 4 | 5 | impl ColumnInfo { 6 | pub fn write(&self) -> ColumnDef { 7 | let mut col_info = self.clone(); 8 | let mut extras: Vec = Vec::new(); 9 | if let Some(default) = self.default.as_ref() { 10 | if default.0.starts_with("nextval") { 11 | col_info = Self::convert_to_serial(col_info); 12 | } else { 13 | let mut string = "".to_owned(); 14 | write!(&mut string, "DEFAULT {}", default.0).unwrap(); 15 | extras.push(string); 16 | } 17 | } 18 | let col_type = col_info.write_col_type(); 19 | let mut col_def = ColumnDef::new_with_type(Alias::new(self.name.as_str()), col_type); 20 | if self.is_identity { 21 | col_info = Self::convert_to_serial(col_info); 22 | } 23 | if matches!( 24 | col_info.col_type, 25 | Type::SmallSerial | Type::Serial | Type::BigSerial 26 | ) { 27 | col_def.auto_increment(); 28 | } 29 | if self.not_null.is_some() { 30 | col_def.not_null(); 31 | } 32 | if !extras.is_empty() { 33 | col_def.extra(extras.join(" ")); 34 | } 35 | col_def 36 | } 37 | 38 | fn convert_to_serial(mut col_info: ColumnInfo) -> ColumnInfo { 39 | match col_info.col_type { 40 | Type::SmallInt => { 41 | col_info.col_type = Type::SmallSerial; 42 | } 43 | Type::Integer => { 44 | col_info.col_type = Type::Serial; 45 | } 46 | Type::BigInt => { 47 | col_info.col_type = Type::BigSerial; 48 | } 49 | _ => {} 50 | }; 51 | col_info 52 | } 53 | 54 | pub fn write_col_type(&self) -> ColumnType { 55 | fn write_type(col_type: &Type) -> ColumnType { 56 | match col_type { 57 | Type::SmallInt => ColumnType::SmallInteger, 58 | Type::Integer => ColumnType::Integer, 59 | Type::BigInt => ColumnType::BigInteger, 60 | Type::Decimal(num_attr) | Type::Numeric(num_attr) => { 61 | match (num_attr.precision, num_attr.scale) { 62 | (None, None) => ColumnType::Decimal(None), 63 | (precision, scale) => ColumnType::Decimal(Some(( 64 | precision.unwrap_or(0).into(), 65 | scale.unwrap_or(0).into(), 66 | ))), 67 | } 68 | } 69 | Type::Real => ColumnType::Float, 70 | Type::DoublePrecision => ColumnType::Double, 71 | Type::SmallSerial => ColumnType::SmallInteger, 72 | Type::Serial => ColumnType::Integer, 73 | Type::BigSerial => ColumnType::BigInteger, 74 | Type::Money => ColumnType::Money(None), 75 | Type::Varchar(string_attr) => match string_attr.length { 76 | Some(length) => ColumnType::String(StringLen::N(length.into())), 77 | None => ColumnType::String(StringLen::None), 78 | }, 79 | Type::Char(string_attr) => ColumnType::Char(string_attr.length.map(Into::into)), 80 | Type::Text => ColumnType::Text, 81 | Type::Bytea => ColumnType::VarBinary(StringLen::None), 82 | // The SQL standard requires that writing just timestamp be equivalent to timestamp without time zone, 83 | // and PostgreSQL honors that behavior. (https://www.postgresql.org/docs/current/datatype-datetime.html) 84 | Type::Timestamp(_) => ColumnType::DateTime, 85 | Type::TimestampWithTimeZone(_) => ColumnType::TimestampWithTimeZone, 86 | Type::Date => ColumnType::Date, 87 | Type::Time(_) => ColumnType::Time, 88 | Type::TimeWithTimeZone(_) => ColumnType::Time, 89 | Type::Interval(interval_attr) => { 90 | let field = match &interval_attr.field { 91 | Some(field) => PgInterval::try_from(field).ok(), 92 | None => None, 93 | }; 94 | let precision = interval_attr.precision.map(Into::into); 95 | ColumnType::Interval(field, precision) 96 | } 97 | Type::Boolean => ColumnType::Boolean, 98 | Type::Point => ColumnType::Custom(Alias::new("point").into_iden()), 99 | Type::Line => ColumnType::Custom(Alias::new("line").into_iden()), 100 | Type::Lseg => ColumnType::Custom(Alias::new("lseg").into_iden()), 101 | Type::Box => ColumnType::Custom(Alias::new("box").into_iden()), 102 | Type::Path => ColumnType::Custom(Alias::new("path").into_iden()), 103 | Type::Polygon => ColumnType::Custom(Alias::new("polygon").into_iden()), 104 | Type::Circle => ColumnType::Custom(Alias::new("circle").into_iden()), 105 | Type::Cidr => ColumnType::Custom(Alias::new("cidr").into_iden()), 106 | Type::Inet => ColumnType::Custom(Alias::new("inet").into_iden()), 107 | Type::MacAddr => ColumnType::Custom(Alias::new("macaddr").into_iden()), 108 | Type::MacAddr8 => ColumnType::Custom(Alias::new("macaddr8").into_iden()), 109 | Type::Bit(bit_attr) => ColumnType::Bit(bit_attr.length.map(Into::into)), 110 | Type::VarBit(bit_attr) => ColumnType::VarBit(bit_attr.length.unwrap_or(1).into()), 111 | Type::TsVector => ColumnType::Custom(Alias::new("tsvector").into_iden()), 112 | Type::TsQuery => ColumnType::Custom(Alias::new("tsquery").into_iden()), 113 | Type::Uuid => ColumnType::Uuid, 114 | Type::Xml => ColumnType::Custom(Alias::new("xml").into_iden()), 115 | Type::Json => ColumnType::Json, 116 | Type::JsonBinary => ColumnType::JsonBinary, 117 | Type::Int4Range => ColumnType::Custom(Alias::new("int4range").into_iden()), 118 | Type::Int8Range => ColumnType::Custom(Alias::new("int8range").into_iden()), 119 | Type::NumRange => ColumnType::Custom(Alias::new("numrange").into_iden()), 120 | Type::TsRange => ColumnType::Custom(Alias::new("tsrange").into_iden()), 121 | Type::TsTzRange => ColumnType::Custom(Alias::new("tstzrange").into_iden()), 122 | Type::DateRange => ColumnType::Custom(Alias::new("daterange").into_iden()), 123 | Type::PgLsn => ColumnType::Custom(Alias::new("pg_lsn").into_iden()), 124 | #[cfg(feature = "postgres-vector")] 125 | Type::Vector(vector_attr) => match vector_attr.length { 126 | Some(length) => ColumnType::Vector(Some(length)), 127 | None => ColumnType::Vector(None), 128 | }, 129 | Type::Unknown(s) => ColumnType::Custom(Alias::new(s).into_iden()), 130 | Type::Enum(enum_def) => { 131 | let name = Alias::new(&enum_def.typename).into_iden(); 132 | let variants: Vec = enum_def 133 | .values 134 | .iter() 135 | .map(|variant| Alias::new(variant).into_iden()) 136 | .collect(); 137 | ColumnType::Enum { name, variants } 138 | } 139 | Type::Array(array_def) => ColumnType::Array(RcOrArc::new(write_type( 140 | array_def.col_type.as_ref().expect("Array type not defined"), 141 | ))), 142 | } 143 | } 144 | write_type(&self.col_type) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/postgres/writer/constraints.rs: -------------------------------------------------------------------------------- 1 | use crate::postgres::def::{ForeignKeyAction, PrimaryKey, References, Unique}; 2 | use sea_query::{Alias, ForeignKey, ForeignKeyCreateStatement, Index, IndexCreateStatement}; 3 | 4 | impl PrimaryKey { 5 | pub fn write(&self) -> IndexCreateStatement { 6 | let mut idx = Index::create(); 7 | idx.primary().name(&self.name); 8 | for col in self.columns.iter() { 9 | idx.col(Alias::new(col)); 10 | } 11 | idx.take() 12 | } 13 | } 14 | 15 | impl Unique { 16 | pub fn write(&self) -> IndexCreateStatement { 17 | let mut idx = Index::create(); 18 | idx.unique().name(&self.name); 19 | for col in self.columns.iter() { 20 | idx.col(Alias::new(col)); 21 | } 22 | idx.take() 23 | } 24 | } 25 | 26 | impl References { 27 | pub fn write(&self) -> ForeignKeyCreateStatement { 28 | let mut key = ForeignKey::create(); 29 | key.name(&self.name); 30 | key.to_tbl(Alias::new(&self.table)); 31 | for column in self.columns.iter() { 32 | key.from_col(Alias::new(column.as_str())); 33 | } 34 | for ref_col in self.foreign_columns.iter() { 35 | key.to_col(Alias::new(ref_col.as_str())); 36 | } 37 | if let Some(on_update) = &self.on_update { 38 | key.on_update(match on_update { 39 | ForeignKeyAction::Cascade => sea_query::ForeignKeyAction::Cascade, 40 | ForeignKeyAction::SetNull => sea_query::ForeignKeyAction::SetNull, 41 | ForeignKeyAction::SetDefault => sea_query::ForeignKeyAction::SetDefault, 42 | ForeignKeyAction::Restrict => sea_query::ForeignKeyAction::Restrict, 43 | ForeignKeyAction::NoAction => sea_query::ForeignKeyAction::NoAction, 44 | }); 45 | } 46 | if let Some(on_delete) = &self.on_delete { 47 | key.on_delete(match on_delete { 48 | ForeignKeyAction::Cascade => sea_query::ForeignKeyAction::Cascade, 49 | ForeignKeyAction::SetNull => sea_query::ForeignKeyAction::SetNull, 50 | ForeignKeyAction::SetDefault => sea_query::ForeignKeyAction::SetDefault, 51 | ForeignKeyAction::Restrict => sea_query::ForeignKeyAction::Restrict, 52 | ForeignKeyAction::NoAction => sea_query::ForeignKeyAction::NoAction, 53 | }); 54 | } 55 | key.take() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/postgres/writer/enumeration.rs: -------------------------------------------------------------------------------- 1 | use crate::postgres::def::EnumDef; 2 | use sea_query::{ 3 | Alias, 4 | extension::postgres::{Type, TypeCreateStatement}, 5 | }; 6 | 7 | impl EnumDef { 8 | /// Converts the [EnumDef] to a [TypeCreateStatement] 9 | pub fn write(&self) -> TypeCreateStatement { 10 | Type::create() 11 | .as_enum(Alias::new(self.typename.as_str())) 12 | .values(self.values.iter().map(|val| Alias::new(val.as_str()))) 13 | .to_owned() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/postgres/writer/mod.rs: -------------------------------------------------------------------------------- 1 | mod column; 2 | mod constraints; 3 | mod enumeration; 4 | mod schema; 5 | mod table; 6 | mod types; 7 | 8 | use super::def::Schema; 9 | use sea_query::TableCreateStatement; 10 | 11 | impl Schema { 12 | pub fn write(&self) -> Vec { 13 | self.tables.iter().map(|table| table.write()).collect() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/postgres/writer/schema.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/postgres/writer/table.rs: -------------------------------------------------------------------------------- 1 | use crate::postgres::def::TableDef; 2 | use sea_query::{Alias, Table, TableCreateStatement}; 3 | 4 | impl TableDef { 5 | pub fn write(&self) -> TableCreateStatement { 6 | let mut table = Table::create(); 7 | table.table(Alias::new(&self.info.name)); 8 | for col in self.columns.iter() { 9 | table.col(col.write()); 10 | } 11 | for primary_key in self.primary_key_constraints.iter() { 12 | table.primary_key(&mut primary_key.write()); 13 | } 14 | for unique in self.unique_constraints.iter() { 15 | table.index(&mut unique.write()); 16 | } 17 | for reference in self.reference_constraints.iter() { 18 | table.foreign_key(&mut reference.write()); 19 | } 20 | table 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/postgres/writer/types.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/probe.rs: -------------------------------------------------------------------------------- 1 | use sea_query::{Condition, Expr, Iden, Query, SelectStatement, SimpleExpr}; 2 | 3 | pub trait SchemaProbe { 4 | fn get_current_schema() -> SimpleExpr; 5 | 6 | fn query_tables(&self) -> SelectStatement; 7 | 8 | fn has_table(&self, table: T) -> SelectStatement 9 | where 10 | T: AsRef, 11 | { 12 | let mut subquery = self.query_tables(); 13 | subquery.cond_where(Expr::col(Schema::TableName).eq(table.as_ref())); 14 | Query::select() 15 | .expr_as(Expr::cust("COUNT(*) > 0"), Has::Table) 16 | .from_subquery(subquery, Subquery) 17 | .take() 18 | } 19 | 20 | fn has_column(&self, table: T, column: C) -> SelectStatement 21 | where 22 | T: AsRef, 23 | C: AsRef, 24 | { 25 | Query::select() 26 | .expr_as(Expr::cust("COUNT(*) > 0"), Has::Column) 27 | .from((Schema::Info, Schema::Columns)) 28 | .cond_where( 29 | Condition::all() 30 | .add( 31 | Expr::expr(Self::get_current_schema()) 32 | .equals((Schema::Columns, Schema::TableSchema)), 33 | ) 34 | .add(Expr::col(Schema::TableName).eq(table.as_ref())) 35 | .add(Expr::col(Schema::ColumnName).eq(column.as_ref())), 36 | ) 37 | .take() 38 | } 39 | 40 | fn has_index(&self, table: T, index: C) -> SelectStatement 41 | where 42 | T: AsRef, 43 | C: AsRef; 44 | } 45 | 46 | #[derive(Debug, Iden)] 47 | pub enum Has { 48 | #[iden = "has_table"] 49 | Table, 50 | #[iden = "has_column"] 51 | Column, 52 | #[iden = "has_index"] 53 | Index, 54 | } 55 | 56 | #[allow(clippy::enum_variant_names)] 57 | #[derive(Debug, Iden)] 58 | pub(crate) enum Schema { 59 | #[iden = "information_schema"] 60 | Info, 61 | Columns, 62 | TableName, 63 | ColumnName, 64 | TableSchema, 65 | } 66 | 67 | #[derive(Debug, Iden)] 68 | struct Subquery; 69 | -------------------------------------------------------------------------------- /src/sqlite/def/column.rs: -------------------------------------------------------------------------------- 1 | use super::DefaultType; 2 | use sea_query::{ 3 | Alias, ColumnType, Index, IndexCreateStatement, 4 | foreign_key::ForeignKeyAction as SeaQueryForeignKeyAction, 5 | }; 6 | use std::num::ParseIntError; 7 | 8 | #[allow(unused_imports)] 9 | use crate::sqlx_types::{Row, sqlite::SqliteRow}; 10 | 11 | /// An SQLite column definition 12 | #[derive(Debug, PartialEq, Clone)] 13 | pub struct ColumnInfo { 14 | pub cid: i32, 15 | pub name: String, 16 | pub r#type: ColumnType, 17 | pub not_null: bool, 18 | pub default_value: DefaultType, 19 | pub primary_key: bool, 20 | } 21 | 22 | #[cfg(feature = "sqlx-sqlite")] 23 | impl ColumnInfo { 24 | /// Map an [SqliteRow] into a column definition type [ColumnInfo] 25 | pub fn to_column_def(row: &SqliteRow) -> Result { 26 | let col_not_null: i8 = row.get(3); 27 | let is_pk: i8 = row.get(5); 28 | let default_value: &str = row.get(4); 29 | Ok(ColumnInfo { 30 | cid: row.get(0), 31 | name: row.get(1), 32 | r#type: super::parse_type(row.get(2))?, 33 | not_null: col_not_null != 0, 34 | default_value: if default_value == "NULL" { 35 | DefaultType::Null 36 | } else if default_value.is_empty() { 37 | DefaultType::Unspecified 38 | } else { 39 | let value = default_value.to_owned().replace('\'', ""); 40 | 41 | if let Ok(is_int) = value.parse::() { 42 | DefaultType::Integer(is_int) 43 | } else if let Ok(is_float) = value.parse::() { 44 | DefaultType::Float(is_float) 45 | } else if value == "CURRENT_TIMESTAMP" { 46 | DefaultType::CurrentTimestamp 47 | } else { 48 | DefaultType::String(value) 49 | } 50 | }, 51 | primary_key: is_pk != 0, 52 | }) 53 | } 54 | } 55 | 56 | #[cfg(not(feature = "sqlx-sqlite"))] 57 | impl ColumnInfo { 58 | pub fn to_column_def(_: &SqliteRow) -> Result { 59 | unimplemented!() 60 | } 61 | } 62 | 63 | /// Maps the index and all columns in the index which is the result of queries 64 | /// `PRAGMA index_list(table_name)` and 65 | /// `SELECT * FROM sqlite_master where name = 'index_name'` 66 | #[derive(Debug, Default, Clone)] 67 | pub struct IndexInfo { 68 | /// Is it a SQLindex 69 | pub r#type: String, 70 | pub index_name: String, 71 | pub table_name: String, 72 | pub unique: bool, 73 | pub origin: String, 74 | pub partial: i32, 75 | pub columns: Vec, 76 | } 77 | 78 | impl IndexInfo { 79 | /// Write all the discovered index into a [IndexCreateStatement] 80 | pub fn write(&self) -> IndexCreateStatement { 81 | let mut new_index = Index::create(); 82 | // The name is only correct if the index was created by a CREATE INDEX statement. Otherwise it's autogenerated, so we drop it. 83 | if self.origin.as_str() == "c" { 84 | new_index.name(&self.index_name); 85 | } 86 | new_index.table(Alias::new(&self.table_name)); 87 | 88 | if self.unique { 89 | new_index.unique(); 90 | } 91 | 92 | self.columns.iter().for_each(|column| { 93 | new_index.col(Alias::new(column)); 94 | }); 95 | 96 | new_index 97 | } 98 | } 99 | 100 | /// Maps the index all columns as a result of using query 101 | /// `PRAGMA index_list(table_name)` 102 | #[allow(dead_code)] 103 | #[derive(Debug, Default, Clone)] 104 | pub(crate) struct PartialIndexInfo { 105 | pub(crate) seq: i32, 106 | pub(crate) name: String, 107 | pub(crate) unique: bool, 108 | pub(crate) origin: String, 109 | pub(crate) partial: i32, 110 | } 111 | 112 | #[cfg(feature = "sqlx-sqlite")] 113 | impl From<&SqliteRow> for PartialIndexInfo { 114 | fn from(row: &SqliteRow) -> Self { 115 | let is_unique: i8 = row.get(2); 116 | Self { 117 | seq: row.get(0), 118 | name: row.get(1), 119 | unique: is_unique != 0, 120 | origin: row.get(3), 121 | partial: row.get(4), 122 | } 123 | } 124 | } 125 | 126 | #[cfg(not(feature = "sqlx-sqlite"))] 127 | impl From<&SqliteRow> for PartialIndexInfo { 128 | fn from(_: &SqliteRow) -> Self { 129 | Self::default() 130 | } 131 | } 132 | 133 | /// Maps all the columns in an index as a result of using query 134 | /// `SELECT * FROM sqlite_master where name = 'index_name'` 135 | #[allow(dead_code)] 136 | #[derive(Debug, Default, Clone)] 137 | pub(crate) struct IndexedColumns { 138 | pub(crate) r#type: String, 139 | pub(crate) name: String, 140 | pub(crate) table: String, 141 | pub(crate) root_page: i32, 142 | pub(crate) indexed_columns: Vec, 143 | } 144 | 145 | #[cfg(feature = "sqlx-sqlite")] 146 | impl From<(&SqliteRow, &[SqliteRow])> for IndexedColumns { 147 | fn from((row, rows): (&SqliteRow, &[SqliteRow])) -> Self { 148 | let columns_to_index = rows.iter().map(|row| row.get(2)).collect::>(); 149 | 150 | Self { 151 | r#type: row.get(0), 152 | name: row.get(1), 153 | table: row.get(2), 154 | root_page: row.get(3), 155 | indexed_columns: columns_to_index, 156 | } 157 | } 158 | } 159 | 160 | #[cfg(not(feature = "sqlx-sqlite"))] 161 | impl From<(&SqliteRow, &[SqliteRow])> for IndexedColumns { 162 | fn from(_: (&SqliteRow, &[SqliteRow])) -> Self { 163 | Self::default() 164 | } 165 | } 166 | 167 | /// Confirms if a table's primary key is set to autoincrement as a result of using query 168 | /// `SELECT COUNT(*) from sqlite_sequence where name = 'table_name'; 169 | #[allow(dead_code)] 170 | #[derive(Debug, Default, Clone)] 171 | pub(crate) struct PrimaryKeyAutoincrement(pub(crate) u8); 172 | 173 | #[cfg(feature = "sqlx-sqlite")] 174 | impl From<&SqliteRow> for PrimaryKeyAutoincrement { 175 | fn from(row: &SqliteRow) -> Self { 176 | Self(row.get(0)) 177 | } 178 | } 179 | 180 | #[cfg(not(feature = "sqlx-sqlite"))] 181 | impl From<&SqliteRow> for PrimaryKeyAutoincrement { 182 | fn from(_: &SqliteRow) -> Self { 183 | Self::default() 184 | } 185 | } 186 | 187 | /// Indexes the foreign keys 188 | #[allow(dead_code)] 189 | #[derive(Debug, Default, Clone)] 190 | pub struct ForeignKeysInfo { 191 | pub(crate) id: i32, 192 | pub(crate) seq: i32, 193 | pub(crate) table: String, 194 | pub(crate) from: Vec, 195 | pub(crate) to: Vec, 196 | pub(crate) on_update: ForeignKeyAction, 197 | pub(crate) on_delete: ForeignKeyAction, 198 | pub(crate) r#match: MatchAction, 199 | } 200 | 201 | #[cfg(feature = "sqlx-sqlite")] 202 | impl From<&SqliteRow> for ForeignKeysInfo { 203 | fn from(row: &SqliteRow) -> Self { 204 | Self { 205 | id: row.get(0), 206 | seq: row.get(1), 207 | table: row.get(2), 208 | from: vec![row.get(3)], 209 | to: vec![row.get(4)], 210 | on_update: { 211 | let op: &str = row.get(5); 212 | op.into() 213 | }, 214 | on_delete: { 215 | let op: &str = row.get(6); 216 | op.into() 217 | }, 218 | r#match: { 219 | let op: &str = row.get(7); 220 | op.into() 221 | }, 222 | } 223 | } 224 | } 225 | 226 | #[cfg(not(feature = "sqlx-sqlite"))] 227 | impl From<&SqliteRow> for ForeignKeysInfo { 228 | fn from(_: &SqliteRow) -> Self { 229 | Self::default() 230 | } 231 | } 232 | 233 | /// Indexes the actions performed on the foreign keys of a table 234 | #[derive(Debug, PartialEq, Eq, Clone)] 235 | pub enum ForeignKeyAction { 236 | NoAction, 237 | Restrict, 238 | SetNull, 239 | SetDefault, 240 | Cascade, 241 | } 242 | 243 | impl Default for ForeignKeyAction { 244 | fn default() -> Self { 245 | Self::NoAction 246 | } 247 | } 248 | 249 | impl From<&str> for ForeignKeyAction { 250 | fn from(action: &str) -> Self { 251 | match action { 252 | "NO ACTION" => Self::NoAction, 253 | "RESTRICT" => Self::Restrict, 254 | "SET NULL" => Self::SetNull, 255 | "SET DEFAULT" => Self::SetDefault, 256 | "CASCADE" => Self::Cascade, 257 | _ => Self::NoAction, 258 | } 259 | } 260 | } 261 | 262 | impl ForeignKeyAction { 263 | pub(crate) fn to_seaquery_foreign_key_action(&self) -> SeaQueryForeignKeyAction { 264 | match self { 265 | Self::NoAction => SeaQueryForeignKeyAction::NoAction, 266 | Self::Restrict => SeaQueryForeignKeyAction::Restrict, 267 | Self::SetNull => SeaQueryForeignKeyAction::SetNull, 268 | Self::SetDefault => SeaQueryForeignKeyAction::SetDefault, 269 | Self::Cascade => SeaQueryForeignKeyAction::Cascade, 270 | } 271 | } 272 | } 273 | 274 | /// Maps to the SQLite `MATCH` actions 275 | #[derive(Debug, PartialEq, Eq, Clone)] 276 | pub enum MatchAction { 277 | Simple, 278 | Partial, 279 | Full, 280 | None, 281 | } 282 | 283 | impl Default for MatchAction { 284 | fn default() -> Self { 285 | Self::None 286 | } 287 | } 288 | 289 | impl From<&str> for MatchAction { 290 | fn from(action: &str) -> Self { 291 | match action { 292 | "MATCH SIMPLE" => Self::Simple, 293 | "MATCH PARTIAL" => Self::Partial, 294 | "MATCH FULL" => Self::Full, 295 | "MATCH NONE" => Self::None, 296 | _ => Self::None, 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/sqlite/def/mod.rs: -------------------------------------------------------------------------------- 1 | mod column; 2 | mod schema; 3 | mod table; 4 | mod types; 5 | 6 | pub use column::*; 7 | pub use schema::*; 8 | pub use table::*; 9 | pub use types::*; 10 | -------------------------------------------------------------------------------- /src/sqlite/def/schema.rs: -------------------------------------------------------------------------------- 1 | use super::{IndexInfo, TableDef}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Schema { 5 | pub tables: Vec, 6 | pub indexes: Vec, 7 | } 8 | 9 | impl Schema { 10 | pub fn merge_indexes_into_table(mut self) -> Self { 11 | for table in self.tables.iter_mut() { 12 | for index in self.indexes.iter() { 13 | if index.unique && index.table_name == table.name { 14 | table.constraints.push(index.clone()); 15 | } 16 | } 17 | } 18 | self 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/sqlite/def/types.rs: -------------------------------------------------------------------------------- 1 | use sea_query::{ColumnType, StringLen}; 2 | use std::num::ParseIntError; 3 | 4 | pub fn parse_type(data_type: &str) -> Result { 5 | let mut type_name = data_type; 6 | let mut parts: Vec = Vec::new(); 7 | if let Some((prefix, suffix)) = data_type.split_once('(') { 8 | if let Some(suffix) = suffix.strip_suffix(')') { 9 | type_name = prefix; 10 | for part in suffix.split(',') { 11 | if let Ok(part) = part.trim().parse() { 12 | parts.push(part); 13 | } else { 14 | break; 15 | } 16 | } 17 | } 18 | } 19 | Ok(match type_name.to_lowercase().as_str() { 20 | "char" => ColumnType::Char(parts.into_iter().next()), 21 | "varchar" => ColumnType::String(match parts.into_iter().next() { 22 | Some(length) => StringLen::N(length), 23 | None => StringLen::None, 24 | }), 25 | "text" => ColumnType::Text, 26 | "tinyint" => ColumnType::TinyInteger, 27 | "smallint" => ColumnType::SmallInteger, 28 | "int" | "integer" => ColumnType::Integer, 29 | "bigint" => ColumnType::BigInteger, 30 | "float" => ColumnType::Float, 31 | "double" => ColumnType::Double, 32 | "decimal" | "real" => ColumnType::Decimal(if parts.len() == 2 { 33 | Some((parts[0], parts[1])) 34 | } else { 35 | None 36 | }), 37 | "datetime_text" => ColumnType::DateTime, 38 | "timestamp" | "timestamp_text" => ColumnType::Timestamp, 39 | "timestamp_with_timezone_text" => ColumnType::TimestampWithTimeZone, 40 | "time_text" => ColumnType::Time, 41 | "date_text" => ColumnType::Date, 42 | "blob" => { 43 | if parts.len() == 1 { 44 | ColumnType::Binary(parts[0]) 45 | } else { 46 | ColumnType::Blob 47 | } 48 | } 49 | "varbinary_blob" if parts.len() == 1 => { 50 | ColumnType::VarBinary(match parts.into_iter().next() { 51 | Some(length) => StringLen::N(length), 52 | None => StringLen::None, 53 | }) 54 | } 55 | "boolean" => ColumnType::Boolean, 56 | "real_money" => ColumnType::Money(if parts.len() == 2 { 57 | Some((parts[0], parts[1])) 58 | } else { 59 | None 60 | }), 61 | "json_text" => ColumnType::Json, 62 | "jsonb_text" => ColumnType::JsonBinary, 63 | "uuid_text" => ColumnType::Uuid, 64 | _ => ColumnType::custom(data_type), 65 | }) 66 | } 67 | 68 | /// The default types for an SQLite `dflt_value` 69 | #[derive(Debug, PartialEq, Clone)] 70 | pub enum DefaultType { 71 | Integer(i32), 72 | Float(f32), 73 | String(String), 74 | Null, 75 | Unspecified, 76 | CurrentTimestamp, 77 | } 78 | -------------------------------------------------------------------------------- /src/sqlite/discovery.rs: -------------------------------------------------------------------------------- 1 | use sea_query::{Alias, Expr, SelectStatement}; 2 | 3 | use super::def::{IndexInfo, Schema, TableDef}; 4 | pub use super::error::DiscoveryResult; 5 | use super::executor::{Executor, IntoExecutor}; 6 | use super::query::SqliteMaster; 7 | use crate::sqlx_types::SqlitePool; 8 | 9 | /// Performs all the methods for schema discovery of a SQLite database 10 | pub struct SchemaDiscovery { 11 | pub executor: Executor, 12 | } 13 | 14 | impl SchemaDiscovery { 15 | /// Instantiate a new database connection to the database specified 16 | pub fn new(sqlite_pool: SqlitePool) -> Self { 17 | SchemaDiscovery { 18 | executor: sqlite_pool.into_executor(), 19 | } 20 | } 21 | 22 | /// Discover all the tables in a SQLite database 23 | pub async fn discover(&self) -> DiscoveryResult { 24 | let get_tables = SelectStatement::new() 25 | .column(Alias::new("name")) 26 | .from(SqliteMaster) 27 | .and_where(Expr::col(Alias::new("type")).eq("table")) 28 | .and_where(Expr::col(Alias::new("name")).ne("sqlite_sequence")) 29 | .to_owned(); 30 | 31 | let mut tables = Vec::new(); 32 | for row in self.executor.fetch_all(get_tables).await? { 33 | let mut table: TableDef = (&row).into(); 34 | table.pk_is_autoincrement(&self.executor).await?; 35 | table.get_foreign_keys(&self.executor).await?; 36 | table.get_column_info(&self.executor).await?; 37 | table.get_constraints(&self.executor).await?; 38 | tables.push(table); 39 | } 40 | 41 | let indexes = self.discover_indexes().await?; 42 | 43 | Ok(Schema { tables, indexes }) 44 | } 45 | 46 | /// Discover table indexes 47 | pub async fn discover_indexes(&self) -> DiscoveryResult> { 48 | let get_tables = SelectStatement::new() 49 | .column(Alias::new("name")) 50 | .from(SqliteMaster) 51 | .and_where(Expr::col(Alias::new("type")).eq("table")) 52 | .and_where(Expr::col(Alias::new("name")).ne("sqlite_sequence")) 53 | .to_owned(); 54 | 55 | let mut discovered_indexes = Vec::new(); 56 | let rows = self.executor.fetch_all(get_tables).await?; 57 | for row in rows { 58 | let mut table: TableDef = (&row).into(); 59 | table.get_indexes(&self.executor).await?; 60 | discovered_indexes.append(&mut table.indexes); 61 | } 62 | 63 | Ok(discovered_indexes) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/sqlite/error.rs: -------------------------------------------------------------------------------- 1 | use std::num::{ParseFloatError, ParseIntError}; 2 | 3 | use crate::sqlx_types::SqlxError; 4 | 5 | /// This type simplifies error handling 6 | pub type DiscoveryResult = Result; 7 | 8 | /// All the errors that can be encountered when using this module 9 | #[derive(Debug)] 10 | pub enum SqliteDiscoveryError { 11 | /// An error parsing a string from the result of an SQLite query into an rust-language integer 12 | ParseIntError, 13 | /// An error parsing a string from the result of an SQLite query into an rust-language float 14 | ParseFloatError, 15 | /// The error as defined in [SqlxError] 16 | SqlxError(SqlxError), 17 | /// An operation to discover the indexes in a table was invoked 18 | /// but the target table contains no indexes 19 | NoIndexesFound, 20 | } 21 | 22 | impl From for SqliteDiscoveryError { 23 | fn from(_: ParseIntError) -> Self { 24 | SqliteDiscoveryError::ParseIntError 25 | } 26 | } 27 | 28 | impl From for SqliteDiscoveryError { 29 | fn from(_: ParseFloatError) -> Self { 30 | SqliteDiscoveryError::ParseFloatError 31 | } 32 | } 33 | 34 | impl From for SqliteDiscoveryError { 35 | fn from(error: SqlxError) -> Self { 36 | SqliteDiscoveryError::SqlxError(error) 37 | } 38 | } 39 | 40 | impl std::error::Error for SqliteDiscoveryError {} 41 | 42 | impl std::fmt::Display for SqliteDiscoveryError { 43 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 44 | match self { 45 | SqliteDiscoveryError::ParseIntError => write!(f, "Parse Integer Error"), 46 | SqliteDiscoveryError::ParseFloatError => write!(f, "Parse Float Error Error"), 47 | SqliteDiscoveryError::SqlxError(e) => write!(f, "SQLx Error: {:?}", e), 48 | SqliteDiscoveryError::NoIndexesFound => write!(f, "No Indexes Found Error"), 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/sqlite/executor/mock.rs: -------------------------------------------------------------------------------- 1 | use crate::sqlx_types::{SqlitePool, sqlite::SqliteRow}; 2 | use sea_query::{SelectStatement, SqliteQueryBuilder}; 3 | 4 | use crate::{debug_print, sqlx_types::SqlxError}; 5 | 6 | #[allow(dead_code)] 7 | pub struct Executor { 8 | pool: SqlitePool, 9 | } 10 | 11 | pub trait IntoExecutor { 12 | fn into_executor(self) -> Executor; 13 | } 14 | 15 | impl IntoExecutor for SqlitePool { 16 | fn into_executor(self) -> Executor { 17 | Executor { pool: self } 18 | } 19 | } 20 | 21 | impl Executor { 22 | pub async fn fetch_all(&self, select: SelectStatement) -> Result, SqlxError> { 23 | let (_sql, _values) = select.build(SqliteQueryBuilder); 24 | debug_print!("{}, {:?}", _sql, _values); 25 | 26 | panic!("This is a mock Executor"); 27 | } 28 | 29 | pub async fn fetch_one(&self, select: SelectStatement) -> Result { 30 | let (_sql, _values) = select.build(SqliteQueryBuilder); 31 | debug_print!("{}, {:?}", _sql, _values); 32 | 33 | panic!("This is a mock Executor"); 34 | } 35 | 36 | pub async fn fetch_all_raw(&self, _sql: String) -> Result, SqlxError> { 37 | debug_print!("{}", _sql); 38 | 39 | panic!("This is a mock Executor"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/sqlite/executor/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "sqlx-sqlite")] 2 | mod real; 3 | #[cfg(feature = "sqlx-sqlite")] 4 | pub use real::*; 5 | 6 | #[cfg(not(feature = "sqlx-sqlite"))] 7 | mod mock; 8 | #[cfg(not(feature = "sqlx-sqlite"))] 9 | pub use mock::*; 10 | -------------------------------------------------------------------------------- /src/sqlite/executor/real.rs: -------------------------------------------------------------------------------- 1 | use sea_query::{SelectStatement, SqliteQueryBuilder}; 2 | use sea_query_binder::SqlxBinder; 3 | use sqlx::{SqlitePool, sqlite::SqliteRow}; 4 | 5 | use crate::{debug_print, sqlx_types::SqlxError}; 6 | 7 | pub struct Executor { 8 | pool: SqlitePool, 9 | } 10 | 11 | pub trait IntoExecutor { 12 | fn into_executor(self) -> Executor; 13 | } 14 | 15 | impl IntoExecutor for SqlitePool { 16 | fn into_executor(self) -> Executor { 17 | Executor { pool: self } 18 | } 19 | } 20 | 21 | impl Executor { 22 | pub async fn fetch_all(&self, select: SelectStatement) -> Result, SqlxError> { 23 | let (sql, values) = select.build_sqlx(SqliteQueryBuilder); 24 | debug_print!("{}, {:?}", sql, values); 25 | 26 | sqlx::query_with(&sql, values) 27 | .fetch_all(&mut *self.pool.acquire().await?) 28 | .await 29 | } 30 | 31 | pub async fn fetch_one(&self, select: SelectStatement) -> Result { 32 | let (sql, values) = select.build_sqlx(SqliteQueryBuilder); 33 | debug_print!("{}, {:?}", sql, values); 34 | 35 | sqlx::query_with(&sql, values) 36 | .fetch_one(&mut *self.pool.acquire().await?) 37 | .await 38 | } 39 | 40 | pub async fn fetch_all_raw(&self, sql: String) -> Result, SqlxError> { 41 | debug_print!("{}", sql); 42 | 43 | sqlx::query(&sql) 44 | .fetch_all(&mut *self.pool.acquire().await?) 45 | .await 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/sqlite/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module handles discovery of a schema from an SQLite database. 2 | //! Note that only the types specified by official 3 | //! [SQLite documentation](https://www.sqlite.org/datatype3.html) are discovered. 4 | 5 | pub struct Sqlite; 6 | 7 | #[cfg(feature = "def")] 8 | #[cfg_attr(docsrs, doc(cfg(feature = "def")))] 9 | pub mod def; 10 | 11 | #[cfg(feature = "discovery")] 12 | #[cfg_attr(docsrs, doc(cfg(feature = "discovery")))] 13 | pub mod discovery; 14 | 15 | mod error; 16 | mod executor; 17 | 18 | #[cfg(feature = "query")] 19 | #[cfg_attr(docsrs, doc(cfg(feature = "query")))] 20 | pub mod query; 21 | 22 | #[cfg(feature = "probe")] 23 | #[cfg_attr(docsrs, doc(cfg(feature = "probe")))] 24 | pub mod probe; 25 | -------------------------------------------------------------------------------- /src/sqlite/probe.rs: -------------------------------------------------------------------------------- 1 | use sea_query::{Condition, Expr, Query, SelectStatement, SimpleExpr}; 2 | 3 | use super::Sqlite; 4 | use super::query::{SqliteMaster, SqliteSchema}; 5 | use crate::probe::{Has, Schema, SchemaProbe}; 6 | 7 | impl SchemaProbe for Sqlite { 8 | fn get_current_schema() -> SimpleExpr { 9 | unimplemented!() 10 | } 11 | 12 | fn query_tables(&self) -> SelectStatement { 13 | Query::select() 14 | .expr_as(Expr::col(SqliteSchema::Name), Schema::TableName) 15 | .from(SqliteMaster) 16 | .cond_where( 17 | Condition::all() 18 | .add(Expr::col(SqliteSchema::Type).eq("table")) 19 | .add(Expr::col(SqliteSchema::Name).ne("sqlite_sequence")), 20 | ) 21 | .take() 22 | } 23 | 24 | fn has_column(&self, table: T, column: C) -> SelectStatement 25 | where 26 | T: AsRef, 27 | C: AsRef, 28 | { 29 | Query::select() 30 | .expr(Expr::cust_with_values( 31 | "COUNT(*) > 0 AS \"has_column\" FROM pragma_table_info(?)", 32 | [table.as_ref()], 33 | )) 34 | .and_where(Expr::col(SqliteSchema::Name).eq(column.as_ref())) 35 | .take() 36 | } 37 | 38 | fn has_index(&self, table: T, index: C) -> SelectStatement 39 | where 40 | T: AsRef, 41 | C: AsRef, 42 | { 43 | Query::select() 44 | .expr_as(Expr::cust("COUNT(*) > 0"), Has::Index) 45 | .from(SqliteMaster) 46 | .cond_where( 47 | Condition::all() 48 | .add(Expr::col(SqliteSchema::Type).eq("index")) 49 | .add(Expr::col(SqliteSchema::TblName).eq(table.as_ref())) 50 | .add(Expr::col(SqliteSchema::Name).eq(index.as_ref())), 51 | ) 52 | .take() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/sqlite/query.rs: -------------------------------------------------------------------------------- 1 | use sea_query::Iden; 2 | 3 | #[derive(Debug, Iden)] 4 | pub struct SqliteMaster; 5 | 6 | #[derive(Debug, Iden)] 7 | pub enum SqliteSchema { 8 | Type, 9 | Name, 10 | TblName, 11 | #[iden = "rootpage"] 12 | RootPage, 13 | Sql, 14 | } 15 | -------------------------------------------------------------------------------- /src/sqlx_types/mock.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | pub struct MySqlPool; 4 | 5 | pub mod mysql { 6 | pub struct MySqlRow; 7 | } 8 | 9 | pub struct PgPool; 10 | 11 | pub mod postgres { 12 | pub struct PgRow; 13 | } 14 | 15 | pub struct SqlitePool; 16 | 17 | pub mod sqlite { 18 | pub struct SqliteRow; 19 | } 20 | 21 | pub trait Row {} 22 | 23 | #[derive(Debug)] 24 | pub struct Error; 25 | 26 | #[derive(Debug)] 27 | pub enum SqlxError { 28 | RowNotFound, 29 | } 30 | -------------------------------------------------------------------------------- /src/sqlx_types/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "sqlx-dep")] 2 | mod real; 3 | #[cfg(feature = "sqlx-dep")] 4 | pub use real::*; 5 | 6 | #[cfg(not(feature = "sqlx-dep"))] 7 | mod mock; 8 | #[cfg(not(feature = "sqlx-dep"))] 9 | pub use mock::*; 10 | -------------------------------------------------------------------------------- /src/sqlx_types/real.rs: -------------------------------------------------------------------------------- 1 | pub use sqlx::*; 2 | 3 | pub type SqlxError = sqlx::Error; 4 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | #[cfg(feature = "debug-print")] 3 | macro_rules! debug_print { 4 | ($( $args:expr ),*) => { log::debug!( $( $args ),* ); } 5 | } 6 | 7 | #[macro_export] 8 | // Non-debug version 9 | #[cfg(not(feature = "debug-print"))] 10 | macro_rules! debug_print { 11 | ($( $args:expr ),*) => { 12 | true; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /tests/discovery/mysql/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sea-schema-discovery-test-mysql" 3 | version = "0.1.0" 4 | edition = "2024" 5 | rust-version = "1.85.0" 6 | publish = false 7 | 8 | [dependencies] 9 | async-std = { version = "1.8", features = [ "attributes", "tokio1" ] } 10 | sea-schema = { path = "../../../", default-features = false, features = [ "with-serde", "sqlx-mysql", "runtime-async-std-native-tls", "discovery", "debug-print" ] } 11 | serde_json = { version = "1" } 12 | sqlx = { version = "0.8" } 13 | env_logger = { version = "0" } 14 | log = { version = "0" } -------------------------------------------------------------------------------- /tests/discovery/mysql/Readme.md: -------------------------------------------------------------------------------- 1 | # Run 2 | 3 | ```sh 4 | cargo run > schema.rs 5 | ``` -------------------------------------------------------------------------------- /tests/discovery/mysql/src/main.rs: -------------------------------------------------------------------------------- 1 | use sea_schema::mysql::discovery::SchemaDiscovery; 2 | use sqlx::MySqlPool; 3 | 4 | #[async_std::main] 5 | async fn main() { 6 | // env_logger::builder() 7 | // .filter_level(log::LevelFilter::Debug) 8 | // .is_test(true) 9 | // .init(); 10 | 11 | let url = std::env::var("DATABASE_URL_SAKILA") 12 | .unwrap_or_else(|_| "mysql://root:root@localhost".to_owned()); 13 | 14 | let connection = MySqlPool::connect(&url).await.unwrap(); 15 | 16 | let schema_discovery = SchemaDiscovery::new(connection, "sakila"); 17 | 18 | let schema = schema_discovery.discover().await; 19 | 20 | // println!("{}", serde_json::to_string_pretty(&schema).unwrap()); 21 | 22 | println!("{:#?}", schema); 23 | } 24 | -------------------------------------------------------------------------------- /tests/discovery/postgres/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sea-schema-discovery-test-postgres" 3 | version = "0.1.0" 4 | edition = "2024" 5 | rust-version = "1.85.0" 6 | publish = false 7 | 8 | [dependencies] 9 | async-std = { version = "1.8", features = [ "attributes", "tokio1" ] } 10 | sea-schema = { path = "../../../", default-features = false, features = [ "with-serde", "sqlx-postgres", "runtime-async-std-native-tls", "discovery", "debug-print" ] } 11 | serde_json = { version = "1" } 12 | sqlx = { version = "0.8" } 13 | env_logger = { version = "0" } 14 | log = { version = "0" } -------------------------------------------------------------------------------- /tests/discovery/postgres/Readme.md: -------------------------------------------------------------------------------- 1 | # Run 2 | 3 | ```sh 4 | cargo run > schema.rs 5 | ``` -------------------------------------------------------------------------------- /tests/discovery/postgres/src/main.rs: -------------------------------------------------------------------------------- 1 | use sea_schema::postgres::discovery::SchemaDiscovery; 2 | use sqlx::PgPool; 3 | 4 | #[async_std::main] 5 | async fn main() { 6 | // env_logger::builder() 7 | // .filter_level(log::LevelFilter::Debug) 8 | // .is_test(true) 9 | // .init(); 10 | 11 | let url = std::env::var("DATABASE_URL_SAKILA") 12 | .unwrap_or_else(|_| "postgres://root:root@localhost/sakila".to_owned()); 13 | 14 | let connection = PgPool::connect(&url).await.unwrap(); 15 | 16 | let schema_discovery = SchemaDiscovery::new(connection, "public"); 17 | 18 | let schema = schema_discovery.discover().await; 19 | 20 | // println!("{}", serde_json::to_string_pretty(&schema).unwrap()); 21 | 22 | println!("{:#?}", schema); 23 | } 24 | -------------------------------------------------------------------------------- /tests/discovery/sqlite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sea-schema-discovery-test-sqlite" 3 | version = "0.1.0" 4 | edition = "2024" 5 | rust-version = "1.85.0" 6 | publish = false 7 | 8 | [dependencies] 9 | async-std = { version = "1.8", features = [ "attributes", "tokio1" ] } 10 | sea-schema = { path = "../../../", default-features = false, features = [ "with-serde", "sqlx-sqlite", "runtime-async-std-native-tls", "discovery", "debug-print" ] } 11 | serde_json = { version = "1" } 12 | sqlx = { version = "0.8" } -------------------------------------------------------------------------------- /tests/discovery/sqlite/Readme.md: -------------------------------------------------------------------------------- 1 | # Run 2 | 3 | ```sh 4 | cargo run > schema.rs 5 | ``` -------------------------------------------------------------------------------- /tests/discovery/sqlite/schema.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeaQL/sea-schema/9bee1b1674b1ae31cbd90b3f5f2d32ab65009bca/tests/discovery/sqlite/schema.rs -------------------------------------------------------------------------------- /tests/discovery/sqlite/src/main.rs: -------------------------------------------------------------------------------- 1 | use sea_schema::sqlite::discovery::{DiscoveryResult, SchemaDiscovery}; 2 | use sqlx::SqlitePool; 3 | 4 | #[async_std::main] 5 | async fn main() -> DiscoveryResult<()> { 6 | let url = std::env::var("DATABASE_URL_SAKILA") 7 | .unwrap_or_else(|_| "sqlite://tests/sakila/sqlite/sakila.db".to_owned()); 8 | 9 | let connection = SqlitePool::connect(&url).await.unwrap(); 10 | 11 | let schema_discovery = SchemaDiscovery::new(connection); 12 | 13 | let schema = schema_discovery.discover().await?; 14 | 15 | // println!("{}", serde_json::to_string_pretty(&schema).unwrap()); 16 | 17 | println!("{:#?}", schema); 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /tests/live/mysql/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sea-schema-live-test-mysql" 3 | version = "0.1.0" 4 | edition = "2024" 5 | rust-version = "1.85.0" 6 | publish = false 7 | 8 | [dependencies] 9 | async-std = { version = "1.8", features = [ "attributes", "tokio1" ] } 10 | sea-schema = { path = "../../../", default-features = false, features = [ "sqlx-mysql", "runtime-async-std-native-tls", "discovery", "writer", "debug-print" ] } 11 | serde_json = { version = "1" } 12 | sqlx = { version = "0.8" } 13 | pretty_assertions = { version = "0.7" } 14 | regex = { version = "1" } 15 | env_logger = { version = "0" } 16 | log = { version = "0" } -------------------------------------------------------------------------------- /tests/live/mysql/Readme.md: -------------------------------------------------------------------------------- 1 | # Run 2 | 3 | ```sh 4 | cargo run 5 | ``` -------------------------------------------------------------------------------- /tests/live/postgres/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sea-schema-live-test-postgres" 3 | version = "0.1.0" 4 | edition = "2024" 5 | rust-version = "1.85.0" 6 | publish = false 7 | 8 | [dependencies] 9 | async-std = { version = "1.8", features = [ "attributes", "tokio1" ] } 10 | sea-schema = { path = "../../../", default-features = false, features = [ "sqlx-postgres", "runtime-async-std-native-tls", "discovery", "writer", "debug-print" ] } 11 | serde_json = { version = "1" } 12 | sqlx = { version = "0.8" } 13 | env_logger = { version = "0" } 14 | log = { version = "0" } 15 | pretty_assertions = { version = "0.7" } 16 | -------------------------------------------------------------------------------- /tests/live/postgres/Readme.md: -------------------------------------------------------------------------------- 1 | # Run 2 | 3 | ```sh 4 | cargo run 5 | ``` -------------------------------------------------------------------------------- /tests/live/sqlite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sqlite" 3 | version = "0.1.0" 4 | edition = "2024" 5 | rust-version = "1.85.0" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | async-std = { version = "1.8", features = [ "attributes", "tokio1" ] } 11 | sea-schema = { path = "../../../", default-features = false, features = [ 12 | "runtime-async-std-native-tls", 13 | "discovery", 14 | "writer", 15 | "debug-print", 16 | "parser", 17 | "sqlx-sqlite", 18 | "sqlite", 19 | ] } 20 | serde_json = { version = "1" } 21 | sqlx = { version = "0.8", features = [ 22 | "sqlite", 23 | "runtime-async-std-native-tls", 24 | ] } 25 | pretty_assertions = { version = "0.7" } 26 | env_logger = { version = "0" } 27 | log = { version = "0" } 28 | -------------------------------------------------------------------------------- /tests/sakila/.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored -------------------------------------------------------------------------------- /tests/sakila/.gitignore: -------------------------------------------------------------------------------- 1 | vendor -------------------------------------------------------------------------------- /tests/sakila/Readme.md: -------------------------------------------------------------------------------- 1 | # Import 2 | 3 | First import the the sakila dataset 4 | 5 | # Install 6 | 7 | First install necessary PHP extensions 8 | ```sh 9 | sudo apt install php-mysql php-pgsql 10 | ``` 11 | 12 | Then install dependencies 13 | ```sh 14 | composer install 15 | ``` 16 | 17 | # Run 18 | 19 | ```sh 20 | php index.php > schema.json 21 | ``` 22 | 23 | # Dump 24 | 25 | I can't seem to be able to reproduce the original .sql 26 | The best I got is 27 | 28 | ```sh 29 | mysqldump sakila --no-data --skip-opt --skip-quote-names --skip-set-charset 30 | ``` -------------------------------------------------------------------------------- /tests/sakila/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "doctrine/dbal": "^3.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/sakila/index.php: -------------------------------------------------------------------------------- 1 | 'mysql://sea:sea@localhost/sakila', 7 | // 'url' => 'postgres://sea:sea@localhost/sakila', 8 | ); 9 | $conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams); 10 | 11 | $sm = $conn->getSchemaManager(); 12 | 13 | $sm->getDatabasePlatform()->registerDoctrineTypeMapping('geometry', 'string'); 14 | $sm->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); 15 | $sm->getDatabasePlatform()->registerDoctrineTypeMapping('mpaa_rating', 'string'); 16 | $sm->getDatabasePlatform()->registerDoctrineTypeMapping('_text', 'string'); 17 | 18 | echo json_encode(getSchema($sm), JSON_PRETTY_PRINT)."\n"; 19 | 20 | function getSchema($sm) 21 | { 22 | return [ 23 | 'tables' => array_map('getTable', $sm->listTables()), 24 | ]; 25 | } 26 | 27 | function getTable($table) 28 | { 29 | return [ 30 | 'name' => $table->getName(), 31 | 'columns' => array_map('getColumn', array_values($table->getColumns())), 32 | 'indexes' => array_map('getIndex', array_values($table->getIndexes())), 33 | 'foreignKeys' => array_map('getForeignKey', array_values($table->getForeignKeys())), 34 | ]; 35 | } 36 | 37 | function getColumn($column) 38 | { 39 | return [ 40 | 'name' => $column->getName(), 41 | 'type' => $column->getType()->getName(), 42 | 'notNull' => $column->getNotNull(), 43 | 'default' => $column->getDefault(), 44 | 'length' => $column->getLength(), 45 | 'fixed' => $column->getFixed(), 46 | 'precision' => $column->getPrecision(), 47 | 'scale' => $column->getScale(), 48 | 'unsigned' => $column->getUnsigned(), 49 | 'platformOptions' => $column->getPlatformOptions(), 50 | 'autoincrement' => $column->getAutoincrement(), 51 | 'definition' => $column->getColumnDefinition(), 52 | 'comment' => $column->getComment(), 53 | ]; 54 | } 55 | 56 | function getIndex($index) 57 | { 58 | return [ 59 | 'name' => $index->getName(), 60 | 'columns' => $index->getColumns(), 61 | 'isUnique' => $index->isUnique(), 62 | 'isPrimary' => $index->isPrimary(), 63 | 'flags' => $index->getFlags(), 64 | 'options' => $index->getOptions(), 65 | ]; 66 | } 67 | 68 | function getForeignKey($key) 69 | { 70 | return [ 71 | 'name' => $key->getName(), 72 | 'localTable' => $key->getLocalTableName(), 73 | 'foreignTable' => $key->getForeignTableName(), 74 | 'localColumns' => $key->getLocalColumns(), 75 | 'foreignColumns' => $key->getForeignColumns(), 76 | 'onUpdate' => $key->onUpdate(), 77 | 'onDelete' => $key->onDelete(), 78 | 'options' => removeKeys($key->getOptions(), ['onUpdate', 'onDelete']), 79 | ]; 80 | } 81 | 82 | function removeKeys($arr, $keys) 83 | { 84 | foreach ($keys as $key) { 85 | unset($arr[$key]); 86 | } 87 | return $arr; 88 | } -------------------------------------------------------------------------------- /tests/sakila/phpinfo.php: -------------------------------------------------------------------------------- 1 | schema.sql 5 | ``` -------------------------------------------------------------------------------- /tests/writer/mysql/src/main.rs: -------------------------------------------------------------------------------- 1 | use sea_schema::mysql::discovery::SchemaDiscovery; 2 | use sea_schema::sea_query::MysqlQueryBuilder; 3 | use sqlx::MySqlPool; 4 | 5 | #[async_std::main] 6 | async fn main() { 7 | // env_logger::builder() 8 | // .filter_level(log::LevelFilter::Debug) 9 | // .is_test(true) 10 | // .init(); 11 | 12 | let url = std::env::var("DATABASE_URL_SAKILA") 13 | .unwrap_or_else(|_| "mysql://root:root@localhost".to_owned()); 14 | 15 | let connection = MySqlPool::connect(&url).await.unwrap(); 16 | 17 | let schema_discovery = SchemaDiscovery::new(connection, "sakila"); 18 | 19 | let schema = schema_discovery 20 | .discover() 21 | .await 22 | .expect("Error discovering schema"); 23 | 24 | for table in schema.tables.iter() { 25 | println!("{};", table.write().to_string(MysqlQueryBuilder)); 26 | println!(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/writer/postgres/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sea-schema-writer-test-postgres" 3 | version = "0.1.0" 4 | edition = "2024" 5 | rust-version = "1.85.0" 6 | publish = false 7 | 8 | [dependencies] 9 | pretty_assertions = { version = "0.7" } 10 | async-std = { version = "1.8", features = [ "attributes", "tokio1" ] } 11 | sea-schema = { path = "../../../", default-features = false, features = [ "sqlx-postgres", "runtime-async-std-native-tls", "discovery", "writer", "debug-print" ] } 12 | serde_json = { version = "1" } 13 | sqlx = { version = "0.8" } 14 | env_logger = { version = "0" } 15 | log = { version = "0" } 16 | -------------------------------------------------------------------------------- /tests/writer/postgres/Readme.md: -------------------------------------------------------------------------------- 1 | # Run 2 | 3 | ```sh 4 | cargo run > schema.sql 5 | ``` -------------------------------------------------------------------------------- /tests/writer/postgres/src/main.rs: -------------------------------------------------------------------------------- 1 | use sea_schema::postgres::discovery::SchemaDiscovery; 2 | use sea_schema::sea_query::PostgresQueryBuilder; 3 | use sqlx::PgPool; 4 | 5 | #[async_std::main] 6 | async fn main() { 7 | // env_logger::builder() 8 | // .filter_level(log::LevelFilter::Debug) 9 | // .is_test(true) 10 | // .init(); 11 | 12 | let url = std::env::var("DATABASE_URL_SAKILA") 13 | .unwrap_or_else(|_| "postgres://root:root@localhost/sakila".to_owned()); 14 | 15 | let connection = PgPool::connect(&url).await.unwrap(); 16 | 17 | let schema_discovery = SchemaDiscovery::new(connection, "public"); 18 | 19 | let schema = schema_discovery 20 | .discover() 21 | .await 22 | .expect("Error discovering schema"); 23 | 24 | for table in schema.tables.iter() { 25 | println!("{};", table.write().to_string(PostgresQueryBuilder)); 26 | println!(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/writer/sqlite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sea-schema-writer-test-sqlite" 3 | version = "0.1.0" 4 | edition = "2024" 5 | rust-version = "1.85.0" 6 | publish = false 7 | 8 | [dependencies] 9 | async-std = { version = "1.8", features = [ "attributes", "tokio1" ] } 10 | sea-schema = { path = "../../../", default-features = false, features = [ "sqlx-sqlite", "runtime-async-std-native-tls", "discovery", "writer", "debug-print" ] } 11 | serde_json = { version = "1" } 12 | sqlx = { version = "0.8" } -------------------------------------------------------------------------------- /tests/writer/sqlite/Readme.md: -------------------------------------------------------------------------------- 1 | # Run 2 | 3 | ```sh 4 | cargo run > schema.sql 5 | ``` -------------------------------------------------------------------------------- /tests/writer/sqlite/src/main.rs: -------------------------------------------------------------------------------- 1 | use sea_schema::sea_query::SqliteQueryBuilder; 2 | use sea_schema::sqlite::discovery::{DiscoveryResult, SchemaDiscovery}; 3 | use sqlx::sqlite::SqlitePool; 4 | 5 | #[async_std::main] 6 | async fn main() -> DiscoveryResult<()> { 7 | let url = std::env::var("DATABASE_URL_SAKILA") 8 | .unwrap_or_else(|_| "sqlite://tests/sakila/sqlite/sakila.db".to_owned()); 9 | 10 | let sqlite_pool = SqlitePool::connect(&url).await.unwrap(); 11 | 12 | let schema_discovery = SchemaDiscovery::new(sqlite_pool); 13 | 14 | let discover_tables = schema_discovery.discover().await?; 15 | 16 | for table in discover_tables.tables.iter() { 17 | println!("{};", table.write().to_string(SqliteQueryBuilder)); 18 | } 19 | 20 | let discover_indexes = schema_discovery.discover_indexes().await?; 21 | 22 | for index in discover_indexes.iter() { 23 | println!("{};", index.write().to_string(SqliteQueryBuilder)); 24 | } 25 | 26 | Ok(()) 27 | } 28 | --------------------------------------------------------------------------------