├── .github └── workflows │ └── CI.yaml ├── .gitignore ├── Cargo.toml ├── Justfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src └── lib.rs ├── tests ├── Cargo.lock ├── Cargo.toml ├── docker-compose.yaml └── src │ ├── clone_impl.rs │ ├── common.rs │ ├── complex_join.rs │ ├── lib.rs │ ├── nullable.rs │ ├── pg_array.rs │ ├── pg_remote_type.rs │ ├── rename.rs │ ├── simple.rs │ └── value_style.rs └── tests_with_diesel_cli ├── Cargo.lock ├── Cargo.toml ├── README.md ├── custom.diesel.toml ├── docker-compose.yaml ├── migrations ├── .keep ├── 00000000000000_diesel_initial_setup │ ├── down.sql │ └── up.sql └── 2022-10-30-154319_setup │ ├── down.sql │ └── up.sql ├── run.sh └── src ├── lib.rs ├── with_custom_schema.rs └── with_default_schema.rs /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | build: 7 | name: Tests 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | rust: 12 | - stable 13 | - "1.82" 14 | 15 | services: 16 | postgres: 17 | image: postgres 18 | ports: 19 | - 5432:5432 20 | env: 21 | POSTGRES_PASSWORD: postgres 22 | mysql: 23 | image: mysql 24 | ports: 25 | - 3306:3306 26 | env: 27 | MYSQL_ROOT_PASSWORD: mysql 28 | MYSQL_DATABASE: test 29 | 30 | steps: 31 | - name: Install dependencies 32 | run: sudo apt install -y libpq-dev libsqlite3-dev libmysqlclient-dev 33 | 34 | - name: Checkout 35 | uses: actions/checkout@v2 36 | 37 | - name: Install toolchain 38 | uses: actions-rs/toolchain@v1 39 | with: 40 | toolchain: ${{ matrix.rust }} 41 | override: true 42 | 43 | - name: Test Postgres 44 | uses: actions-rs/cargo@v1 45 | with: 46 | command: test 47 | args: --manifest-path tests/Cargo.toml --features postgres 48 | env: 49 | PG_TEST_DATABASE_URL: "postgres://postgres:postgres@localhost:5432" 50 | RUST_TEST_THREADS: 1 51 | 52 | - name: Create MySQL database 53 | run: mysql -h 127.0.0.1 --port 3306 -u root -pmysql -e 'CREATE DATABASE IF NOT EXISTS test;' 54 | 55 | - name: Test MySQL 56 | uses: actions-rs/cargo@v1 57 | with: 58 | command: test 59 | args: --manifest-path tests/Cargo.toml --features mysql 60 | env: 61 | MYSQL_TEST_DATABASE_URL: "mysql://root:mysql@127.0.0.1:3306/test" 62 | RUST_TEST_THREADS: 1 63 | 64 | - name: Test sqlite 65 | uses: actions-rs/cargo@v1 66 | with: 67 | command: test 68 | args: --manifest-path tests/Cargo.toml --features sqlite 69 | 70 | - name: Install Diesel-CLI 71 | run: | 72 | cargo install --features postgres --no-default-features diesel_cli 73 | diesel --help 74 | 75 | - name: Run Diesel-CLI tests 76 | env: 77 | DATABASE_URL: postgres://postgres:postgres@localhost:5432 78 | run: | 79 | cd tests_with_diesel_cli 80 | 81 | # with default schema 82 | diesel setup 83 | diesel migration run 84 | cargo test 85 | diesel migration revert 86 | rm src/schema.rs diesel.toml 87 | 88 | # create a custom schema 89 | diesel migration run --config-file custom.diesel.toml 90 | cargo test --features custom 91 | rm -rf src/custom_schema.rs 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | target/ 3 | **/*.rs.bk 4 | /Cargo.lock 5 | 6 | tests_with_diesel_cli/src/custom_schema.rs 7 | tests_with_diesel_cli/src/schema.rs 8 | 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diesel-derive-enum" 3 | version = "3.0.0-beta.1" 4 | description = "Derive diesel boilerplate for using enums in databases" 5 | authors = ["Alex Whitney "] 6 | repository = "http://github.com/adwhit/diesel-derive-enum" 7 | homepage = "http://github.com/adwhit/diesel-derive-enum" 8 | keywords = ["diesel", "postgres", "sqlite", "mysql", "sql"] 9 | license = "MIT OR Apache-2.0" 10 | readme = "README.md" 11 | edition = "2021" 12 | 13 | [dependencies] 14 | quote = "1" 15 | syn = "2" 16 | heck = "0.4.0" 17 | proc-macro2 = "1" 18 | 19 | [features] 20 | postgres = [] 21 | sqlite = [] 22 | mysql = [] 23 | 24 | [lib] 25 | name = "diesel_derive_enum" 26 | proc-macro = true 27 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | local-tests: 2 | cargo test 3 | 4 | cd tests && cargo test --features postgres --no-run 5 | cd tests && cargo test --features mysql --no-run 6 | cd tests && cargo test --features sqlite --no-run 7 | 8 | docker-compose -f tests/docker-compose.yaml up -d 9 | sleep 2 10 | 11 | cd tests && PG_TEST_DATABASE_URL="postgres://postgres:postgres@localhost:54321" cargo test --features postgres 12 | cd tests && MYSQL_TEST_DATABASE_URL="mysql://root:mysql@127.0.0.1:3306/test" cargo test --features mysql 13 | cd tests && cargo test --features sqlite 14 | 15 | reset: 16 | docker-compose -f tests/docker-compose.yaml down 17 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2018 Alex Whitney 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Alex Whitney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diesel-derive-enum 2 | [![crates.io](https://img.shields.io/crates/v/diesel-derive-enum.svg)](https://crates.io/crates/diesel-derive-enum) 3 | ![Build Status](https://github.com/adwhit/diesel-derive-enum/workflows/CI/badge.svg) 4 | 5 | Use Rust enums directly with [`diesel`](https://github.com/diesel-rs/diesel) ORM. 6 | 7 | Diesel is great, but wouldn't it be nice if this would work? 8 | 9 | ``` rust 10 | 11 | use crate::schema::my_table; 12 | 13 | pub enum MyEnum { 14 | Foo, 15 | Bar, 16 | BazQuxx, 17 | } 18 | 19 | fn do_some_work(data: MyEnum, connection: &mut Connection) { 20 | insert_into(my_table) 21 | .values(&data) 22 | .execute(connection) 23 | .unwrap(); 24 | } 25 | ``` 26 | 27 | Unfortunately, it won't work out of the box, because any type which 28 | we wish to use with Diesel must implement various traits. 29 | Tedious to do by hand, easy to do with a `derive` macro - enter `diesel-derive-enum`. 30 | 31 | The latest release, `3.0.0-beta.1`, is tested against `diesel 2.2.8` and `rustc 1.82` 32 | For earlier versions of `diesel`, check out the `2.1.0` and earlier releases of this crate. 33 | 34 | ## What's New in v3 35 | 36 | Version 3.0.0-beta.1 introduces several significant **BREAKING CHANGES**: 37 | 38 | 1. **Attribute Namespacing**: All attributes are now namespaced under `db_enum(...)` for better clarity and organization 39 | 2. **Clone Implementation Change**: Clone is no longer implemented by default on SQL types (now opt-in rather than opt-out) 40 | 3. **MSRV Update**: The minimum supported Rust version is now 1.82 41 | 42 | ## Setup with Diesel CLI 43 | 44 | This crate integrates nicely with 45 | [diesel-cli](http://diesel.rs/guides/configuring-diesel-cli.html) - 46 | this is the recommended workflow. 47 | Note that for now, this **only** works with Postgres - for other databases, 48 | or if not using Diesel CLI, see the next section. 49 | 50 | Cargo.toml: 51 | ```toml 52 | [dependencies] 53 | diesel-derive-enum = { version = "3.0.0-beta.1", features = ["postgres"] } 54 | ``` 55 | 56 | Suppose our project has the following `diesel.toml` (as generated by `diesel setup`): 57 | 58 | ``` toml 59 | [print_schema] 60 | file = "src/schema.rs" 61 | custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] 62 | ``` 63 | 64 | And the following SQL: 65 | ```sql 66 | CREATE TYPE my_enum AS ENUM ('foo', 'bar', 'baz_quxx'); 67 | 68 | CREATE TABLE my_table ( 69 | id SERIAL PRIMARY KEY, 70 | some_enum my_enum NOT NULL 71 | ); 72 | ``` 73 | 74 | Then running `$ diesel migration run` will generate code something like: 75 | 76 | ```rust 77 | // src/schema.rs -- autogenerated 78 | 79 | pub mod sql_types { 80 | #[derive(diesel::sql_types::SqlType, diesel::query_builder::QueryId, Clone)] 81 | #[diesel(postgres_type(name = "my_enum"))] 82 | pub struct MyEnum; 83 | } 84 | 85 | table! { 86 | use diesel::types::Integer; 87 | use super::sql_types::MyEnum; 88 | 89 | my_table { 90 | id -> Integer, 91 | some_enum -> MyEnum 92 | } 93 | } 94 | ``` 95 | 96 | Now we can use `diesel-derive-enum` to hook in our own enum: 97 | 98 | ```rust 99 | // src/my_code.rs 100 | 101 | #[derive(diesel_derive_enum::DbEnum)] 102 | #[db_enum(existing_type_path = "crate::schema::sql_types::MyEnum")] 103 | pub enum MyEnum { 104 | Foo, 105 | Bar, 106 | BazQuxx, 107 | } 108 | ``` 109 | 110 | Note the `db_enum(existing_type_path = "...")` attribute. This instructs this crate to import the 111 | (remote, autogenerated) type and implement various traits upon it. That's it! 112 | Now we can use `MyEnum` with `diesel` (see 'Usage' below). 113 | 114 | 115 | ## Setup without Diesel CLI 116 | 117 | If you are using `mysql` or `sqlite`, or you aren't using `diesel-cli` to generate 118 | your schema, the setup is a little different. 119 | 120 | ### Postgres 121 | 122 | Cargo.toml: 123 | ```toml 124 | [dependencies] 125 | diesel-derive-enum = { version = "3.0.0-beta.1", features = ["postgres"] } 126 | ``` 127 | 128 | SQL: 129 | ```sql 130 | CREATE TYPE my_enum AS ENUM ('foo', 'bar', 'baz_quxx'); 131 | 132 | CREATE TABLE my_table ( 133 | id SERIAL PRIMARY KEY, 134 | some_enum my_enum NOT NULL 135 | ); 136 | ``` 137 | 138 | Rust: 139 | ```rust 140 | #[derive(diesel_derive_enum::DbEnum)] 141 | pub enum MyEnum { 142 | Foo, 143 | Bar, 144 | BazQuxx, 145 | } 146 | 147 | // define your table 148 | table! { 149 | use diesel::types::Integer; 150 | use super::MyEnumMapping; 151 | my_table { 152 | id -> Integer, 153 | some_enum -> MyEnumMapping, // Generated Diesel type - see below for explanation 154 | } 155 | } 156 | ``` 157 | 158 | ### MySQL 159 | 160 | Cargo.toml: 161 | ```toml 162 | [dependencies] 163 | diesel-derive-enum = { version = "3.0.0-beta.1", features = ["mysql"] } 164 | ``` 165 | 166 | SQL: 167 | ```sql 168 | CREATE TABLE my_table ( 169 | id SERIAL PRIMARY KEY, 170 | my_enum enum('foo', 'bar', 'baz_quxx') NOT NULL -- note: snake_case 171 | ); 172 | ``` 173 | 174 | Rust: 175 | ```rust 176 | #[derive(diesel_derive_enum::DbEnum)] 177 | pub enum MyEnum { 178 | Foo, 179 | Bar, 180 | BazQuxx, 181 | } 182 | 183 | // define your table 184 | table! { 185 | use diesel::types::Integer; 186 | use super::MyEnumMapping; 187 | my_table { 188 | id -> Integer, 189 | some_enum -> MyEnumMapping, // Generated Diesel type - see below for explanation 190 | } 191 | } 192 | ``` 193 | 194 | ### sqlite 195 | 196 | 197 | Cargo.toml: 198 | ```toml 199 | [dependencies] 200 | diesel-derive-enum = { version = "3.0.0-beta.1", features = ["sqlite"] } 201 | ``` 202 | 203 | SQL: 204 | ```sql 205 | CREATE TABLE my_table ( 206 | id SERIAL PRIMARY KEY, 207 | my_enum TEXT CHECK(my_enum IN ('foo', 'bar', 'baz_quxx')) NOT NULL -- note: snake_case 208 | ); 209 | ``` 210 | 211 | Rust: 212 | ``` rust 213 | #[derive(diesel_derive_enum::DbEnum)] 214 | pub enum MyEnum { 215 | Foo, 216 | Bar, 217 | BazQuxx, 218 | } 219 | 220 | // define your table 221 | table! { 222 | use diesel::types::Integer; 223 | use super::MyEnumMapping; 224 | my_table { 225 | id -> Integer, 226 | some_enum -> MyEnumMapping, // Generated Diesel type - see below for explanation 227 | } 228 | } 229 | ``` 230 | 231 | ## Usage 232 | 233 | Once set up, usage is similar regardless of your chosen database. 234 | We can define a struct with which to populate/query the table: 235 | 236 | ``` rust 237 | #[derive(Insertable, Queryable, Identifiable, Debug, PartialEq)] 238 | #[diesel(table_name = my_table)] 239 | struct MyRow { 240 | id: i32, 241 | some_enum: MyEnum, 242 | } 243 | ``` 244 | 245 | And use it in the natural way: 246 | 247 | ```rust 248 | let data = vec![ 249 | MyRow { 250 | id: 1, 251 | some_enum: MyEnum::Foo, 252 | }, 253 | MyRow { 254 | id: 2, 255 | some_enum: MyEnum::BazQuxx, 256 | }, 257 | ]; 258 | let connection = PgConnection::establish(/*...*/).unwrap(); 259 | let inserted = insert_into(my_table::table) 260 | .values(&data) 261 | .get_results(&connection) 262 | .unwrap(); 263 | assert_eq!(data, inserted); 264 | ``` 265 | Postgres arrays work too! See [this example.](tests/src/pg_array.rs) 266 | 267 | ## Attribute Reference 268 | 269 | ### Type attributes 270 | 271 | | Attribute | Description | Default | Example | 272 | |-----------|-------------|---------|---------| 273 | | `existing_type_path` | Path to corresponding Diesel type | None | `#[db_enum(existing_type_path = "crate::schema::sql_types::MyEnum")]` | 274 | | `diesel_type` | Name for the Diesel type to create | `Mapping` | `#[db_enum(diesel_type = "CustomMapping")]` | 275 | | `pg_type` | Name of PostgreSQL type | `` | `#[db_enum(pg_type = "custom_type")]` | 276 | | `value_style` | Renaming style from Rust enum to database | `snake_case` | `#[db_enum(value_style = "camelCase")]` | 277 | | `impl_clone_on_sql_mapping` | Implement Clone for the SQL type | `false` | `#[db_enum(impl_clone_on_sql_mapping)]` | 278 | 279 | ### Variant attributes 280 | 281 | | Attribute | Description | Example | 282 | |-----------|-------------|---------| 283 | | `rename` | Specify database name for a variant | `#[db_enum(rename = "custom_name")]` | 284 | 285 | ### Enums Representations 286 | 287 | Enums are not part of the SQL standard and have database-specific implementations. 288 | 289 | * In Postgres, we declare an enum as a separate type within a schema (`CREATE TYPE ...`), 290 | which may then be used in multiple tables. Internally, an enum value is encoded as an int (four bytes) 291 | and stored inline within a row (a much more efficient representation than a string). 292 | 293 | * MySQL is similar except the enum is not declared as a separate type and is 'local' to 294 | it's parent table. It is encoded as either one or two bytes. 295 | 296 | * sqlite does not have enums - in fact, it does 297 | [not really have types](https://dba.stackexchange.com/questions/106364/text-string-stored-in-sqlite-integer-column); 298 | you can store any kind of data in any column. Instead we emulate static checking by 299 | adding the `CHECK` command, as per above. This does not give a more compact encoding 300 | but does ensure better data integrity. Note that if you somehow retrieve some other invalid 301 | text as an enum, `diesel` will error at the point of deserialization. 302 | 303 | ### How It Works 304 | 305 | Diesel maintains a set of internal types which correspond one-to-one to the types available in various 306 | relational databases. Each internal type in turn maps to some kind of Rust native type. 307 | e.g. Postgres `INTEGER` maps to `diesel::types::Integer` maps to `i32`. 308 | 309 | *For `postgres` only*, as of `diesel-2.0.0`, diesel-cli will create the 'dummy' internal 310 | enum mapping type as part of the schema generation process. 311 | We then specify the location of this type with the `existing_type_path` attribute. 312 | 313 | In the case where `existing_type_path` is **not** specified, we assume the internal type 314 | has *not* already been generated, so this macro will instead create it 315 | with the default name `{enum_name}Mapping`. This name can be overridden with the `diesel_type` attribute. 316 | 317 | In either case, this macro will then implement various traits on the internal type. 318 | This macro will also implement various traits on the user-defined `enum` type. 319 | The net result of this is that the user-defined enum can be directly inserted into (and retrieved 320 | from) the diesel database. 321 | 322 | Note that by default we assume that the possible SQL ENUM variants are simply the Rust enum variants 323 | translated to `snake_case`. These can be renamed with the inline annotation `#[db_enum(rename = "...")]`. 324 | 325 | See [tests/src/pg_remote_type.rs](tests/src/pg_remote_type.rs) for an example of using the `existing_type_path` attribute. 326 | 327 | You can override the `snake_case` assumption for the entire enum using the `#[db_enum(value_style = "...")]` 328 | attribute. Individual variants can still be renamed using `#[db_enum(rename = "...")]`. 329 | 330 | | Value Style | Variant | Value | 331 | |:-------------------:|:---------:|:---| 332 | | camelCase | BazQuxx | "bazQuxx" | 333 | | kebab-case | BazQuxx | "baz-quxx" | 334 | | PascalCase | BazQuxx | "BazQuxx" | 335 | | SCREAMING_SNAKE_CASE | BazQuxx | "BAZ_QUXX" | 336 | | UPPERCASE | BazQuxx | "BAZQUXX" | 337 | | snake_case | BazQuxx | "baz_quxx" | 338 | | verbatim | Baz__quxx | "Baz__quxx" | 339 | 340 | See [tests/src/value_style.rs](tests/src/value_style.rs) for an example of changing the output style. 341 | 342 | ### License 343 | 344 | Licensed under either of these: 345 | 346 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 347 | https://www.apache.org/licenses/LICENSE-2.0) 348 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 349 | https://opensource.org/licenses/MIT) 350 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | extern crate proc_macro; 4 | 5 | use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; 6 | use proc_macro::TokenStream; 7 | use proc_macro2::{Ident, Span}; 8 | use quote::quote; 9 | use syn::{ 10 | parse_macro_input, punctuated::Punctuated, Attribute, Data, DeriveInput, Fields, LitByteStr, 11 | LitStr, Meta, Result, Variant, 12 | }; 13 | 14 | /// Implement the traits necessary for inserting the enum directly into a database 15 | /// 16 | /// # Attributes 17 | /// 18 | /// ## Type attributes 19 | /// 20 | /// * `#[db_enum(existing_type_path = "crate::schema::sql_types::NewEnum")]` specifies 21 | /// the path to a corresponding diesel type that was already created by the 22 | /// diesel CLI. If omitted, the type will be generated by this macro. 23 | /// *Note*: Only applies to `postgres`, will error if specified for other databases 24 | /// * `#[db_enum(diesel_type = "NewEnumMapping")]` specifies the name for the diesel type 25 | /// to create. If omitted, uses `Mapping`. 26 | /// *Note*: Cannot be specified alongside `existing_type_path` 27 | /// * `#[db_enum(value_style = "snake_case")]` specifies a renaming style from each of 28 | /// the rust enum variants to each of the database variants. Either `camelCase`, 29 | /// `kebab-case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, `snake_case`, 30 | /// `verbatim`. If omitted, uses `snake_case`. 31 | /// * `#[db_enum(pg_type = "pg-enum-name")]` specifies the name of the enum type 32 | /// as created in the Postgres database (does not apply to other databases) 33 | /// If omitted, uses rust enum name, snake_cased. 34 | /// * `#[db_enum(impl_clone_on_sql_mapping)]` opt-in to implementing `Clone` for the SQL type. 35 | /// By default, Diesel itself already implements `Clone` for SQL types through custom_type_derives. 36 | /// 37 | /// ## Variant attributes 38 | /// 39 | /// * `#[db_enum(rename = "renamed-variant")]` specifies the db name for a specific variant. 40 | #[proc_macro_derive(DbEnum, attributes(db_enum))] 41 | pub fn derive(input: TokenStream) -> TokenStream { 42 | let input: DeriveInput = parse_macro_input!(input as DeriveInput); 43 | 44 | // Gather and validate type-level attributes 45 | let attrs = match gather_db_enum_attrs(&input.attrs) { 46 | Ok(ok) => ok, 47 | Err(e) => return e.to_compile_error().into(), 48 | }; 49 | 50 | // Check for feature-specific constraints 51 | if !cfg!(feature = "postgres") && attrs.existing_type_path.is_some() { 52 | return syn::Error::new( 53 | Span::call_site(), 54 | "existing_type_path attribute only applies when the 'postgres' feature is enabled", 55 | ) 56 | .to_compile_error() 57 | .into(); 58 | } 59 | 60 | if attrs.existing_type_path.is_some() && attrs.pg_type.is_some() { 61 | return syn::Error::new( 62 | Span::call_site(), 63 | "Cannot specify both `existing_type_path` and `pg_type` attributes", 64 | ) 65 | .to_compile_error() 66 | .into(); 67 | } 68 | 69 | if attrs.existing_type_path.is_some() && attrs.diesel_type.is_some() { 70 | return syn::Error::new( 71 | Span::call_site(), 72 | "Cannot specify both `existing_type_path` and `diesel_type` attributes", 73 | ) 74 | .to_compile_error() 75 | .into(); 76 | } 77 | 78 | let pg_internal_type = attrs 79 | .pg_type 80 | .unwrap_or_else(|| input.ident.to_string().to_snake_case()); 81 | let new_diesel_mapping = attrs 82 | .diesel_type 83 | .unwrap_or_else(|| format!("{}Mapping", input.ident)); 84 | let case_style = CaseStyle::from_string( 85 | &attrs 86 | .value_style 87 | .unwrap_or_else(|| "snake_case".to_string()), 88 | ); 89 | let with_clone = attrs.impl_clone_on_sql_mapping; 90 | 91 | let existing_mapping_path = attrs.existing_type_path.map(|v| { 92 | v.parse::() 93 | .expect("existing_type_path is not a valid token") 94 | }); 95 | let new_diesel_mapping = Ident::new(new_diesel_mapping.as_ref(), Span::call_site()); 96 | if let Data::Enum(syn::DataEnum { 97 | variants: data_variants, 98 | .. 99 | }) = input.data 100 | { 101 | generate_derive_enum_impls( 102 | &existing_mapping_path, 103 | &new_diesel_mapping, 104 | &pg_internal_type, 105 | case_style, 106 | &input.ident, 107 | with_clone, 108 | &data_variants, 109 | ) 110 | } else { 111 | syn::Error::new( 112 | Span::call_site(), 113 | "derive(DbEnum) can only be applied to enums", 114 | ) 115 | .to_compile_error() 116 | .into() 117 | } 118 | } 119 | 120 | /// Container for all type-level attributes for DbEnum 121 | #[derive(Debug, Default)] 122 | struct DbEnumTypeAttrs { 123 | existing_type_path: Option, 124 | diesel_type: Option, 125 | value_style: Option, 126 | pg_type: Option, 127 | impl_clone_on_sql_mapping: bool, 128 | } 129 | 130 | /// Defines the casing for the database representation. Follows serde naming convention. 131 | #[derive(Copy, Clone, Debug, PartialEq)] 132 | enum CaseStyle { 133 | Camel, 134 | Kebab, 135 | Pascal, 136 | Upper, 137 | ScreamingSnake, 138 | Snake, 139 | Verbatim, 140 | } 141 | 142 | impl CaseStyle { 143 | fn from_string(name: &str) -> Self { 144 | match name { 145 | "camelCase" => CaseStyle::Camel, 146 | "kebab-case" => CaseStyle::Kebab, 147 | "PascalCase" => CaseStyle::Pascal, 148 | "SCREAMING_SNAKE_CASE" => CaseStyle::ScreamingSnake, 149 | "UPPERCASE" => CaseStyle::Upper, 150 | "snake_case" => CaseStyle::Snake, 151 | "verbatim" | "verbatimcase" => CaseStyle::Verbatim, 152 | s => panic!("unsupported casing: `{}`", s), 153 | } 154 | } 155 | } 156 | 157 | /// Gather and validate all db_enum attributes from a list of attributes 158 | fn gather_db_enum_attrs(attrs: &[Attribute]) -> Result { 159 | let mut result = DbEnumTypeAttrs::default(); 160 | 161 | for attr in attrs.iter() { 162 | if attr.path().is_ident("db_enum") { 163 | let Meta::List(nested) = &attr.meta else { 164 | continue; 165 | }; 166 | 167 | // Process all the nested meta items in this db_enum attribute 168 | nested.parse_nested_meta(|meta| { 169 | let attr_name = meta 170 | .path 171 | .get_ident() 172 | .ok_or_else(|| meta.error("expected ident"))? 173 | .to_string(); 174 | match attr_name.as_str() { 175 | "existing_type_path" => { 176 | if let Ok(value) = meta.value()?.parse::() { 177 | result.existing_type_path = Some(value.value()); 178 | } 179 | } 180 | "diesel_type" => { 181 | if let Ok(value) = meta.value()?.parse::() { 182 | result.diesel_type = Some(value.value()); 183 | } 184 | } 185 | "value_style" => { 186 | if let Ok(value) = meta.value()?.parse::() { 187 | result.value_style = Some(value.value()); 188 | } 189 | } 190 | "pg_type" => { 191 | if let Ok(value) = meta.value()?.parse::() { 192 | result.pg_type = Some(value.value()); 193 | } 194 | } 195 | "impl_clone_on_sql_mapping" => { 196 | result.impl_clone_on_sql_mapping = true; 197 | } 198 | other => { 199 | return Err(meta.error(format!("Unknown attribute: '{other}'"))); 200 | } 201 | } 202 | Ok(()) 203 | })?; 204 | } 205 | } 206 | Ok(result) 207 | } 208 | 209 | /// Gets a variant-level attribute value, with validation for attribute names 210 | fn get_variant_db_enum_attr_value(attrs: &[Attribute]) -> Result> { 211 | for attr in attrs.iter() { 212 | if attr.path().is_ident("db_enum") { 213 | let Meta::List(nested) = &attr.meta else { 214 | continue; 215 | }; 216 | 217 | let mut result: Option = None; 218 | 219 | nested.parse_nested_meta(|meta| { 220 | let attr_name = meta 221 | .path 222 | .get_ident() 223 | .ok_or_else(|| meta.error("expected ident"))?; 224 | if attr_name == "rename" { 225 | if let Ok(value) = meta.value()?.parse::() { 226 | result = Some(value.value()); 227 | return Ok(()); 228 | } else { 229 | return Err(meta.error("attribute 'rename' has no value")); 230 | } 231 | } else { 232 | return Err(meta.error(format!("Unhandled attribute: '{attr_name}'"))); 233 | } 234 | })?; 235 | 236 | if result.is_some() { 237 | return Ok(result); 238 | } 239 | } 240 | } 241 | Ok(None) 242 | } 243 | 244 | fn generate_derive_enum_impls( 245 | existing_mapping_path: &Option, 246 | new_diesel_mapping: &Ident, 247 | pg_internal_type: &str, 248 | case_style: CaseStyle, 249 | enum_ty: &Ident, 250 | with_clone: bool, 251 | variants: &Punctuated, 252 | ) -> TokenStream { 253 | let modname = Ident::new(&format!("db_enum_impl_{}", enum_ty), Span::call_site()); 254 | let variant_ids: Vec = variants 255 | .iter() 256 | .map(|variant| { 257 | if let Fields::Unit = variant.fields { 258 | let id = &variant.ident; 259 | quote! { 260 | #enum_ty::#id 261 | } 262 | } else { 263 | panic!("Variants must be fieldless") 264 | } 265 | }) 266 | .collect(); 267 | 268 | let variants_db: Vec = match variants 269 | .iter() 270 | .map(|variant| { 271 | let rename_result = get_variant_db_enum_attr_value(&variant.attrs)?; 272 | match rename_result { 273 | Some(rename) => Ok(rename), 274 | None => Ok(stylize_value(&variant.ident.to_string(), case_style)), 275 | } 276 | }) 277 | .collect::>>() 278 | { 279 | Ok(ok) => ok, 280 | Err(e) => return e.to_compile_error().into(), 281 | }; 282 | let variants_db_bytes: Vec = variants_db 283 | .iter() 284 | .map(|variant_str| LitByteStr::new(variant_str.as_bytes(), Span::call_site())) 285 | .collect(); 286 | 287 | let common = generate_common(enum_ty, &variant_ids, &variants_db, &variants_db_bytes); 288 | let (diesel_mapping_def, diesel_mapping_use) = 289 | // Skip this part if we already have an existing mapping 290 | if existing_mapping_path.is_some() { 291 | (None, None) 292 | } else { 293 | let new_diesel_mapping_def = generate_new_diesel_mapping(new_diesel_mapping, pg_internal_type); 294 | let common_impls_on_new_diesel_mapping = 295 | generate_common_impls("e! { #new_diesel_mapping }, enum_ty); 296 | ( 297 | Some(quote! { 298 | #new_diesel_mapping_def 299 | #common_impls_on_new_diesel_mapping 300 | }), 301 | Some(quote! { 302 | pub use self::#modname::#new_diesel_mapping; 303 | }), 304 | ) 305 | }; 306 | 307 | let pg_impl = if cfg!(feature = "postgres") { 308 | match existing_mapping_path { 309 | Some(path) => { 310 | let common_impls_on_existing_diesel_mapping = generate_common_impls(path, enum_ty); 311 | let postgres_impl = generate_postgres_impl(path, enum_ty, with_clone); 312 | Some(quote! { 313 | #common_impls_on_existing_diesel_mapping 314 | #postgres_impl 315 | }) 316 | } 317 | None => Some(generate_postgres_impl( 318 | "e! { #new_diesel_mapping }, 319 | enum_ty, 320 | with_clone, 321 | )), 322 | } 323 | } else { 324 | None 325 | }; 326 | 327 | let mysql_impl = if cfg!(feature = "mysql") { 328 | Some(generate_mysql_impl(new_diesel_mapping, enum_ty)) 329 | } else { 330 | None 331 | }; 332 | 333 | let sqlite_impl = if cfg!(feature = "sqlite") { 334 | Some(generate_sqlite_impl(new_diesel_mapping, enum_ty)) 335 | } else { 336 | None 337 | }; 338 | 339 | let imports = quote! { 340 | use super::*; 341 | use diesel::{ 342 | backend::{self, Backend}, 343 | deserialize::{self, FromSql}, 344 | expression::AsExpression, 345 | internal::derives::as_expression::Bound, 346 | query_builder::{bind_collector::RawBytesBindCollector}, 347 | row::Row, 348 | serialize::{self, IsNull, Output, ToSql}, 349 | sql_types::*, 350 | Queryable, 351 | }; 352 | use std::io::Write; 353 | }; 354 | 355 | let quoted = quote! { 356 | #diesel_mapping_use 357 | #[allow(non_snake_case)] 358 | mod #modname { 359 | #imports 360 | 361 | #common 362 | #diesel_mapping_def 363 | #pg_impl 364 | #mysql_impl 365 | #sqlite_impl 366 | } 367 | }; 368 | 369 | quoted.into() 370 | } 371 | 372 | fn stylize_value(value: &str, style: CaseStyle) -> String { 373 | match style { 374 | CaseStyle::Camel => value.to_lower_camel_case(), 375 | CaseStyle::Kebab => value.to_kebab_case(), 376 | CaseStyle::Pascal => value.to_upper_camel_case(), 377 | CaseStyle::Upper => value.to_uppercase(), 378 | CaseStyle::ScreamingSnake => value.to_shouty_snake_case(), 379 | CaseStyle::Snake => value.to_snake_case(), 380 | CaseStyle::Verbatim => value.to_string(), 381 | } 382 | } 383 | 384 | fn generate_common( 385 | enum_ty: &Ident, 386 | variants_rs: &[proc_macro2::TokenStream], 387 | variants_db: &[String], 388 | variants_db_bytes: &[LitByteStr], 389 | ) -> proc_macro2::TokenStream { 390 | quote! { 391 | fn db_str_representation(e: &#enum_ty) -> &'static str { 392 | match *e { 393 | #(#variants_rs => #variants_db,)* 394 | } 395 | } 396 | 397 | fn from_db_binary_representation(bytes: &[u8]) -> deserialize::Result<#enum_ty> { 398 | match bytes { 399 | #(#variants_db_bytes => Ok(#variants_rs),)* 400 | v => Err(format!("Unrecognized enum variant: '{}'", 401 | String::from_utf8_lossy(v)).into()), 402 | } 403 | } 404 | } 405 | } 406 | 407 | fn generate_new_diesel_mapping( 408 | new_diesel_mapping: &Ident, 409 | pg_internal_type: &str, 410 | ) -> proc_macro2::TokenStream { 411 | // Note - we only generate a new mapping for mysql and sqlite, postgres 412 | // should already have one 413 | quote! { 414 | #[derive(Clone, SqlType, diesel::query_builder::QueryId)] 415 | #[diesel(mysql_type(name = "Enum"))] 416 | #[diesel(sqlite_type(name = "Text"))] 417 | #[diesel(postgres_type(name = #pg_internal_type))] 418 | pub struct #new_diesel_mapping; 419 | } 420 | } 421 | 422 | fn generate_common_impls( 423 | diesel_mapping: &proc_macro2::TokenStream, 424 | enum_ty: &Ident, 425 | ) -> proc_macro2::TokenStream { 426 | quote! { 427 | impl AsExpression<#diesel_mapping> for #enum_ty { 428 | type Expression = Bound<#diesel_mapping, Self>; 429 | 430 | fn as_expression(self) -> Self::Expression { 431 | Bound::new(self) 432 | } 433 | } 434 | 435 | impl AsExpression> for #enum_ty { 436 | type Expression = Bound, Self>; 437 | 438 | fn as_expression(self) -> Self::Expression { 439 | Bound::new(self) 440 | } 441 | } 442 | 443 | impl<'a> AsExpression<#diesel_mapping> for &'a #enum_ty { 444 | type Expression = Bound<#diesel_mapping, Self>; 445 | 446 | fn as_expression(self) -> Self::Expression { 447 | Bound::new(self) 448 | } 449 | } 450 | 451 | impl<'a> AsExpression> for &'a #enum_ty { 452 | type Expression = Bound, Self>; 453 | 454 | fn as_expression(self) -> Self::Expression { 455 | Bound::new(self) 456 | } 457 | } 458 | 459 | impl<'a, 'b> AsExpression<#diesel_mapping> for &'a &'b #enum_ty { 460 | type Expression = Bound<#diesel_mapping, Self>; 461 | 462 | fn as_expression(self) -> Self::Expression { 463 | Bound::new(self) 464 | } 465 | } 466 | 467 | impl<'a, 'b> AsExpression> for &'a &'b #enum_ty { 468 | type Expression = Bound, Self>; 469 | 470 | fn as_expression(self) -> Self::Expression { 471 | Bound::new(self) 472 | } 473 | } 474 | 475 | impl ToSql, DB> for #enum_ty 476 | where 477 | DB: Backend, 478 | Self: ToSql<#diesel_mapping, DB>, 479 | { 480 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result { 481 | ToSql::<#diesel_mapping, DB>::to_sql(self, out) 482 | } 483 | } 484 | } 485 | } 486 | 487 | fn generate_postgres_impl( 488 | diesel_mapping: &proc_macro2::TokenStream, 489 | enum_ty: &Ident, 490 | with_clone: bool, 491 | ) -> proc_macro2::TokenStream { 492 | // If with_clone is true, we add a manual Clone impl for the diesel mapping type 493 | // This is usually not necessary as the diesel.toml custom_type_derives now includes Clone by default 494 | let clone_impl = if with_clone { 495 | Some(quote! { 496 | impl Clone for #diesel_mapping { 497 | fn clone(&self) -> Self { 498 | #diesel_mapping 499 | } 500 | } 501 | }) 502 | } else { 503 | None 504 | }; 505 | 506 | quote! { 507 | mod pg_impl { 508 | use super::*; 509 | use diesel::pg::{Pg, PgValue}; 510 | 511 | #clone_impl 512 | 513 | impl FromSql<#diesel_mapping, Pg> for #enum_ty { 514 | fn from_sql(raw: PgValue) -> deserialize::Result { 515 | from_db_binary_representation(raw.as_bytes()) 516 | } 517 | } 518 | 519 | impl ToSql<#diesel_mapping, Pg> for #enum_ty 520 | { 521 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { 522 | out.write_all(db_str_representation(self).as_bytes())?; 523 | Ok(IsNull::No) 524 | } 525 | } 526 | 527 | impl Queryable<#diesel_mapping, Pg> for #enum_ty { 528 | type Row = Self; 529 | 530 | fn build(row: Self::Row) -> deserialize::Result { 531 | Ok(row) 532 | } 533 | } 534 | } 535 | } 536 | } 537 | 538 | fn generate_mysql_impl(diesel_mapping: &Ident, enum_ty: &Ident) -> proc_macro2::TokenStream { 539 | quote! { 540 | mod mysql_impl { 541 | use super::*; 542 | use diesel; 543 | use diesel::mysql::{Mysql, MysqlValue}; 544 | 545 | impl FromSql<#diesel_mapping, Mysql> for #enum_ty { 546 | fn from_sql(raw: MysqlValue) -> deserialize::Result { 547 | from_db_binary_representation(raw.as_bytes()) 548 | } 549 | } 550 | 551 | impl ToSql<#diesel_mapping, Mysql> for #enum_ty 552 | { 553 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> serialize::Result { 554 | out.write_all(db_str_representation(self).as_bytes())?; 555 | Ok(IsNull::No) 556 | } 557 | } 558 | 559 | impl Queryable<#diesel_mapping, Mysql> for #enum_ty { 560 | type Row = Self; 561 | 562 | fn build(row: Self::Row) -> deserialize::Result { 563 | Ok(row) 564 | } 565 | } 566 | } 567 | } 568 | } 569 | 570 | fn generate_sqlite_impl(diesel_mapping: &Ident, enum_ty: &Ident) -> proc_macro2::TokenStream { 571 | quote! { 572 | mod sqlite_impl { 573 | use super::*; 574 | use diesel; 575 | use diesel::sql_types; 576 | use diesel::sqlite::Sqlite; 577 | 578 | impl FromSql<#diesel_mapping, Sqlite> for #enum_ty { 579 | fn from_sql(value: backend::RawValue) -> deserialize::Result { 580 | let bytes = as FromSql>::from_sql(value)?; 581 | from_db_binary_representation(bytes.as_slice()) 582 | } 583 | } 584 | 585 | impl ToSql<#diesel_mapping, Sqlite> for #enum_ty { 586 | fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result { 587 | >::to_sql(db_str_representation(self), out) 588 | } 589 | } 590 | 591 | impl Queryable<#diesel_mapping, Sqlite> for #enum_ty { 592 | type Row = Self; 593 | 594 | fn build(row: Self::Row) -> deserialize::Result { 595 | Ok(row) 596 | } 597 | } 598 | } 599 | } 600 | } 601 | 602 | #[cfg(test)] 603 | mod tests { 604 | use super::*; 605 | use syn::parse_quote; 606 | 607 | #[test] 608 | fn test_db_enum_macro_validation() { 609 | // Test valid attribute 610 | let valid_attr: Attribute = parse_quote! { 611 | #[db_enum(diesel_type = "MyType")] 612 | }; 613 | 614 | let invalid_attr: Attribute = parse_quote! { 615 | #[db_enum(deisel_type = "MyType")] 616 | }; 617 | 618 | // Test valid attribute 619 | let valid_result = gather_db_enum_attrs(&[valid_attr]); 620 | assert!(valid_result.is_ok(), "Valid attribute should be accepted"); 621 | if let Ok(attrs) = valid_result { 622 | assert_eq!(attrs.diesel_type, Some("MyType".to_string())); 623 | } 624 | 625 | // Test invalid attribute 626 | let invalid_result = gather_db_enum_attrs(&[invalid_attr]); 627 | assert!( 628 | invalid_result.is_err(), 629 | "Invalid attribute should be rejected" 630 | ); 631 | if let Err(e) = invalid_result { 632 | let error_msg = e.to_string(); 633 | assert!( 634 | error_msg.contains("deisel_type"), 635 | "Error message should mention the invalid attribute" 636 | ); 637 | } 638 | } 639 | 640 | #[test] 641 | fn test_variant_attribute() { 642 | { 643 | let variant_attr: Attribute = parse_quote! { 644 | #[db_enum(rename = "custom_name")] 645 | }; 646 | 647 | let result = get_variant_db_enum_attr_value(&[variant_attr.clone()]); 648 | assert!(result.is_ok()); 649 | assert_eq!(result.unwrap(), Some("custom_name".to_string())); 650 | } 651 | 652 | { 653 | let variant_attr_phony: Attribute = parse_quote! { 654 | #[db_enum(phony = "phony")] 655 | }; 656 | 657 | let result = get_variant_db_enum_attr_value(&[variant_attr_phony.clone()]); 658 | assert!(result.is_err()); 659 | } 660 | 661 | { 662 | let variant_attr_fake: Attribute = parse_quote! { 663 | #[db_enum(fake)] 664 | }; 665 | 666 | let result = get_variant_db_enum_attr_value(&[variant_attr_fake.clone()]); 667 | assert!(result.is_err()); 668 | } 669 | } 670 | } 671 | -------------------------------------------------------------------------------- /tests/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "2.3.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" 10 | 11 | [[package]] 12 | name = "byteorder" 13 | version = "1.4.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 16 | 17 | [[package]] 18 | name = "darling" 19 | version = "0.20.10" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 22 | dependencies = [ 23 | "darling_core", 24 | "darling_macro", 25 | ] 26 | 27 | [[package]] 28 | name = "darling_core" 29 | version = "0.20.10" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 32 | dependencies = [ 33 | "fnv", 34 | "ident_case", 35 | "proc-macro2", 36 | "quote", 37 | "strsim", 38 | "syn", 39 | ] 40 | 41 | [[package]] 42 | name = "darling_macro" 43 | version = "0.20.10" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 46 | dependencies = [ 47 | "darling_core", 48 | "quote", 49 | "syn", 50 | ] 51 | 52 | [[package]] 53 | name = "diesel" 54 | version = "2.2.8" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "470eb10efc8646313634c99bb1593f402a6434cbd86e266770c6e39219adb86a" 57 | dependencies = [ 58 | "bitflags", 59 | "byteorder", 60 | "diesel_derives", 61 | "itoa", 62 | "libsqlite3-sys", 63 | "mysqlclient-sys", 64 | "percent-encoding", 65 | "pq-sys", 66 | "time", 67 | "url", 68 | ] 69 | 70 | [[package]] 71 | name = "diesel-derive-enum" 72 | version = "3.0.0-beta.1" 73 | dependencies = [ 74 | "heck 0.4.1", 75 | "proc-macro2", 76 | "quote", 77 | "syn", 78 | ] 79 | 80 | [[package]] 81 | name = "diesel_derives" 82 | version = "2.2.4" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "a93958254b70bea63b4187ff73d10180599d9d8d177071b7f91e6da4e0c0ad55" 85 | dependencies = [ 86 | "diesel_table_macro_syntax", 87 | "dsl_auto_type", 88 | "proc-macro2", 89 | "quote", 90 | "syn", 91 | ] 92 | 93 | [[package]] 94 | name = "diesel_table_macro_syntax" 95 | version = "0.2.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" 98 | dependencies = [ 99 | "syn", 100 | ] 101 | 102 | [[package]] 103 | name = "dsl_auto_type" 104 | version = "0.1.3" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" 107 | dependencies = [ 108 | "darling", 109 | "either", 110 | "heck 0.5.0", 111 | "proc-macro2", 112 | "quote", 113 | "syn", 114 | ] 115 | 116 | [[package]] 117 | name = "either" 118 | version = "1.15.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 121 | 122 | [[package]] 123 | name = "fnv" 124 | version = "1.0.7" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 127 | 128 | [[package]] 129 | name = "form_urlencoded" 130 | version = "1.1.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 133 | dependencies = [ 134 | "percent-encoding", 135 | ] 136 | 137 | [[package]] 138 | name = "heck" 139 | version = "0.4.1" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 142 | 143 | [[package]] 144 | name = "heck" 145 | version = "0.5.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 148 | 149 | [[package]] 150 | name = "ident_case" 151 | version = "1.0.1" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 154 | 155 | [[package]] 156 | name = "idna" 157 | version = "0.3.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 160 | dependencies = [ 161 | "unicode-bidi", 162 | "unicode-normalization", 163 | ] 164 | 165 | [[package]] 166 | name = "itoa" 167 | version = "1.0.6" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 170 | 171 | [[package]] 172 | name = "libsqlite3-sys" 173 | version = "0.26.0" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" 176 | dependencies = [ 177 | "pkg-config", 178 | "vcpkg", 179 | ] 180 | 181 | [[package]] 182 | name = "mysqlclient-sys" 183 | version = "0.2.5" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "f61b381528ba293005c42a409dd73d034508e273bf90481f17ec2e964a6e969b" 186 | dependencies = [ 187 | "pkg-config", 188 | "vcpkg", 189 | ] 190 | 191 | [[package]] 192 | name = "percent-encoding" 193 | version = "2.2.0" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 196 | 197 | [[package]] 198 | name = "pkg-config" 199 | version = "0.3.27" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" 202 | 203 | [[package]] 204 | name = "pq-sys" 205 | version = "0.4.8" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd" 208 | dependencies = [ 209 | "vcpkg", 210 | ] 211 | 212 | [[package]] 213 | name = "proc-macro2" 214 | version = "1.0.59" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" 217 | dependencies = [ 218 | "unicode-ident", 219 | ] 220 | 221 | [[package]] 222 | name = "quote" 223 | version = "1.0.28" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" 226 | dependencies = [ 227 | "proc-macro2", 228 | ] 229 | 230 | [[package]] 231 | name = "serde" 232 | version = "1.0.163" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" 235 | 236 | [[package]] 237 | name = "strsim" 238 | version = "0.11.1" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 241 | 242 | [[package]] 243 | name = "syn" 244 | version = "2.0.18" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" 247 | dependencies = [ 248 | "proc-macro2", 249 | "quote", 250 | "unicode-ident", 251 | ] 252 | 253 | [[package]] 254 | name = "tests" 255 | version = "0.1.0" 256 | dependencies = [ 257 | "diesel", 258 | "diesel-derive-enum", 259 | ] 260 | 261 | [[package]] 262 | name = "time" 263 | version = "0.3.21" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" 266 | dependencies = [ 267 | "itoa", 268 | "serde", 269 | "time-core", 270 | "time-macros", 271 | ] 272 | 273 | [[package]] 274 | name = "time-core" 275 | version = "0.1.1" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" 278 | 279 | [[package]] 280 | name = "time-macros" 281 | version = "0.2.9" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" 284 | dependencies = [ 285 | "time-core", 286 | ] 287 | 288 | [[package]] 289 | name = "tinyvec" 290 | version = "1.6.0" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 293 | dependencies = [ 294 | "tinyvec_macros", 295 | ] 296 | 297 | [[package]] 298 | name = "tinyvec_macros" 299 | version = "0.1.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 302 | 303 | [[package]] 304 | name = "unicode-bidi" 305 | version = "0.3.13" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 308 | 309 | [[package]] 310 | name = "unicode-ident" 311 | version = "1.0.9" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" 314 | 315 | [[package]] 316 | name = "unicode-normalization" 317 | version = "0.1.22" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 320 | dependencies = [ 321 | "tinyvec", 322 | ] 323 | 324 | [[package]] 325 | name = "url" 326 | version = "2.3.1" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 329 | dependencies = [ 330 | "form_urlencoded", 331 | "idna", 332 | "percent-encoding", 333 | ] 334 | 335 | [[package]] 336 | name = "vcpkg" 337 | version = "0.2.15" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 340 | -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests" 3 | version = "0.1.0" 4 | authors = ["Alex Whitney "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | diesel = "2.2.8" 9 | diesel-derive-enum = { path = "./.." } 10 | 11 | [features] 12 | postgres = [ "diesel/postgres", "diesel-derive-enum/postgres"] 13 | sqlite = [ "diesel/sqlite", "diesel-derive-enum/sqlite"] 14 | mysql = [ "diesel/mysql", "diesel-derive-enum/mysql"] -------------------------------------------------------------------------------- /tests/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres: 4 | image: "postgres:alpine" 5 | ports: 6 | - "54321:5432" 7 | environment: 8 | POSTGRES_USER: postgres 9 | POSTGRES_PASSWORD: postgres 10 | mysql: 11 | image: mysql 12 | ports: 13 | - "3306:3306" 14 | environment: 15 | MYSQL_ROOT_PASSWORD: mysql 16 | MYSQL_DATABASE: test 17 | -------------------------------------------------------------------------------- /tests/src/clone_impl.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | // SCENARIO 1: Using a SQL type that ALREADY has Clone 4 | // We don't need to add impl_clone_on_sql_mapping here since the SQL type already has Clone 5 | #[derive(Debug, PartialEq, Clone, diesel_derive_enum::DbEnum)] 6 | #[db_enum(existing_type_path = "AlreadyHasCloneMapping")] 7 | pub enum AlreadyHasCloneEnum { 8 | A, 9 | B, 10 | } 11 | 12 | #[derive(diesel::sql_types::SqlType, Clone)] 13 | pub struct AlreadyHasCloneMapping; 14 | 15 | // SCENARIO 2: Using a SQL type that NEEDS Clone 16 | #[derive(Debug, PartialEq, Clone, diesel_derive_enum::DbEnum)] 17 | #[db_enum(existing_type_path = "NeedsCloneMapping")] 18 | #[db_enum(impl_clone_on_sql_mapping)] 19 | pub enum NeedsCloneEnum { 20 | X, 21 | Y, 22 | } 23 | 24 | #[derive(diesel::sql_types::SqlType)] 25 | pub struct NeedsCloneMapping; 26 | 27 | #[test] 28 | fn test_already_has_clone() { 29 | let _ = AlreadyHasCloneMapping.clone(); 30 | } 31 | 32 | #[test] 33 | fn test_needs_clone() { 34 | let _ = NeedsCloneMapping.clone(); 35 | } 36 | -------------------------------------------------------------------------------- /tests/src/common.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | use diesel::connection::SimpleConnection; 4 | use diesel_derive_enum::DbEnum; 5 | 6 | #[derive(Debug, PartialEq, DbEnum, Clone)] 7 | pub enum MyEnum { 8 | Foo, 9 | Bar, 10 | BazQuxx, 11 | } 12 | 13 | table! { 14 | use diesel::sql_types::Integer; 15 | use super::MyEnumMapping; 16 | test_simple { 17 | id -> Integer, 18 | my_enum -> MyEnumMapping, 19 | } 20 | } 21 | 22 | #[derive(Insertable, Queryable, Identifiable, Debug, Clone, PartialEq)] 23 | #[diesel(table_name = test_simple)] 24 | pub struct Simple { 25 | pub id: i32, 26 | pub my_enum: MyEnum, 27 | } 28 | 29 | #[cfg(feature = "postgres")] 30 | pub fn get_connection() -> PgConnection { 31 | let database_url = 32 | ::std::env::var("PG_TEST_DATABASE_URL").expect("Env var PG_TEST_DATABASE_URL not set"); 33 | let mut conn = PgConnection::establish(&database_url) 34 | .expect(&format!("Failed to connect to {}", database_url)); 35 | conn.batch_execute("SET search_path TO pg_temp;").unwrap(); 36 | conn 37 | } 38 | 39 | #[cfg(feature = "mysql")] 40 | pub fn get_connection() -> MysqlConnection { 41 | let database_url = ::std::env::var("MYSQL_TEST_DATABASE_URL") 42 | .expect("Env var MYSQL_TEST_DATABASE_URL not set"); 43 | MysqlConnection::establish(&database_url) 44 | .expect(&format!("Failed to connect to {}", database_url)) 45 | } 46 | 47 | #[cfg(feature = "sqlite")] 48 | pub fn get_connection() -> SqliteConnection { 49 | let database_url = ":memory:"; 50 | SqliteConnection::establish(&database_url) 51 | .expect(&format!("Failed to connect to {}", database_url)) 52 | } 53 | 54 | pub fn sample_data() -> Vec { 55 | vec![ 56 | Simple { 57 | id: 1, 58 | my_enum: MyEnum::Foo, 59 | }, 60 | Simple { 61 | id: 2, 62 | my_enum: MyEnum::BazQuxx, 63 | }, 64 | Simple { 65 | id: 33, 66 | my_enum: MyEnum::Bar, 67 | }, 68 | Simple { 69 | id: 44, 70 | my_enum: MyEnum::Foo, 71 | }, 72 | Simple { 73 | id: 555, 74 | my_enum: MyEnum::Foo, 75 | }, 76 | ] 77 | } 78 | 79 | #[cfg(feature = "postgres")] 80 | pub fn create_table(conn: &mut PgConnection) { 81 | conn.batch_execute( 82 | r#" 83 | CREATE TYPE my_enum AS ENUM ('foo', 'bar', 'baz_quxx'); 84 | CREATE TABLE test_simple ( 85 | id SERIAL PRIMARY KEY, 86 | my_enum my_enum NOT NULL 87 | ); 88 | "#, 89 | ) 90 | .unwrap(); 91 | } 92 | 93 | #[cfg(feature = "mysql")] 94 | pub fn create_table(conn: &mut MysqlConnection) { 95 | conn.batch_execute( 96 | r#" 97 | CREATE TEMPORARY TABLE IF NOT EXISTS test_simple ( 98 | id SERIAL PRIMARY KEY, 99 | my_enum enum('foo', 'bar', 'baz_quxx') NOT NULL 100 | ); 101 | "#, 102 | ) 103 | .unwrap(); 104 | } 105 | 106 | #[cfg(feature = "sqlite")] 107 | pub fn create_table(conn: &mut SqliteConnection) { 108 | conn.batch_execute( 109 | r#" 110 | CREATE TABLE test_simple ( 111 | id SERIAL PRIMARY KEY, 112 | my_enum TEXT CHECK(my_enum IN ('foo', 'bar', 'baz_quxx')) NOT NULL 113 | ); 114 | "#, 115 | ) 116 | .unwrap(); 117 | } 118 | -------------------------------------------------------------------------------- /tests/src/complex_join.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | use crate::common::*; 4 | 5 | table! { 6 | users (id) { 7 | id -> Integer, 8 | } 9 | } 10 | 11 | table! { 12 | use diesel::sql_types::*; 13 | use super::Server_status; 14 | servers (id) { 15 | id -> Integer, 16 | user_id -> Integer, 17 | status -> Server_status, 18 | } 19 | } 20 | 21 | joinable!(servers -> users (user_id)); 22 | allow_tables_to_appear_in_same_query!(users, servers); 23 | 24 | #[derive(diesel_derive_enum::DbEnum, Clone, Debug, PartialEq)] 25 | #[db_enum(diesel_type = "Server_status")] 26 | enum ServerStatus { 27 | Started, 28 | Stopped, 29 | } 30 | 31 | #[derive(Insertable, Identifiable, Queryable, PartialEq, Debug)] 32 | #[diesel(table_name = users)] 33 | struct User { 34 | id: i32, 35 | } 36 | 37 | #[derive(Insertable, Queryable, Associations, PartialEq, Debug)] 38 | #[diesel(belongs_to(User))] 39 | #[diesel(table_name = servers)] 40 | struct Server { 41 | id: i32, 42 | user_id: i32, 43 | status: ServerStatus, 44 | } 45 | 46 | #[cfg(feature = "postgres")] 47 | pub fn create_table(conn: &mut PgConnection) { 48 | use diesel::connection::SimpleConnection; 49 | conn.batch_execute( 50 | r#" 51 | CREATE TYPE server_status AS ENUM ('started', 'stopped'); 52 | CREATE TABLE users ( 53 | id SERIAL PRIMARY KEY 54 | ); 55 | CREATE TABLE servers ( 56 | id SERIAL PRIMARY KEY, 57 | user_id INTEGER REFERENCES users (id), 58 | status server_status 59 | ); 60 | "#, 61 | ) 62 | .unwrap(); 63 | } 64 | 65 | #[test] 66 | #[cfg(feature = "postgres")] 67 | fn test_complex_join() { 68 | let conn = &mut get_connection(); 69 | create_table(conn); 70 | let some_users = vec![User { id: 1 }, User { id: 2 }]; 71 | let some_servers = vec![ 72 | Server { 73 | id: 1, 74 | user_id: 1, 75 | status: ServerStatus::Started, 76 | }, 77 | Server { 78 | id: 2, 79 | user_id: 1, 80 | status: ServerStatus::Stopped, 81 | }, 82 | Server { 83 | id: 3, 84 | user_id: 2, 85 | status: ServerStatus::Started, 86 | }, 87 | ]; 88 | diesel::insert_into(users::table) 89 | .values(&some_users) 90 | .execute(conn) 91 | .unwrap(); 92 | diesel::insert_into(servers::table) 93 | .values(&some_servers) 94 | .execute(conn) 95 | .unwrap(); 96 | let (user, server) = users::table 97 | .find(1) 98 | .left_join( 99 | servers::table.on(servers::dsl::user_id 100 | .eq(users::dsl::id) 101 | .and(servers::dsl::status.eq(ServerStatus::Started))), 102 | ) 103 | .first::<(User, Option)>(conn) 104 | .unwrap(); 105 | assert_eq!(user, User { id: 1 }); 106 | assert_eq!( 107 | server.unwrap(), 108 | Server { 109 | id: 1, 110 | user_id: 1, 111 | status: ServerStatus::Started 112 | } 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /tests/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | #![allow(non_snake_case)] 3 | #![allow(dead_code)] 4 | #![allow(unused_imports)] 5 | 6 | #[cfg(feature = "postgres")] 7 | mod clone_impl; 8 | mod common; 9 | mod complex_join; 10 | mod nullable; 11 | #[cfg(feature = "postgres")] 12 | mod pg_array; 13 | #[cfg(feature = "postgres")] 14 | mod pg_remote_type; 15 | mod rename; 16 | mod simple; 17 | mod value_style; 18 | -------------------------------------------------------------------------------- /tests/src/nullable.rs: -------------------------------------------------------------------------------- 1 | use diesel::insert_into; 2 | use diesel::prelude::*; 3 | 4 | use crate::common::*; 5 | 6 | #[derive(Insertable, Queryable, Identifiable, Debug, PartialEq)] 7 | #[diesel(table_name = test_nullable)] 8 | struct Nullable { 9 | id: i32, 10 | my_enum: Option, 11 | } 12 | 13 | #[derive(Insertable, Queryable, Identifiable, Debug, PartialEq)] 14 | #[diesel(table_name = test_nullable)] 15 | struct MaybeNullable { 16 | id: i32, 17 | my_enum: MyEnum, 18 | } 19 | 20 | table! { 21 | use diesel::sql_types::{Integer, Nullable}; 22 | use super::MyEnumMapping; 23 | test_nullable { 24 | id -> Integer, 25 | my_enum -> Nullable, 26 | } 27 | } 28 | 29 | #[cfg(feature = "postgres")] 30 | pub fn create_null_table(conn: &mut PgConnection) { 31 | use diesel::connection::SimpleConnection; 32 | conn.batch_execute( 33 | r#" 34 | DROP TYPE IF EXISTS my_enum; 35 | CREATE TYPE my_enum AS ENUM ('foo', 'bar', 'baz_quxx'); 36 | CREATE TEMP TABLE IF NOT EXISTS test_nullable ( 37 | id SERIAL PRIMARY KEY, 38 | my_enum my_enum 39 | ); 40 | "#, 41 | ) 42 | .unwrap(); 43 | } 44 | 45 | #[cfg(feature = "mysql")] 46 | pub fn create_null_table(conn: &mut MysqlConnection) { 47 | use diesel::connection::SimpleConnection; 48 | conn.batch_execute( 49 | r#" 50 | CREATE TEMPORARY TABLE IF NOT EXISTS test_nullable ( 51 | id SERIAL PRIMARY KEY, 52 | my_enum enum ('foo', 'bar', 'baz_quxx') 53 | ); 54 | "#, 55 | ) 56 | .unwrap(); 57 | } 58 | 59 | #[cfg(feature = "sqlite")] 60 | pub fn create_null_table(conn: &mut SqliteConnection) { 61 | use diesel::connection::SimpleConnection; 62 | conn.batch_execute( 63 | r#" 64 | CREATE TABLE test_nullable ( 65 | id SERIAL PRIMARY KEY, 66 | my_enum TEXT CHECK(my_enum IN ('foo', 'bar', 'baz_quxx')) 67 | ); 68 | "#, 69 | ) 70 | .unwrap(); 71 | } 72 | 73 | #[test] 74 | fn nullable_enum_round_trip() { 75 | let connection = &mut get_connection(); 76 | create_null_table(connection); 77 | let data = vec![ 78 | Nullable { 79 | id: 1, 80 | my_enum: None, 81 | }, 82 | Nullable { 83 | id: 2, 84 | my_enum: Some(MyEnum::Bar), 85 | }, 86 | ]; 87 | let sql = insert_into(test_nullable::table).values(&data); 88 | let ct = sql.execute(connection).unwrap(); 89 | assert_eq!(data.len(), ct); 90 | let items = test_nullable::table.load::(connection).unwrap(); 91 | assert_eq!(data, items); 92 | } 93 | 94 | #[test] 95 | fn not_nullable_enum_round_trip() { 96 | let connection = &mut get_connection(); 97 | create_null_table(connection); 98 | let data = vec![ 99 | MaybeNullable { 100 | id: 1, 101 | my_enum: MyEnum::Foo, 102 | }, 103 | MaybeNullable { 104 | id: 2, 105 | my_enum: MyEnum::BazQuxx, 106 | }, 107 | ]; 108 | let ct = insert_into(test_nullable::table) 109 | .values(&data) 110 | .execute(connection) 111 | .unwrap(); 112 | assert_eq!(data.len(), ct); 113 | } 114 | -------------------------------------------------------------------------------- /tests/src/pg_array.rs: -------------------------------------------------------------------------------- 1 | use diesel::insert_into; 2 | use diesel::prelude::*; 3 | 4 | use crate::common::*; 5 | 6 | pub fn create_table(conn: &mut PgConnection) { 7 | use diesel::connection::SimpleConnection; 8 | conn.batch_execute( 9 | r#" 10 | CREATE TYPE my_enum AS ENUM ('foo', 'bar', 'baz_quxx'); 11 | CREATE TABLE test_array ( 12 | id SERIAL PRIMARY KEY, 13 | my_enum_arr my_enum[] NOT NULL 14 | ); 15 | "#, 16 | ) 17 | .unwrap(); 18 | } 19 | 20 | #[test] 21 | fn enum_query() { 22 | let connection = &mut get_connection(); 23 | create_table(connection); 24 | let data_item = TestArray { 25 | id: 1, 26 | my_enum_arr: vec![MyEnum::Foo], 27 | }; 28 | let data = vec![data_item]; 29 | let ct = insert_into(test_array::table) 30 | .values(&data) 31 | .execute(connection) 32 | .unwrap(); 33 | assert_eq!(data.len(), ct); 34 | let item = test_array::table 35 | .find(1) 36 | .get_results::(connection) 37 | .unwrap(); 38 | assert_eq!(data, item); 39 | } 40 | 41 | table! { 42 | use diesel::sql_types::{Integer, Array}; 43 | use super::MyEnumMapping; 44 | test_array { 45 | id -> Integer, 46 | my_enum_arr -> Array, 47 | } 48 | } 49 | 50 | #[derive(Insertable, Queryable, Identifiable, Debug, Clone, PartialEq)] 51 | #[diesel(table_name = test_array)] 52 | struct TestArray { 53 | id: i32, 54 | my_enum_arr: Vec, 55 | } 56 | -------------------------------------------------------------------------------- /tests/src/pg_remote_type.rs: -------------------------------------------------------------------------------- 1 | use crate::common::*; 2 | use diesel::prelude::*; 3 | 4 | #[cfg(feature = "postgres")] 5 | #[derive(diesel::sql_types::SqlType)] 6 | #[diesel(postgres_type(name = "my_remote_enum"))] 7 | pub struct MyRemoteEnumMapping; 8 | 9 | table! { 10 | use diesel::sql_types::Integer; 11 | use super::MyRemoteEnumMapping; 12 | test_remote { 13 | id -> Integer, 14 | my_enum -> MyRemoteEnumMapping, 15 | } 16 | } 17 | 18 | #[derive(Insertable, Queryable, Identifiable, Debug, Clone, PartialEq)] 19 | #[diesel(table_name = test_remote)] 20 | struct Data { 21 | id: i32, 22 | my_enum: MyRemoteEnum, 23 | } 24 | 25 | #[derive(Debug, PartialEq, Clone, diesel_derive_enum::DbEnum)] 26 | #[db_enum(existing_type_path = "MyRemoteEnumMapping")] 27 | pub enum MyRemoteEnum { 28 | This, 29 | That, 30 | } 31 | 32 | #[test] 33 | fn enum_round_trip() { 34 | let connection = &mut get_connection(); 35 | use diesel::connection::SimpleConnection; 36 | 37 | connection 38 | .batch_execute( 39 | r#" 40 | CREATE TYPE my_remote_enum AS ENUM ('this', 'that'); 41 | CREATE TABLE test_remote ( 42 | id SERIAL PRIMARY KEY, 43 | my_enum my_remote_enum NOT NULL 44 | ); 45 | "#, 46 | ) 47 | .unwrap(); 48 | 49 | create_table(connection); 50 | let data = Data { 51 | id: 123, 52 | my_enum: MyRemoteEnum::This, 53 | }; 54 | let res = diesel::insert_into(test_remote::table) 55 | .values(&data) 56 | .get_result(connection) 57 | .unwrap(); 58 | assert_eq!(data, res); 59 | } 60 | -------------------------------------------------------------------------------- /tests/src/rename.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | #[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))] 4 | use crate::common::get_connection; 5 | 6 | #[derive(Debug, PartialEq, diesel_derive_enum::DbEnum)] 7 | #[db_enum(diesel_type = "Some_Internal_Type", pg_type = "Some_External_Type")] 8 | pub enum SomeEnum { 9 | #[db_enum(rename = "mod")] 10 | Mod, 11 | #[db_enum(rename = "type")] 12 | typo, 13 | #[db_enum(rename = "with spaces")] 14 | WithASpace, 15 | } 16 | 17 | table! { 18 | use diesel::sql_types::Integer; 19 | use super::Some_Internal_Type; 20 | test_rename { 21 | id -> Integer, 22 | renamed -> Some_Internal_Type, 23 | } 24 | } 25 | 26 | #[derive(Insertable, Queryable, Identifiable, Debug, PartialEq)] 27 | #[diesel(table_name = test_rename)] 28 | struct TestRename { 29 | id: i32, 30 | renamed: SomeEnum, 31 | } 32 | 33 | #[test] 34 | #[cfg(feature = "postgres")] 35 | fn rename_round_trip() { 36 | use diesel::connection::SimpleConnection; 37 | use diesel::insert_into; 38 | let data = vec![ 39 | TestRename { 40 | id: 1, 41 | renamed: SomeEnum::Mod, 42 | }, 43 | TestRename { 44 | id: 2, 45 | renamed: SomeEnum::WithASpace, 46 | }, 47 | ]; 48 | let connection = &mut get_connection(); 49 | connection 50 | .batch_execute( 51 | r#" 52 | CREATE TYPE "Some_External_Type" AS ENUM ('mod', 'type', 'with spaces'); 53 | CREATE TABLE test_rename ( 54 | id SERIAL PRIMARY KEY, 55 | renamed "Some_External_Type" NOT NULL 56 | ); 57 | "#, 58 | ) 59 | .unwrap(); 60 | let inserted = insert_into(test_rename::table) 61 | .values(&data) 62 | .get_results(connection) 63 | .unwrap(); 64 | assert_eq!(data, inserted); 65 | } 66 | 67 | #[test] 68 | #[cfg(feature = "mysql")] 69 | fn rename_round_trip() { 70 | use diesel::connection::SimpleConnection; 71 | use diesel::insert_into; 72 | let data = vec![ 73 | TestRename { 74 | id: 1, 75 | renamed: SomeEnum::Mod, 76 | }, 77 | TestRename { 78 | id: 2, 79 | renamed: SomeEnum::WithASpace, 80 | }, 81 | ]; 82 | let connection = &mut get_connection(); 83 | connection 84 | .batch_execute( 85 | r#" 86 | CREATE TEMPORARY TABLE IF NOT EXISTS test_rename ( 87 | id SERIAL PRIMARY KEY, 88 | renamed enum('mod', 'type', 'with spaces') NOT NULL 89 | ); 90 | "#, 91 | ) 92 | .unwrap(); 93 | insert_into(test_rename::table) 94 | .values(&data) 95 | .execute(connection) 96 | .unwrap(); 97 | let inserted = test_rename::table.load::(connection).unwrap(); 98 | assert_eq!(data, inserted); 99 | } 100 | -------------------------------------------------------------------------------- /tests/src/simple.rs: -------------------------------------------------------------------------------- 1 | use diesel::insert_into; 2 | use diesel::prelude::*; 3 | 4 | use crate::common::*; 5 | 6 | #[test] 7 | #[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))] 8 | fn enum_round_trip() { 9 | let connection = &mut get_connection(); 10 | create_table(connection); 11 | let data = sample_data(); 12 | let ct = insert_into(test_simple::table) 13 | .values(&data) 14 | .execute(connection) 15 | .unwrap(); 16 | assert_eq!(data.len(), ct); 17 | let items = test_simple::table.load::(connection).unwrap(); 18 | assert_eq!(data, items); 19 | } 20 | 21 | #[test] 22 | #[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))] 23 | fn filter_by_enum() { 24 | use crate::common::test_simple::dsl::*; 25 | let connection = &mut get_connection(); 26 | create_table(connection); 27 | let data = sample_data(); 28 | let ct = insert_into(test_simple) 29 | .values(&data) 30 | .execute(connection) 31 | .unwrap(); 32 | assert_eq!(data.len(), ct); 33 | let results = test_simple 34 | .filter(my_enum.eq(MyEnum::Foo)) 35 | .limit(2) 36 | .load::(connection) 37 | .unwrap(); 38 | assert_eq!( 39 | results, 40 | vec![ 41 | Simple { 42 | id: 1, 43 | my_enum: MyEnum::Foo, 44 | }, 45 | Simple { 46 | id: 44, 47 | my_enum: MyEnum::Foo, 48 | }, 49 | ] 50 | ); 51 | } 52 | 53 | #[test] 54 | #[cfg(feature = "sqlite")] 55 | fn sqlite_invalid_enum() { 56 | use diesel::connection::SimpleConnection; 57 | let connection = &mut get_connection(); 58 | let data = sample_data(); 59 | connection 60 | .batch_execute( 61 | r#" 62 | CREATE TABLE test_simple ( 63 | id SERIAL PRIMARY KEY, 64 | my_enum TEXT CHECK(my_enum IN ('food', 'bar', 'baz_quxx')) NOT NULL 65 | ); 66 | "#, 67 | ) 68 | .unwrap(); 69 | if let Err(e) = insert_into(test_simple::table) 70 | .values(&data) 71 | .execute(connection) 72 | { 73 | let err = format!("{}", e); 74 | assert!(err.contains("CHECK constraint failed")); 75 | } else { 76 | panic!("should have failed to insert") 77 | } 78 | } 79 | 80 | // test snakey naming - should compile and not clobber above definitions 81 | // (but we won't actually bother round-tripping) 82 | 83 | #[derive(Debug, PartialEq, diesel_derive_enum::DbEnum)] 84 | pub enum my_enum { 85 | foo, 86 | bar, 87 | bazQuxx, 88 | } 89 | 90 | table! { 91 | use diesel::sql_types::Integer; 92 | use super::my_enumMapping; 93 | test_snakey { 94 | id -> Integer, 95 | my_enum -> my_enumMapping, 96 | } 97 | } 98 | 99 | #[derive(Insertable, Queryable, Identifiable, Debug, PartialEq)] 100 | #[diesel(table_name = test_snakey)] 101 | struct test_snake { 102 | id: i32, 103 | my_enum: my_enum, 104 | } 105 | -------------------------------------------------------------------------------- /tests/src/value_style.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | #[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))] 4 | use crate::common::get_connection; 5 | 6 | #[derive(Debug, PartialEq, diesel_derive_enum::DbEnum)] 7 | #[db_enum(diesel_type = "Stylized_Internal_Type")] 8 | #[db_enum(pg_type = "Stylized_External_Type")] 9 | #[db_enum(value_style = "PascalCase")] 10 | pub enum StylizedEnum { 11 | FirstVariant, 12 | secondThing, 13 | third_item, 14 | FOURTH_VALUE, 15 | #[db_enum(rename = "crazy fifth")] 16 | cRaZy_FiFtH, 17 | } 18 | 19 | table! { 20 | use diesel::sql_types::Integer; 21 | use super::Stylized_Internal_Type; 22 | test_value_style { 23 | id -> Integer, 24 | value -> Stylized_Internal_Type, 25 | } 26 | } 27 | 28 | #[derive(Insertable, Queryable, Identifiable, Debug, PartialEq)] 29 | #[diesel(table_name = test_value_style)] 30 | struct TestStylized { 31 | id: i32, 32 | value: StylizedEnum, 33 | } 34 | 35 | fn sample_data() -> Vec { 36 | vec![ 37 | TestStylized { 38 | id: 1, 39 | value: StylizedEnum::FirstVariant, 40 | }, 41 | TestStylized { 42 | id: 2, 43 | value: StylizedEnum::secondThing, 44 | }, 45 | TestStylized { 46 | id: 3, 47 | value: StylizedEnum::third_item, 48 | }, 49 | TestStylized { 50 | id: 4, 51 | value: StylizedEnum::FOURTH_VALUE, 52 | }, 53 | TestStylized { 54 | id: 5, 55 | value: StylizedEnum::cRaZy_FiFtH, 56 | }, 57 | ] 58 | } 59 | 60 | #[test] 61 | #[cfg(feature = "postgres")] 62 | fn stylized_round_trip() { 63 | use diesel::connection::SimpleConnection; 64 | use diesel::insert_into; 65 | let data = sample_data(); 66 | let connection = &mut get_connection(); 67 | connection 68 | .batch_execute( 69 | r#" 70 | CREATE TYPE "Stylized_External_Type" AS ENUM ( 71 | 'FirstVariant', 'SecondThing', 'ThirdItem', 'FourthValue', 'crazy fifth'); 72 | CREATE TABLE test_value_style ( 73 | id SERIAL PRIMARY KEY, 74 | value "Stylized_External_Type" NOT NULL 75 | ); 76 | "#, 77 | ) 78 | .unwrap(); 79 | let inserted = insert_into(test_value_style::table) 80 | .values(&data) 81 | .get_results(connection) 82 | .unwrap(); 83 | assert_eq!(data, inserted); 84 | } 85 | 86 | #[test] 87 | #[cfg(feature = "mysql")] 88 | fn stylized_round_trip() { 89 | use diesel::connection::SimpleConnection; 90 | use diesel::insert_into; 91 | let data = sample_data(); 92 | let connection = &mut get_connection(); 93 | connection 94 | .batch_execute( 95 | r#" 96 | CREATE TEMPORARY TABLE IF NOT EXISTS test_value_style ( 97 | id SERIAL PRIMARY KEY, 98 | value enum('FirstVariant', 'SecondThing', 'ThirdItem', 'FourthValue', 'crazy fifth') 99 | NOT NULL 100 | ); 101 | "#, 102 | ) 103 | .unwrap(); 104 | insert_into(test_value_style::table) 105 | .values(&data) 106 | .execute(connection) 107 | .unwrap(); 108 | let inserted = test_value_style::table 109 | .load::(connection) 110 | .unwrap(); 111 | assert_eq!(data, inserted); 112 | } 113 | 114 | #[test] 115 | #[cfg(feature = "sqlite")] 116 | fn stylized_round_trip() { 117 | use diesel::connection::SimpleConnection; 118 | use diesel::insert_into; 119 | let data = sample_data(); 120 | let connection = &mut get_connection(); 121 | connection 122 | .batch_execute( 123 | r#" 124 | CREATE TABLE test_value_style ( 125 | id SERIAL PRIMARY KEY, 126 | value TEXT CHECK(value IN ( 127 | 'FirstVariant', 'SecondThing', 'ThirdItem', 'FourthValue', 'crazy fifth' 128 | )) NOT NULL 129 | ); 130 | "#, 131 | ) 132 | .unwrap(); 133 | insert_into(test_value_style::table) 134 | .values(&data) 135 | .execute(connection) 136 | .unwrap(); 137 | let inserted = test_value_style::table 138 | .load::(connection) 139 | .unwrap(); 140 | assert_eq!(data, inserted); 141 | } 142 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "2.3.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" 10 | 11 | [[package]] 12 | name = "byteorder" 13 | version = "1.4.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 16 | 17 | [[package]] 18 | name = "darling" 19 | version = "0.20.10" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 22 | dependencies = [ 23 | "darling_core", 24 | "darling_macro", 25 | ] 26 | 27 | [[package]] 28 | name = "darling_core" 29 | version = "0.20.10" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 32 | dependencies = [ 33 | "fnv", 34 | "ident_case", 35 | "proc-macro2", 36 | "quote", 37 | "strsim", 38 | "syn", 39 | ] 40 | 41 | [[package]] 42 | name = "darling_macro" 43 | version = "0.20.10" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 46 | dependencies = [ 47 | "darling_core", 48 | "quote", 49 | "syn", 50 | ] 51 | 52 | [[package]] 53 | name = "diesel" 54 | version = "2.2.8" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "470eb10efc8646313634c99bb1593f402a6434cbd86e266770c6e39219adb86a" 57 | dependencies = [ 58 | "bitflags", 59 | "byteorder", 60 | "diesel_derives", 61 | "itoa", 62 | "pq-sys", 63 | ] 64 | 65 | [[package]] 66 | name = "diesel-derive-enum" 67 | version = "3.0.0-beta.1" 68 | dependencies = [ 69 | "heck 0.4.1", 70 | "proc-macro2", 71 | "quote", 72 | "syn", 73 | ] 74 | 75 | [[package]] 76 | name = "diesel_derives" 77 | version = "2.2.4" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "a93958254b70bea63b4187ff73d10180599d9d8d177071b7f91e6da4e0c0ad55" 80 | dependencies = [ 81 | "diesel_table_macro_syntax", 82 | "dsl_auto_type", 83 | "proc-macro2", 84 | "quote", 85 | "syn", 86 | ] 87 | 88 | [[package]] 89 | name = "diesel_table_macro_syntax" 90 | version = "0.2.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" 93 | dependencies = [ 94 | "syn", 95 | ] 96 | 97 | [[package]] 98 | name = "dsl_auto_type" 99 | version = "0.1.3" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" 102 | dependencies = [ 103 | "darling", 104 | "either", 105 | "heck 0.5.0", 106 | "proc-macro2", 107 | "quote", 108 | "syn", 109 | ] 110 | 111 | [[package]] 112 | name = "either" 113 | version = "1.15.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 116 | 117 | [[package]] 118 | name = "fnv" 119 | version = "1.0.7" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 122 | 123 | [[package]] 124 | name = "heck" 125 | version = "0.4.1" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 128 | 129 | [[package]] 130 | name = "heck" 131 | version = "0.5.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 134 | 135 | [[package]] 136 | name = "ident_case" 137 | version = "1.0.1" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 140 | 141 | [[package]] 142 | name = "itoa" 143 | version = "1.0.6" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 146 | 147 | [[package]] 148 | name = "pq-sys" 149 | version = "0.4.8" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd" 152 | dependencies = [ 153 | "vcpkg", 154 | ] 155 | 156 | [[package]] 157 | name = "proc-macro2" 158 | version = "1.0.59" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" 161 | dependencies = [ 162 | "unicode-ident", 163 | ] 164 | 165 | [[package]] 166 | name = "quote" 167 | version = "1.0.28" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" 170 | dependencies = [ 171 | "proc-macro2", 172 | ] 173 | 174 | [[package]] 175 | name = "strsim" 176 | version = "0.11.1" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 179 | 180 | [[package]] 181 | name = "syn" 182 | version = "2.0.18" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" 185 | dependencies = [ 186 | "proc-macro2", 187 | "quote", 188 | "unicode-ident", 189 | ] 190 | 191 | [[package]] 192 | name = "tests_with_diesel_cli" 193 | version = "0.1.0" 194 | dependencies = [ 195 | "diesel", 196 | "diesel-derive-enum", 197 | ] 198 | 199 | [[package]] 200 | name = "unicode-ident" 201 | version = "1.0.9" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" 204 | 205 | [[package]] 206 | name = "vcpkg" 207 | version = "0.2.15" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 210 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests_with_diesel_cli" 3 | version = "0.1.0" 4 | authors = ["Alex Whitney "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | diesel = { version = "2.2.8", features = ["postgres"] } 9 | diesel-derive-enum = { path = "./..", features = ["postgres"]} 10 | 11 | [features] 12 | custom = [] 13 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/README.md: -------------------------------------------------------------------------------- 1 | The purpose of these tests are to make sure the this crate integrates nicely with `diesel_cli`. First we run with default configuration (created by `diesel setup`), then with a custom config `custom.diesel.toml` which instructs `diesel_cli` not to generate the mapping types. 2 | 3 | To run these tests locally: 4 | 5 | * Make sure you have `libpq` installed 6 | - You may have to mess around with your paths etc, see [stackoverflow](https://stackoverflow.com/questions/44654216/correct-way-to-install-psql-without-full-postgres-on-macos) 7 | * Also install `docker`/`docker-compose` and start daemon 8 | * Install the latest `diesel_cl` 9 | - `cargo install diesel_cli -f --features postgres --no-default-features` 10 | 11 | Then: 12 | 13 | ```bash 14 | ./run.sh 15 | ``` 16 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/custom.diesel.toml: -------------------------------------------------------------------------------- 1 | [print_schema] 2 | file = "src/custom_schema.rs" 3 | 4 | # We stop diesel from generating the enum mapping type 5 | # Instead we will do it through DbEnum 6 | generate_missing_sql_type_definitions = false 7 | 8 | import_types = ["diesel::sql_types::*", "crate::with_custom_schema::export::MyEnum"] 9 | 10 | [migrations_directory] 11 | dir = "migrations" 12 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres: 4 | image: "postgres:alpine" 5 | ports: 6 | - "5432:5432" 7 | environment: 8 | POSTGRES_USER: postgres 9 | POSTGRES_PASSWORD: postgres 10 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/migrations/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adwhit/diesel-derive-enum/816ebe062a99056a69a194b4ba15532980558c19/tests_with_diesel_cli/migrations/.keep -------------------------------------------------------------------------------- /tests_with_diesel_cli/migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/migrations/2022-10-30-154319_setup/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE simple; 2 | DROP TYPE my_enum; 3 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/migrations/2022-10-30-154319_setup/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE my_enum AS ENUM ('foo', 'bar', 'baz_quxx'); 2 | 3 | CREATE TABLE simple ( 4 | id SERIAL PRIMARY KEY, 5 | some_value my_enum NOT NULL 6 | ); 7 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | export DATABASE_URL=postgres://postgres:postgres@localhost:5432 5 | 6 | rm -f src/schema.rs 7 | rm -f src/custom_schema.rs 8 | 9 | # create a 'default' schema 10 | docker-compose down 11 | docker-compose up -d 12 | sleep 2 13 | 14 | rm -f src/schema.rs diesel.toml 15 | diesel setup 16 | diesel migration run 17 | cargo test 18 | diesel migration revert 19 | rm src/schema.rs diesel.toml 20 | 21 | # create a custom schema 22 | diesel migration run --config-file custom.diesel.toml 23 | cargo test --features custom 24 | rm src/custom_schema.rs 25 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "custom"))] 2 | pub mod schema; 3 | #[cfg(not(feature = "custom"))] 4 | pub mod with_default_schema; 5 | 6 | #[cfg(feature = "custom")] 7 | pub mod custom_schema; 8 | 9 | #[cfg(feature = "custom")] 10 | pub mod with_custom_schema; 11 | 12 | pub use diesel::pg::PgConnection as Conn; 13 | pub use diesel::Connection; 14 | 15 | pub fn get_connection() -> Conn { 16 | let database_url = ::std::env::var("DATABASE_URL").expect("Env var DATABASE_URL not set"); 17 | let conn = 18 | Conn::establish(&database_url).expect(&format!("Failed to connect to {}", database_url)); 19 | conn 20 | } 21 | 22 | #[cfg(test)] 23 | mod tests { 24 | 25 | #[cfg(not(feature = "custom"))] 26 | use crate::schema::simple; 27 | #[cfg(not(feature = "custom"))] 28 | use crate::with_default_schema::*; 29 | 30 | #[cfg(feature = "custom")] 31 | use crate::custom_schema::simple; 32 | #[cfg(feature = "custom")] 33 | use crate::with_custom_schema::*; 34 | 35 | use diesel::prelude::*; 36 | 37 | #[test] 38 | fn round_trip() { 39 | let mut conn = crate::get_connection(); 40 | let this = Simple { 41 | id: 1, 42 | some_value: MyEnum::Foo, 43 | }; 44 | let that = insert(&mut conn, &this).unwrap(); 45 | assert_eq!(this, that); 46 | 47 | // make a query that requires QueryId trait to exist 48 | let _: Vec = simple::table 49 | .filter(simple::some_value.eq(MyEnum::Foo)) 50 | .limit(1) 51 | .load(&mut conn) 52 | .unwrap(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/src/with_custom_schema.rs: -------------------------------------------------------------------------------- 1 | use diesel::pg::PgConnection as Conn; 2 | use diesel::prelude::*; 3 | use diesel::result::Error; 4 | 5 | use crate::custom_schema::simple; 6 | 7 | #[derive(diesel_derive_enum::DbEnum, Debug, Copy, Clone, PartialEq, Eq)] 8 | // NOTE: no existing_type_path, so we generate the mapping type ourselves 9 | pub enum MyEnum { 10 | Foo, 11 | Bar, 12 | BazQuxx, 13 | } 14 | 15 | #[derive(Insertable, Queryable, Identifiable, Debug, Clone, PartialEq)] 16 | #[diesel(table_name = simple)] 17 | pub struct Simple { 18 | pub id: i32, 19 | pub some_value: MyEnum, 20 | } 21 | 22 | pub fn insert(conn: &mut Conn, value: &Simple) -> Result { 23 | diesel::insert_into(simple::table) 24 | .values(value) 25 | .get_result(conn) 26 | } 27 | 28 | pub(crate) mod export { 29 | pub use super::MyEnumMapping as MyEnum; 30 | } 31 | -------------------------------------------------------------------------------- /tests_with_diesel_cli/src/with_default_schema.rs: -------------------------------------------------------------------------------- 1 | use diesel::pg::PgConnection as Conn; 2 | use diesel::prelude::*; 3 | use diesel::result::Error; 4 | 5 | use crate::schema::simple; 6 | 7 | #[derive(diesel_derive_enum::DbEnum, Debug, Copy, Clone, PartialEq, Eq)] 8 | #[db_enum(existing_type_path = "crate::schema::sql_types::MyEnum")] 9 | pub enum MyEnum { 10 | Foo, 11 | Bar, 12 | BazQuxx, 13 | } 14 | 15 | #[derive(Insertable, Queryable, Identifiable, Debug, Clone, PartialEq)] 16 | #[diesel(table_name = simple)] 17 | pub struct Simple { 18 | pub id: i32, 19 | pub some_value: MyEnum, 20 | } 21 | 22 | pub fn insert(conn: &mut Conn, value: &Simple) -> Result { 23 | diesel::insert_into(simple::table) 24 | .values(value) 25 | .get_result(conn) 26 | } 27 | --------------------------------------------------------------------------------