├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── core ├── Cargo.toml ├── README.md ├── src │ ├── analyze.rs │ ├── codegen.rs │ ├── expand.rs │ ├── lib.rs │ ├── lower.rs │ ├── parse.rs │ ├── snapshot_tests.rs │ └── snapshots │ │ ├── sqlx_conditional_queries_core__snapshot_tests__both_parameter_kinds@MySql.snap │ │ ├── sqlx_conditional_queries_core__snapshot_tests__both_parameter_kinds@PostgreSql.snap │ │ ├── sqlx_conditional_queries_core__snapshot_tests__both_parameter_kinds@Sqlite.snap │ │ ├── sqlx_conditional_queries_core__snapshot_tests__only_compile_time_bound_parameters@MySql.snap │ │ ├── sqlx_conditional_queries_core__snapshot_tests__only_compile_time_bound_parameters@PostgreSql.snap │ │ ├── sqlx_conditional_queries_core__snapshot_tests__only_compile_time_bound_parameters@Sqlite.snap │ │ ├── sqlx_conditional_queries_core__snapshot_tests__only_runtime_bound_parameters@MySql.snap │ │ ├── sqlx_conditional_queries_core__snapshot_tests__only_runtime_bound_parameters@PostgreSql.snap │ │ └── sqlx_conditional_queries_core__snapshot_tests__only_runtime_bound_parameters@Sqlite.snap └── tests │ ├── lib.rs │ └── regressions │ ├── issue_4.rs │ └── mod.rs ├── macros ├── Cargo.toml ├── README.md └── src │ └── lib.rs └── src └── lib.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: ~ 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: dtolnay/rust-toolchain@stable 15 | - uses: baptiste0928/cargo-install@v2 16 | with: 17 | crate: cargo-sort 18 | 19 | - name: cargo-fmt 20 | run: cargo fmt --check 21 | 22 | - name: clippy 23 | run: cargo clippy --all-features 24 | 25 | - name: cargo-sort 26 | run: cargo sort --check --workspace 27 | 28 | test: 29 | strategy: 30 | matrix: 31 | database-type: ["postgres", "mysql", "sqlite"] 32 | 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v3 37 | - uses: dtolnay/rust-toolchain@stable 38 | 39 | - name: cargo-check 40 | run: cargo check --workspace --features ${{ matrix.database-type }} 41 | 42 | - name: cargo-test 43 | run: cargo test --workspace --features ${{ matrix.database-type }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /.* 4 | !/.github 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | 11 | ## [0.3.2] - 2025-02-03 12 | 13 | ### Fixed 14 | - Catch duplicate compile-time bindings and cyclical compile-time binding references. 15 | ([#26](https://github.com/kyrias/sqlx-conditional-queries/pull/26)) 16 | 17 | 18 | ## [0.3.1] - 2025-01-09 19 | 20 | ### Changed 21 | - Switch from `proc-macro-error` to `proc-macro-error2`. 22 | ([#24](https://github.com/kyrias/sqlx-conditional-queries/pull/24)) 23 | 24 | 25 | ## [0.3.0] - 2025-01-07 26 | 27 | ### Fixed 28 | - Fix queries only containing run-time bound parameters. 29 | ([#20](https://github.com/kyrias/sqlx-conditional-queries/issues/20), 30 | [#22](https://github.com/kyrias/sqlx-conditional-queries/pull/22)) 31 | 32 | ### Changed 33 | - Upgrade all dependencies. 34 | ([#23](https://github.com/kyrias/sqlx-conditional-queries/pull/23)) 35 | - Made `sqlx-conditional-queries-core` take database type at run-time. 36 | ([#21](https://github.com/kyrias/sqlx-conditional-queries/pull/21)) 37 | 38 | 39 | ## [0.2.1] - 2024-08-19 40 | 41 | ### Fixed 42 | - Internal dependency versions weren't updated in 0.2.0. 43 | 44 | 45 | ## [0.2.0] - 2024-08-19 46 | 47 | ### Changed 48 | - Upgrade all dependencies. ([#19](https://github.com/kyrias/sqlx-conditional-queries/pull/19)) 49 | 50 | 51 | ## [0.1.4] - 2024-03-27 52 | 53 | ### Deprecated 54 | 55 | - Deprecated `fetch_many` method since it was deprecated in `sqlx` 0.7.4. ([#17](https://github.com/kyrias/sqlx-conditional-queries/pull/17)) 56 | 57 | 58 | ## [0.1.3] - 2023-07-12 59 | 60 | ### Changed 61 | 62 | - Drop patch version bound of internal crates. 63 | - Switch from using type ascription synatx to using `as` for type overrides. ([#12](https://github.com/kyrias/sqlx-conditional-queries/issues/12), [#13](https://github.com/kyrias/sqlx-conditional-queries/issues/13)) 64 | 65 | 66 | ## [0.1.2] - 2023-02-16 67 | 68 | ### Fixed 69 | 70 | - Fixed bug introduced when removing brace escaping support that lead to out-of-bound panics when two bound parameter references were too far apart. ([#4](https://github.com/kyrias/sqlx-conditional-queries/issues/4)) 71 | 72 | 73 | [Unreleased]: https://github.com/kyrias/sqlx-conditional-queries/compare/0.3.2...main 74 | [0.3.1]: https://github.com/kyrias/sqlx-conditional-queries/compare/0.3.1...0.3.2 75 | [0.3.1]: https://github.com/kyrias/sqlx-conditional-queries/compare/0.3.0...0.3.1 76 | [0.3.0]: https://github.com/kyrias/sqlx-conditional-queries/compare/0.2.1...0.3.0 77 | [0.2.1]: https://github.com/kyrias/sqlx-conditional-queries/compare/0.2.0...0.2.1 78 | [0.2.0]: https://github.com/kyrias/sqlx-conditional-queries/compare/0.1.4...0.2.0 79 | [0.1.3]: https://github.com/kyrias/sqlx-conditional-queries/compare/0.1.3...0.1.4 80 | [0.1.3]: https://github.com/kyrias/sqlx-conditional-queries/compare/0.1.2...0.1.3 81 | [0.1.2]: https://github.com/kyrias/sqlx-conditional-queries/compare/0.1.1...0.1.2 82 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["core", "macros"] 3 | 4 | [package] 5 | name = "sqlx-conditional-queries" 6 | version = "0.3.2" 7 | edition = "2021" 8 | description = "Compile-time conditional queries for SQLx" 9 | repository = "https://github.com/kyrias/sqlx-conditional-queries" 10 | license = "MIT OR Apache-2.0" 11 | keywords = ["sqlx", "database"] 12 | categories = ["database"] 13 | 14 | [package.metadata.docs.rs] 15 | features = ["postgres"] 16 | 17 | [dependencies] 18 | futures-core = "0.3.31" 19 | sqlx-conditional-queries-macros = { path = "macros", version = "0.3" } 20 | 21 | [features] 22 | mysql = ["sqlx-conditional-queries-macros/mysql"] 23 | postgres = ["sqlx-conditional-queries-macros/postgres"] 24 | sqlite = ["sqlx-conditional-queries-macros/sqlite"] 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) the sqlx-conditional-queries developers 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 | # Conditional compile-time verified queries with SQLx 2 | 3 | [github](https://github.com/kyrias/sqlx-conditional-queries) 4 | [crates.io](https://crates.io/crates/sqlx-conditional-queries) 5 | [docs.rs](https://docs.rs/sqlx-conditional-queries) 6 | 7 | This crate provides a macro for generating conditional compile-time verified 8 | queries while using the SQLx `query_as!` macro. This allows you to have parts 9 | of the query conditional in ways in which your chosen database doesn't allow by 10 | emitting multiple `query_as!` invocations that are chosen over by a match 11 | statement. 12 | 13 | 14 | ## Variant growth 15 | 16 | Note that this means that we end up emitting as many `query_as!` invocations as 17 | there are elements in the [Cartesian product] of all of the different 18 | conditionals. This means that the number of variants increase very rapidly! 19 | 20 | [Cartesian product]: https://en.wikipedia.org/wiki/Cartesian_product 21 | 22 | 23 | ## Features 24 | 25 | Which database type should be supported is specified by activating one of the 26 | following features. If more than one feature is activated then the first one 27 | in the list takes precedence. 28 | 29 | - `postgres` 30 | - `mysql` 31 | - `sqlite` 32 | 33 | 34 | #### License 35 | 36 | 37 | Licensed under either of Apache License, Version 38 | 2.0 or MIT license at your option. 39 | 40 | 41 |
42 | 43 | 44 | Unless you explicitly state otherwise, any contribution intentionally submitted 45 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 46 | be dual licensed as above, without any additional terms or conditions. 47 | 48 | -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sqlx-conditional-queries-core" 3 | version = "0.3.2" 4 | edition = "2021" 5 | description = "Internal functions for sqlx-conditional-queries" 6 | repository = "https://github.com/kyrias/sqlx-conditional-queries" 7 | license = "MIT OR Apache-2.0" 8 | 9 | [dependencies] 10 | itertools = "0.14.0" 11 | proc-macro2 = "1.0.92" 12 | quote = "1.0.38" 13 | syn = { version = "2.0.95", features = ["extra-traits", "full"] } 14 | thiserror = "2.0.9" 15 | 16 | [dev-dependencies] 17 | insta = "1.42.0" 18 | prettyplease = "0.2.27" 19 | rstest = "0.24.0" 20 | 21 | [profile.dev.package.insta] 22 | opt-level = 3 23 | 24 | [profile.dev.package.similar] 25 | opt-level = 3 26 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | **Note**: This is an internal library only meant to be used by the macro 2 | exposed by the sqlx-conditional-queries crate. 3 | 4 | ## Features 5 | 6 | Which database type should be supported is specified by activating one of the 7 | following features. If more than one feature is activated then the first one 8 | in the list takes precedence. 9 | 10 | - `postgres` 11 | - `mysql` 12 | - `sqlite` 13 | 14 | 15 | #### License 16 | 17 | 18 | Licensed under either of Apache License, Version 19 | 2.0 or MIT license at your option. 20 | 21 | 22 |
23 | 24 | 25 | Unless you explicitly state otherwise, any contribution intentionally submitted 26 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 27 | be dual licensed as above, without any additional terms or conditions. 28 | 29 | -------------------------------------------------------------------------------- /core/src/analyze.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use syn::spanned::Spanned; 4 | 5 | use crate::parse::ParsedConditionalQueryAs; 6 | 7 | #[derive(Debug, thiserror::Error)] 8 | pub enum AnalyzeError { 9 | #[error("expected string literal")] 10 | ExpectedStringLiteral(proc_macro2::Span), 11 | #[error("mismatch between number of names ({names}) and values ({values})")] 12 | BindingNameValueLengthMismatch { 13 | names: usize, 14 | names_span: proc_macro2::Span, 15 | values: usize, 16 | values_span: proc_macro2::Span, 17 | }, 18 | #[error("found two compile-time bindings with the same binding: {first}")] 19 | DuplicatedCompileTimeBindingsFound { 20 | first: proc_macro2::Ident, 21 | second: proc_macro2::Ident, 22 | }, 23 | #[error("found cycle in compile-time bindings: {path}")] 24 | CompileTimeBindingCycleDetected { 25 | root_ident: proc_macro2::Ident, 26 | path: String, 27 | }, 28 | } 29 | 30 | /// This represents the finished second step in the processing pipeline. 31 | /// The compile time bindings have been further processed to a form that allows us to easily create 32 | /// the cartesian product and thereby all query variations in the next step. 33 | #[derive(Debug)] 34 | pub(crate) struct AnalyzedConditionalQueryAs { 35 | pub(crate) output_type: syn::Ident, 36 | pub(crate) query_string: syn::LitStr, 37 | pub(crate) compile_time_bindings: Vec, 38 | } 39 | 40 | /// This represents a single combination of a single compiletime binding of a query. 41 | #[derive(Debug)] 42 | pub(crate) struct CompileTimeBinding { 43 | /// The actual expression used in the match statement. 44 | /// E.g. for `match something`, this would be `something`. 45 | pub(crate) expression: syn::Expr, 46 | /// Each entry in this Vec represents a single expanded `match` and the 47 | /// binding names with the binding values from that specific arm. 48 | /// (`match arm pattern`, Vec(binding_name, binding_value)` 49 | pub(crate) arms: Vec<(syn::Pat, Vec<(syn::Ident, syn::LitStr)>)>, 50 | } 51 | 52 | /// Further parse and analyze all compiletime binding statements. 53 | /// Each binding is split into individual entries of this form: 54 | /// (`match arm pattern`, Vec(binding_name, binding_value)` 55 | pub(crate) fn analyze( 56 | parsed: ParsedConditionalQueryAs, 57 | ) -> Result { 58 | let mut compile_time_bindings = Vec::new(); 59 | 60 | let mut known_binding_names = HashSet::new(); 61 | 62 | for (names, match_expr) in parsed.compile_time_bindings { 63 | let binding_names_span = names.span(); 64 | // Convert the OneOrPunctuated enum in a list of `Ident`s. 65 | // `One(T)` will be converted into a Vec with a single entry. 66 | let binding_names: Vec<_> = names.into_iter().collect(); 67 | 68 | // Find duplicate compile-time bindings. 69 | for name in &binding_names { 70 | let Some(first) = known_binding_names.get(name) else { 71 | known_binding_names.insert(name.clone()); 72 | continue; 73 | }; 74 | return Err(AnalyzeError::DuplicatedCompileTimeBindingsFound { 75 | first: first.clone(), 76 | second: name.clone(), 77 | }); 78 | } 79 | 80 | let mut bindings = Vec::new(); 81 | for arm in match_expr.arms { 82 | let arm_span = arm.body.span(); 83 | 84 | let binding_values = match *arm.body { 85 | // If the match arm expression just contains a literal, use that. 86 | syn::Expr::Lit(syn::ExprLit { 87 | lit: syn::Lit::Str(literal), 88 | .. 89 | }) => vec![literal], 90 | 91 | // If there's a tuple, treat each literal inside that tuple as a binding value. 92 | syn::Expr::Tuple(tuple) => { 93 | let mut values = Vec::new(); 94 | for elem in tuple.elems { 95 | match elem { 96 | syn::Expr::Lit(syn::ExprLit { 97 | lit: syn::Lit::Str(literal), 98 | .. 99 | }) => values.push(literal), 100 | 101 | _ => return Err(AnalyzeError::ExpectedStringLiteral(elem.span())), 102 | } 103 | } 104 | values 105 | } 106 | 107 | body => return Err(AnalyzeError::ExpectedStringLiteral(body.span())), 108 | }; 109 | 110 | // There must always be a matching amount of binding values in each match arm. 111 | // Error if there are more or fewer values than binding names. 112 | if binding_names.len() != binding_values.len() { 113 | return Err(AnalyzeError::BindingNameValueLengthMismatch { 114 | names: binding_names.len(), 115 | names_span: binding_names_span, 116 | values: binding_values.len(), 117 | values_span: arm_span, 118 | }); 119 | } 120 | 121 | bindings.push(( 122 | arm.pat, 123 | binding_names 124 | .iter() 125 | .cloned() 126 | .zip(binding_values) 127 | .collect::>(), 128 | )); 129 | } 130 | 131 | compile_time_bindings.push(CompileTimeBinding { 132 | expression: *match_expr.expr, 133 | arms: bindings, 134 | }); 135 | } 136 | 137 | compile_time_bindings::validate_compile_time_bindings(&compile_time_bindings)?; 138 | 139 | Ok(AnalyzedConditionalQueryAs { 140 | output_type: parsed.output_type, 141 | query_string: parsed.query_string, 142 | compile_time_bindings, 143 | }) 144 | } 145 | 146 | mod compile_time_bindings { 147 | use std::collections::{HashMap, HashSet}; 148 | 149 | use super::{AnalyzeError, CompileTimeBinding}; 150 | 151 | pub(super) fn validate_compile_time_bindings( 152 | compile_time_bindings: &[CompileTimeBinding], 153 | ) -> Result<(), AnalyzeError> { 154 | let mut bindings = HashMap::new(); 155 | 156 | for (_, binding_values) in compile_time_bindings 157 | .iter() 158 | .flat_map(|bindings| &bindings.arms) 159 | { 160 | for (binding, value) in binding_values { 161 | let name = binding.to_string(); 162 | 163 | let (_, references) = bindings 164 | .entry(name) 165 | .or_insert_with(|| (binding, HashSet::new())); 166 | fill_references(references, &value.value()); 167 | } 168 | } 169 | 170 | for (name, (ident, _)) in &bindings { 171 | validate_references(&bindings, ident, &[], name)?; 172 | } 173 | 174 | Ok(()) 175 | } 176 | 177 | fn fill_references(references: &mut HashSet, mut fragment: &str) { 178 | while let Some(start_idx) = fragment.find("{#") { 179 | fragment = &fragment[start_idx + 2..]; 180 | if let Some(end_idx) = fragment.find("}") { 181 | references.insert(fragment[..end_idx].to_string()); 182 | fragment = &fragment[end_idx + 1..]; 183 | } else { 184 | break; 185 | } 186 | } 187 | } 188 | 189 | fn validate_references( 190 | bindings: &HashMap)>, 191 | root_ident: &syn::Ident, 192 | path: &[&str], 193 | name: &str, 194 | ) -> Result<(), AnalyzeError> { 195 | let mut path = path.to_vec(); 196 | path.push(name); 197 | 198 | if path.iter().filter(|component| **component == name).count() > 1 { 199 | return Err(AnalyzeError::CompileTimeBindingCycleDetected { 200 | root_ident: root_ident.clone(), 201 | path: path.join(" -> "), 202 | }); 203 | } 204 | 205 | let Some((_, references)) = bindings.get(name) else { 206 | // This error is caught and handled in all contexts in the expand stage. 207 | return Ok(()); 208 | }; 209 | 210 | for reference in references { 211 | validate_references(bindings, root_ident, &path, reference)?; 212 | } 213 | 214 | Ok(()) 215 | } 216 | } 217 | 218 | #[cfg(test)] 219 | mod tests { 220 | use quote::ToTokens; 221 | 222 | use super::*; 223 | 224 | #[test] 225 | fn valid_syntax() { 226 | let parsed = syn::parse_str::( 227 | r#" 228 | SomeType, 229 | "some SQL query", 230 | #binding = match foo { 231 | bar => "baz", 232 | }, 233 | #(a, b) = match c { 234 | d => ("e", "f"), 235 | }, 236 | "#, 237 | ) 238 | .unwrap(); 239 | let mut analyzed = analyze(parsed.clone()).unwrap(); 240 | 241 | assert_eq!(parsed.output_type, analyzed.output_type); 242 | assert_eq!(parsed.query_string, analyzed.query_string); 243 | 244 | assert_eq!(analyzed.compile_time_bindings.len(), 2); 245 | 246 | { 247 | let compile_time_binding = dbg!(analyzed.compile_time_bindings.remove(0)); 248 | assert_eq!( 249 | compile_time_binding 250 | .expression 251 | .to_token_stream() 252 | .to_string(), 253 | "foo", 254 | ); 255 | 256 | assert_eq!(compile_time_binding.arms.len(), 1); 257 | { 258 | let arm = &compile_time_binding.arms[0]; 259 | assert_eq!(arm.0.to_token_stream().to_string(), "bar"); 260 | assert_eq!( 261 | arm.1 262 | .iter() 263 | .map(|v| ( 264 | v.0.to_token_stream().to_string(), 265 | v.1.to_token_stream().to_string(), 266 | )) 267 | .collect::>(), 268 | &[("binding".to_string(), "\"baz\"".to_string())], 269 | ); 270 | } 271 | } 272 | 273 | { 274 | let compile_time_binding = dbg!(analyzed.compile_time_bindings.remove(0)); 275 | assert_eq!( 276 | compile_time_binding 277 | .expression 278 | .to_token_stream() 279 | .to_string(), 280 | "c", 281 | ); 282 | 283 | assert_eq!( 284 | compile_time_binding 285 | .arms 286 | .iter() 287 | .map(|v| v.0.to_token_stream().to_string()) 288 | .collect::>(), 289 | &["d"], 290 | ); 291 | 292 | assert_eq!(compile_time_binding.arms.len(), 1); 293 | { 294 | let arm = &compile_time_binding.arms[0]; 295 | assert_eq!(arm.0.to_token_stream().to_string(), "d"); 296 | assert_eq!( 297 | arm.1 298 | .iter() 299 | .map(|v| ( 300 | v.0.to_token_stream().to_string(), 301 | v.1.to_token_stream().to_string(), 302 | )) 303 | .collect::>(), 304 | &[ 305 | ("a".to_string(), "\"e\"".to_string()), 306 | ("b".to_string(), "\"f\"".to_string()) 307 | ], 308 | ); 309 | } 310 | } 311 | } 312 | 313 | #[test] 314 | fn duplicate_compile_time_bindings() { 315 | let parsed = syn::parse_str::( 316 | r##" 317 | SomeType, 318 | r#"{#a}"#, 319 | #a = match _ { 320 | _ => "1", 321 | }, 322 | #a = match _ { 323 | _ => "2", 324 | }, 325 | "##, 326 | ) 327 | .unwrap(); 328 | let analyzed = analyze(parsed.clone()).unwrap_err(); 329 | 330 | assert!(matches!( 331 | analyzed, 332 | AnalyzeError::DuplicatedCompileTimeBindingsFound { .. } 333 | )); 334 | } 335 | 336 | #[test] 337 | fn compile_time_binding_cycle_detected() { 338 | let parsed = syn::parse_str::( 339 | r##" 340 | SomeType, 341 | r#"{#a}"#, 342 | #a = match _ { 343 | _ => "{#b}", 344 | }, 345 | #b = match _ { 346 | _ => "{#a}", 347 | }, 348 | "##, 349 | ) 350 | .unwrap(); 351 | let analyzed = analyze(parsed.clone()).unwrap_err(); 352 | 353 | assert!(matches!( 354 | analyzed, 355 | AnalyzeError::CompileTimeBindingCycleDetected { .. } 356 | )); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /core/src/codegen.rs: -------------------------------------------------------------------------------- 1 | use quote::{format_ident, quote}; 2 | 3 | use crate::expand::ExpandedConditionalQueryAs; 4 | 5 | /// This is the final step of the macro generation pipeline. 6 | /// The match arms and the respective query fragments are now used to generate a giant match 7 | /// statement, which covers all variants of the bindings' match statements' cartesian products. 8 | pub(crate) fn codegen(expanded: ExpandedConditionalQueryAs) -> proc_macro2::TokenStream { 9 | let mut match_arms = Vec::new(); 10 | for (idx, arm) in expanded.match_arms.iter().enumerate() { 11 | let patterns = &arm.patterns; 12 | let variant = format_ident!("Variant{}", idx); 13 | let output_type = &expanded.output_type; 14 | let query_fragments = &arm.query_fragments; 15 | let run_time_bindings = 16 | arm.run_time_bindings 17 | .iter() 18 | .map(|(name, type_override)| match type_override { 19 | Some(ty) => quote!(#name as #ty), 20 | None => quote!(#name), 21 | }); 22 | 23 | match_arms.push(quote! { 24 | (#(#patterns,)*) => { 25 | ConditionalMap::#variant( 26 | ::sqlx::query_as!( 27 | #output_type, 28 | #(#query_fragments)+*, 29 | #(#run_time_bindings),* 30 | ) 31 | ) 32 | }, 33 | }); 34 | } 35 | 36 | let conditional_map = build_conditional_map(expanded.match_arms.len()); 37 | let match_expressions = expanded.match_expressions; 38 | 39 | quote! { 40 | { 41 | #conditional_map 42 | 43 | match (#(#match_expressions,)*) { 44 | #(#match_arms)* 45 | } 46 | } 47 | } 48 | } 49 | 50 | fn build_conditional_map(variant_count: usize) -> proc_macro2::TokenStream { 51 | let function_params: Vec<_> = (0..variant_count) 52 | .map(|index| format_ident!("F{}", index)) 53 | .collect(); 54 | let variants: Vec<_> = (0..variant_count) 55 | .map(|index| format_ident!("Variant{}", index)) 56 | .collect(); 57 | 58 | quote! { 59 | enum ConditionalMap<'q, DB: ::sqlx::Database, A, #(#function_params),*> { 60 | #( 61 | #variants( 62 | ::sqlx::query::Map<'q, DB, #function_params, A> 63 | ), 64 | )* 65 | } 66 | 67 | impl<'q, DB, A, O, #(#function_params),*> ConditionalMap<'q, DB, A, #(#function_params),*> 68 | where 69 | DB: ::sqlx::Database, 70 | A: 'q + ::sqlx::IntoArguments<'q, DB> + ::std::marker::Send, 71 | O: ::std::marker::Unpin + ::std::marker::Send, 72 | #( 73 | #function_params: ::std::ops::FnMut(DB::Row) -> ::sqlx::Result + ::std::marker::Send, 74 | )* 75 | { 76 | /// See [`sqlx::query::Map::fetch`] 77 | pub fn fetch<'e, 'c: 'e, E>( 78 | self, 79 | executor: E, 80 | ) -> ::sqlx_conditional_queries::exports::BoxStream<'e, ::sqlx::Result> 81 | where 82 | 'q: 'e, 83 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 84 | DB: 'e, 85 | O: 'e, 86 | #( 87 | #function_params: 'e, 88 | )* 89 | { 90 | match self { 91 | #( 92 | Self::#variants(map) => map.fetch(executor), 93 | )* 94 | } 95 | } 96 | 97 | /// See [`sqlx::query::Map::fetch_many`] 98 | #[deprecated = "Only the SQLite driver supports multiple statements in one prepared statement and that behavior is deprecated. Use `sqlx::raw_sql()` instead. See https://github.com/launchbadge/sqlx/issues/3108 for discussion."] 99 | pub fn fetch_many<'e, 'c: 'e, E>( 100 | mut self, 101 | executor: E, 102 | ) -> ::sqlx_conditional_queries::exports::BoxStream<'e, ::sqlx::Result<::sqlx::Either>> 103 | where 104 | 'q: 'e, 105 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 106 | DB: 'e, 107 | O: 'e, 108 | #( 109 | #function_params: 'e, 110 | )* 111 | { 112 | match self { 113 | #( 114 | Self::#variants(map) => { 115 | #[allow(deprecated)] 116 | map.fetch_many(executor) 117 | } 118 | )* 119 | } 120 | } 121 | 122 | /// See [`sqlx::query::Map::fetch_all`] 123 | pub async fn fetch_all<'e, 'c: 'e, E>( 124 | self, 125 | executor: E, 126 | ) -> ::sqlx::Result<::std::vec::Vec> 127 | where 128 | 'q: 'e, 129 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 130 | DB: 'e, 131 | O: 'e, 132 | #( 133 | #function_params: 'e, 134 | )* 135 | { 136 | match self { 137 | #( 138 | Self::#variants(map) => map.fetch_all(executor).await, 139 | )* 140 | } 141 | } 142 | 143 | /// See [`sqlx::query::Map::fetch_one`] 144 | pub async fn fetch_one<'e, 'c: 'e, E>( 145 | self, 146 | executor: E, 147 | ) -> ::sqlx::Result 148 | where 149 | 'q: 'e, 150 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 151 | DB: 'e, 152 | O: 'e, 153 | #( 154 | #function_params: 'e, 155 | )* 156 | { 157 | match self { 158 | #( 159 | Self::#variants(map) => map.fetch_one(executor).await, 160 | )* 161 | } 162 | } 163 | 164 | /// See [`sqlx::query::Map::fetch_optional`] 165 | pub async fn fetch_optional<'e, 'c: 'e, E>( 166 | self, 167 | executor: E, 168 | ) -> ::sqlx::Result<::std::option::Option> 169 | where 170 | 'q: 'e, 171 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 172 | DB: 'e, 173 | O: 'e, 174 | #( 175 | #function_params: 'e, 176 | )* 177 | { 178 | match self { 179 | #( 180 | Self::#variants(map) => map.fetch_optional(executor).await, 181 | )* 182 | } 183 | } 184 | } 185 | } 186 | } 187 | 188 | #[cfg(test)] 189 | mod tests { 190 | use crate::DatabaseType; 191 | 192 | use super::*; 193 | 194 | #[rstest::rstest] 195 | #[case(DatabaseType::PostgreSql)] 196 | #[case(DatabaseType::MySql)] 197 | #[case(DatabaseType::Sqlite)] 198 | fn valid_syntax(#[case] database_type: DatabaseType) { 199 | let parsed = syn::parse_str::( 200 | r#" 201 | SomeType, 202 | "some {#a} {#b} {#j} query", 203 | #(a, b) = match c { 204 | d => ("e", "f"), 205 | g => ("h", "i"), 206 | }, 207 | #j = match i { 208 | k => "l", 209 | m => "n", 210 | }, 211 | "#, 212 | ) 213 | .unwrap(); 214 | let analyzed = crate::analyze::analyze(parsed.clone()).unwrap(); 215 | let lowered = crate::lower::lower(analyzed); 216 | let expanded = crate::expand::expand(database_type, lowered).unwrap(); 217 | let _codegened = codegen(expanded); 218 | } 219 | 220 | #[rstest::rstest] 221 | #[case(DatabaseType::PostgreSql)] 222 | #[case(DatabaseType::MySql)] 223 | #[case(DatabaseType::Sqlite)] 224 | fn type_override(#[case] database_type: DatabaseType) { 225 | let parsed = syn::parse_str::( 226 | r#" 227 | SomeType, 228 | "{some_binding:ty}", 229 | "#, 230 | ) 231 | .unwrap(); 232 | let analyzed = crate::analyze::analyze(parsed.clone()).unwrap(); 233 | let lowered = crate::lower::lower(analyzed); 234 | let expanded = crate::expand::expand(database_type, lowered).unwrap(); 235 | let codegened = codegen(expanded); 236 | 237 | let stringified = codegened.to_string(); 238 | assert!( 239 | stringified.contains(" some_binding as ty"), 240 | "binding type override was not correctly generated: {stringified}" 241 | ); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /core/src/expand.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{lower::LoweredConditionalQueryAs, DatabaseType}; 4 | 5 | #[derive(Debug, thiserror::Error)] 6 | pub enum ExpandError { 7 | #[error("missing compile-time binding: {0}")] 8 | MissingCompileTimeBinding(String, proc_macro2::Span), 9 | #[error("missing binding closing brace")] 10 | MissingBindingClosingBrace(proc_macro2::Span), 11 | #[error("failed to parse type override in binding reference: {0}")] 12 | BindingReferenceTypeOverrideParseError(proc_macro2::LexError, proc_macro2::Span), 13 | } 14 | 15 | #[derive(Debug)] 16 | pub(crate) struct ExpandedConditionalQueryAs { 17 | pub(crate) output_type: syn::Ident, 18 | pub(crate) match_expressions: Vec, 19 | pub(crate) match_arms: Vec, 20 | } 21 | 22 | #[derive(Debug)] 23 | pub(crate) struct MatchArm { 24 | pub(crate) patterns: Vec, 25 | pub(crate) query_fragments: Vec, 26 | pub(crate) run_time_bindings: Vec<(syn::Ident, Option)>, 27 | } 28 | 29 | /// Corresponds to a single run-time binding name. 30 | #[derive(Debug)] 31 | struct RunTimeBinding { 32 | /// List of all argument index positions at which this binding needs to be bound. 33 | /// 34 | /// - For PostgreSQL only contains one element. 35 | /// - For MySQL and SQLite it contains one index for each time the binding was referenced. 36 | indices: Vec, 37 | 38 | /// Type-override fragment to pass on To SQLx 39 | type_override: Option, 40 | } 41 | 42 | #[derive(Debug)] 43 | struct RunTimeBindings { 44 | database_type: DatabaseType, 45 | counter: usize, 46 | bindings: HashMap, 47 | } 48 | 49 | impl RunTimeBindings { 50 | fn new(database_type: DatabaseType) -> Self { 51 | Self { 52 | database_type, 53 | counter: 0, 54 | bindings: Default::default(), 55 | } 56 | } 57 | 58 | /// Returns a database-appropriate run-time binding string for the given binding name. 59 | /// 60 | /// Database type selection is done based on the features this crate was built with. 61 | /// 62 | /// - PostgreSQL uses 1-indexed references such as `$1`, which means that multiple references 63 | /// to the same parameter only need to be bound once. 64 | /// - MySQL and SQLite always use `?` which means that the arguments need to specified in 65 | /// order and be duplicated for as many times as they're used. 66 | fn get_binding_string( 67 | &mut self, 68 | binding_name: syn::LitStr, 69 | type_override: Option, 70 | ) -> syn::LitStr { 71 | match self.database_type { 72 | DatabaseType::PostgreSql => { 73 | let span = binding_name.span(); 74 | let binding = self.bindings.entry(binding_name).or_insert_with(|| { 75 | self.counter += 1; 76 | RunTimeBinding { 77 | indices: vec![self.counter], 78 | type_override, 79 | } 80 | }); 81 | syn::LitStr::new(&format!("${}", binding.indices.first().unwrap()), span) 82 | } 83 | DatabaseType::MySql | DatabaseType::Sqlite => { 84 | let span = binding_name.span(); 85 | self.counter += 1; 86 | 87 | // For MySQL and SQLite bindings we need to specify the same argument multiple 88 | // times if it's reused and so generate a unique index every time. This ensures 89 | // that `get_run_time_bindings` will generate the arguments in the correct order. 90 | self.bindings 91 | .entry(binding_name) 92 | .and_modify(|binding| binding.indices.push(self.counter)) 93 | .or_insert_with(|| RunTimeBinding { 94 | indices: vec![self.counter], 95 | type_override, 96 | }); 97 | syn::LitStr::new("?", span) 98 | } 99 | } 100 | } 101 | 102 | /// Returns the `query_as!` arguments for all referenced run-time bindings. 103 | fn get_arguments(self) -> Vec<(syn::Ident, Option)> { 104 | let mut run_time_bindings: Vec<_> = self 105 | .bindings 106 | .into_iter() 107 | .flat_map(|(name, binding)| { 108 | binding 109 | .indices 110 | .into_iter() 111 | .map(|index| { 112 | ( 113 | syn::Ident::new(&name.value(), name.span()), 114 | binding.type_override.clone(), 115 | index, 116 | ) 117 | }) 118 | .collect::>() 119 | }) 120 | .collect(); 121 | 122 | run_time_bindings.sort_by_key(|(_, _, index)| *index); 123 | 124 | run_time_bindings 125 | .into_iter() 126 | .map(|(ident, type_override, _)| (ident, type_override)) 127 | .collect() 128 | } 129 | } 130 | 131 | /// This function takes the original query string that was supplied to the macro and adjusts it for 132 | /// each arm of the previously generated cartesian product of all bindings' match arms. 133 | /// 134 | /// The `{#binding_name}` placeholder are then replaced with the string literals from match clauses 135 | /// and all `{scope_variable} placeholder are replaced with the positional variables of the respective 136 | /// database engine whose feature is enabled. For more info take a look at [RunTimeBindings]. 137 | pub(crate) fn expand( 138 | database_type: DatabaseType, 139 | lowered: LoweredConditionalQueryAs, 140 | ) -> Result { 141 | let mut match_arms = Vec::new(); 142 | 143 | for arm in lowered.match_arms { 144 | let mut fragments = vec![lowered.query_string.clone()]; 145 | while fragments 146 | .iter() 147 | .any(|fragment| fragment.value().contains("{#")) 148 | { 149 | fragments = expand_compile_time_bindings(fragments, &arm.compile_time_bindings)?; 150 | } 151 | 152 | // Substitute 153 | let mut run_time_bindings = RunTimeBindings::new(database_type); 154 | let expanded = expand_run_time_bindings(fragments, &mut run_time_bindings)?; 155 | 156 | match_arms.push(MatchArm { 157 | patterns: arm.patterns, 158 | query_fragments: expanded, 159 | run_time_bindings: run_time_bindings.get_arguments(), 160 | }); 161 | } 162 | 163 | Ok(ExpandedConditionalQueryAs { 164 | output_type: lowered.output_type, 165 | match_expressions: lowered.match_expressions, 166 | match_arms, 167 | }) 168 | } 169 | 170 | /// This function takes the list of query fragments and substitutes all `{#binding_name}` 171 | /// occurrences with their literal strings from the respective match statements. 172 | /// 173 | /// These literal strings however, can once again contain another `{#binding_name}`, which is why 174 | /// this function is called from a while loop. 175 | /// Since this function might get called multiple times, some fragments might already be expanded 176 | /// at this point, despite the variable name. 177 | fn expand_compile_time_bindings( 178 | unexpanded_fragments: Vec, 179 | compile_time_bindings: &HashMap, 180 | ) -> Result, ExpandError> { 181 | let mut expanded_fragments = Vec::new(); 182 | 183 | for fragment in unexpanded_fragments { 184 | let fragment_string = fragment.value(); 185 | let mut fragment_str = fragment_string.as_str(); 186 | 187 | while let Some(start_of_binding) = fragment_str.find('{') { 188 | // We've hit either a compile-time or a run-time binding, so first we push any prefix 189 | // before the binding. 190 | if !fragment_str[..start_of_binding].is_empty() { 191 | expanded_fragments.push(syn::LitStr::new( 192 | &fragment_str[..start_of_binding], 193 | fragment.span(), 194 | )); 195 | fragment_str = &fragment_str[start_of_binding..]; 196 | } 197 | 198 | // Then we find the matching closing brace. 199 | let end_of_binding = if let Some(end_of_binding) = fragment_str.find('}') { 200 | end_of_binding 201 | } else { 202 | return Err(ExpandError::MissingBindingClosingBrace(fragment.span())); 203 | }; 204 | 205 | if fragment_str.chars().nth(1) == Some('#') { 206 | // If the binding is a compile-time binding, expand it. 207 | let binding_name = &fragment_str[2..end_of_binding]; 208 | if let Some(binding) = compile_time_bindings.get(binding_name) { 209 | expanded_fragments.push(binding.clone()); 210 | } else { 211 | return Err(ExpandError::MissingCompileTimeBinding( 212 | binding_name.to_string(), 213 | fragment.span(), 214 | )); 215 | } 216 | } else { 217 | // Otherwise push it as-is for the next pass. 218 | expanded_fragments.push(syn::LitStr::new( 219 | &fragment_str[..end_of_binding + 1], 220 | fragment.span(), 221 | )); 222 | } 223 | 224 | fragment_str = &fragment_str[end_of_binding + 1..]; 225 | } 226 | 227 | // Push trailing query fragment. 228 | if !fragment_str.is_empty() { 229 | expanded_fragments.push(syn::LitStr::new(fragment_str, fragment.span())); 230 | } 231 | } 232 | 233 | Ok(expanded_fragments) 234 | } 235 | 236 | /// Take all fragments and substitute any `{name}` occurrences with the respective database 237 | /// binding. Since the parameter syntax is different for various databases, [RunTimeBinding] is 238 | /// used in combination with feature flags to abstract this variance away. 239 | fn expand_run_time_bindings( 240 | unexpanded_fragments: Vec, 241 | run_time_bindings: &mut RunTimeBindings, 242 | ) -> Result, ExpandError> { 243 | let mut expanded_query = Vec::new(); 244 | 245 | for fragment in unexpanded_fragments { 246 | let fragment_string = fragment.value(); 247 | let mut fragment_str = fragment_string.as_str(); 248 | 249 | while let Some(start_of_binding) = fragment_str.find('{') { 250 | // Otherwise we've hit a run-time binding, so first we push any prefix before the 251 | // binding. 252 | expanded_query.push(syn::LitStr::new( 253 | &fragment_str[..start_of_binding], 254 | fragment.span(), 255 | )); 256 | 257 | // Then we find the matching closing brace. 258 | fragment_str = &fragment_str[start_of_binding + 1..]; 259 | let end_of_binding = if let Some(end_of_binding) = fragment_str.find('}') { 260 | end_of_binding 261 | } else { 262 | return Err(ExpandError::MissingBindingClosingBrace(fragment.span())); 263 | }; 264 | 265 | let binding_name = &fragment_str[..end_of_binding]; 266 | let (binding_name, type_override) = if let Some(offset) = binding_name.find(':') { 267 | let (binding_name, type_override) = binding_name.split_at(offset); 268 | let type_override = type_override[1..] 269 | .parse::() 270 | .map_err(|err| { 271 | ExpandError::BindingReferenceTypeOverrideParseError(err, fragment.span()) 272 | })?; 273 | (binding_name.trim(), Some(type_override)) 274 | } else { 275 | (binding_name, None) 276 | }; 277 | 278 | // And finally we push a bound parameter argument 279 | let binding = run_time_bindings.get_binding_string( 280 | syn::LitStr::new(binding_name, fragment.span()), 281 | type_override, 282 | ); 283 | expanded_query.push(binding); 284 | 285 | fragment_str = &fragment_str[end_of_binding + 1..]; 286 | } 287 | 288 | // Push trailing query fragment. 289 | if !fragment_str.is_empty() { 290 | expanded_query.push(syn::LitStr::new(fragment_str, fragment.span())); 291 | } 292 | } 293 | 294 | Ok(expanded_query) 295 | } 296 | 297 | #[cfg(test)] 298 | mod tests { 299 | use quote::ToTokens; 300 | 301 | use super::*; 302 | 303 | #[rstest::rstest] 304 | #[case(DatabaseType::PostgreSql)] 305 | #[case(DatabaseType::MySql)] 306 | #[case(DatabaseType::Sqlite)] 307 | fn expands_compile_time_bindings(#[case] database_type: DatabaseType) { 308 | let parsed = syn::parse_str::( 309 | r#" 310 | SomeType, 311 | "some {#a} {#b} {#j} query", 312 | #(a, b) = match c { 313 | d => ("e", "f"), 314 | g => ("h", "i"), 315 | }, 316 | #j = match i { 317 | k => "l", 318 | m => "n", 319 | }, 320 | "#, 321 | ) 322 | .unwrap(); 323 | let analyzed = crate::analyze::analyze(parsed.clone()).unwrap(); 324 | let lowered = crate::lower::lower(analyzed); 325 | let expanded = expand(database_type, lowered).unwrap(); 326 | 327 | assert_eq!( 328 | expanded.match_arms[0] 329 | .query_fragments 330 | .iter() 331 | .map(|qs| qs.to_token_stream().to_string()) 332 | .collect::>(), 333 | &[ 334 | "\"some \"", 335 | "\"e\"", 336 | "\" \"", 337 | "\"f\"", 338 | "\" \"", 339 | "\"l\"", 340 | "\" query\"" 341 | ], 342 | ); 343 | } 344 | 345 | #[rstest::rstest] 346 | #[case(DatabaseType::PostgreSql)] 347 | #[case(DatabaseType::MySql)] 348 | #[case(DatabaseType::Sqlite)] 349 | fn expands_run_time_bindings(#[case] database_type: DatabaseType) { 350 | let parsed = syn::parse_str::( 351 | r#" 352 | SomeType, 353 | "some {foo:ty} {bar} {foo} query", 354 | "#, 355 | ) 356 | .unwrap(); 357 | let analyzed = crate::analyze::analyze(parsed.clone()).unwrap(); 358 | let lowered = crate::lower::lower(analyzed); 359 | let expanded = expand(database_type, lowered).unwrap(); 360 | 361 | // Check that run-time binding references are generated properly. 362 | assert_eq!( 363 | expanded.match_arms[0] 364 | .query_fragments 365 | .iter() 366 | .map(|qs| qs.to_token_stream().to_string()) 367 | .collect::>(), 368 | match database_type { 369 | DatabaseType::PostgreSql => &[ 370 | "\"some \"", 371 | "\"$1\"", 372 | "\" \"", 373 | "\"$2\"", 374 | "\" \"", 375 | "\"$1\"", 376 | "\" query\"" 377 | ], 378 | DatabaseType::MySql | DatabaseType::Sqlite => &[ 379 | "\"some \"", 380 | "\"?\"", 381 | "\" \"", 382 | "\"?\"", 383 | "\" \"", 384 | "\"?\"", 385 | "\" query\"" 386 | ], 387 | } 388 | ); 389 | 390 | // Check that type overrides are parsed properly. 391 | let run_time_bindings: Vec<_> = expanded.match_arms[0] 392 | .run_time_bindings 393 | .iter() 394 | .map(|(ident, ts)| (ident.to_string(), ts.as_ref().map(|ts| ts.to_string()))) 395 | .collect(); 396 | assert_eq!( 397 | run_time_bindings, 398 | match database_type { 399 | DatabaseType::PostgreSql => vec![ 400 | ("foo".to_string(), Some("ty".to_string())), 401 | ("bar".to_string(), None), 402 | ], 403 | DatabaseType::MySql | DatabaseType::Sqlite => vec![ 404 | ("foo".to_string(), Some("ty".to_string())), 405 | ("bar".to_string(), None), 406 | ("foo".to_string(), Some("ty".to_string())), 407 | ], 408 | } 409 | ); 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | pub use analyze::AnalyzeError; 4 | pub use expand::ExpandError; 5 | 6 | mod analyze; 7 | mod codegen; 8 | mod expand; 9 | mod lower; 10 | mod parse; 11 | 12 | #[cfg(test)] 13 | mod snapshot_tests; 14 | 15 | #[derive(Clone, Copy, Debug)] 16 | pub enum DatabaseType { 17 | PostgreSql, 18 | MySql, 19 | Sqlite, 20 | } 21 | 22 | #[derive(Debug, thiserror::Error)] 23 | pub enum Error { 24 | #[error("syn error: {0}")] 25 | SynError(#[from] syn::Error), 26 | #[error("analyze error: {0}")] 27 | AnalyzeError(#[from] analyze::AnalyzeError), 28 | #[error("expand error: {0}")] 29 | ExpandError(#[from] expand::ExpandError), 30 | } 31 | 32 | pub fn conditional_query_as( 33 | database_type: DatabaseType, 34 | input: proc_macro2::TokenStream, 35 | ) -> Result { 36 | let parsed = syn::parse2::(input)?; 37 | let analyzed = analyze::analyze(parsed)?; 38 | let lowered = lower::lower(analyzed); 39 | let expanded = expand::expand(database_type, lowered)?; 40 | let codegened = codegen::codegen(expanded); 41 | 42 | Ok(codegened) 43 | } 44 | -------------------------------------------------------------------------------- /core/src/lower.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use itertools::Itertools; 4 | use syn::parse_quote; 5 | 6 | use crate::analyze::AnalyzedConditionalQueryAs; 7 | 8 | #[derive(Debug)] 9 | pub(crate) struct LoweredConditionalQueryAs { 10 | pub(crate) output_type: syn::Ident, 11 | pub(crate) query_string: syn::LitStr, 12 | /// All expressions that're matched upon. 13 | /// These expressions are in the same order as the patterns in the `match_arms` field. 14 | pub(crate) match_expressions: Vec, 15 | pub(crate) match_arms: Vec, 16 | } 17 | 18 | #[derive(Debug)] 19 | pub(crate) struct MatchArm { 20 | pub(crate) patterns: Vec, 21 | pub(crate) compile_time_bindings: HashMap, 22 | } 23 | 24 | /// Take all compile time bindings and create the cartesian product between all match statement 25 | /// arms of each binding. 26 | /// This allows us to easily create one gigantic match statement that covers all possible cases in 27 | /// the next step. 28 | pub(crate) fn lower(analyzed: AnalyzedConditionalQueryAs) -> LoweredConditionalQueryAs { 29 | // Unzip all bindings for easier iteration. 30 | let (bindings, mut match_expressions): (Vec<_>, Vec<_>) = analyzed 31 | .compile_time_bindings 32 | .into_iter() 33 | .map(|binding| (binding.arms.into_iter(), binding.expression)) 34 | .unzip(); 35 | 36 | // This for loop generates all possible permutations of all match arm binding statements. 37 | // E.g. if there are three match statements: 38 | // ``` 39 | // let a_t = match a { 40 | // a_1 => a_s1 41 | // a_2 => a_s2 42 | // } 43 | // 44 | // let b_t = match b { 45 | // b_1 => b_s1 46 | // } 47 | // 48 | // let (c_t1, ct2) = match c { 49 | // c_1 => (c_s1_1, c_s1_2) 50 | // c_2 => (c_s1_2, c_s2_2) 51 | // } 52 | // ``` 53 | // This function will generate this cartesian product: 54 | // ` 55 | // [ 56 | // [ 57 | // ('a_1', [(`a_t`, `a_s1`)]), 58 | // ('b_1', [(`b_t`, `b_s1`)]), 59 | // ('c_1', [(`c_t1`, `c_s1_1`), (`c_t2`, `c_s1_2`)]), 60 | // ], 61 | // [ 62 | // ('a_2', [(`a_t`, `a_s1`)]), 63 | // ('b_1', [(`b_t`, `b_s1`)]), 64 | // ('c_1', [(`c_t1`, `c_s1_1`), (`c_t2`, `c_s1_2`)]), 65 | // ], 66 | // [ 67 | // ('a_1', [(`a_t`, `a_s1`)]), 68 | // ('b_1', [(`b_t`, `b_s1`)]), 69 | // ('c_2', [(`c_t1`, `c_s2_1`), (`c_t2`, `c_s2_2`)]), 70 | // ], 71 | // [ 72 | // ('a_2', [(`a_t`, `a_s1`)]), 73 | // ('b_1', [(`b_t`, `b_s1`)]), 74 | // ('c_2', [(`c_t1`, `c_s2_1`), (`c_t2`, `c_s2_2`)]), 75 | // ], 76 | // ] 77 | //` 78 | // 79 | // Note how the order in the product always stays the same! 80 | // This is an important guarantee we rely on, as this is also the same order the 81 | // `match_expressions` are in. 82 | // Due this ordering guarantee, we can lateron assemble the match statements without having to 83 | // keep track of which match expressions belongs to which part of the match arm's expression. 84 | let mut match_arms = Vec::new(); 85 | for binding in bindings.into_iter().multi_cartesian_product() { 86 | // `multi_cartesian_product` returns one empty `Vec` if the iterator was empty. 87 | if binding.is_empty() { 88 | continue; 89 | } 90 | 91 | let mut guards = Vec::new(); 92 | let mut bindings = HashMap::new(); 93 | binding.into_iter().for_each(|(g, b)| { 94 | guards.push(g); 95 | bindings.extend(b.into_iter().map(|(name, value)| (name.to_string(), value))); 96 | }); 97 | match_arms.push(MatchArm { 98 | patterns: guards, 99 | compile_time_bindings: bindings, 100 | }); 101 | } 102 | 103 | // If no compile-time bindings were specified we add an always-true option. 104 | // TODO: Think about whether we just return earlier in the pipeline and just return the 105 | // original macro input as a `query_as!` macro. 106 | if match_expressions.is_empty() { 107 | match_expressions.push(parse_quote!(())); 108 | match_arms.push(crate::lower::MatchArm { 109 | patterns: vec![parse_quote!(())], 110 | compile_time_bindings: HashMap::new(), 111 | }); 112 | } 113 | 114 | LoweredConditionalQueryAs { 115 | output_type: analyzed.output_type, 116 | query_string: analyzed.query_string, 117 | match_expressions, 118 | match_arms, 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use super::*; 125 | 126 | #[test] 127 | fn valid_syntax() { 128 | let parsed = syn::parse_str::( 129 | r#" 130 | SomeType, 131 | "some SQL query", 132 | #(a, b) = match c { 133 | d => ("e", "f"), 134 | g => ("h", "i"), 135 | }, 136 | #j = match i { 137 | k => "l", 138 | m => "n", 139 | }, 140 | "#, 141 | ) 142 | .unwrap(); 143 | let analyzed = crate::analyze::analyze(parsed.clone()).unwrap(); 144 | let _lowered = lower(analyzed); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /core/src/parse.rs: -------------------------------------------------------------------------------- 1 | use syn::{parenthesized, parse::Parse}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub(crate) struct ParsedConditionalQueryAs { 5 | /// This is the equivalent of sqlx's output type in a `query_as!` macro. 6 | pub(crate) output_type: syn::Ident, 7 | /// The actual string of the query. 8 | pub(crate) query_string: syn::LitStr, 9 | /// All compile time bindings, each with its variables and associated `match` statement. 10 | pub(crate) compile_time_bindings: Vec<( 11 | OneOrPunctuated, 12 | syn::ExprMatch, 13 | )>, 14 | } 15 | 16 | /// This enum represents the identifier (`#foo`, `#(foo, bar)`) of single binding expression 17 | /// inside a query. 18 | /// 19 | /// Normal statements such as `#foo = match something {...}` are represented by the `One(T)` 20 | /// variant. 21 | /// 22 | /// It's also possible to match tuples such as: 23 | /// `#(order_dir, order_dir_rev) = match order_dir {...}` 24 | /// These are represented by the `Punctuated(...)` variant. 25 | #[derive(Clone, Debug)] 26 | pub(crate) enum OneOrPunctuated { 27 | One(T), 28 | Punctuated(syn::punctuated::Punctuated, proc_macro2::Span), 29 | } 30 | 31 | impl OneOrPunctuated { 32 | pub(crate) fn span(&self) -> proc_macro2::Span { 33 | match self { 34 | OneOrPunctuated::One(t) => t.span(), 35 | OneOrPunctuated::Punctuated(_, span) => *span, 36 | } 37 | } 38 | } 39 | 40 | impl IntoIterator for OneOrPunctuated { 41 | type Item = T; 42 | 43 | type IntoIter = std::vec::IntoIter; 44 | 45 | fn into_iter(self) -> Self::IntoIter { 46 | match self { 47 | OneOrPunctuated::One(item) => vec![item].into_iter(), 48 | OneOrPunctuated::Punctuated(punctuated, _) => { 49 | punctuated.into_iter().collect::>().into_iter() 50 | } 51 | } 52 | } 53 | } 54 | 55 | impl Parse for ParsedConditionalQueryAs { 56 | /// Take a given raw token stream from a macro invocation and parse it into our own 57 | /// representation for further processing. 58 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 59 | // Parse the ident of the output type that we're going to pass to `query_as!`. 60 | let output_type = input.parse::()?; 61 | input.parse::()?; 62 | 63 | // Parse the actual query string literal. 64 | let query_string = input.parse::()?; 65 | 66 | // The rest of the input has to be an optional sequence of compile-time binding 67 | // expressions. 68 | let mut compile_time_bindings = Vec::new(); 69 | while !input.is_empty() { 70 | // Every binding expression has to be preceded by a comma, and we also allow the final 71 | // comma to be optional. 72 | input.parse::()?; 73 | if input.is_empty() { 74 | break; 75 | } 76 | 77 | // Every binding expression starts with a #. 78 | input.parse::()?; 79 | 80 | // Then we parse the binding names. 81 | let binding_names = if input.peek(syn::token::Paren) { 82 | // If the binding names start with parens we're parsing a tuple of binding names. 83 | let content; 84 | let paren_token = parenthesized!(content in input); 85 | OneOrPunctuated::Punctuated( 86 | content.parse_terminated(syn::Ident::parse, syn::token::Comma)?, 87 | paren_token.span.join(), 88 | ) 89 | } else { 90 | // Otherwise we only parse a single ident. 91 | let name = input.parse::()?; 92 | OneOrPunctuated::One(name) 93 | }; 94 | 95 | // Binding names and match is delimited by equals sign. 96 | input.parse::()?; 97 | 98 | // And finally we parse a match expression. 99 | let match_expression = input.parse::()?; 100 | 101 | compile_time_bindings.push((binding_names, match_expression)); 102 | } 103 | 104 | Ok(ParsedConditionalQueryAs { 105 | output_type, 106 | query_string, 107 | compile_time_bindings, 108 | }) 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | 116 | #[test] 117 | fn valid_syntax() { 118 | let mut parsed = syn::parse_str::( 119 | r#" 120 | SomeType, 121 | "some SQL query", 122 | #binding = match foo { 123 | bar => "baz", 124 | }, 125 | #(a, b) = match c { 126 | d => ("e", "f"), 127 | }, 128 | "#, 129 | ) 130 | .unwrap(); 131 | 132 | assert_eq!( 133 | parsed.output_type, 134 | syn::Ident::new("SomeType", proc_macro2::Span::call_site()), 135 | ); 136 | 137 | assert_eq!( 138 | parsed.query_string, 139 | syn::LitStr::new("some SQL query", proc_macro2::Span::call_site()), 140 | ); 141 | 142 | assert_eq!(parsed.compile_time_bindings.len(), 2); 143 | 144 | { 145 | let (names, _) = parsed.compile_time_bindings.remove(0); 146 | 147 | assert_eq!( 148 | names.into_iter().collect::>(), 149 | [syn::Ident::new("binding", proc_macro2::Span::call_site())] 150 | ); 151 | } 152 | 153 | { 154 | let (names, _) = parsed.compile_time_bindings.remove(0); 155 | 156 | assert_eq!( 157 | names.into_iter().collect::>(), 158 | [ 159 | syn::Ident::new("a", proc_macro2::Span::call_site()), 160 | syn::Ident::new("b", proc_macro2::Span::call_site()), 161 | ] 162 | ); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /core/src/snapshot_tests.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | 3 | use crate::DatabaseType; 4 | 5 | macro_rules! set_snapshot_suffix { 6 | ($($expr:expr),*) => { 7 | let mut settings = insta::Settings::clone_current(); 8 | settings.set_snapshot_suffix(format!($($expr,)*)); 9 | let _guard = settings.bind_to_scope(); 10 | } 11 | } 12 | 13 | fn prettyprint(ts: TokenStream) -> String { 14 | // `prettyplease` operates on full files so we have to wrap the generated output in a dummy 15 | // function. 16 | let ts = quote::quote! { 17 | fn dummy() { 18 | #ts 19 | } 20 | }; 21 | let file = syn::parse2(ts).expect("failed to parse wrapped output as a file"); 22 | prettyplease::unparse(&file) 23 | } 24 | 25 | #[rstest::rstest] 26 | #[case::postgres(DatabaseType::PostgreSql)] 27 | #[case::mysql(DatabaseType::MySql)] 28 | #[case::sqlite(DatabaseType::Sqlite)] 29 | fn only_runtime_bound_parameters(#[case] database_type: DatabaseType) { 30 | set_snapshot_suffix!("{:?}", database_type); 31 | let input = quote::quote! { 32 | OutputType, 33 | r#" 34 | SELECT column 35 | FROM table 36 | WHERE created_at > {created_at} 37 | "#, 38 | }; 39 | let output = crate::conditional_query_as(database_type, input).unwrap(); 40 | insta::assert_snapshot!(prettyprint(output)); 41 | } 42 | 43 | #[rstest::rstest] 44 | #[case::postgres(DatabaseType::PostgreSql)] 45 | #[case::mysql(DatabaseType::MySql)] 46 | #[case::sqlite(DatabaseType::Sqlite)] 47 | fn only_compile_time_bound_parameters(#[case] database_type: DatabaseType) { 48 | set_snapshot_suffix!("{:?}", database_type); 49 | let hash = proc_macro2::Punct::new('#', proc_macro2::Spacing::Alone); 50 | let input = quote::quote! { 51 | OutputType, 52 | r#" 53 | SELECT column 54 | FROM table 55 | WHERE value = {#value} 56 | "#, 57 | #hash value = match value { 58 | _ => "value", 59 | }, 60 | }; 61 | let output = crate::conditional_query_as(database_type, input).unwrap(); 62 | insta::assert_snapshot!(prettyprint(output)); 63 | } 64 | 65 | #[rstest::rstest] 66 | #[case::postgres(DatabaseType::PostgreSql)] 67 | #[case::mysql(DatabaseType::MySql)] 68 | #[case::sqlite(DatabaseType::Sqlite)] 69 | fn both_parameter_kinds(#[case] database_type: DatabaseType) { 70 | set_snapshot_suffix!("{:?}", database_type); 71 | let hash = proc_macro2::Punct::new('#', proc_macro2::Spacing::Alone); 72 | let input = quote::quote! { 73 | OutputType, 74 | r#" 75 | SELECT column 76 | FROM table 77 | WHERE 78 | created_at > {created_at} 79 | AND value = {#value} 80 | "#, 81 | #hash value = match value { 82 | _ => "value", 83 | }, 84 | }; 85 | let output = crate::conditional_query_as(database_type, input).unwrap(); 86 | insta::assert_snapshot!(prettyprint(output)); 87 | } 88 | -------------------------------------------------------------------------------- /core/src/snapshots/sqlx_conditional_queries_core__snapshot_tests__both_parameter_kinds@MySql.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/src/snapshot_tests.rs 3 | expression: prettyprint(output) 4 | snapshot_kind: text 5 | --- 6 | fn dummy() { 7 | { 8 | enum ConditionalMap<'q, DB: ::sqlx::Database, A, F0> { 9 | Variant0(::sqlx::query::Map<'q, DB, F0, A>), 10 | } 11 | impl<'q, DB, A, O, F0> ConditionalMap<'q, DB, A, F0> 12 | where 13 | DB: ::sqlx::Database, 14 | A: 'q + ::sqlx::IntoArguments<'q, DB> + ::std::marker::Send, 15 | O: ::std::marker::Unpin + ::std::marker::Send, 16 | F0: ::std::ops::FnMut(DB::Row) -> ::sqlx::Result + ::std::marker::Send, 17 | { 18 | /// See [`sqlx::query::Map::fetch`] 19 | pub fn fetch<'e, 'c: 'e, E>( 20 | self, 21 | executor: E, 22 | ) -> ::sqlx_conditional_queries::exports::BoxStream<'e, ::sqlx::Result> 23 | where 24 | 'q: 'e, 25 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 26 | DB: 'e, 27 | O: 'e, 28 | F0: 'e, 29 | { 30 | match self { 31 | Self::Variant0(map) => map.fetch(executor), 32 | } 33 | } 34 | /// See [`sqlx::query::Map::fetch_many`] 35 | #[deprecated = "Only the SQLite driver supports multiple statements in one prepared statement and that behavior is deprecated. Use `sqlx::raw_sql()` instead. See https://github.com/launchbadge/sqlx/issues/3108 for discussion."] 36 | pub fn fetch_many<'e, 'c: 'e, E>( 37 | mut self, 38 | executor: E, 39 | ) -> ::sqlx_conditional_queries::exports::BoxStream< 40 | 'e, 41 | ::sqlx::Result<::sqlx::Either>, 42 | > 43 | where 44 | 'q: 'e, 45 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 46 | DB: 'e, 47 | O: 'e, 48 | F0: 'e, 49 | { 50 | match self { 51 | Self::Variant0(map) => #[allow(deprecated)] map.fetch_many(executor), 52 | } 53 | } 54 | /// See [`sqlx::query::Map::fetch_all`] 55 | pub async fn fetch_all<'e, 'c: 'e, E>( 56 | self, 57 | executor: E, 58 | ) -> ::sqlx::Result<::std::vec::Vec> 59 | where 60 | 'q: 'e, 61 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 62 | DB: 'e, 63 | O: 'e, 64 | F0: 'e, 65 | { 66 | match self { 67 | Self::Variant0(map) => map.fetch_all(executor).await, 68 | } 69 | } 70 | /// See [`sqlx::query::Map::fetch_one`] 71 | pub async fn fetch_one<'e, 'c: 'e, E>(self, executor: E) -> ::sqlx::Result 72 | where 73 | 'q: 'e, 74 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 75 | DB: 'e, 76 | O: 'e, 77 | F0: 'e, 78 | { 79 | match self { 80 | Self::Variant0(map) => map.fetch_one(executor).await, 81 | } 82 | } 83 | /// See [`sqlx::query::Map::fetch_optional`] 84 | pub async fn fetch_optional<'e, 'c: 'e, E>( 85 | self, 86 | executor: E, 87 | ) -> ::sqlx::Result<::std::option::Option> 88 | where 89 | 'q: 'e, 90 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 91 | DB: 'e, 92 | O: 'e, 93 | F0: 'e, 94 | { 95 | match self { 96 | Self::Variant0(map) => map.fetch_optional(executor).await, 97 | } 98 | } 99 | } 100 | match (value,) { 101 | (_,) => { 102 | ConditionalMap::Variant0( 103 | ::sqlx::query_as!( 104 | OutputType, 105 | "\n SELECT column\n FROM table\n WHERE\n created_at > " 106 | + "" + "?" + "\n AND value = " + "value" + 107 | "\n ", created_at 108 | ), 109 | ) 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /core/src/snapshots/sqlx_conditional_queries_core__snapshot_tests__both_parameter_kinds@PostgreSql.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/src/snapshot_tests.rs 3 | expression: prettyprint(output) 4 | snapshot_kind: text 5 | --- 6 | fn dummy() { 7 | { 8 | enum ConditionalMap<'q, DB: ::sqlx::Database, A, F0> { 9 | Variant0(::sqlx::query::Map<'q, DB, F0, A>), 10 | } 11 | impl<'q, DB, A, O, F0> ConditionalMap<'q, DB, A, F0> 12 | where 13 | DB: ::sqlx::Database, 14 | A: 'q + ::sqlx::IntoArguments<'q, DB> + ::std::marker::Send, 15 | O: ::std::marker::Unpin + ::std::marker::Send, 16 | F0: ::std::ops::FnMut(DB::Row) -> ::sqlx::Result + ::std::marker::Send, 17 | { 18 | /// See [`sqlx::query::Map::fetch`] 19 | pub fn fetch<'e, 'c: 'e, E>( 20 | self, 21 | executor: E, 22 | ) -> ::sqlx_conditional_queries::exports::BoxStream<'e, ::sqlx::Result> 23 | where 24 | 'q: 'e, 25 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 26 | DB: 'e, 27 | O: 'e, 28 | F0: 'e, 29 | { 30 | match self { 31 | Self::Variant0(map) => map.fetch(executor), 32 | } 33 | } 34 | /// See [`sqlx::query::Map::fetch_many`] 35 | #[deprecated = "Only the SQLite driver supports multiple statements in one prepared statement and that behavior is deprecated. Use `sqlx::raw_sql()` instead. See https://github.com/launchbadge/sqlx/issues/3108 for discussion."] 36 | pub fn fetch_many<'e, 'c: 'e, E>( 37 | mut self, 38 | executor: E, 39 | ) -> ::sqlx_conditional_queries::exports::BoxStream< 40 | 'e, 41 | ::sqlx::Result<::sqlx::Either>, 42 | > 43 | where 44 | 'q: 'e, 45 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 46 | DB: 'e, 47 | O: 'e, 48 | F0: 'e, 49 | { 50 | match self { 51 | Self::Variant0(map) => #[allow(deprecated)] map.fetch_many(executor), 52 | } 53 | } 54 | /// See [`sqlx::query::Map::fetch_all`] 55 | pub async fn fetch_all<'e, 'c: 'e, E>( 56 | self, 57 | executor: E, 58 | ) -> ::sqlx::Result<::std::vec::Vec> 59 | where 60 | 'q: 'e, 61 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 62 | DB: 'e, 63 | O: 'e, 64 | F0: 'e, 65 | { 66 | match self { 67 | Self::Variant0(map) => map.fetch_all(executor).await, 68 | } 69 | } 70 | /// See [`sqlx::query::Map::fetch_one`] 71 | pub async fn fetch_one<'e, 'c: 'e, E>(self, executor: E) -> ::sqlx::Result 72 | where 73 | 'q: 'e, 74 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 75 | DB: 'e, 76 | O: 'e, 77 | F0: 'e, 78 | { 79 | match self { 80 | Self::Variant0(map) => map.fetch_one(executor).await, 81 | } 82 | } 83 | /// See [`sqlx::query::Map::fetch_optional`] 84 | pub async fn fetch_optional<'e, 'c: 'e, E>( 85 | self, 86 | executor: E, 87 | ) -> ::sqlx::Result<::std::option::Option> 88 | where 89 | 'q: 'e, 90 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 91 | DB: 'e, 92 | O: 'e, 93 | F0: 'e, 94 | { 95 | match self { 96 | Self::Variant0(map) => map.fetch_optional(executor).await, 97 | } 98 | } 99 | } 100 | match (value,) { 101 | (_,) => { 102 | ConditionalMap::Variant0( 103 | ::sqlx::query_as!( 104 | OutputType, 105 | "\n SELECT column\n FROM table\n WHERE\n created_at > " 106 | + "" + "$1" + "\n AND value = " + "value" + 107 | "\n ", created_at 108 | ), 109 | ) 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /core/src/snapshots/sqlx_conditional_queries_core__snapshot_tests__both_parameter_kinds@Sqlite.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/src/snapshot_tests.rs 3 | expression: prettyprint(output) 4 | snapshot_kind: text 5 | --- 6 | fn dummy() { 7 | { 8 | enum ConditionalMap<'q, DB: ::sqlx::Database, A, F0> { 9 | Variant0(::sqlx::query::Map<'q, DB, F0, A>), 10 | } 11 | impl<'q, DB, A, O, F0> ConditionalMap<'q, DB, A, F0> 12 | where 13 | DB: ::sqlx::Database, 14 | A: 'q + ::sqlx::IntoArguments<'q, DB> + ::std::marker::Send, 15 | O: ::std::marker::Unpin + ::std::marker::Send, 16 | F0: ::std::ops::FnMut(DB::Row) -> ::sqlx::Result + ::std::marker::Send, 17 | { 18 | /// See [`sqlx::query::Map::fetch`] 19 | pub fn fetch<'e, 'c: 'e, E>( 20 | self, 21 | executor: E, 22 | ) -> ::sqlx_conditional_queries::exports::BoxStream<'e, ::sqlx::Result> 23 | where 24 | 'q: 'e, 25 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 26 | DB: 'e, 27 | O: 'e, 28 | F0: 'e, 29 | { 30 | match self { 31 | Self::Variant0(map) => map.fetch(executor), 32 | } 33 | } 34 | /// See [`sqlx::query::Map::fetch_many`] 35 | #[deprecated = "Only the SQLite driver supports multiple statements in one prepared statement and that behavior is deprecated. Use `sqlx::raw_sql()` instead. See https://github.com/launchbadge/sqlx/issues/3108 for discussion."] 36 | pub fn fetch_many<'e, 'c: 'e, E>( 37 | mut self, 38 | executor: E, 39 | ) -> ::sqlx_conditional_queries::exports::BoxStream< 40 | 'e, 41 | ::sqlx::Result<::sqlx::Either>, 42 | > 43 | where 44 | 'q: 'e, 45 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 46 | DB: 'e, 47 | O: 'e, 48 | F0: 'e, 49 | { 50 | match self { 51 | Self::Variant0(map) => #[allow(deprecated)] map.fetch_many(executor), 52 | } 53 | } 54 | /// See [`sqlx::query::Map::fetch_all`] 55 | pub async fn fetch_all<'e, 'c: 'e, E>( 56 | self, 57 | executor: E, 58 | ) -> ::sqlx::Result<::std::vec::Vec> 59 | where 60 | 'q: 'e, 61 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 62 | DB: 'e, 63 | O: 'e, 64 | F0: 'e, 65 | { 66 | match self { 67 | Self::Variant0(map) => map.fetch_all(executor).await, 68 | } 69 | } 70 | /// See [`sqlx::query::Map::fetch_one`] 71 | pub async fn fetch_one<'e, 'c: 'e, E>(self, executor: E) -> ::sqlx::Result 72 | where 73 | 'q: 'e, 74 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 75 | DB: 'e, 76 | O: 'e, 77 | F0: 'e, 78 | { 79 | match self { 80 | Self::Variant0(map) => map.fetch_one(executor).await, 81 | } 82 | } 83 | /// See [`sqlx::query::Map::fetch_optional`] 84 | pub async fn fetch_optional<'e, 'c: 'e, E>( 85 | self, 86 | executor: E, 87 | ) -> ::sqlx::Result<::std::option::Option> 88 | where 89 | 'q: 'e, 90 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 91 | DB: 'e, 92 | O: 'e, 93 | F0: 'e, 94 | { 95 | match self { 96 | Self::Variant0(map) => map.fetch_optional(executor).await, 97 | } 98 | } 99 | } 100 | match (value,) { 101 | (_,) => { 102 | ConditionalMap::Variant0( 103 | ::sqlx::query_as!( 104 | OutputType, 105 | "\n SELECT column\n FROM table\n WHERE\n created_at > " 106 | + "" + "?" + "\n AND value = " + "value" + 107 | "\n ", created_at 108 | ), 109 | ) 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /core/src/snapshots/sqlx_conditional_queries_core__snapshot_tests__only_compile_time_bound_parameters@MySql.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/src/snapshot_tests.rs 3 | expression: prettyprint(output) 4 | snapshot_kind: text 5 | --- 6 | fn dummy() { 7 | { 8 | enum ConditionalMap<'q, DB: ::sqlx::Database, A, F0> { 9 | Variant0(::sqlx::query::Map<'q, DB, F0, A>), 10 | } 11 | impl<'q, DB, A, O, F0> ConditionalMap<'q, DB, A, F0> 12 | where 13 | DB: ::sqlx::Database, 14 | A: 'q + ::sqlx::IntoArguments<'q, DB> + ::std::marker::Send, 15 | O: ::std::marker::Unpin + ::std::marker::Send, 16 | F0: ::std::ops::FnMut(DB::Row) -> ::sqlx::Result + ::std::marker::Send, 17 | { 18 | /// See [`sqlx::query::Map::fetch`] 19 | pub fn fetch<'e, 'c: 'e, E>( 20 | self, 21 | executor: E, 22 | ) -> ::sqlx_conditional_queries::exports::BoxStream<'e, ::sqlx::Result> 23 | where 24 | 'q: 'e, 25 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 26 | DB: 'e, 27 | O: 'e, 28 | F0: 'e, 29 | { 30 | match self { 31 | Self::Variant0(map) => map.fetch(executor), 32 | } 33 | } 34 | /// See [`sqlx::query::Map::fetch_many`] 35 | #[deprecated = "Only the SQLite driver supports multiple statements in one prepared statement and that behavior is deprecated. Use `sqlx::raw_sql()` instead. See https://github.com/launchbadge/sqlx/issues/3108 for discussion."] 36 | pub fn fetch_many<'e, 'c: 'e, E>( 37 | mut self, 38 | executor: E, 39 | ) -> ::sqlx_conditional_queries::exports::BoxStream< 40 | 'e, 41 | ::sqlx::Result<::sqlx::Either>, 42 | > 43 | where 44 | 'q: 'e, 45 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 46 | DB: 'e, 47 | O: 'e, 48 | F0: 'e, 49 | { 50 | match self { 51 | Self::Variant0(map) => #[allow(deprecated)] map.fetch_many(executor), 52 | } 53 | } 54 | /// See [`sqlx::query::Map::fetch_all`] 55 | pub async fn fetch_all<'e, 'c: 'e, E>( 56 | self, 57 | executor: E, 58 | ) -> ::sqlx::Result<::std::vec::Vec> 59 | where 60 | 'q: 'e, 61 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 62 | DB: 'e, 63 | O: 'e, 64 | F0: 'e, 65 | { 66 | match self { 67 | Self::Variant0(map) => map.fetch_all(executor).await, 68 | } 69 | } 70 | /// See [`sqlx::query::Map::fetch_one`] 71 | pub async fn fetch_one<'e, 'c: 'e, E>(self, executor: E) -> ::sqlx::Result 72 | where 73 | 'q: 'e, 74 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 75 | DB: 'e, 76 | O: 'e, 77 | F0: 'e, 78 | { 79 | match self { 80 | Self::Variant0(map) => map.fetch_one(executor).await, 81 | } 82 | } 83 | /// See [`sqlx::query::Map::fetch_optional`] 84 | pub async fn fetch_optional<'e, 'c: 'e, E>( 85 | self, 86 | executor: E, 87 | ) -> ::sqlx::Result<::std::option::Option> 88 | where 89 | 'q: 'e, 90 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 91 | DB: 'e, 92 | O: 'e, 93 | F0: 'e, 94 | { 95 | match self { 96 | Self::Variant0(map) => map.fetch_optional(executor).await, 97 | } 98 | } 99 | } 100 | match (value,) { 101 | (_,) => { 102 | ConditionalMap::Variant0( 103 | ::sqlx::query_as!( 104 | OutputType, 105 | "\n SELECT column\n FROM table\n WHERE value = " 106 | + "value" + "\n ", 107 | ), 108 | ) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /core/src/snapshots/sqlx_conditional_queries_core__snapshot_tests__only_compile_time_bound_parameters@PostgreSql.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/src/snapshot_tests.rs 3 | expression: prettyprint(output) 4 | snapshot_kind: text 5 | --- 6 | fn dummy() { 7 | { 8 | enum ConditionalMap<'q, DB: ::sqlx::Database, A, F0> { 9 | Variant0(::sqlx::query::Map<'q, DB, F0, A>), 10 | } 11 | impl<'q, DB, A, O, F0> ConditionalMap<'q, DB, A, F0> 12 | where 13 | DB: ::sqlx::Database, 14 | A: 'q + ::sqlx::IntoArguments<'q, DB> + ::std::marker::Send, 15 | O: ::std::marker::Unpin + ::std::marker::Send, 16 | F0: ::std::ops::FnMut(DB::Row) -> ::sqlx::Result + ::std::marker::Send, 17 | { 18 | /// See [`sqlx::query::Map::fetch`] 19 | pub fn fetch<'e, 'c: 'e, E>( 20 | self, 21 | executor: E, 22 | ) -> ::sqlx_conditional_queries::exports::BoxStream<'e, ::sqlx::Result> 23 | where 24 | 'q: 'e, 25 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 26 | DB: 'e, 27 | O: 'e, 28 | F0: 'e, 29 | { 30 | match self { 31 | Self::Variant0(map) => map.fetch(executor), 32 | } 33 | } 34 | /// See [`sqlx::query::Map::fetch_many`] 35 | #[deprecated = "Only the SQLite driver supports multiple statements in one prepared statement and that behavior is deprecated. Use `sqlx::raw_sql()` instead. See https://github.com/launchbadge/sqlx/issues/3108 for discussion."] 36 | pub fn fetch_many<'e, 'c: 'e, E>( 37 | mut self, 38 | executor: E, 39 | ) -> ::sqlx_conditional_queries::exports::BoxStream< 40 | 'e, 41 | ::sqlx::Result<::sqlx::Either>, 42 | > 43 | where 44 | 'q: 'e, 45 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 46 | DB: 'e, 47 | O: 'e, 48 | F0: 'e, 49 | { 50 | match self { 51 | Self::Variant0(map) => #[allow(deprecated)] map.fetch_many(executor), 52 | } 53 | } 54 | /// See [`sqlx::query::Map::fetch_all`] 55 | pub async fn fetch_all<'e, 'c: 'e, E>( 56 | self, 57 | executor: E, 58 | ) -> ::sqlx::Result<::std::vec::Vec> 59 | where 60 | 'q: 'e, 61 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 62 | DB: 'e, 63 | O: 'e, 64 | F0: 'e, 65 | { 66 | match self { 67 | Self::Variant0(map) => map.fetch_all(executor).await, 68 | } 69 | } 70 | /// See [`sqlx::query::Map::fetch_one`] 71 | pub async fn fetch_one<'e, 'c: 'e, E>(self, executor: E) -> ::sqlx::Result 72 | where 73 | 'q: 'e, 74 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 75 | DB: 'e, 76 | O: 'e, 77 | F0: 'e, 78 | { 79 | match self { 80 | Self::Variant0(map) => map.fetch_one(executor).await, 81 | } 82 | } 83 | /// See [`sqlx::query::Map::fetch_optional`] 84 | pub async fn fetch_optional<'e, 'c: 'e, E>( 85 | self, 86 | executor: E, 87 | ) -> ::sqlx::Result<::std::option::Option> 88 | where 89 | 'q: 'e, 90 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 91 | DB: 'e, 92 | O: 'e, 93 | F0: 'e, 94 | { 95 | match self { 96 | Self::Variant0(map) => map.fetch_optional(executor).await, 97 | } 98 | } 99 | } 100 | match (value,) { 101 | (_,) => { 102 | ConditionalMap::Variant0( 103 | ::sqlx::query_as!( 104 | OutputType, 105 | "\n SELECT column\n FROM table\n WHERE value = " 106 | + "value" + "\n ", 107 | ), 108 | ) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /core/src/snapshots/sqlx_conditional_queries_core__snapshot_tests__only_compile_time_bound_parameters@Sqlite.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/src/snapshot_tests.rs 3 | expression: prettyprint(output) 4 | snapshot_kind: text 5 | --- 6 | fn dummy() { 7 | { 8 | enum ConditionalMap<'q, DB: ::sqlx::Database, A, F0> { 9 | Variant0(::sqlx::query::Map<'q, DB, F0, A>), 10 | } 11 | impl<'q, DB, A, O, F0> ConditionalMap<'q, DB, A, F0> 12 | where 13 | DB: ::sqlx::Database, 14 | A: 'q + ::sqlx::IntoArguments<'q, DB> + ::std::marker::Send, 15 | O: ::std::marker::Unpin + ::std::marker::Send, 16 | F0: ::std::ops::FnMut(DB::Row) -> ::sqlx::Result + ::std::marker::Send, 17 | { 18 | /// See [`sqlx::query::Map::fetch`] 19 | pub fn fetch<'e, 'c: 'e, E>( 20 | self, 21 | executor: E, 22 | ) -> ::sqlx_conditional_queries::exports::BoxStream<'e, ::sqlx::Result> 23 | where 24 | 'q: 'e, 25 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 26 | DB: 'e, 27 | O: 'e, 28 | F0: 'e, 29 | { 30 | match self { 31 | Self::Variant0(map) => map.fetch(executor), 32 | } 33 | } 34 | /// See [`sqlx::query::Map::fetch_many`] 35 | #[deprecated = "Only the SQLite driver supports multiple statements in one prepared statement and that behavior is deprecated. Use `sqlx::raw_sql()` instead. See https://github.com/launchbadge/sqlx/issues/3108 for discussion."] 36 | pub fn fetch_many<'e, 'c: 'e, E>( 37 | mut self, 38 | executor: E, 39 | ) -> ::sqlx_conditional_queries::exports::BoxStream< 40 | 'e, 41 | ::sqlx::Result<::sqlx::Either>, 42 | > 43 | where 44 | 'q: 'e, 45 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 46 | DB: 'e, 47 | O: 'e, 48 | F0: 'e, 49 | { 50 | match self { 51 | Self::Variant0(map) => #[allow(deprecated)] map.fetch_many(executor), 52 | } 53 | } 54 | /// See [`sqlx::query::Map::fetch_all`] 55 | pub async fn fetch_all<'e, 'c: 'e, E>( 56 | self, 57 | executor: E, 58 | ) -> ::sqlx::Result<::std::vec::Vec> 59 | where 60 | 'q: 'e, 61 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 62 | DB: 'e, 63 | O: 'e, 64 | F0: 'e, 65 | { 66 | match self { 67 | Self::Variant0(map) => map.fetch_all(executor).await, 68 | } 69 | } 70 | /// See [`sqlx::query::Map::fetch_one`] 71 | pub async fn fetch_one<'e, 'c: 'e, E>(self, executor: E) -> ::sqlx::Result 72 | where 73 | 'q: 'e, 74 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 75 | DB: 'e, 76 | O: 'e, 77 | F0: 'e, 78 | { 79 | match self { 80 | Self::Variant0(map) => map.fetch_one(executor).await, 81 | } 82 | } 83 | /// See [`sqlx::query::Map::fetch_optional`] 84 | pub async fn fetch_optional<'e, 'c: 'e, E>( 85 | self, 86 | executor: E, 87 | ) -> ::sqlx::Result<::std::option::Option> 88 | where 89 | 'q: 'e, 90 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 91 | DB: 'e, 92 | O: 'e, 93 | F0: 'e, 94 | { 95 | match self { 96 | Self::Variant0(map) => map.fetch_optional(executor).await, 97 | } 98 | } 99 | } 100 | match (value,) { 101 | (_,) => { 102 | ConditionalMap::Variant0( 103 | ::sqlx::query_as!( 104 | OutputType, 105 | "\n SELECT column\n FROM table\n WHERE value = " 106 | + "value" + "\n ", 107 | ), 108 | ) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /core/src/snapshots/sqlx_conditional_queries_core__snapshot_tests__only_runtime_bound_parameters@MySql.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/src/snapshot_tests.rs 3 | expression: prettyprint(output) 4 | snapshot_kind: text 5 | --- 6 | fn dummy() { 7 | { 8 | enum ConditionalMap<'q, DB: ::sqlx::Database, A, F0> { 9 | Variant0(::sqlx::query::Map<'q, DB, F0, A>), 10 | } 11 | impl<'q, DB, A, O, F0> ConditionalMap<'q, DB, A, F0> 12 | where 13 | DB: ::sqlx::Database, 14 | A: 'q + ::sqlx::IntoArguments<'q, DB> + ::std::marker::Send, 15 | O: ::std::marker::Unpin + ::std::marker::Send, 16 | F0: ::std::ops::FnMut(DB::Row) -> ::sqlx::Result + ::std::marker::Send, 17 | { 18 | /// See [`sqlx::query::Map::fetch`] 19 | pub fn fetch<'e, 'c: 'e, E>( 20 | self, 21 | executor: E, 22 | ) -> ::sqlx_conditional_queries::exports::BoxStream<'e, ::sqlx::Result> 23 | where 24 | 'q: 'e, 25 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 26 | DB: 'e, 27 | O: 'e, 28 | F0: 'e, 29 | { 30 | match self { 31 | Self::Variant0(map) => map.fetch(executor), 32 | } 33 | } 34 | /// See [`sqlx::query::Map::fetch_many`] 35 | #[deprecated = "Only the SQLite driver supports multiple statements in one prepared statement and that behavior is deprecated. Use `sqlx::raw_sql()` instead. See https://github.com/launchbadge/sqlx/issues/3108 for discussion."] 36 | pub fn fetch_many<'e, 'c: 'e, E>( 37 | mut self, 38 | executor: E, 39 | ) -> ::sqlx_conditional_queries::exports::BoxStream< 40 | 'e, 41 | ::sqlx::Result<::sqlx::Either>, 42 | > 43 | where 44 | 'q: 'e, 45 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 46 | DB: 'e, 47 | O: 'e, 48 | F0: 'e, 49 | { 50 | match self { 51 | Self::Variant0(map) => #[allow(deprecated)] map.fetch_many(executor), 52 | } 53 | } 54 | /// See [`sqlx::query::Map::fetch_all`] 55 | pub async fn fetch_all<'e, 'c: 'e, E>( 56 | self, 57 | executor: E, 58 | ) -> ::sqlx::Result<::std::vec::Vec> 59 | where 60 | 'q: 'e, 61 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 62 | DB: 'e, 63 | O: 'e, 64 | F0: 'e, 65 | { 66 | match self { 67 | Self::Variant0(map) => map.fetch_all(executor).await, 68 | } 69 | } 70 | /// See [`sqlx::query::Map::fetch_one`] 71 | pub async fn fetch_one<'e, 'c: 'e, E>(self, executor: E) -> ::sqlx::Result 72 | where 73 | 'q: 'e, 74 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 75 | DB: 'e, 76 | O: 'e, 77 | F0: 'e, 78 | { 79 | match self { 80 | Self::Variant0(map) => map.fetch_one(executor).await, 81 | } 82 | } 83 | /// See [`sqlx::query::Map::fetch_optional`] 84 | pub async fn fetch_optional<'e, 'c: 'e, E>( 85 | self, 86 | executor: E, 87 | ) -> ::sqlx::Result<::std::option::Option> 88 | where 89 | 'q: 'e, 90 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 91 | DB: 'e, 92 | O: 'e, 93 | F0: 'e, 94 | { 95 | match self { 96 | Self::Variant0(map) => map.fetch_optional(executor).await, 97 | } 98 | } 99 | } 100 | match ((),) { 101 | ((),) => { 102 | ConditionalMap::Variant0( 103 | ::sqlx::query_as!( 104 | OutputType, 105 | "\n SELECT column\n FROM table\n WHERE created_at > " 106 | + "?" + "\n ", created_at 107 | ), 108 | ) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /core/src/snapshots/sqlx_conditional_queries_core__snapshot_tests__only_runtime_bound_parameters@PostgreSql.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/src/snapshot_tests.rs 3 | expression: prettyprint(output) 4 | snapshot_kind: text 5 | --- 6 | fn dummy() { 7 | { 8 | enum ConditionalMap<'q, DB: ::sqlx::Database, A, F0> { 9 | Variant0(::sqlx::query::Map<'q, DB, F0, A>), 10 | } 11 | impl<'q, DB, A, O, F0> ConditionalMap<'q, DB, A, F0> 12 | where 13 | DB: ::sqlx::Database, 14 | A: 'q + ::sqlx::IntoArguments<'q, DB> + ::std::marker::Send, 15 | O: ::std::marker::Unpin + ::std::marker::Send, 16 | F0: ::std::ops::FnMut(DB::Row) -> ::sqlx::Result + ::std::marker::Send, 17 | { 18 | /// See [`sqlx::query::Map::fetch`] 19 | pub fn fetch<'e, 'c: 'e, E>( 20 | self, 21 | executor: E, 22 | ) -> ::sqlx_conditional_queries::exports::BoxStream<'e, ::sqlx::Result> 23 | where 24 | 'q: 'e, 25 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 26 | DB: 'e, 27 | O: 'e, 28 | F0: 'e, 29 | { 30 | match self { 31 | Self::Variant0(map) => map.fetch(executor), 32 | } 33 | } 34 | /// See [`sqlx::query::Map::fetch_many`] 35 | #[deprecated = "Only the SQLite driver supports multiple statements in one prepared statement and that behavior is deprecated. Use `sqlx::raw_sql()` instead. See https://github.com/launchbadge/sqlx/issues/3108 for discussion."] 36 | pub fn fetch_many<'e, 'c: 'e, E>( 37 | mut self, 38 | executor: E, 39 | ) -> ::sqlx_conditional_queries::exports::BoxStream< 40 | 'e, 41 | ::sqlx::Result<::sqlx::Either>, 42 | > 43 | where 44 | 'q: 'e, 45 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 46 | DB: 'e, 47 | O: 'e, 48 | F0: 'e, 49 | { 50 | match self { 51 | Self::Variant0(map) => #[allow(deprecated)] map.fetch_many(executor), 52 | } 53 | } 54 | /// See [`sqlx::query::Map::fetch_all`] 55 | pub async fn fetch_all<'e, 'c: 'e, E>( 56 | self, 57 | executor: E, 58 | ) -> ::sqlx::Result<::std::vec::Vec> 59 | where 60 | 'q: 'e, 61 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 62 | DB: 'e, 63 | O: 'e, 64 | F0: 'e, 65 | { 66 | match self { 67 | Self::Variant0(map) => map.fetch_all(executor).await, 68 | } 69 | } 70 | /// See [`sqlx::query::Map::fetch_one`] 71 | pub async fn fetch_one<'e, 'c: 'e, E>(self, executor: E) -> ::sqlx::Result 72 | where 73 | 'q: 'e, 74 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 75 | DB: 'e, 76 | O: 'e, 77 | F0: 'e, 78 | { 79 | match self { 80 | Self::Variant0(map) => map.fetch_one(executor).await, 81 | } 82 | } 83 | /// See [`sqlx::query::Map::fetch_optional`] 84 | pub async fn fetch_optional<'e, 'c: 'e, E>( 85 | self, 86 | executor: E, 87 | ) -> ::sqlx::Result<::std::option::Option> 88 | where 89 | 'q: 'e, 90 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 91 | DB: 'e, 92 | O: 'e, 93 | F0: 'e, 94 | { 95 | match self { 96 | Self::Variant0(map) => map.fetch_optional(executor).await, 97 | } 98 | } 99 | } 100 | match ((),) { 101 | ((),) => { 102 | ConditionalMap::Variant0( 103 | ::sqlx::query_as!( 104 | OutputType, 105 | "\n SELECT column\n FROM table\n WHERE created_at > " 106 | + "$1" + "\n ", created_at 107 | ), 108 | ) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /core/src/snapshots/sqlx_conditional_queries_core__snapshot_tests__only_runtime_bound_parameters@Sqlite.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/src/snapshot_tests.rs 3 | expression: prettyprint(output) 4 | snapshot_kind: text 5 | --- 6 | fn dummy() { 7 | { 8 | enum ConditionalMap<'q, DB: ::sqlx::Database, A, F0> { 9 | Variant0(::sqlx::query::Map<'q, DB, F0, A>), 10 | } 11 | impl<'q, DB, A, O, F0> ConditionalMap<'q, DB, A, F0> 12 | where 13 | DB: ::sqlx::Database, 14 | A: 'q + ::sqlx::IntoArguments<'q, DB> + ::std::marker::Send, 15 | O: ::std::marker::Unpin + ::std::marker::Send, 16 | F0: ::std::ops::FnMut(DB::Row) -> ::sqlx::Result + ::std::marker::Send, 17 | { 18 | /// See [`sqlx::query::Map::fetch`] 19 | pub fn fetch<'e, 'c: 'e, E>( 20 | self, 21 | executor: E, 22 | ) -> ::sqlx_conditional_queries::exports::BoxStream<'e, ::sqlx::Result> 23 | where 24 | 'q: 'e, 25 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 26 | DB: 'e, 27 | O: 'e, 28 | F0: 'e, 29 | { 30 | match self { 31 | Self::Variant0(map) => map.fetch(executor), 32 | } 33 | } 34 | /// See [`sqlx::query::Map::fetch_many`] 35 | #[deprecated = "Only the SQLite driver supports multiple statements in one prepared statement and that behavior is deprecated. Use `sqlx::raw_sql()` instead. See https://github.com/launchbadge/sqlx/issues/3108 for discussion."] 36 | pub fn fetch_many<'e, 'c: 'e, E>( 37 | mut self, 38 | executor: E, 39 | ) -> ::sqlx_conditional_queries::exports::BoxStream< 40 | 'e, 41 | ::sqlx::Result<::sqlx::Either>, 42 | > 43 | where 44 | 'q: 'e, 45 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 46 | DB: 'e, 47 | O: 'e, 48 | F0: 'e, 49 | { 50 | match self { 51 | Self::Variant0(map) => #[allow(deprecated)] map.fetch_many(executor), 52 | } 53 | } 54 | /// See [`sqlx::query::Map::fetch_all`] 55 | pub async fn fetch_all<'e, 'c: 'e, E>( 56 | self, 57 | executor: E, 58 | ) -> ::sqlx::Result<::std::vec::Vec> 59 | where 60 | 'q: 'e, 61 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 62 | DB: 'e, 63 | O: 'e, 64 | F0: 'e, 65 | { 66 | match self { 67 | Self::Variant0(map) => map.fetch_all(executor).await, 68 | } 69 | } 70 | /// See [`sqlx::query::Map::fetch_one`] 71 | pub async fn fetch_one<'e, 'c: 'e, E>(self, executor: E) -> ::sqlx::Result 72 | where 73 | 'q: 'e, 74 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 75 | DB: 'e, 76 | O: 'e, 77 | F0: 'e, 78 | { 79 | match self { 80 | Self::Variant0(map) => map.fetch_one(executor).await, 81 | } 82 | } 83 | /// See [`sqlx::query::Map::fetch_optional`] 84 | pub async fn fetch_optional<'e, 'c: 'e, E>( 85 | self, 86 | executor: E, 87 | ) -> ::sqlx::Result<::std::option::Option> 88 | where 89 | 'q: 'e, 90 | E: 'e + ::sqlx::Executor<'c, Database = DB>, 91 | DB: 'e, 92 | O: 'e, 93 | F0: 'e, 94 | { 95 | match self { 96 | Self::Variant0(map) => map.fetch_optional(executor).await, 97 | } 98 | } 99 | } 100 | match ((),) { 101 | ((),) => { 102 | ConditionalMap::Variant0( 103 | ::sqlx::query_as!( 104 | OutputType, 105 | "\n SELECT column\n FROM table\n WHERE created_at > " 106 | + "?" + "\n ", created_at 107 | ), 108 | ) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /core/tests/lib.rs: -------------------------------------------------------------------------------- 1 | mod regressions; 2 | -------------------------------------------------------------------------------- /core/tests/regressions/issue_4.rs: -------------------------------------------------------------------------------- 1 | use sqlx_conditional_queries_core::DatabaseType; 2 | 3 | #[rstest::rstest] 4 | #[case(DatabaseType::PostgreSql)] 5 | #[case(DatabaseType::MySql)] 6 | #[case(DatabaseType::Sqlite)] 7 | fn regression_test_(#[case] database_type: DatabaseType) { 8 | let input: proc_macro2::TokenStream = r##" 9 | SomeType, 10 | r#"{#a}______{c}"#, 11 | #a = match _ { 12 | _ => "b", 13 | }, 14 | "## 15 | .parse() 16 | .unwrap(); 17 | 18 | sqlx_conditional_queries_core::conditional_query_as(database_type, input).unwrap(); 19 | } 20 | -------------------------------------------------------------------------------- /core/tests/regressions/mod.rs: -------------------------------------------------------------------------------- 1 | mod issue_4; 2 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sqlx-conditional-queries-macros" 3 | version = "0.3.2" 4 | edition = "2021" 5 | description = "Macro definition for sqlx-conditional-queries" 6 | repository = "https://github.com/kyrias/sqlx-conditional-queries" 7 | license = "MIT OR Apache-2.0" 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | proc-macro-error2 = "2.0.1" 14 | proc-macro2 = "1.0.92" 15 | sqlx-conditional-queries-core = { path = "../core", version = "0.3" } 16 | 17 | [features] 18 | mysql = [] 19 | postgres = [] 20 | sqlite = [] 21 | -------------------------------------------------------------------------------- /macros/README.md: -------------------------------------------------------------------------------- 1 | **Note**: This is an internal library only meant to be used by the sqlx-conditional-queries crate. 2 | 3 | The reason for this crate being separate from the main sqlx-conditional-queries 4 | crate is to be able to export additional types through that crate which are 5 | necessary for the generated code. The main proc-macro crate cannot expose them 6 | because they're only allowed to export macros. 7 | 8 | 9 | ## Features 10 | 11 | Which database type should be supported is specified by activating one of the 12 | following features. If more than one feature is activated then the first one 13 | in the list takes precedence. 14 | 15 | - `postgres` 16 | - `mysql` 17 | - `sqlite` 18 | 19 | 20 | #### License 21 | 22 | 23 | Licensed under either of Apache License, Version 24 | 2.0 or MIT license at your option. 25 | 26 | 27 |
28 | 29 | 30 | Unless you explicitly state otherwise, any contribution intentionally submitted 31 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 32 | be dual licensed as above, without any additional terms or conditions. 33 | 34 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | use proc_macro_error2::abort; 4 | use sqlx_conditional_queries_core::{AnalyzeError, DatabaseType, Error, ExpandError}; 5 | 6 | const DATABASE_TYPE: DatabaseType = if cfg!(feature = "postgres") { 7 | DatabaseType::PostgreSql 8 | } else if cfg!(feature = "mysql") { 9 | DatabaseType::MySql 10 | } else if cfg!(feature = "sqlite") { 11 | DatabaseType::Sqlite 12 | } else { 13 | panic!("No database feature was enabled") 14 | }; 15 | 16 | // The public docs for this macro live in the sql-conditional-queries crate. 17 | #[proc_macro_error2::proc_macro_error] 18 | #[proc_macro] 19 | pub fn conditional_query_as(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 20 | let input: proc_macro2::TokenStream = input.into(); 21 | 22 | let ts = match sqlx_conditional_queries_core::conditional_query_as(DATABASE_TYPE, input) { 23 | Ok(ts) => ts, 24 | Err(Error::SynError(err)) => { 25 | return err.to_compile_error().into(); 26 | } 27 | Err(Error::AnalyzeError(err)) => match err { 28 | AnalyzeError::ExpectedStringLiteral(span) => abort!( 29 | span, 30 | "expected string literal"; 31 | help = "only string literals or tuples of string literals are supported in compile-time bindings"; 32 | ), 33 | AnalyzeError::BindingNameValueLengthMismatch { 34 | names, 35 | names_span, 36 | values, 37 | values_span, 38 | } => abort!( 39 | names_span, 40 | "mismatch between number of names and values"; 41 | names = names_span => "number of names: {}", names; 42 | values = values_span => "number of values: {}", values; 43 | ), 44 | AnalyzeError::DuplicatedCompileTimeBindingsFound { first: _, second } => { 45 | abort!(second.span(), "found duplicate compile-time binding") 46 | } 47 | AnalyzeError::CompileTimeBindingCycleDetected { root_ident, path } => abort!( 48 | root_ident.span(), 49 | "detected compile-time binding cycle: {}", 50 | path 51 | ), 52 | }, 53 | Err(Error::ExpandError(err)) => match err { 54 | // TODO: Make this span point at the binding reference. Requires https://github.com/rust-lang/rust/issues/54725 55 | ExpandError::MissingCompileTimeBinding(binding, span) => abort!( 56 | span, 57 | "missing compile-time binding"; 58 | help = "found no compile-time binding with the specified name: {}", binding; 59 | ), 60 | // TODO: Make this span point at the opening brace. Requires https://github.com/rust-lang/rust/issues/54725 61 | ExpandError::MissingBindingClosingBrace(span) => abort!( 62 | span, 63 | "missing closing brace for compile-time binding reference" 64 | ), 65 | ExpandError::BindingReferenceTypeOverrideParseError(err, span) => abort!( 66 | span, 67 | "failed to parse type override in binding reference: {}", 68 | err 69 | ), 70 | }, 71 | }; 72 | 73 | let output: proc_macro::TokenStream = ts.into(); 74 | output 75 | } 76 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | /// # Emit conditional compile-time verified `query_as!` invocations 4 | /// 5 | /// The generated type exposes the same methods as `sqlx::query::Map`, with the exception of `map` 6 | /// and `try_map`. 7 | /// 8 | /// 9 | /// ## Bound parameters 10 | /// 11 | /// This macro supports two kind of bound parameters, run-time bound and compile-time bound. 12 | /// 13 | /// 14 | /// ### Run-time bound parameters 15 | /// 16 | /// Run-time bound parameers are regular bound parameters that are passed as separate parameters to 17 | /// the database, allowing you to use variable data without the risk of SQL injections. 18 | /// Additionally since the same prepared query can be reused it means that the database knows to 19 | /// generate a more optimized query-plan if it's used a lot. 20 | /// 21 | /// Unlike in SQLx's macros you specify run-time bound parameters using a format-string-like 22 | /// syntax: `{foo}`. There must be a variable with the given name in scope, and it must be a valid 23 | /// type to pass to `query_as!`. 24 | /// 25 | /// You can pass type overrides to SQLx using a colon after the binding reference. E.g. 26 | /// `{foo:_}` 27 | /// 28 | /// Which kind of bound parameter references are generated depends on the activated features. 29 | /// 30 | /// 31 | /// ### Compile-time bound parameters 32 | /// 33 | /// Compile-time bound parameters are expanded at compile-time. This means that each possible 34 | /// option will generate a unique `query_as!` invocation. 35 | /// 36 | /// They are referenced by prepending the bound name with a hash: `{#foo}`. 37 | /// 38 | /// They are bound by following the query string with a series of match statements in the following 39 | /// format. Compile-time binding expressions can evaluate to either a string literal or a tuple of 40 | /// string literals. 41 | /// 42 | /// ```rust,ignore 43 | /// #foo = match something { 44 | /// true => "BAR", 45 | /// false => "BAZ", 46 | /// } 47 | /// ``` 48 | /// 49 | /// 50 | /// ## Examples 51 | /// 52 | /// ```rust,ignore 53 | /// enum OrderDirection { 54 | /// Ascending, 55 | /// Descending, 56 | /// } 57 | /// 58 | /// let limit = Some(42); 59 | /// let order_dir = OrderDirection::Ascending; 60 | /// 61 | /// conditional_query_as!( 62 | /// OutputType, 63 | /// r#" 64 | /// SELECT id, foo, bar 65 | /// FROM table 66 | /// {#limit} 67 | /// ORDER BY id {#order_dir}, foo {#order_dir_rev} 68 | /// "#, 69 | /// #limit = match limit { 70 | /// Some(_) => "LIMIT {limit}", 71 | /// None => "", 72 | /// }, 73 | /// #(order_dir, order_dir_rev) = match order_dir { 74 | /// OrderDirection::Ascending => ("ASC", "DESC"), 75 | /// OrderDirection::Descending => ("DESC", "ASC"), 76 | /// }, 77 | /// ) 78 | /// .fetch_all(&mut *tx) 79 | /// .await?; 80 | /// ``` 81 | pub use sqlx_conditional_queries_macros::conditional_query_as; 82 | 83 | /// Do not use this module. It is only meant to be used by the generated by 84 | /// [`conditional_query_as!`] macro. 85 | #[doc(hidden)] 86 | pub mod exports { 87 | pub use futures_core::stream::BoxStream; 88 | } 89 | --------------------------------------------------------------------------------