Skip to main content

qusql_sqlx_type_macro/
lib.rs

1//! Implementation of typed db query macros
2//!
3//! Used the exposed macros from the qusql-sqlx-type crate
4//!
5#![forbid(unsafe_code)]
6
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::{Arc, Mutex};
10use std::time::SystemTime;
11
12use ariadne::{Color, Label, Report, ReportKind, Source};
13use proc_macro::TokenStream;
14use proc_macro2::Span;
15use quote::{format_ident, quote, quote_spanned};
16use qusql_type::schema::{parse_schemas, Schemas};
17use qusql_type::{
18    type_statement, ByteToChar, Issue, SQLArguments, SQLDialect, SelectTypeColumn, TypeOptions,
19};
20use syn::spanned::Spanned;
21use syn::{parse::Parse, punctuated::Punctuated, Expr, Ident, LitStr, Token};
22use yoke::{Yoke, Yokeable};
23
24/// Cache of resolved schema paths, keyed by `CARGO_MANIFEST_DIR`.
25///
26/// Path resolution is cheap when the schema sits next to `Cargo.toml`, but
27/// falls back to `cargo metadata` (a subprocess) when it doesn't.  Either
28/// way the result is stable for the lifetime of the proc-macro server process,
29/// so we cache it per manifest-dir.  A plain `Lazy<PathBuf>` would be wrong
30/// here because rust-analyzer reuses the same process for multiple crates and
31/// each crate has a different `CARGO_MANIFEST_DIR`.
32static RESOLVED_SCHEMA_PATHS: Mutex<Option<HashMap<PathBuf, PathBuf>>> = Mutex::new(None);
33
34/// Resolve the path to `sqlx-type-schema.sql` for the crate currently being
35/// compiled.  The result is cached per `CARGO_MANIFEST_DIR` so the
36/// (potentially expensive) `cargo metadata` fallback runs at most once per
37/// crate per proc-macro server process.
38fn resolve_schema_path() -> PathBuf {
39    let manifest_dir: PathBuf = std::env::var("CARGO_MANIFEST_DIR")
40        .expect("`CARGO_MANIFEST_DIR` must be set")
41        .into();
42
43    let mut cache_guard = RESOLVED_SCHEMA_PATHS
44        .lock()
45        .expect("resolved schema paths lock poisoned");
46    let cache = cache_guard.get_or_insert_with(HashMap::new);
47
48    if let Some(cached) = cache.get(&manifest_dir) {
49        return cached.clone();
50    }
51
52    let mut schema_path = manifest_dir.join("sqlx-type-schema.sql");
53
54    if !schema_path.exists() {
55        use serde::Deserialize;
56        use std::process::Command;
57
58        let cargo = std::env::var("CARGO").expect("`CARGO` must be set");
59
60        let output = Command::new(cargo)
61            .args(["metadata", "--format-version=1"])
62            .current_dir(&manifest_dir)
63            .env_remove("__CARGO_FIX_PLZ")
64            .output()
65            .expect("Could not fetch metadata");
66
67        /// Struct used to deserialize the cargo metadata
68        #[derive(Deserialize)]
69        struct CargoMetadata {
70            /// The root of the workspace
71            workspace_root: PathBuf,
72        }
73
74        let metadata: CargoMetadata =
75            serde_json::from_slice(&output.stdout).expect("Invalid `cargo metadata` output");
76
77        schema_path = metadata.workspace_root.join("sqlx-type-schema.sql");
78    }
79    if !schema_path.exists() {
80        panic!("Unable to locate sqlx-type-schema.sql");
81    }
82
83    cache.insert(manifest_dir, schema_path.clone());
84    schema_path
85}
86
87/// Construct a none color report for an issue (used as a compile_error! message)
88fn issue_to_report(issue: Issue, b2c: &ByteToChar) -> Report<'static, std::ops::Range<usize>> {
89    let span = b2c.map_span(issue.span);
90    let kind = match issue.level {
91        qusql_type::Level::Warning => ReportKind::Warning,
92        qusql_type::Level::Error => ReportKind::Error,
93    };
94    let mut builder = Report::build(kind, span.clone())
95        .with_config(ariadne::Config::default().with_color(false))
96        .with_message(&issue.message)
97        .with_label(
98            Label::new(span)
99                .with_order(-1)
100                .with_priority(-1)
101                .with_message(issue.message),
102        );
103    for frag in issue.fragments {
104        builder =
105            builder.with_label(Label::new(b2c.map_span(frag.span)).with_message(frag.message));
106    }
107    if let Some(help) = issue.help {
108        builder = builder.with_help(help);
109    }
110    builder.finish()
111}
112
113/// Construct a color report for an issue (printed to stderr during schema errors)
114fn issue_to_report_color(
115    issue: Issue,
116    b2c: &ByteToChar,
117) -> Report<'static, std::ops::Range<usize>> {
118    let span = b2c.map_span(issue.span);
119    let err_color = match issue.level {
120        qusql_type::Level::Warning => Color::Yellow,
121        qusql_type::Level::Error => Color::Red,
122    };
123    let mut builder = Report::build(
124        match issue.level {
125            qusql_type::Level::Warning => ReportKind::Warning,
126            qusql_type::Level::Error => ReportKind::Error,
127        },
128        span.clone(),
129    )
130    .with_config(ariadne::Config::default().with_compact(true))
131    .with_message(&issue.message)
132    .with_label(
133        Label::new(span)
134            .with_color(err_color)
135            .with_order(-1)
136            .with_priority(-1)
137            .with_message(issue.message),
138    );
139    for frag in issue.fragments {
140        builder = builder.with_label(
141            Label::new(b2c.map_span(frag.span))
142                .with_color(Color::Blue)
143                .with_message(frag.message),
144        );
145    }
146    if let Some(help) = issue.help {
147        builder = builder.with_help(help);
148    }
149    builder.finish()
150}
151
152/// Source name adaptor for ariadne
153struct NamedSource<'a>(&'a str, Source<&'a str>);
154
155impl<'a> ariadne::Cache<()> for &NamedSource<'a> {
156    type Storage = &'a str;
157
158    fn display<'b>(&self, _: &'b ()) -> Option<impl std::fmt::Display + 'b> {
159        Some(self.0.to_string())
160    }
161
162    fn fetch(&mut self, _: &()) -> Result<&Source<Self::Storage>, impl std::fmt::Debug> {
163        Ok::<_, ()>(&self.1)
164    }
165}
166
167#[derive(Yokeable)]
168/// `Yokeable` wrapper that lets `Schemas<'a>` be stored in a `Yoke`.
169struct SchemasYoke<'a>(Schemas<'a>);
170
171/// Parsed schema and its source string, kept alive together so neither
172/// outlives the other.  Stored behind an `Arc` so callers can cheaply
173/// clone a handle without holding the mutex.
174struct SchemaCacheEntry {
175    /// The parsed schema yoked to the source it borrows from.
176    schemas: Yoke<SchemasYoke<'static>, String>,
177    /// SQL dialect detected from the schema file header.
178    dialect: SQLDialect,
179    /// Resolved path to the schema file, used in generated include_bytes! calls.
180    path: PathBuf,
181    /// File byte-length at the time of parsing, used for cache invalidation.
182    file_len: u64,
183    /// File modification time at the time of parsing, used for cache invalidation.
184    modified: SystemTime,
185    /// Hash of the schema content
186    hash: u64,
187}
188
189/// Per-path cache of parsed schemas, invalidated by file mtime/size.
190///
191/// Keyed by resolved schema path so that multiple crates compiled in the same
192/// proc-macro server process (e.g. rust-analyzer) each get their own entry.
193static SCHEMA_CACHE: Mutex<Option<HashMap<PathBuf, Arc<SchemaCacheEntry>>>> = Mutex::new(None);
194
195/// Return the parsed schema for the crate currently being compiled, reparsing
196/// from disk only when the file's mtime or size has changed.
197fn get_schemas() -> Arc<SchemaCacheEntry> {
198    let path = resolve_schema_path();
199    let meta = std::fs::metadata(&path).unwrap_or_else(|e| panic!("Cannot stat {path:?}: {e}"));
200    let file_len = meta.len();
201    let modified = meta.modified().unwrap_or(SystemTime::UNIX_EPOCH);
202
203    let mut cache_guard = SCHEMA_CACHE.lock().expect("schema cache lock poisoned");
204    let cache = cache_guard.get_or_insert_with(HashMap::new);
205    if let Some(entry) = cache.get(&path) {
206        if entry.file_len == file_len && entry.modified == modified {
207            return Arc::clone(entry);
208        }
209    }
210
211    // Cache miss or stale - read and reparse.
212    let src_string = std::fs::read_to_string(&path)
213        .unwrap_or_else(|e| panic!("Unable to read schema from {path:?}: {e}"));
214
215    // Detect dialect from the first two lines before moving src_box into the
216    // Yoke cart.  The result is moved into the closure by value.
217    let dialect = {
218        let header = src_string.lines().take(2).collect::<Vec<_>>().join(" ");
219        if header.contains("qusql-type-variant: postgis") || header.contains("sql-product: postgis")
220        {
221            SQLDialect::PostGIS
222        } else if header.contains("sql-product: postgres") {
223            SQLDialect::PostgreSQL
224        } else if header.contains("sql-product: sqlite") {
225            SQLDialect::Sqlite
226        } else {
227            SQLDialect::MariaDB
228        }
229    };
230
231    let dialect_for_closure = dialect.clone();
232
233    // Compute the schema hash from the fresh source before we move it into the cart.
234    let schema_hash = {
235        use std::hash::{DefaultHasher, Hash, Hasher};
236        let mut hasher = DefaultHasher::new();
237        src_string.hash(&mut hasher);
238        hasher.finish()
239    };
240
241    let schemas = Yoke::attach_to_cart(src_string, move |src| {
242        let options = TypeOptions::new().dialect(dialect_for_closure);
243        let mut issues = qusql_type::Issues::new(src);
244        let parsed = parse_schemas(src, &mut issues, &options);
245        if !issues.is_ok() {
246            let b2c = ByteToChar::new(src.as_bytes());
247            let source = NamedSource("sqlx-type-schema.sql", Source::from(src));
248            let mut err = false;
249            for issue in issues.into_vec() {
250                if issue.level == qusql_type::Level::Error {
251                    err = true;
252                }
253                issue_to_report_color(issue, &b2c).eprint(&source).unwrap();
254            }
255            if err {
256                panic!("Errors processing sqlx-type-schema.sql");
257            }
258        }
259        SchemasYoke(parsed)
260    });
261
262    let entry = Arc::new(SchemaCacheEntry {
263        schemas,
264        dialect,
265        path: path.clone(),
266        file_len,
267        modified,
268        hash: schema_hash,
269    });
270    cache.insert(path, Arc::clone(&entry));
271    entry
272}
273
274/// Produce quoted arguments for a query
275fn quote_args(
276    errors: &mut Vec<proc_macro2::TokenStream>,
277    query: &str,
278    last_span: Span,
279    args: &[Expr],
280    arguments: &[(qusql_type::ArgumentKey<'_>, qusql_type::FullType)],
281    dialect: &SQLDialect,
282) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
283    let cls = match dialect {
284        SQLDialect::MariaDB => quote!(sqlx::mysql::MySql),
285        SQLDialect::Sqlite => quote!(sqlx::sqlite::Sqlite),
286        SQLDialect::PostgreSQL | SQLDialect::PostGIS => quote!(sqlx::postgres::Postgres),
287    };
288
289    let mut at = Vec::new();
290    let inv = qusql_type::FullType::invalid();
291    for (k, v) in arguments {
292        match k {
293            qusql_type::ArgumentKey::Index(i) => {
294                while at.len() <= *i {
295                    at.push(&inv);
296                }
297                at[*i] = v;
298            }
299            qusql_type::ArgumentKey::Identifier(_) => {
300                errors.push(
301                    syn::Error::new(last_span.span(), "Named arguments not supported")
302                        .to_compile_error(),
303                );
304            }
305        }
306    }
307
308    if at.len() > args.len() {
309        errors.push(
310            syn::Error::new(
311                last_span,
312                format!("Expected {} additional arguments", at.len() - args.len()),
313            )
314            .to_compile_error(),
315        );
316    }
317
318    if let Some(args) = args.get(at.len()..) {
319        for arg in args {
320            errors.push(syn::Error::new(arg.span(), "unexpected argument").to_compile_error());
321        }
322    }
323
324    let arg_names = (0..args.len())
325        .map(|i| format_ident!("arg{}", i))
326        .collect::<Vec<_>>();
327
328    let mut arg_bindings = Vec::new();
329    let mut arg_add = Vec::new();
330
331    let mut list_lengths = Vec::new();
332
333    for ((qa, ta), name) in args.iter().zip(at).zip(&arg_names) {
334        let mut t = match ta.t {
335            qusql_type::Type::U8 => quote! {u8},
336            qusql_type::Type::I8 => quote! {i8},
337            qusql_type::Type::U16 => quote! {u16},
338            qusql_type::Type::I16 => quote! {i16},
339            qusql_type::Type::U24 => quote! {u32},
340            qusql_type::Type::I24 => quote! {i32},
341            qusql_type::Type::U32 => quote! {u32},
342            qusql_type::Type::I32 => quote! {i32},
343            qusql_type::Type::U64 => quote! {u64},
344            qusql_type::Type::I64 => quote! {i64},
345            qusql_type::Type::Base(qusql_type::BaseType::Any) => quote! {qusql_sqlx_type::Any},
346            qusql_type::Type::Base(qusql_type::BaseType::Bool) => quote! {bool},
347            qusql_type::Type::Base(qusql_type::BaseType::Bytes) => quote! {&[u8]},
348            qusql_type::Type::Base(qusql_type::BaseType::Date) => quote! {qusql_sqlx_type::Date},
349            qusql_type::Type::Base(qusql_type::BaseType::DateTime) => {
350                quote! {qusql_sqlx_type::DateTime}
351            }
352            qusql_type::Type::Base(qusql_type::BaseType::Float) => quote! {qusql_sqlx_type::Float},
353            qusql_type::Type::Base(qusql_type::BaseType::Integer) => {
354                quote! {qusql_sqlx_type::Integer}
355            }
356            qusql_type::Type::Base(qusql_type::BaseType::String) => quote! {&str},
357            qusql_type::Type::Base(qusql_type::BaseType::Time) => quote! {qusql_sqlx_type::Time},
358            qusql_type::Type::Base(qusql_type::BaseType::TimeInterval) => todo!("time_interval"),
359            qusql_type::Type::Base(qusql_type::BaseType::TimeStamp) => {
360                quote! {qusql_sqlx_type::Timestamp}
361            }
362            qusql_type::Type::Base(qusql_type::BaseType::Uuid) => quote! {qusql_sqlx_type::Uuid},
363            qusql_type::Type::Null => todo!("null"),
364            qusql_type::Type::Invalid => quote! {std::convert::Infallible},
365            qusql_type::Type::Enum(_) => quote! {&str},
366            qusql_type::Type::Set(_) => quote! {&str},
367            qusql_type::Type::Args(_, _) => todo!("args"),
368            qusql_type::Type::F32 => quote! {f32},
369            qusql_type::Type::F64 => quote! {f64},
370            qusql_type::Type::JSON => quote! {qusql_sqlx_type::Any},
371            qusql_type::Type::Geometry => quote! {qusql_sqlx_type::Any},
372            qusql_type::Type::Range(_) => quote! {qusql_sqlx_type::Any},
373            qusql_type::Type::Array(_) => quote! {qusql_sqlx_type::Any},
374        };
375        if !ta.not_null {
376            t = quote! {Option<#t>}
377        }
378        let span = qa.span();
379        if ta.list_hack {
380            list_lengths.push(quote!(#name.len()));
381            arg_bindings.push(quote_spanned! {span=>
382                let #name = &(#qa);
383                args_count += #name.len();
384                for v in #name.iter() {
385                    size_hints += ::sqlx::encode::Encode::<#cls>::size_hint(v);
386                }
387                if false {
388                    qusql_sqlx_type::check_arg_list_hack::<#t, _>(#name);
389                    ::std::panic!();
390                }
391            });
392            arg_add.push(quote!(
393                for v in #name.iter() {
394                    e = e.and_then(|()| query_args.add(v));
395                }
396            ));
397        } else {
398            arg_bindings.push(quote_spanned! {span=>
399                let #name = &(#qa);
400                args_count += 1;
401                size_hints += ::sqlx::encode::Encode::<#cls>::size_hint(#name);
402                if false {
403                    qusql_sqlx_type::check_arg::<#t, _>(#name);
404                    ::std::panic!();
405                }
406            });
407            arg_add.push(quote!(e = e.and_then(|()| query_args.add(#name));));
408        }
409    }
410
411    let query = if list_lengths.is_empty() {
412        quote!(#query)
413    } else {
414        quote!(
415            &qusql_sqlx_type::convert_list_query(#query, &[#(#list_lengths),*])
416        )
417    };
418
419    (
420        quote! {
421            let mut size_hints = 0;
422            let mut args_count = 0;
423            #(#arg_bindings)*
424
425            let mut query_args = <#cls as ::sqlx::database::Database>::Arguments::default();
426            query_args.reserve(args_count, size_hints);
427            let mut e = Ok(());
428            #(#arg_add)*
429            let query_args = e.and_then(|()| Ok(query_args));
430        },
431        query,
432    )
433}
434
435/// Output an [Issue] as a compile error
436fn issues_to_errors(issues: Vec<Issue>, source: &str, span: Span) -> Vec<proc_macro2::TokenStream> {
437    if !issues.is_empty() {
438        let b2c = ByteToChar::new(source.as_bytes());
439        let source = NamedSource("query", Source::from(source));
440        let mut err = false;
441        let mut out = Vec::new();
442        for issue in issues {
443            if issue.level == qusql_type::Level::Error {
444                err = true;
445            }
446            let r = issue_to_report(issue, &b2c);
447            r.write(&source, &mut out).unwrap();
448        }
449        if err {
450            let raw = String::from_utf8(out).unwrap();
451            // Strip ariadne's first "Error: <message>" line — rustc provides
452            // its own "error:" heading, so keeping ariadne's makes it double.
453            let body = raw
454                .find('\n')
455                .map(|i| raw[i + 1..].trim_start_matches('\n').trim_end())
456                .unwrap_or(raw.trim_end());
457            return vec![syn::Error::new(span, body).to_compile_error()];
458        }
459    }
460    Vec::new()
461}
462
463/// Construct row struct members, and fill in statements
464fn construct_row(
465    columns: &[SelectTypeColumn],
466    is_postgres: bool,
467) -> (Vec<proc_macro2::TokenStream>, Vec<proc_macro2::TokenStream>) {
468    let mut row_members = Vec::new();
469    let mut row_construct = Vec::new();
470    for (i, c) in columns.iter().enumerate() {
471        let mut t = match c.type_.t {
472            qusql_type::Type::U8 => quote! {u8},
473            qusql_type::Type::I8 => quote! {i8},
474            qusql_type::Type::U16 => quote! {u16},
475            qusql_type::Type::I16 => quote! {i16},
476            qusql_type::Type::U24 => quote! {u32},
477            qusql_type::Type::I24 => quote! {i32},
478            qusql_type::Type::U32 => quote! {u32},
479            qusql_type::Type::I32 => quote! {i32},
480            qusql_type::Type::U64 => quote! {u64},
481            qusql_type::Type::I64 => quote! {i64},
482            qusql_type::Type::Base(qusql_type::BaseType::Any) => todo!("from_any"),
483            qusql_type::Type::Base(qusql_type::BaseType::Bool) => quote! {bool},
484            qusql_type::Type::Base(qusql_type::BaseType::Bytes) => quote! {Vec<u8>},
485            qusql_type::Type::Base(qusql_type::BaseType::Date) => quote! {chrono::NaiveDate},
486            qusql_type::Type::Base(qusql_type::BaseType::DateTime) => {
487                quote! {chrono::NaiveDateTime}
488            }
489            qusql_type::Type::Base(qusql_type::BaseType::Float) => quote! {f64},
490            qusql_type::Type::Base(qusql_type::BaseType::Integer) => {
491                if is_postgres {
492                    quote! {i32}
493                } else {
494                    quote! {i64}
495                }
496            }
497            qusql_type::Type::Base(qusql_type::BaseType::String) => quote! {String},
498            qusql_type::Type::Base(qusql_type::BaseType::Time) => todo!("from_time"),
499            qusql_type::Type::Base(qusql_type::BaseType::TimeInterval) => {
500                todo!("from_time_interval")
501            }
502            qusql_type::Type::Base(qusql_type::BaseType::TimeStamp) => {
503                quote! {sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>}
504            }
505            qusql_type::Type::Base(qusql_type::BaseType::Uuid) => {
506                quote! {qusql_sqlx_type::UuidValue}
507            }
508            qusql_type::Type::Null => todo!("from_null"),
509            qusql_type::Type::Invalid => quote! {i64},
510            qusql_type::Type::Enum(_) => quote! {String},
511            qusql_type::Type::Set(_) => quote! {String},
512            qusql_type::Type::Args(_, _) => todo!("from_args"),
513            qusql_type::Type::F32 => quote! {f32},
514            qusql_type::Type::F64 => quote! {f64},
515            qusql_type::Type::JSON => quote! {String},
516            qusql_type::Type::Geometry => quote! {Vec<u8>},
517            qusql_type::Type::Range(_) => quote! {Vec<u8>},
518            qusql_type::Type::Array(_) => quote! {qusql_sqlx_type::Any},
519        };
520        let name = match &c.name {
521            Some(v) => v,
522            None => continue,
523        };
524
525        // Handle sqlx "column!" convention: strip trailing ! and force not_null
526        let name_str = name.value;
527        let (name_str, force_not_null) = if let Some(stripped) = name_str.strip_suffix('!') {
528            (stripped, true)
529        } else {
530            (name_str, false)
531        };
532
533        let ident = String::from("r#") + name_str;
534        let ident: Ident = if let Ok(ident) = syn::parse_str(&ident) {
535            ident
536        } else {
537            // TODO error
538            //errors.push(syn::Error::new(span, String::from_utf8(out).unwrap()).to_compile_error().into());
539            continue;
540        };
541
542        if !c.type_.not_null && !force_not_null {
543            t = quote! {Option<#t>};
544        }
545        row_members.push(quote! {
546            #ident : #t
547        });
548        row_construct.push(quote! {
549            #ident: sqlx::Row::get(&row, #i)
550        });
551    }
552    (row_members, row_construct)
553}
554
555/// Parsed query! macro
556struct Query {
557    /// The query expression
558    query: String,
559    /// The span of the query expression
560    query_span: Span,
561    /// The arguments to supply to the query
562    args: Vec<Expr>,
563    /// The last span parsed
564    last_span: Span,
565}
566
567impl Parse for Query {
568    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
569        let query_ = Punctuated::<LitStr, Token![+]>::parse_separated_nonempty(input)?;
570        let query: String = query_.iter().map(LitStr::value).collect();
571        let query_span = query_.span();
572        let mut last_span = query_span;
573        let mut args = Vec::new();
574        while !input.is_empty() {
575            let _ = input.parse::<syn::token::Comma>()?;
576            if input.is_empty() {
577                break;
578            }
579            let arg = input.parse::<Expr>()?;
580            last_span = arg.span();
581            args.push(arg);
582        }
583        Ok(Self {
584            query,
585            query_span,
586            args,
587            last_span,
588        })
589    }
590}
591
592/// Statically checked SQL query, similarly to sqlx::query!.
593///
594/// This expands to an instance of query::Map that outputs an ad-hoc anonymous struct type.
595#[proc_macro]
596pub fn query(input: TokenStream) -> TokenStream {
597    let query = syn::parse_macro_input!(input as Query);
598    let cache = get_schemas();
599    let (schemas, dialect, schema_hash) = (cache.schemas.get(), &cache.dialect, cache.hash);
600    let options = TypeOptions::new()
601        .dialect(dialect.clone())
602        .arguments(match &dialect {
603            SQLDialect::MariaDB => SQLArguments::QuestionMark,
604            SQLDialect::Sqlite => SQLArguments::QuestionMark,
605            SQLDialect::PostgreSQL | SQLDialect::PostGIS => SQLArguments::Dollar,
606        })
607        .list_hack(true);
608    let mut issues = qusql_type::Issues::new(&query.query);
609    let stmt = type_statement(&schemas.0, &query.query, &mut issues, &options);
610    let sp = cache.path.to_str().unwrap();
611    let mut errors = issues_to_errors(issues.into_vec(), &query.query, query.query_span);
612    match &stmt {
613        qusql_type::StatementType::Select { columns, arguments } => {
614            let (args_tokens, q) = quote_args(
615                &mut errors,
616                &query.query,
617                query.last_span,
618                &query.args,
619                arguments,
620                dialect,
621            );
622            let (row_members, row_construct) = construct_row(columns, dialect.is_postgresql());
623            let s = quote! { {
624                use ::sqlx::Arguments as _;
625                let _ = std::include_bytes!(#sp);
626                #(#errors; )*
627                #args_tokens;
628
629                struct Row {
630                    #(#row_members),*
631                };
632                sqlx::__query_with_result(#q, query_args).map(|row|
633                    Row{
634                        #(#row_construct),*
635                    }
636                )
637            }};
638            s.into()
639        }
640        qusql_type::StatementType::Delete {
641            arguments,
642            returning,
643        } => {
644            let (args_tokens, q) = quote_args(
645                &mut errors,
646                &query.query,
647                query.last_span,
648                &query.args,
649                arguments,
650                dialect,
651            );
652            let s = match returning.as_ref() {
653                Some(returning) => {
654                    let (row_members, row_construct) =
655                        construct_row(returning, dialect.is_postgresql());
656                    quote! { {
657                        use ::sqlx::Arguments as _;
658                        let _ = std::include_bytes!(#sp);
659                        #(#errors; )*
660                        #args_tokens
661
662                        struct Row {
663                            #(#row_members),*
664                        };
665                        sqlx::__query_with_result(#q, query_args).map(|row|
666                            Row{
667                                #(#row_construct),*
668                            }
669                        )
670                    }}
671                }
672                None => quote! { {
673                    use ::sqlx::Arguments as _;
674                    const _SCHEMA_HASH: u64 = #schema_hash;
675                    #(#errors; )*
676                    #args_tokens
677                    sqlx::__query_with_result(#q, query_args)
678                }
679                },
680            };
681            s.into()
682        }
683        qusql_type::StatementType::Insert {
684            arguments,
685            returning,
686            ..
687        } => {
688            let (args_tokens, q) = quote_args(
689                &mut errors,
690                &query.query,
691                query.last_span,
692                &query.args,
693                arguments,
694                dialect,
695            );
696            let s = match returning.as_ref() {
697                Some(returning) => {
698                    let (row_members, row_construct) =
699                        construct_row(returning, dialect.is_postgresql());
700                    quote! { {
701                        use ::sqlx::Arguments as _;
702                        let _ = std::include_bytes!(#sp);
703                        #(#errors; )*
704                        #args_tokens
705
706                        struct Row {
707                            #(#row_members),*
708                        };
709                        sqlx::__query_with_result(#q, query_args).map(|row|
710                            Row{
711                                #(#row_construct),*
712                            }
713                        )
714                    }}
715                }
716                None => quote! { {
717                    use ::sqlx::Arguments as _;
718                    const _SCHEMA_HASH: u64 = #schema_hash;
719                    #(#errors; )*
720                    #args_tokens
721                    sqlx::__query_with_result(#q, query_args)
722                }
723                },
724            };
725            s.into()
726        }
727        qusql_type::StatementType::Update {
728            arguments,
729            returning,
730        } => {
731            let (args_tokens, q) = quote_args(
732                &mut errors,
733                &query.query,
734                query.last_span,
735                &query.args,
736                arguments,
737                dialect,
738            );
739
740            let s = match returning.as_ref() {
741                Some(returning) => {
742                    let (row_members, row_construct) =
743                        construct_row(returning, dialect.is_postgresql());
744                    quote! { {
745                        use ::sqlx::Arguments as _;
746                        let _ = std::include_bytes!(#sp);
747                        #(#errors; )*
748                        #args_tokens
749
750                        struct Row {
751                            #(#row_members),*
752                        };
753                        sqlx::__query_with_result(#q, query_args).map(|row|
754                            Row{
755                                #(#row_construct),*
756                            }
757                        )
758                    }}
759                }
760                None => quote! { {
761                    use ::sqlx::Arguments as _;
762                    const _SCHEMA_HASH: u64 = #schema_hash;
763                    #(#errors; )*
764                    #args_tokens
765                    sqlx::__query_with_result(#q, query_args)
766                }
767                },
768            };
769            s.into()
770        }
771        qusql_type::StatementType::Replace {
772            arguments,
773            returning,
774        } => {
775            let (args_tokens, q) = quote_args(
776                &mut errors,
777                &query.query,
778                query.last_span,
779                &query.args,
780                arguments,
781                dialect,
782            );
783            let s = match returning.as_ref() {
784                Some(returning) => {
785                    let (row_members, row_construct) =
786                        construct_row(returning, dialect.is_postgresql());
787                    quote! { {
788                        use ::sqlx::Arguments as _;
789                        const _SCHEMA_HASH: u64 = #schema_hash;
790                        let _ = std::include_bytes!(#sp);
791                        #(#errors; )*
792                        #args_tokens
793
794                        struct Row {
795                            #(#row_members),*
796                        };
797                        sqlx::__query_with_result(#q, query_args).map(|row|
798                            Row{
799                                #(#row_construct),*
800                            }
801                        )
802                    }}
803                }
804                None => quote! { {
805                    use ::sqlx::Arguments as _;
806                    #(#errors; )*
807                    #args_tokens
808                    sqlx::__query_with_result(#q, query_args)
809                }
810                },
811            };
812            s.into()
813        }
814        qusql_type::StatementType::Truncate => {
815            errors.push(
816                syn::Error::new(query.query_span, "TRUNCATE not supported in query!")
817                    .to_compile_error(),
818            );
819            quote! { {
820                #(#errors; )*
821                todo!("truncate")
822            }}
823            .into()
824        }
825        qusql_type::StatementType::Call { .. } => {
826            errors.push(
827                syn::Error::new(query.query_span, "CALL not supported in query!")
828                    .to_compile_error(),
829            );
830            quote! { {
831                #(#errors; )*
832                todo!("call")
833            }}
834            .into()
835        }
836        qusql_type::StatementType::Transaction => {
837            errors.push(
838                syn::Error::new(
839                    query.query_span,
840                    "Transaction control not supported in query!",
841                )
842                .to_compile_error(),
843            );
844            quote! { {
845                #(#errors; )*
846                todo!("transaction")
847            }}
848            .into()
849        }
850        qusql_type::StatementType::Set => {
851            errors.push(
852                syn::Error::new(query.query_span, "SET not supported in query!").to_compile_error(),
853            );
854            quote! { {
855                #(#errors; )*
856                todo!("set")
857            }}
858            .into()
859        }
860        qusql_type::StatementType::Lock => {
861            errors.push(
862                syn::Error::new(query.query_span, "LOCK not supported in query!")
863                    .to_compile_error(),
864            );
865            quote! { {
866                #(#errors; )*
867                todo!("lock")
868            }}
869            .into()
870        }
871        qusql_type::StatementType::Invalid => {
872            let s = quote! { {
873                #(#errors; )*;
874                todo!("Invalid")
875            }};
876            s.into()
877        }
878    }
879}
880
881/// Fill in row values in a query_as struct
882fn construct_row2(
883    columns: &[SelectTypeColumn],
884    is_postgres: bool,
885) -> Vec<proc_macro2::TokenStream> {
886    let mut row_construct = Vec::new();
887    for (i, c) in columns.iter().enumerate() {
888        let mut t = match c.type_.t {
889            qusql_type::Type::U8 => quote! {u8},
890            qusql_type::Type::I8 => quote! {i8},
891            qusql_type::Type::U16 => quote! {u16},
892            qusql_type::Type::I16 => quote! {i16},
893            qusql_type::Type::U24 => quote! {u32},
894            qusql_type::Type::I24 => quote! {i32},
895            qusql_type::Type::U32 => quote! {u32},
896            qusql_type::Type::I32 => quote! {i32},
897            qusql_type::Type::U64 => quote! {u64},
898            qusql_type::Type::I64 => quote! {i64},
899            qusql_type::Type::Base(qusql_type::BaseType::Any) => todo!("from_any"),
900            qusql_type::Type::Base(qusql_type::BaseType::Bool) => quote! {bool},
901            qusql_type::Type::Base(qusql_type::BaseType::Bytes) => quote! {Vec<u8>},
902            qusql_type::Type::Base(qusql_type::BaseType::Date) => quote! {chrono::NaiveDate},
903            qusql_type::Type::Base(qusql_type::BaseType::DateTime) => {
904                quote! {chrono::NaiveDateTime}
905            }
906            qusql_type::Type::Base(qusql_type::BaseType::Float) => quote! {f64},
907            qusql_type::Type::Base(qusql_type::BaseType::Integer) => {
908                if is_postgres {
909                    quote! {i32}
910                } else {
911                    quote! {i64}
912                }
913            }
914            qusql_type::Type::Base(qusql_type::BaseType::String) => quote! {String},
915            qusql_type::Type::Base(qusql_type::BaseType::Time) => todo!("from_time"),
916            qusql_type::Type::Base(qusql_type::BaseType::TimeInterval) => {
917                todo!("from_time_interval")
918            }
919            qusql_type::Type::Base(qusql_type::BaseType::TimeStamp) => {
920                quote! {sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>}
921            }
922            qusql_type::Type::Base(qusql_type::BaseType::Uuid) => {
923                quote! {qusql_sqlx_type::UuidValue}
924            }
925            qusql_type::Type::Null => todo!("from_null"),
926            qusql_type::Type::Invalid => quote! {i64},
927            qusql_type::Type::Enum(_) => quote! {String},
928            qusql_type::Type::Set(_) => quote! {String},
929            qusql_type::Type::Args(_, _) => todo!("from_args"),
930            qusql_type::Type::F32 => quote! {f32},
931            qusql_type::Type::F64 => quote! {f64},
932            qusql_type::Type::JSON => quote! {String},
933            qusql_type::Type::Geometry => quote! {Vec<u8>},
934            qusql_type::Type::Range(_) => quote! {Vec<u8>},
935            qusql_type::Type::Array(_) => quote! {qusql_sqlx_type::Any},
936        };
937        let name = match &c.name {
938            Some(v) => v,
939            None => continue,
940        };
941
942        // Handle sqlx "column!" convention: strip trailing ! and force not_null
943        let name_str = name.value;
944        let (name_str, force_not_null) = if let Some(stripped) = name_str.strip_suffix('!') {
945            (stripped, true)
946        } else {
947            (name_str, false)
948        };
949
950        let ident = String::from("r#") + name_str;
951        let ident: Ident = if let Ok(ident) = syn::parse_str(&ident) {
952            ident
953        } else {
954            // TODO error
955            //errors.push(syn::Error::new(span, String::from_utf8(out).unwrap()).to_compile_error().into());
956            continue;
957        };
958
959        if !c.type_.not_null && !force_not_null {
960            t = quote! {Option<#t>};
961        }
962        row_construct.push(quote! {
963            #ident: qusql_sqlx_type::arg_out::<#t, _, #i>(sqlx::Row::get(&row, #i))
964        });
965    }
966    row_construct
967}
968
969/// Parse result of a query_as! macro
970struct QueryAs {
971    /// Name of output type
972    as_: Ident,
973    /// The query to execute
974    query: String,
975    /// The span of the query to execute
976    query_span: Span,
977    /// The arguments to supply
978    args: Vec<Expr>,
979    /// The last span parsed
980    last_span: Span,
981}
982
983impl Parse for QueryAs {
984    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
985        let as_ = input.parse::<Ident>()?;
986        let _ = input.parse::<syn::token::Comma>()?;
987
988        let query_ = Punctuated::<LitStr, Token![+]>::parse_separated_nonempty(input)?;
989        let query: String = query_.iter().map(LitStr::value).collect();
990        let query_span = query_.span();
991
992        let mut last_span = query_span;
993        let mut args = Vec::new();
994        while !input.is_empty() {
995            let _ = input.parse::<syn::token::Comma>()?;
996            if input.is_empty() {
997                break;
998            }
999            let arg = input.parse::<Expr>()?;
1000            last_span = arg.span();
1001            args.push(arg);
1002        }
1003        Ok(Self {
1004            as_,
1005            query,
1006            query_span,
1007            args,
1008            last_span,
1009        })
1010    }
1011}
1012
1013/// A variant of query! which takes a path to an explicitly defined struct as the output type.
1014///
1015/// This lets you return the struct from a function or add your own trait implementations.
1016#[proc_macro]
1017pub fn query_as(input: TokenStream) -> TokenStream {
1018    let query_as = syn::parse_macro_input!(input as QueryAs);
1019    let cache = get_schemas();
1020    let (schemas, dialect) = (cache.schemas.get(), &cache.dialect);
1021    let options = TypeOptions::new()
1022        .dialect(dialect.clone())
1023        .arguments(match &dialect {
1024            SQLDialect::MariaDB => SQLArguments::QuestionMark,
1025            SQLDialect::Sqlite => SQLArguments::QuestionMark,
1026            SQLDialect::PostgreSQL | SQLDialect::PostGIS => SQLArguments::Dollar,
1027        })
1028        .list_hack(true);
1029    let mut issues = qusql_type::Issues::new(&query_as.query);
1030    let stmt = type_statement(&schemas.0, &query_as.query, &mut issues, &options);
1031
1032    let mut errors = issues_to_errors(issues.into_vec(), &query_as.query, query_as.query_span);
1033    match &stmt {
1034        qusql_type::StatementType::Select { columns, arguments } => {
1035            let (args_tokens, q) = quote_args(
1036                &mut errors,
1037                &query_as.query,
1038                query_as.last_span,
1039                &query_as.args,
1040                arguments,
1041                dialect,
1042            );
1043
1044            let row_construct = construct_row2(columns, dialect.is_postgresql());
1045            let row = query_as.as_;
1046            let s = quote! { {
1047                use ::sqlx::Arguments as _;
1048                #(#errors; )*
1049                #args_tokens
1050                sqlx::__query_with_result(#q, query_args).map(|row|
1051                    #row{
1052                        #(#row_construct),*
1053                    }
1054                )
1055            }};
1056            //println!("TOKENS: {}", s);
1057            s.into()
1058        }
1059        qusql_type::StatementType::Delete { .. } => {
1060            errors.push(
1061                syn::Error::new(query_as.query_span, "DELETE not support in query_as")
1062                    .to_compile_error(),
1063            );
1064            quote! { {
1065                #(#errors; )*
1066                todo!("delete")
1067            }}
1068            .into()
1069        }
1070        qusql_type::StatementType::Insert {
1071            returning: None, ..
1072        } => {
1073            errors.push(
1074                syn::Error::new(
1075                    query_as.query_span,
1076                    "INSERT without RETURNING not support in query_as",
1077                )
1078                .to_compile_error(),
1079            );
1080            quote! { {
1081                #(#errors; )*
1082                todo!("insert")
1083            }}
1084            .into()
1085        }
1086        qusql_type::StatementType::Insert {
1087            arguments,
1088            returning: Some(returning),
1089            ..
1090        } => {
1091            let (args_tokens, q) = quote_args(
1092                &mut errors,
1093                &query_as.query,
1094                query_as.last_span,
1095                &query_as.args,
1096                arguments,
1097                dialect,
1098            );
1099
1100            let row_construct = construct_row2(returning, dialect.is_postgresql());
1101            let row = query_as.as_;
1102            let s = quote! { {
1103                use ::sqlx::Arguments as _;
1104                #(#errors; )*
1105                #args_tokens
1106                sqlx::__query_with_result(#q, query_args).map(|row|
1107                    #row{
1108                        #(#row_construct),*
1109                    }
1110                )
1111            }};
1112            s.into()
1113        }
1114        qusql_type::StatementType::Update {
1115            returning: None, ..
1116        } => {
1117            errors.push(
1118                syn::Error::new(
1119                    query_as.query_span,
1120                    "UPDATE without RETURNING not support in query_as",
1121                )
1122                .to_compile_error(),
1123            );
1124            quote! { {
1125                #(#errors; )*
1126                todo!("update")
1127            }}
1128            .into()
1129        }
1130        qusql_type::StatementType::Update {
1131            arguments,
1132            returning: Some(returning),
1133            ..
1134        } => {
1135            let (args_tokens, q) = quote_args(
1136                &mut errors,
1137                &query_as.query,
1138                query_as.last_span,
1139                &query_as.args,
1140                arguments,
1141                dialect,
1142            );
1143
1144            let row_construct = construct_row2(returning, dialect.is_postgresql());
1145            let row = query_as.as_;
1146            let s = quote! { {
1147                use ::sqlx::Arguments as _;
1148                #(#errors; )*
1149                #args_tokens
1150                sqlx::__query_with_result(#q, query_args).map(|row|
1151                    #row{
1152                        #(#row_construct),*
1153                    }
1154                )
1155            }};
1156            s.into()
1157        }
1158        qusql_type::StatementType::Replace {
1159            returning: None, ..
1160        } => {
1161            errors.push(
1162                syn::Error::new(
1163                    query_as.query_span,
1164                    "REPLACE without RETURNING not support in query_as",
1165                )
1166                .to_compile_error(),
1167            );
1168            quote! { {
1169                #(#errors; )*
1170                todo!("replace")
1171            }}
1172            .into()
1173        }
1174        qusql_type::StatementType::Replace {
1175            arguments,
1176            returning: Some(returning),
1177            ..
1178        } => {
1179            let (args_tokens, q) = quote_args(
1180                &mut errors,
1181                &query_as.query,
1182                query_as.last_span,
1183                &query_as.args,
1184                arguments,
1185                dialect,
1186            );
1187
1188            let row_construct = construct_row2(returning, dialect.is_postgresql());
1189            let row = query_as.as_;
1190            let s = quote! { {
1191                use ::sqlx::Arguments as _;
1192                #(#errors; )*
1193                #args_tokens
1194                sqlx::__query_with_result(#q, query_args).map(|row|
1195                    #row{
1196                        #(#row_construct),*
1197                    }
1198                )
1199            }};
1200            s.into()
1201        }
1202        qusql_type::StatementType::Truncate => {
1203            errors.push(
1204                syn::Error::new(query_as.query_span, "TRUNCATE not supported in query_as!")
1205                    .to_compile_error(),
1206            );
1207            quote! { {
1208                #(#errors; )*
1209                todo!("truncate")
1210            }}
1211            .into()
1212        }
1213        qusql_type::StatementType::Call { .. } => {
1214            errors.push(
1215                syn::Error::new(query_as.query_span, "CALL not supported in query_as!")
1216                    .to_compile_error(),
1217            );
1218            quote! { {
1219                #(#errors; )*
1220                todo!("call")
1221            }}
1222            .into()
1223        }
1224        qusql_type::StatementType::Transaction => {
1225            errors.push(
1226                syn::Error::new(
1227                    query_as.query_span,
1228                    "Transaction control not supported in query_as!",
1229                )
1230                .to_compile_error(),
1231            );
1232            quote! { {
1233                #(#errors; )*
1234                todo!("transaction")
1235            }}
1236            .into()
1237        }
1238        qusql_type::StatementType::Set => {
1239            errors.push(
1240                syn::Error::new(query_as.query_span, "SET not supported in query_as!")
1241                    .to_compile_error(),
1242            );
1243            quote! { {
1244                #(#errors; )*
1245                todo!("set")
1246            }}
1247            .into()
1248        }
1249        qusql_type::StatementType::Lock => {
1250            errors.push(
1251                syn::Error::new(query_as.query_span, "LOCK not supported in query_as!")
1252                    .to_compile_error(),
1253            );
1254            quote! { {
1255                #(#errors; )*
1256                todo!("lock")
1257            }}
1258            .into()
1259        }
1260        qusql_type::StatementType::Invalid => quote! { {
1261            #(#errors; )*;
1262            todo!("invalid")
1263        }}
1264        .into(),
1265    }
1266}