├── .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 | [
](https://github.com/kyrias/sqlx-conditional-queries)
4 | [
](https://crates.io/crates/sqlx-conditional-queries)
5 | [
](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 |
--------------------------------------------------------------------------------