Ubuntu TechHive
inside-sqlx-building-compile-time-checked-sql-from-lego-brick.md
Inside SQLx — building compile-time checked SQL from Lego brick
article.detalhe

Inside SQLx — building compile-time checked SQL from Lego brick

reading.progresso 41 min de leitura

O objetivo é entender, quando se trata de `Rust` e `SQL`, de onde vêm realmente as garantias de consulta em tempo de compilação.

#+title: Por dentro do SQLx — construindo SQL verificado em tempo de compilação a partir de blocos de montar
#+author: ChiefKemist
#+date: <2026-07-25 Sáb>

  • Introdução

Estamos construindo um SQLx de brinquedo para entender alguns de seus principais recursos e como eles poderiam ser construídos. Ao mesmo tempo, exploraremos alguns recursos do Rust + ecossistema, como macros e workspaces do Cargo. O objetivo final é entender, quando se trata de Rust e SQL, de onde vêm realmente as garantias de consulta em tempo de compilação.

Começamos com SQL que o Rust trata como uma string não verificada e terminamos com isto:

#+begin_src rust :tangle no
let users = toy_sqlx::query!(
&connection,
"SELECT id, email FROM users WHERE active = ?1",
true,
)?;
#+end_src

onde uma chamada de macro fará com que três sistemas contribuam para a correção:

  • SQLite prova que o SQL é válido para o esquema de compilação.
  • A macro transforma a evidência do SQLite em código Rust.
  • rustc verifica os campos gerados e as structs da aplicação.

Não vamos clonar o SQLx, em vez disso, estamos expondo o menor pipeline útil de tempo de compilação para impor um SQL correto no código Rust.

  • Sobre Macros

#+begin_quote
“A linguagem inteira está lá o tempo todo. Não há distinção real entre tempo de leitura, tempo de compilação e tempo de execução. Você pode compilar ou executar código durante a leitura, ler ou executar código durante a compilação, e ler ou compilar código em tempo de execução.

Executar código em tempo de leitura permite que os usuários reprogramem a sintaxe do Lisp; executar código em tempo de compilação é a base das macros; compilar em tempo de execução é a base do uso do Lisp como uma linguagem de extensão em programas como o Emacs; e ler em tempo de execução permite que os programas se comuniquem usando s-expressions, uma ideia recentemente reinventada como XML.”
#+end_quote

— Paul Graham, /Revenge of the Nerds/, em /Hackers & Painters/,
seção “What Made Lisp Different,” item 9.

Referência: [[https://paulgraham.com/icad.html][Paul Graham — Revenge of the Nerds]]

  • Estrutura: três crates

Crates do workspace Cargo: toy-sqlx, toy-macro, toy-demo

#+name: toy-sqlx/Cargo.toml
#+begin_src toml :tangle toy-sqlx/Cargo.toml :mkdirp yes :comments no
[package]
name = "toy-sqlx"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true

[dependencies]
toy-sqlx-macros = { path = "../toy-sqlx-macros" }
rusqlite = { version = "0.39", features = ["bundled"] }
#+end_src

#+name: toy-macro/Cargo.toml
#+begin_src toml :tangle toy-sqlx-macros/Cargo.toml :mkdirp yes :comments no
[package]
name = "toy-sqlx-macros"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1"
quote = "1"
rusqlite = { version = "0.39", features = ["bundled", "column_metadata"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
syn = { version = "2", features = ["full"] }
#+end_src

#+name: toy-demo/Cargo.toml
#+begin_src toml :tangle toy-demo/Cargo.toml :mkdirp yes :comments no
[package]
name = "toy-demo"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true

[features]
fail-literal = []
fail-invalid-sql = []
fail-arity = []
fail-query-as-name = []
fail-query-as-type = []
fail-expression-metadata = []
fail-unsupported-shape = []
fail-view-source = []
fail-subquery-source = []

[dependencies]
toy-sqlx = { path = "../toy-sqlx" }
#+end_src

  • Passo 0: expor a falha em tempo de execução
    :PROPERTIES:
    :MINUTES: 5
    :END:

#+begin_quote
Problema: O Rust vê o SQL como texto comum, então ele não pode rejeitar uma coluna ruim.

Por que se importar: o primeiro feedback útil chega apenas após o programa ser compilado,
implantado e o caminho da consulta ser executado.

Este passo: criar um pequeno banco de dados e preparar deliberadamente uma consulta inválida em
tempo de execução. Esta é a linha de base que cada passo posterior deve melhorar.
#+end_quote

O fixture tem apenas a evidência de esquema que precisaremos mais tarde: tipos de coluna declarados
e uma coluna anulável. O programa de configuração não usa nenhuma macro verificada.

#+name: fixture-schema
#+begin_src sql :tangle toy-demo/schema.sql :mkdirp yes :comments no
PRAGMA foreign_keys = ON;

DROP VIEW IF EXISTS hidden_join;
DROP TABLE IF EXISTS users;

CREATE TABLE users (
id INTEGER PRIMARY KEY NOT NULL,
email TEXT NOT NULL UNIQUE,
display_name TEXT,
active BOOLEAN NOT NULL DEFAULT TRUE CHECK (active IN (FALSE, TRUE))
);

INSERT INTO users (email, display_name, active) VALUES
('ada@example.test', 'Ada', TRUE),
('grace@example.test', NULL, TRUE),
('alan@example.test', 'Alan', FALSE);
#+end_src

#+name: fixture-setup
#+begin_src rust :tangle toy-demo/src/bin/setup.rs :mkdirp yes :comments no
fn main() -> Result<(), Box> {
let url = std::env::var("TOY_DATABASE_URL").expect("TOY_DATABASE_URL is required");
let connection = toy_sqlx::connect(&url)?;
connection.execute_batch(include_str!("../../schema.sql"))?;
println!("initialized {}", toy_sqlx::database_path(&url).display());
Ok(())
}
#+end_src

#+name: runtime-failure
#+begin_src rust :tangle toy-demo/src/bin/00_runtime_sql.rs :mkdirp yes :comments no
fn main() -> Result<(), toy_sqlx::rusqlite::Error> {
let url = std::env::var("TOY_DATABASE_URL").expect("TOY_DATABASE_URL is required");
let connection = toy_sqlx::connect(&url)?;
let result = connection.prepare("SELECT id, definitely_missing FROM users");
match result {
Ok(_) => println!("unexpected success"),
Err(error) => println!("runtime SQLite error: {error}"),
}
Ok(())
}
#+end_src

  • Execute o passo:

#+begin_src bash :results output

./scripts/toy-demo.sh 0

#+end_src

#+RESULTS:
: Compiling toy-demo v0.1.0 (/Users/chiefkemist/Documents/native_workspace/mini-sqlx-workshop/toy-demo)
: Finished dev profile [unoptimized + debuginfo] target(s) in 0.27s
: Running target/debug/00_runtime_sql
: runtime SQLite error: no such column: definitely_missing in SELECT id, definitely_missing FROM users at offset 11

  • Observe: O Rust compila; o SQLite relata =definitely_missing= em tempo de execução.
  • Ganhamos: uma falha concreta para mover para mais cedo.
  • Ainda quebrado: o compilador nunca recebe o SQL como entrada inspecionável. O Passo 1
    entrega o SQL para uma macro procedural.
  • Passo 1: tornar o SQL visível em tempo de compilação
    :PROPERTIES:
    :MINUTES: 6
    :END:

#+begin_quote
Problema: uma macro procedural recebe tokens Rust, não o valor escondido dentro de uma
variável em tempo de execução.

Por que se importar: a verificação em tempo de compilação é impossível a menos que o próprio SQL esteja disponível
durante a expansão da macro.

Este passo: exigir um literal de string. A primeira macro apenas analisa e retorna o
literal, para que possamos ver esse limite sem misturar a lógica do banco de dados.
#+end_quote

A crate de tempo de execução abaixo é o encanamento de suporte: conectar ao SQLite e mapear linhas.
Ela não realiza nenhuma análise em tempo de compilação.

#+name: toy-runtime
#+begin_src rust :tangle toy-sqlx/src/lib.rs :mkdirp yes :comments no
use std::path::Path;

pub use rusqlite;
pub use toy_sqlx_macros::{checked_query, checked_sql, query, query_as, sql_literal};

pub fn database_path(url: &str) -> &Path {
let path = url
.strip_prefix("sqlite://")
.or_else(|| url.strip_prefix("sqlite:"))
.unwrap_or(url);
Path::new(path)
}

pub fn connect(url: &str) -> rusqlite::Resultrusqlite::Connection {
let connection = rusqlite::Connection::open(database_path(url))?;
connection.pragma_update(None, "foreign_keys", "ON")?;
Ok(connection)
}

#[doc(hidden)]
pub fn map_rows<T, F>(
connection: &rusqlite::Connection,
sql: &str,
parameters: &[&dyn rusqlite::ToSql],
mut map: F,
) -> rusqlite::Result<Vec>
where
F: FnMut(&rusqlite::Row<'_>) -> rusqlite::Result,
{
let mut statement = connection.prepare(sql)?;
let rows = statement.query_map(parameters, |row| map(row))?;
rows.collect()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn accepts_urls_and_paths() {
    assert_eq!(database_path("sqlite://db/toy.db"), Path::new("db/toy.db"));
    assert_eq!(database_path("sqlite::memory:"), Path::new(":memory:"));
    assert_eq!(database_path("db/toy.db"), Path::new("db/toy.db"));
}

#[test]
fn enables_foreign_keys() {
    let connection = connect("sqlite::memory:").unwrap();
    let enabled: bool = connection
        .query_row("PRAGMA foreign_keys", [], |row| row.get(0))
        .unwrap();
    assert!(enabled);
}

}
#+end_src

#+name: macro-foundation
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
//! Um modelo deliberadamente pequeno, apenas para SQLite, das macros de consulta em tempo de compilação do SQLx.
//!
//! # A ideia central
//!
//! Uma macro procedural é executada enquanto a crate que a chamou está sendo compilada. Ela
//! recebe tokens Rust, pergunta ao SQLite o que uma string SQL significa e emite novos tokens Rust.
//! O Rust emitido então passa pelo compilador Rust normal.
//!
//! O pipeline é:
//!
//! 1. [syn] analisa a entrada da macro em estruturas de dados Rust.
//! 2. [rusqlite] prepara o SQL e retorna metadados de parâmetro/coluna.
//! 3. Esta crate valida o subconjunto de ensino deliberadamente pequeno.
//! 4. [quote!] constrói código Rust comum a partir desses metadados.
//! 5. rustc verifica os campos, tipos e literais de struct gerados.
//!
//! Este é um código de ensino, não um analisador SQL geral. Consultas tipadas aceitam intencionalmente
//! apenas colunas diretas de uma tabela SQLite real.

// Ferramentas da biblioteca padrão usadas para nomes, variáveis de ambiente, arquivos de cache e caminhos.
use std::{collections::HashSet, env, fs, path::PathBuf};

// proc_macro::TokenStream é o tipo de entrada e saída voltado para o compilador.
use proc_macro::TokenStream;
// proc_macro2 fornece tipos de token que são mais fáceis de construir e testar.
use proc_macro2::{Ident, Span, TokenStream as Tokens};
// quote! transforma sintaxe estilo Rust em tokens; format_ident! cria identificadores.
use quote::{format_ident, quote};
// SQLite é tanto o banco de dados em tempo de execução quanto nossa autoridade SQL em tempo de compilação.
use rusqlite::{Connection, OpenFlags};
// Descrições são serializadas para que a mesma evidência possa ser usada offline.
use serde::{Deserialize, Serialize};
// syn analisa literais, expressões, tipos, vírgulas e gramáticas de macro personalizadas.
#[rustfmt::skip]
use syn::{parse::{Parse, ParseStream}, parse_macro_input, punctuated::Punctuated, Expr, LitStr, Token, Type};
#+end_src

#+name: macro-entry-points
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// Exigir que a entrada da macro seja um literal de string, então emitir esse literal inalterado.
///
/// Esta primeira macro de ensino não entende SQL. Seu único trabalho é mostrar
/// que a verificação em tempo de compilação precisa de entrada em tempo de compilação: sql_literal!(variable)
/// falha porque variable não é um [LitStr].
#[proc_macro]
pub fn sql_literal(input: TokenStream) -> TokenStream {
// parse_macro_input! interrompe a expansão e emite um diagnóstico do compilador em caso de falha.
let sql = parse_macro_input!(input as LitStr);
// #sql interpola o literal analisado no fluxo de tokens construído por quote!.
quote!(#sql).into()
}

/// Converter nosso syn::Result interno no fluxo de tokens exigido pelo rustc.
///
/// Macros procedurais não podem retornar Result. Um syn::Error portanto se torna uma
/// invocação de compile_error! colocada no span de origem mais útil.
fn result(result: syn::Result) -> TokenStream {
result.unwrap_or_else(syn::Error::into_compile_error).into()
}
#+end_src

#+name: literal-demo
#+begin_src rust :tangle toy-demo/src/bin/01_literal_macro.rs :mkdirp yes :comments no
fn main() {
#[cfg(feature = "fail-literal")]
{
let sql = "SELECT 40 + 2";
let _ = toy_sqlx::sql_literal!(sql);
}
#[cfg(not(feature = "fail-literal"))]
println!("{}", toy_sqlx::sql_literal!("SELECT 40 + 2 AS answer"));
}
#+end_src

Use =cargo expand= para provar que a macro emite o literal inalterado. O primeiro
comando confirma que =cargo-expand= está instalado. Se estiver faltando, execute
=cargo install cargo-expand --locked= uma vez.

#+begin_src bash :results output

set -euo pipefail
cargo expand --version
cargo expand --color never -p toy-demo --bin 01_literal_macro 2>/dev/null |
grep -C 2 'SELECT 40 + 2 AS answer'

#+end_src

#+RESULTS:
: cargo-expand-expand 1.0.59 + prettyplease 0.2.10
: fn main() {
: {
: ::std::io::_print(format_args!("{0}\n", "SELECT 40 + 2 AS answer"));
: };
: }

A expansão útil é intencionalmente entediante:

#+begin_src rust :tangle no
"SELECT 40 + 2 AS answer"
#+end_src

  • Execute o passo e sua falha esperada:

#+begin_src bash :results output

./scripts/toy-demo.sh 1
./scripts/toy-demo.sh fail-literal

#+end_src

#+RESULTS:
: Compiling toy-demo v0.1.0 (/Users/chiefkemist/Documents/native_workspace/mini-sqlx-workshop/toy-demo)
: Finished dev profile [unoptimized + debuginfo] target(s) in 0.20s
: Running target/debug/01_literal_macro
: SELECT 40 + 2 AS answer
: fail-literal: failed as intended

  • Observe: um literal compila; uma =String= em tempo de execução é rejeitada pelo analisador de macro.
  • Ganhamos: a macro possui o texto SQL durante a compilação.
  • Ainda quebrado: possuir texto não prova um SQL válido. O Passo 2 pergunta ao sistema
    que realmente entende SQLite.
  • Passo 2: deixar o SQLite rejeitar SQL ruim durante a compilação
    :PROPERTIES:
    :MINUTES: 12
    :END:

#+begin_quote
Problema: a macro agora pode ver o SQL, mas ainda não consegue dizer se uma tabela
ou coluna existe.

Por que se importar: escrever um segundo analisador SQLite dentro da macro seria grande,
incompleto e menos confiável que o próprio SQLite.

Este passo: durante a expansão da macro, abra o banco de dados de compilação como somente leitura e peça ao
SQLite para preparar a consulta. Converta a resposta do SQLite em uma pequena =Description=
que passos posteriores podem reutilizar.
#+end_quote

Existem apenas três peças móveis: a descrição, o ponto de entrada da macro e
o carregador online. Neste passo, observe a chamada para =connection.prepare=. Os
outros campos de descrição tornam-se úteis nos Passos 3, 4 e 6.

#+name: description-ir
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
// Aumente isso sempre que o significado ou a forma dos metadados em cache mudar. Entradas de cache antigas
// então falham claramente em vez de serem interpretadas sob novas regras.
const CACHE_VERSION: u8 = 3;

/// A pequena representação intermediária compartilhada pela verificação online e offline.
///
/// Pense em Description como uma folha de fatos sobre uma string SQL:
///
/// - parameter_count diz quantos valores o SQLite espera;
/// - columns descreve o conjunto de resultados;
/// - source_is_table registra se nossa regra restrita de consulta tipada foi provada;
/// - version, database e sql protegem o carregamento de cache offline.
///
/// Manter uma representação é importante: a geração de código não precisa saber
/// se esses fatos vieram do SQLite ao vivo ou de um arquivo de cache.
#[rustfmt::skip]
#[derive(Debug, Serialize, Deserialize)]
struct Description { version: u8, database: String, sql: String, parameter_count: usize, source_is_table: bool, columns: Vec }

/// A evidência do SQLite necessária para gerar um campo de saída Rust.
///
/// declared_type é o tipo escrito em CREATE TABLE; não é o tipo de
/// cada valor que o SQLite poderia armazenar dinamicamente. nullable decide entre T
/// e Option<T> no Rust gerado.
#[rustfmt::skip]
#[derive(Debug, Serialize, Deserialize)]
struct Column { name: String, declared_type: Option, nullable: bool }
#+end_src

#+name: checked-sql-entry
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// Pedir ao SQLite para validar um literal SQL durante a expansão da macro.
///
/// O valor emitido ainda é apenas o literal de string original. A melhoria
/// é o tempo: SQL inválido torna-se um erro de compilação em vez de um erro de tempo de execução.
#[proc_macro]
pub fn checked_sql(input: TokenStream) -> TokenStream {
let sql = parse_macro_input!(input as LitStr);
// Carregar a descrição realiza a validação; esta macro descarta os fatos.
result(load_description(&sql).map(|_| quote!(#sql)))
}
#+end_src

#+name: online-description
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// Obter evidência de consulta de exatamente uma fonte.
///
/// O modo offline lê uma descrição salva anteriormente. O modo online pergunta ao SQLite
/// diretamente e opcionalmente salva a resposta para uma compilação offline posterior.
fn load_description(sql: &LitStr) -> syn::Result {
if flag("TOY_SQLX_OFFLINE") {
return load_cache(&sql.value(), sql.span());
}
let description = describe_online(&sql.value(), sql.span())?;
if flag("TOY_SQLX_PREPARE") {
save_cache(&description, sql.span())?;
}
Ok(description)
}

/// Reconhecer a forma SQL deliberadamente minúscula suportada por consultas tipadas.
///
/// Este é um guardião de escopo, não um analisador SQL. Ele retorna um nome de tabela apenas para uma
/// projeção de colunas diretas de uma tabela simples. Qualquer incerteza retorna
/// None, o que faz a macro tipada rejeitar a consulta em vez de inventar fatos.
#[rustfmt::skip]
fn direct_source(sql: &str) -> Option<&str> {
// Comentários poderiam esconder cláusulas FROM extras, então este brinquedo os rejeita completamente.
if ["--", "/", "/"].iter().any(|marker| sql.contains(marker)) { return None; }
// Tokenização por espaço em branco é suficiente porque cada forma aceita é simples.
let words = sql.split_whitespace().collect::<Vec<>>();
// Exatamente um FROM exclui subconsultas SELECT comuns e fontes ambíguas.
let from = words.iter().enumerate().filter(|(
, word)| word.eq_ignore_ascii_case("FROM")).map(|(index, )| index).collect::<Vec<>>();
let [from] = from.as_slice() else { return None };
// A primeira palavra após FROM deve ser um identificador de tabela não citado e não qualificado.
let table = *words.get(from + 1)?;
// Pare a cláusula de origem quando a filtragem/ordenação comum começar.
let tail = words[from + 2..].iter().take_while(|word| !matches!(word.to_ascii_uppercase().as_str(), "WHERE" | "GROUP" | "ORDER" | "LIMIT")).copied().collect::<Vec<>>();
let ident = |word: &str| word.chars().all(|c| c.is_ascii_alphanumeric() || c == '
');
// Cada campo selecionado deve ser column, table.column, ou qualquer forma com AS name.
let fields = words[1..*from].join(" ");
let direct_field = |field: &str| { let parts = field.split_whitespace().collect::<Vec<>>(); let path = parts.first()?.split('.').collect::<Vec<>>(); Some((path.len() == 1 || path.len() == 2) && path.iter().all(|part| ident(part)) && (parts.len() == 1 || (parts.len() == 3 && parts[1].eq_ignore_ascii_case("AS") && ident(parts[2])))) };
// Após a tabela, não permitir alias, alias, ou AS alias—nunca uma segunda fonte.
let alias = tail.is_empty() || (tail.len() == 1 && ident(tail[0])) || (tail.len() == 2 && tail[0].eq_ignore_ascii_case("AS") && ident(tail[1]));
(ident(table) && alias && fields.split(',').all(|field| direct_field(field) == Some(true))).then_some(table)
}

/// Pedir a um banco de dados SQLite ao vivo para descrever uma consulta durante a compilação.
///
/// Preparar valida sintaxe, nomes de tabela e nomes de coluna sem executar a
/// consulta. span aponta diagnósticos de volta para o literal SQL no código do chamador.
#[rustfmt::skip]
fn describe_online(sql: &str, span: Span) -> syn::Result {
// O Cargo executa esta função dentro do processo proc-macro, não no programa final.
let url = env::var("TOY_DATABASE_URL")
.map_err(|| syn::Error::new(span, "TOY_DATABASE_URL is required for online checking"))?;
// Aceitar o mesmo pequeno conjunto de formas de URL SQLite que a crate de tempo de execução.
let path = url
.strip_prefix("sqlite://")
.or_else(|| url.strip_prefix("sqlite:"))
.unwrap_or(&url);
// O modo somente leitura impede que uma compilação modifique o banco de dados de ensino.
let connection = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)
.map_err(|error| syn::Error::new(span, format!("cannot open SQLite: {error}")))?;
// Esta é a verificação chave em tempo de compilação: deixar o SQLite validar o SQL do SQLite.
let statement = connection.prepare(sql).map_err(|error| {
syn::Error::new(
span,
format!("SQLite rejected this query during compilation: {error}"),
)
})?;
// Copiar apenas os fatos de saída necessários pela geração de código Rust posterior.
let columns = (0..statement.column_count())
.map(|index| {
let name = statement.column_name(index)?.to_owned();
let metadata = statement.column_metadata(index)?;
// rusqlite expõe vários campos de origem; este brinquedo precisa de declaração e NOT NULL.
let (declared_type, nullable) = match metadata {
Some((
, _, _, declared, _, not_null, _, )) => (
declared.map(|value| value.to_string_lossy().into_owned()),
!not_null,
),
// Evidência de origem ausente nunca deve ser tratada como não nula.
None => (None, true),
};
Ok(Column { name, declared_type, nullable })
})
.collect::<rusqlite::Result<Vec<
>>>()
.map_err(|error| syn::Error::new(span, format!("cannot describe output: {error}")))?;
// O reconhecimento lexical não é suficiente: sqlite_schema deve confirmar uma tabela real, não uma view.
#[rustfmt::skip]
let source_is_table = direct_source(sql).is_some_and(|table| connection.query_row(
"SELECT type = 'table' FROM sqlite_schema WHERE name = ?1", [table], |row| row.get(0),
).unwrap_or(false));
// A própria instrução preparada fornece a contagem de placeholders autoritativa do SQLite.
Ok(Description {
version: CACHE_VERSION,
database: "SQLite".into(),
sql: sql.into(),
parameter_count: statement.parameter_count(),
source_is_table,
columns,
})
}
#+end_src

#+name: prepare-demo
#+begin_src rust :tangle toy-demo/src/bin/02_compile_time_prepare.rs :mkdirp yes :comments no
fn main() {
#[cfg(feature = "fail-invalid-sql")]
let sql = toy_sqlx::checked_sql!("SELECT id, definitely_missing FROM users");
#[cfg(not(feature = "fail-invalid-sql"))]
let sql = toy_sqlx::checked_sql!("SELECT id, email FROM users");
println!("SQLite accepted during compilation: {sql}");
}
#+end_src

  • Execute o passo e sua falha esperada:

#+begin_src bash :results output

./scripts/toy-demo.sh 2
./scripts/toy-demo.sh fail-sql

#+end_src

  • Observe: =definitely_missing= agora é um erro de compilador.
  • Ganhamos: o SQLite aceitou a consulta contra o banco de dados de compilação.
  • Limite: o banco de dados de tempo de execução ainda pode ter um esquema diferente.
  • Ainda quebrado: consultas válidas ainda retornam linhas não tipadas. O Passo 3
    verifica placeholders contra argumentos.
  • Passo 3: rejeitar o número errado de argumentos
    :PROPERTIES:
    :MINUTES: 8
    :END:

#+begin_quote
Problema: uma consulta pode ser válida enquanto seus placeholders =?1=, =?2= não correspondem ao
número de valores fornecidos pelo Rust.

Por que se importar: essa incompatibilidade se torna outra falha de preparação/vinculação em tempo de execução.

Este passo: comparar a contagem de parâmetros do SQLite com o número de argumentos da macro.
Manter as expressões Rust visíveis para o rustc sem avaliá-las.
#+end_quote

Isso é deliberadamente apenas aridade. O SQLite não fornece tipos de vinculação estáticos.

#+name: checked-input
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// Entrada analisada para checked_query!("SQL", arg1, arg2, ...).
///
/// Punctuated é a representação do syn de zero ou mais expressões separadas por
/// vírgulas. Manter expressões como sintaxe nos permite contá-las sem executá-las.
#[rustfmt::skip]
struct CheckedInput { sql: LitStr, args: Punctuated<Expr, Token![,]> }

/// Ensinar ao syn a pequena gramática aceita por checked_query!.
impl Parse for CheckedInput {
fn parse(input: ParseStream<'_>) -> syn::Result {
// O primeiro token deve ser o literal de string SQL.
let sql = input.parse()?;
let args = if input.is_empty() {
// Uma consulta sem placeholders não precisa de vírgula e nem de argumentos.
Punctuated::new()
} else {
// Caso contrário, consuma a vírgula após o SQL, então todas as expressões separadas por vírgula.
input.parse::<Token![,]>()?;
Punctuated::parse_terminated(input)?
};
Ok(Self { sql, args })
}
}
#+end_src

#+name: checked-query-entry
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// Validar SQL mais aridade de parâmetro, mas não executar a consulta.
///
/// Esta macro de transição isola uma lição: o SQLite pode nos dizer quantos
/// parâmetros ele espera, mas não tipos de parâmetro SQLite estáticos.
#[proc_macro]
pub fn checked_query(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as CheckedInput);
result(expand_checked(input))
}
#+end_src

#+name: checked-query-expansion
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// Implementar checked_query! após seus tokens terem sido analisados.
fn expand_checked(input: CheckedInput) -> syn::Result {
let description = load_description(&input.sql)?;
validate_arity(input.args.len(), &description, input.sql.span())?;
let CheckedInput { sql, args } = input;
let args = args.into_iter().collect::<Vec<_>>();
Ok(quote! {{
// Este ramo nunca é executado, mas o rustc ainda resolve e verifica o tipo de cada expressão.
// #(...)* é a sintaxe de repetição do quote: emita o corpo uma vez por argumento.
if false {
#(let _ = &(#args);)*
}
// O valor de tempo de execução da macro permanece a string SQL original.
#sql
}})
}
#+end_src

Expanda a demonstração para ver o argumento não avaliado retido dentro de =if false=:

#+begin_src bash :results output

set -euo pipefail
./scripts/toy-demo.sh setup >/dev/null
export TOY_DATABASE_URL="sqlite://$(pwd)/toy-demo/toy.db"
cargo expand --color never -p toy-demo --bin 03_parameter_arity 2>/dev/null |
grep -A 4 'if false'

#+end_src

#+RESULTS:
: if false {
: let _ = &(deliberately_wrong_type);
: }
: "SELECT id FROM users WHERE active = ?1"
: };

#+name: arity-demo
#+begin_src rust :tangle toy-demo/src/bin/03_parameter_arity.rs :mkdirp yes :comments no
fn main() {
let deliberately_wrong_type = "SQLite accepts dynamic values";
#[cfg(feature = "fail-arity")]
let sql = toy_sqlx::checked_query!(
"SELECT id FROM users WHERE active = ?1 AND id >= ?2",
deliberately_wrong_type,
);
#[cfg(not(feature = "fail-arity"))]
let sql = toy_sqlx::checked_query!(
"SELECT id FROM users WHERE active = ?1",
deliberately_wrong_type,
);
println!("arity checked, parameter type deliberately unchecked: {sql}");
}
#+end_src

  • Execute o passo e sua falha esperada:

#+begin_src bash :results output

./scripts/toy-demo.sh 3
./scripts/toy-demo.sh fail-arity

#+end_src

  • Observe: um valor para dois placeholders falha; um valor para um placeholder
    passa—mesmo quando esse valor é deliberadamente uma string inapropriada.
  • Ganhamos: a contagem de vinculação corresponde.
  • Limite: nenhuma reivindicação de tipo de vinculação SQLite estática é feita.
  • Ainda quebrado: consultas bem-sucedidas ainda retornam linhas não tipadas. O Passo 4 gera
    a forma de saída Rust e o decodificador.
  • Passo 4: gerar um registro tipado e decodificador
    :PROPERTIES:
    :MINUTES: 17
    :END:

#+begin_quote
Problema: mesmo SQL verificado ainda requer chamadas repetitivas =row.get(0)=,
=row.get(1)= e tipos Rust escritos à mão.

Por que se importar: a decodificação posicional é frágil, e a forma do resultado SQL é duplicada
no Rust manualmente.

Este passo: transformar nomes de coluna e tipos declarados em =Description= em um =Record= local,
então gerar o decodificador posicional que o constrói.
#+end_quote

O mapeamento de tipo declarado é intencionalmente visível e minúsculo:

| A declaração SQLite contém | Tipo base Rust gerado |
|---+---|
| =BOOL= ou =BOOLEAN= | =bool= |
| =INT= | =i64= |
| =REAL=, =FLOA=, ou =DOUB= | =f64= |
| =CHAR=, =CLOB=, ou =TEXT= | =String= |
| =BLOB= | =Vec= |
| qualquer outra coisa | erro em tempo de compilação |

O Passo 6 explica quando um tipo base permanece =T= e quando a anulabilidade o envolve em
=Option=.

#+name: query-input
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// Entrada analisada para query!(&connection, "SQL", arg1, arg2, ...).
///
/// A conexão e cada argumento são expressões Rust completas. Eles são mantidos como
/// sintaxe até que a expansão emita código que avalia cada um exatamente uma vez.
#[rustfmt::skip]
struct QueryInput { connection: Expr, sql: LitStr, args: Punctuated<Expr, Token![,]> }

/// Analisar a conexão primeiro, então reutilizar CheckedInput para SQL e argumentos.
#[rustfmt::skip]
impl Parse for QueryInput {
fn parse(input: ParseStream<'_>) -> syn::Result {
let connection = input.parse()?; input.parse::<Token![,]>()?;
let checked = input.parse::()?;
Ok(Self { connection, sql: checked.sql, args: checked.args })
}
}
#+end_src

#+name: query-entry
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// Validar e executar uma consulta cujo registro de saída é gerado pela macro.
///
/// O tipo local gerado tem um campo Rust por coluna SQL selecionada.
#[proc_macro]
pub fn query(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as QueryInput);
result(expand_query(input, Output::Generated))
}
#+end_src

#+name: typed-query-expansion
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// Escolher qual valor Rust o decodificador de linha constrói.
enum Output {
/// query! nos pede para definir um tipo Record local.
Generated,
/// query_as! nos dá um tipo de propriedade da aplicação para construir.
Given(Box),
}

/// Uma coluna de saída SQL verificada traduzida em um nome de campo Rust e tokens de tipo.
#[rustfmt::skip]
struct RustColumn { ident: Ident, ty: Tokens }

/// Construir o programa Rust emitido por query! e query_as!.
///
/// Tudo antes do quote! final é executado durante a compilação. O código dentro
/// desse quote! é o que o programa do chamador executará em tempo de execução.
#[rustfmt::skip]
fn expand_query(input: QueryInput, output: Output) -> syn::Result {
// Fase de tempo de compilação: reunir evidências e rejeitar entrada não suportada.
let description = load_description(&input.sql)?;
validate_arity(input.args.len(), &description, input.sql.span())?;
let columns = typed_columns(&description, input.sql.span())?;

// Transformar cada coluna verificada em tokens `field_name: RustType`.
let fields = columns.iter().map(|column| {
    let ident = &column.ident;
    let ty = &column.ty;
    quote!(#ident: #ty)
});
// Transformar cada coluna verificada em `field_name: row.get::<_, RustType>(index)?`.
let values: Vec<_> = columns
    .iter()
    .enumerate()
    .map(|(index, column)| {
        let ident = &column.ident;
        let ty = &column.ty;
        quote!(#ident: __toy_row.get::<usize, #ty>(#index)?)
    })
    .collect();

// Ambas as macros públicas compartilham um decodificador; apenas seu construtor final difere.
let (definition, expression) = match output {
    Output::Generated => (
        quote!(#[derive(Debug)] struct Record { #(#fields),* }),
        quote!(Record { #(#values),* }),
    ),
    Output::Given(output) => (Tokens::new(), quote!(#output { #(#values),* })),
};

let QueryInput { connection, sql, args } = input;
// Nomes locais gerados nos permitem avaliar cada expressão do chamador exatamente uma vez.
let names: Vec<_> = (0..args.len())
    .map(|index| format_ident!("__toy_arg_{index}"))
    .collect();
let args = args.into_iter().collect::<Vec<_>>();

// Fase de tempo de execução: todo este bloco é inserido no local da chamada da macro.
Ok(quote! {{
    #definition
    // Lados direitos de tupla são avaliados antes que os nomes gerados sejam vinculados.
    let (__toy_connection, #(#names,)*) = (#connection, #(&(#args),)*);
    // rusqlite aceita uma fatia de referências a valores que implementam `ToSql`.
    let __toy_parameters: &[&dyn ::toy_sqlx::rusqlite::ToSql] = &[#(#names),*];
    ::toy_sqlx::map_rows(
        __toy_connection,
        #sql,
        __toy_parameters,
        // Decodificar uma linha SQLite na struct gerada ou fornecida pelo chamador.
        |__toy_row| ::core::result::Result::Ok(#expression),
    )
}})

}

/// Comparar o número de argumentos Rust com a contagem de placeholders do SQLite.
///
/// Isso verifica intencionalmente apenas quantos valores existem. O SQLite não dá
/// a este brinquedo tipos de parâmetro estáticos estáveis, então reivindicar mais seria enganoso.
fn validate_arity(got: usize, description: &Description, span: Span) -> syn::Result<()> {
if got == description.parameter_count {
Ok(())
} else {
let expected = description.parameter_count;
Err(syn::Error::new(
span,
format!("SQLite expects {expected} parameter(s), but the macro received {got}"),
))
}
}

/// Transformar colunas SQLite em nomes de campo e tipos Rust seguros.
///
/// A função primeiro impõe o limite de evidência, então verifica nomes, rejeita
/// campos duplicados, mapeia declarações SQLite e aplica anulabilidade.
fn typed_columns(description: &Description, span: Span) -> syn::Result<Vec> {
ensure_typed_shape(&description.sql, span)?;
if !description.source_is_table {
return Err(syn::Error::new(
span,
"typed query must select direct columns from one table; expressions, views, and subqueries are out of scope",
));
}
if description.columns.is_empty() {
return Err(syn::Error::new(span, "typed query must return columns"));
}
let mut names = HashSet::new();
description
.columns
.iter()
.map(|column| {
// Um nome de saída SQL deve ser utilizável em um literal de struct Rust gerado.
let ident = rust_ident(&column.name, span)?;
if !names.insert(ident.to_string()) {
return Err(syn::Error::new(span, "duplicate output field name"));
}
// Expressões frequentemente não têm declaração de tabela; saída tipada as rejeita.
let declared = column.declared_type.as_deref().ok_or_else(|| {
let name = &column.name;
syn::Error::new(span, format!("SQLite has no declared type for {name:?}; typed queries only support direct table columns"))
})?;
let base = rust_type(declared, span)?;
// Rust representa um valor SQL possivelmente NULL como Option<T>.
let ty = if column.nullable {
quote!(::core::option::Option<#base>)
} else {
base
};
Ok(RustColumn { ident, ty })
})
.collect()
}

/// Rejeitar formas SQL amplas antes de gerar Rust tipado.
///
/// Joins e consultas compostas precisam de análise de anulabilidade que este workshop não
/// implementa. A rejeição conservadora mantém a pequena garantia honesta.
fn ensure_typed_shape(sql: &str, span: Span) -> syn::Result<()> {
let sql = sql.trim_start().to_ascii_uppercase();
let words =
|| sql.split(|character: char| !character.is_ascii_alphanumeric() && character != '_');
let unsupported = !sql.starts_with("SELECT ")
|| words().filter(|word| *word == "SELECT").count() != 1
|| words().any(|word| matches!(word, "JOIN" | "UNION" | "INTERSECT" | "EXCEPT"));
if unsupported {
Err(syn::Error::new(
span,
"typed queries support one direct-table SELECT; joins and compound queries are out of scope",
))
} else {
Ok(())
}
}

/// Converter um nome de saída SQLite em um identificador de campo Rust.
///
/// A forma bruta (r#type, por exemplo) permite palavras-chave Rust quando legal. O
/// fallback lida com identificadores comuns em versões de compilador com comportamento de
/// análise de identificador bruto diferente.
fn rust_ident(name: &str, span: Span) -> syn::Result {
syn::parse_str(&format!("r#{name}"))
.or_else(|| syn::parse_str(name))
.map_err(|
| syn::Error::new(span, format!("{name:?} is not a Rust field name")))
}

/// Mapear um pequeno conjunto de tipos declarados SQLite para tokens de tipo Rust.
///
/// O SQLite usa afinidade de tipo e permite valores armazenados dinâmicos. Esta tabela é um
/// subconjunto de ensino, não uma reivindicação de que cada valor armazenado deve ter este tipo Rust.
///
/// | A declaração contém | Tipo base gerado |
/// | --- | --- |
/// | BOOL ou BOOLEAN | bool |
/// | INT | i64 |
/// | REAL, FLOA, ou DOUB | f64 |
/// | CHAR, CLOB, ou TEXT | String |
/// | BLOB | Vec<u8> |
/// | qualquer outra coisa | erro de compilador |
fn rust_type(declared: &str, span: Span) -> syn::Result {
let name = declared.trim().to_ascii_uppercase();
if matches!(name.as_str(), "BOOL" | "BOOLEAN") {
Ok(quote!(bool))
} else if name.contains("INT") {
Ok(quote!(i64))
} else if name.contains("REAL") || name.contains("FLOA") || name.contains("DOUB") {
Ok(quote!(f64))
} else if name.contains("CHAR") || name.contains("CLOB") || name.contains("TEXT") {
Ok(quote!(::std::string::String))
} else if name.contains("BLOB") {
Ok(quote!(::std::vec::Vec))
} else {
Err(syn::Error::new(
span,
format!("no toy Rust mapping for SQLite declaration {declared:?}"),
))
}
}
#+end_src

#+name: typed-record-demo
#+begin_src rust :tangle toy-demo/src/bin/04_typed_record.rs :mkdirp yes :comments no
fn main() -> Result<(), toy_sqlx::rusqlite::Error> {
let url = std::env::var("TOY_DATABASE_URL").expect("TOY_DATABASE_URL is required");
let connection = toy_sqlx::connect(&url)?;
let users = toy_sqlx::query!(
&connection,
"SELECT id, email, display_name FROM users WHERE active = ?1 ORDER BY id",
true,
)?;
for user in users {
println!("{} {} {:?}", user.id, user.email, user.display_name);
}
Ok(())
}
#+end_src

Expanda a demonstração e selecione o =Record= gerado mais a chamada de mapeamento de linha em tempo de execução:

#+begin_src bash :results output

set -euo pipefail
./scripts/toy-demo.sh setup >/dev/null
export TOY_DATABASE_URL="sqlite://$(pwd)/toy-demo/toy.db"
cargo expand --color never -p toy-demo --bin 04_typed_record 2>/dev/null |
grep -E -A 12 'struct Record|::toy_sqlx::map_rows'

#+end_src

#+RESULTS:
#+begin_example
struct Record {
id: i64,
email: ::std::string::String,
display_name: ::core::option::Option<::std::string::String>,
}
#[automatically_derived]
impl ::core::fmt::Debug for Record {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field3_finish(
f,
"Record",
"id",

    ::toy_sqlx::map_rows(
        __toy_connection,
        "SELECT id, email, display_name FROM users WHERE active = ?1 ORDER BY id",
        __toy_parameters,
        |__toy_row| ::core::result::Result::Ok(Record {
            id: __toy_row.get::<usize, i64>(0usize)?,
            email: __toy_row.get::<usize, ::std::string::String>(1usize)?,
            display_name: __toy_row
                .get::<usize, ::core::option::Option<::std::string::String>>(2usize)?,
        }),
    )
}?;
for user in users {

#+end_example

A forma gerada importante é:

#+begin_src rust :tangle no
struct Record {
id: i64,
email: String,
display_name: Option,
}
map_rows(connection, sql, parameters, |row| Ok(Record {
id: row.get::<usize, i64>(0)?,
email: row.get::<usize, String>(1)?,
display_name: row.get::<usize, Option>(2)?,
}))
#+end_src

  • Execute o passo:

#+begin_src bash :results output

./scripts/toy-demo.sh 4

#+end_src

  • Observe: o loop usa =user.id= e =user.email=; nenhuma chamada =row.get=
    escrita à mão permanece no código da aplicação.
  • Ganhamos: nomes de saída e declarações suportadas tornam-se campos Rust.
  • Pergunta estacionada para o Passo 6: que evidência justifica =String= para =email= mas
    =Option= para =display_name=?
  • Ainda quebrado a seguir: =Record= é local à expansão da macro. O Passo 5 mapeia a
    mesma evidência em uma struct de propriedade da aplicação.
  • Passo 5: popular uma struct de propriedade da aplicação
    :PROPERTIES:
    :MINUTES: 10
    :END:

#+begin_quote
Problema: o =Record= local gerado é conveniente dentro de uma expressão, mas
aplicações já têm tipos de domínio nomeados usados em funções e módulos.

Por que se importar: resultados de consulta devem se ajustar a esses tipos sem adicionar um segundo decodificador manual.

Este passo: gerar um literal comum da struct do chamador. Deixe o rustc—não
a macro—verificar os nomes dos campos e os tipos Rust.
#+end_quote

=query_as!= reutiliza a mesma expansão e altera apenas o construtor final. Ele
não inspeciona ou reflete sobre a definição da struct.

#+name: query-as-input
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// Entrada analisada para query_as!(OutputType, &connection, "SQL", args...).
#[rustfmt::skip]
struct QueryAsInput { output: Type, query: QueryInput }

/// Analisar o tipo Rust do chamador, então reutilizar a gramática completa de query!.
#[rustfmt::skip]
impl Parse for QueryAsInput {
fn parse(input: ParseStream<'_>) -> syn::Result {
let output = input.parse()?; input.parse::<Token![,]>()?;
Ok(Self { output, query: input.parse()? })
}
}
#+end_src

#+name: query-as-entry
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// Validar e executar uma consulta em uma struct Rust fornecida pelo chamador.
///
/// A macro emite um literal de struct comum. O rustc portanto relata campos ausentes,
/// extras ou tipados incorretamente sem maquinaria de reflexão personalizada.
#[proc_macro]
#[rustfmt::skip]
pub fn query_as(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as QueryAsInput);
result(expand_query(input.query, Output::Given(Box::new(input.output))))
}
#+end_src

#+name: query-as-demo
#+begin_src rust :tangle toy-demo/src/bin/05_query_as.rs :mkdirp yes :comments no
struct UserSummary {
id: i64,
email: String,
}

#[cfg(feature = "fail-query-as-name")]
struct WrongName {
id: i64,
address: String,
}

#[cfg(feature = "fail-query-as-type")]
struct WrongType {
id: i32,
email: String,
}

fn main() -> Result<(), toy_sqlx::rusqlite::Error> {
let url = std::env::var("TOY_DATABASE_URL").expect("TOY_DATABASE_URL is required");
let connection = toy_sqlx::connect(&url)?;
#[cfg(feature = "fail-query-as-name")]
let _ = toy_sqlx::query_as!(
WrongName,
&connection,
"SELECT id, email FROM users ORDER BY id"
)?;
#[cfg(feature = "fail-query-as-type")]
let _ = toy_sqlx::query_as!(
WrongType,
&connection,
"SELECT id, email FROM users ORDER BY id"
)?;
#[cfg(not(any(feature = "fail-query-as-name", feature = "fail-query-as-type")))]
for user in toy_sqlx::query_as!(
UserSummary,
&connection,
"SELECT id, email FROM users ORDER BY id"
)? {
println!("{} {}", user.id, user.email);
}
Ok(())
}
#+end_src

Expanda =query_as!= para ver que ele reutiliza =map_rows= mas constrói
=UserSummary= em vez de definir =Record=:

#+begin_src bash :results output

set -euo pipefail
./scripts/toy-demo.sh setup >/dev/null
export TOY_DATABASE_URL="sqlite://$(pwd)/toy-demo/toy.db"
cargo expand --color never -p toy-demo --bin 05_query_as 2>/dev/null |
grep -A 12 '::toy_sqlx::map_rows'

#+end_src

  • Observe: =UserSummary= correto compila; um campo errado ou tipo de campo produz
    um erro rustc comum.
  • Ganhamos: linhas verificadas podem entrar em tipos de propriedade da aplicação sem um segundo
    sistema de decodificação.
  • Ainda quebrado: o Rust gerado só é confiável se os metadados SQL o justificarem.
    O Passo 6 define o limite de evidência honesto.
  • Passo 6: tornar a reivindicação de tipo honesta
    :PROPERTIES:
    :MINUTES: 8
    :END:

#+begin_quote
Problema: gerar =String= para um valor que pode realmente ser =NULL= torna um
recurso de tempo de compilação confiantemente errado.

Por que se importar: joins, views, subconsultas e expressões podem alterar a anulabilidade mesmo
quando uma coluna de tabela subjacente diz =NOT NULL=.

Este passo: aceitar saída tipada apenas para colunas diretas de uma tabela real. Ler
o esquema dessa tabela: =NOT NULL= torna-se =T=; tudo o que é anulável torna-se
=Option=. Rejeitar formas cujas evidências não podemos justificar.
#+end_quote

Este não é um pequeno analisador SQL fingindo entender cada consulta. A rejeição é
o mecanismo de correção.

#+name: nullability-demo
#+begin_src rust :tangle toy-demo/src/bin/06_nullability.rs :mkdirp yes :comments no
fn main() -> Result<(), toy_sqlx::rusqlite::Error> {
let url = std::env::var("TOY_DATABASE_URL").expect("TOY_DATABASE_URL is required");
let connection = toy_sqlx::connect(&url)?;
#[cfg(feature = "fail-expression-metadata")]
let _ = toy_sqlx::query!(&connection, "SELECT COUNT(*) AS user_count FROM users")?;
#[cfg(feature = "fail-unsupported-shape")]
let _ = toy_sqlx::query!(
&connection,
"SELECT a.id FROM users AS a JOIN users AS b ON a.id = b.id",
)?;
#[cfg(feature = "fail-view-source")]
let _ = toy_sqlx::query!(&connection, "SELECT bid FROM hidden_join")?;
#[cfg(feature = "fail-subquery-source")]
let _ = toy_sqlx::query!(
&connection,
"SELECT (VALUES(NULL),(email)) AS email FROM users"
)?;
#[cfg(not(any(
feature = "fail-expression-metadata",
feature = "fail-unsupported-shape",
feature = "fail-view-source",
feature = "fail-subquery-source"
)))]
for user in toy_sqlx::query!(
&connection,
"SELECT email, display_name FROM users ORDER BY id"
)? {
println!("{} {:?}", user.email, user.display_name);
}
Ok(())
}
#+end_src

  • Execute o passo e suas falhas de limite de evidência:

#+begin_src bash :results output

./scripts/toy-demo.sh 6
./scripts/toy-demo.sh fail-expression
./scripts/toy-demo.sh fail-shape
./scripts/toy-demo.sh fail-view
./scripts/toy-demo.sh fail-subquery

#+end_src

  • Observe: =email= direto é =String=; =display_name= anulável é
    =Option=; formas não suportadas falham durante a compilação.
  • Ganhamos: uma reivindicação de consulta tipada restrita apoiada por evidência de esquema direta.
  • Ainda quebrado: a expansão online precisa do banco de dados toda vez. O Passo 7 torna
    a evidência portátil.
  • Passo 7: compilar sem o banco de dados de compilação
    :PROPERTIES:
    :MINUTES: 7
    :END:

#+begin_quote
Problema: a verificação em tempo de compilação agora depende da abertura do banco de dados de compilação.

Por que se importar: desenvolvedores e CI podem precisar de compilações reproduzíveis onde esse banco de dados está
indisponível.

Este passo: salvar a =Description= já validada como JSON, então carregar essa mesma
forma no modo offline. Manter cada etapa de validação e geração de código após o
carregador inalterada.
#+end_quote

O cache é deliberadamente visível: um pequeno hash escolhe o nome do arquivo, enquanto
versão, backend e SQL exato são verificados antes que a evidência em cache seja confiável.

#+name: offline-cache
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// Ler as flags de ambiente Boolean do workshop.
fn flag(name: &str) -> bool {
env::var(name)
.ok()
.is_some_and(|value| matches!(value.as_str(), "1" | "true" | "TRUE"))
}

/// Produzir um nome de arquivo de cache curto e determinístico a partir do texto SQL exato.
///
/// FNV-1a é usado porque seu loop é fácil de ensinar. Um cache de produção
/// normalmente usaria um hash resistente a colisões e garantias de concorrência mais fortes.
fn hash(sql: &str) -> u64 {
sql.as_bytes()
.iter()
.fold(0xcbf29ce484222325, |hash, byte| {
(hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3)
})
}

/// Localizar o arquivo de cache desta consulta dentro da crate que invocou a macro.
///
/// CARGO_MANIFEST_DIR pertence ao chamador, então cada crate obtém seu próprio
/// diretório .toy-sqlx em vez de compartilhar o diretório da crate proc-macro.
fn cache_path(sql: &str, span: Span) -> syn::Result {
let manifest = env::var_os("CARGO_MANIFEST_DIR")
.ok_or_else(|| syn::Error::new(span, "Cargo did not provide CARGO_MANIFEST_DIR"))?;
Ok(PathBuf::from(manifest)
.join(".toy-sqlx")
.join(format!("query-{:016x}.json", hash(sql))))
}

/// Serializar uma descrição SQLite ao vivo como JSON legível para compilação offline.
fn save_cache(description: &Description, span: Span) -> syn::Result<()> {
let path = cache_path(&description.sql, span)?;
fs::create_dir_all(path.parent().unwrap())
.and_then(|_| fs::write(&path, serde_json::to_vec_pretty(description).unwrap()))
.map_err(|error| syn::Error::new(span, format!("cannot write {}: {error}", path.display())))
}

/// Carregar evidência em cache e rejeitar qualquer coisa que não corresponda a esta consulta.
///
/// Verificar versão, backend e SQL exato impede que uma entrada de cache criada sob
/// premissas diferentes conduza silenciosamente a geração de código Rust.
fn load_cache(sql: &str, span: Span) -> syn::Result {
let path = cache_path(sql, span)?;
let bytes = fs::read(&path).map_err(|error| {
syn::Error::new(
span,
format!("offline metadata missing at {}: {error}", path.display()),
)
})?;
let description: Description = serde_json::from_slice(&bytes)
.map_err(|error| syn::Error::new(span, format!("invalid offline metadata: {error}")))?;
// Todas as três verificações fazem parte do limite de confiança do cache.
if description.version != CACHE_VERSION
|| description.database != "SQLite"
|| description.sql != sql
{
Err(syn::Error::new(
span,
"offline metadata does not match this SQLite query",
))
} else {
Ok(description)
}
}
#+end_src

  • Execute a demonstração online, então prove a compilação offline:

#+begin_src bash :results output

./scripts/toy-demo.sh 7
./scripts/toy-demo.sh offline

#+end_src

  • Observe: preparar metadados online, remover o banco de dados, forçar expansão e
    compilar com sucesso a partir do cache.
  • Ganhamos: a geração de código consome uma =Description= independentemente de se
    sua evidência veio do SQLite ao vivo ou de um cache verificado.
  • Limite: frescor do cache, resistência a colisões, gravações atômicas e escritores
    concorrentes são preocupações de produção. O brinquedo verifica metadados ausentes, malformados,
    versão errada, backend errado e SQL errado.

** Verificação emaranhada e corredor de apresentação :noexport:

A história principal termina acima. Os blocos restantes automatizam testes, demonstrações,
falhas esperadas e verificações de tamanho; eles não são etapas de ensino adicionais.

#+name: focused-macro-tests
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
// Estes testes visam as pequenas políticas que são mais fáceis de quebrar ao editar
// o workshop: mapeamento de tipo, rejeição de forma de consulta, nomes de campo e hashes de cache.
#[cfg(test)]
mod tests {
use super::*;

#[test]
#[rustfmt::skip]
fn maps_the_teaching_types() {
    // Compare tokens emitidos como texto porque esses auxiliares geram código, não valores.
    assert_eq!(rust_type("INTEGER", Span::call_site()).unwrap().to_string(), "i64");
    assert_eq!(
        rust_type("TEXT", Span::call_site()).unwrap().to_string(),
        ":: std :: string :: String"
    );
    assert!(rust_type("NUMERIC", Span::call_site()).is_err());
}

#[test]
#[rustfmt::skip]
fn rejects_unsupported_sources_and_shapes() {
    // Cada forma incerta deve falhar fechada em vez de gerar um tipo não sólido.
    assert!(ensure_typed_shape("SELECT * FROM a LEFT JOIN b", Span::call_site()).is_err());
    assert!(ensure_typed_shape("DELETE FROM a RETURNING id", Span::call_site()).is_err());
    assert!(ensure_typed_shape("SELECT (SELECT id FROM a) FROM b", Span::call_site()).is_err());
    assert_eq!(direct_source("SELECT id FROM users"), Some("users"));
    assert_eq!(direct_source("SELECT id FROM users AS u WHERE u.id > 0"), Some("users"));
    assert_eq!(direct_source("SELECT bid /* FROM users */ FROM hidden_join"), None);
    assert_eq!(direct_source("SELECT u.id FROM users u , hidden_join v"), None);
    assert_eq!(direct_source("SELECT (VALUES(NULL),(email)) AS email FROM users"), None);
}

#[test]
#[rustfmt::skip]
fn rejects_bad_and_duplicate_fields() {
    // Nomes SQL inválidos ou repetidos não podem formar uma struct Rust válida.
    assert!(rust_ident("bad name", Span::call_site()).is_err());
    let column = || Column { name: "id".into(), declared_type: Some("INTEGER".into()), nullable: false };
    let description = Description { version: CACHE_VERSION, database: "SQLite".into(), sql: "SELECT id, id FROM users".into(), parameter_count: 0, source_is_table: true, columns: vec![column(), column()] };
    assert!(typed_columns(&description, Span::call_site()).is_err());
}

#[test]
fn hash_is_stable() {
    // Alterar este valor moveria cada arquivo de cache e requer uma decisão explícita.
    assert_eq!(hash("SELECT 1"), 0x199e7bca63ea84f2);
}

}
#+end_src

#+name: offline-demo
#+begin_src rust :tangle toy-demo/src/bin/07_offline_metadata.rs :mkdirp yes :comments no
fn main() -> Result<(), toy_sqlx::rusqlite::Error> {
let url = std::env::var("TOY_DATABASE_URL").expect("TOY_DATABASE_URL is required");
let connection = toy_sqlx::connect(&url)?;
let users = toy_sqlx::query!(&connection, "SELECT id, email FROM users ORDER BY id")?;
println!(
"decoded {} users using a compile-time description",
users.len()
);
Ok(())
}
#+end_src

#+name: presentation-runner
#+begin_src bash :tangle scripts/toy-demo.sh :mkdirp yes :comments no :tangle-mode (identity #o755)
#!/usr/bin/env bash
set -euo pipefail

root="(cd -- "(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$root"
export TOY_DATABASE_URL="{TOY_DATABASE_URL:-sqlite://root/toy-demo/toy.db}"

database_path() {
case "$1" in
sqlite://) printf '%s\n' "${1#sqlite://}" ;;
sqlite:
) printf '%s\n' "${1#sqlite:}" ;;
*) printf '%s\n' "$1" ;;
esac
}

setup() {
cargo run --quiet -p toy-demo --bin setup
}

run_stage() {
local bin="0{1}_2" pattern="$3" output
setup >/dev/null
output="(cargo run -p toy-demo --bin "bin" 2>&1)"
printf '%s\n' "$output"
grep -Eq "pattern" <<<output
}

expect_failure() {
local label="1" bin="2" feature="3" pattern="4" setup_first="${5:-yes}" log
[[ "$setup_first" == yes ]] && setup >/dev/null
log="$(mktemp)"
if cargo check -p toy-demo --bin "bin" --features "feature" >"$log" 2>&1; then
cat "$log"
rm -f "$log"
echo "$label unexpectedly compiled" >&2
return 1
fi
if ! grep -Eq "pattern" "log"; then
cat "$log"
rm -f "$log"
echo "$label failed without the expected diagnostic" >&2
return 1
fi
rm -f "$log"
echo "$label: failed as intended"
}

create_hidden_view() {
local database
database="(database_path "TOY_DATABASE_URL")"
python3 - "$database" <<'PY'
import sqlite3, sys
connection = sqlite3.connect(sys.argv[1])
connection.execute("CREATE VIEW hidden_join AS SELECT b.id AS bid FROM users a LEFT JOIN users b ON b.id = -1")
connection.commit()
PY
}

offline_failure() {
local pattern="$1" log
log="$(mktemp)"
cargo clean -p toy-demo >/dev/null
if TOY_SQLX_OFFLINE=1 cargo check -p toy-demo --bin 07_offline_metadata >"$log" 2>&1; then
cat "$log"
rm -f "$log"
echo "invalid offline metadata unexpectedly compiled" >&2
return 1
fi
grep -Eq "pattern" "log" || { cat "log"; rm -f "log"; return 1; }
rm -f "$log"
}

offline() {
local database backup cache saved
setup >/dev/null
rm -rf toy-demo/.toy-sqlx
cargo clean -p toy-demo >/dev/null
TOY_SQLX_PREPARE=1 cargo check -p toy-demo --bin 07_offline_metadata
database="(database_path "TOY_DATABASE_URL")"
backup="${database}.offline-backup"
cache="$(find toy-demo/.toy-sqlx -name 'query-*.json' -print -quit)"
saved="${cache}.saved"
cp "cache" "saved"
cleanup() {
[[ -e "backup" ]] && mv "backup" "$database"
[[ -e "saved" ]] && mv -f "saved" "$cache"
}
trap cleanup EXIT
rm -f "$backup"
mv "database" "backup"
cargo clean -p toy-demo >/dev/null
TOY_SQLX_OFFLINE=1 cargo check -p toy-demo --bin 07_offline_metadata

rm "$cache"
offline_failure 'offline metadata missing'
cp "saved" "cache"
printf '{' >"$cache"
offline_failure 'invalid offline metadata'
for field in version database sql; do
cp "saved" "cache"
python3 - "cache" "field" <<'PY'
import json, sys
path, field = sys.argv[1:]
data = json.load(open(path))
if field == "version": data[field] += 1
else: data[field] += "-wrong"
open(path, "w").write(json.dumps(data))
PY
offline_failure 'offline metadata does not match'
done

cleanup
trap - EXIT
echo "offline: success plus missing, malformed, version, backend, and SQL mismatch checks passed"
}

size() {
local macro_prod macro_tests runtime demos total
read -r macro_prod macro_tests <<(awk '
BEGIN { test = 0; production = 0; tests = 0 }
/^#[cfg(test)]/ { test = 1 }
/^[[:space:]]$/ || /^[[:space:]]/// { next }
{ if (test) tests++; else production++ }
END { print production, tests }
' toy-sqlx-macros/src/lib.rs)
runtime="(awk '!/^[[:space:]]*/ && !/^[[:space:]]///' toy-sqlx/src/lib.rs | wc -l | tr -d ' ')"
demos="(awk '!/^[[:space:]]*/ && !/^[[:space:]]
///' toy-demo/src/bin/*.rs | wc -l | tr -d ' ')"
total=$((macro_prod + macro_tests + runtime + demos))
printf 'macro production=%s tests=%s runtime=%s demos=%s total=%s\n'
"macro_prod" "macro_tests" "runtime" "demos" "$total"
((macro_prod <= 340 && macro_tests <= 50 && runtime <= 80 && demos <= 180 && total <= 650))
}

workspace() {
setup >/dev/null
scripts/setup-db.sh >/dev/null
cargo test --workspace
cargo clippy -p toy-sqlx -p toy-sqlx-macros -p toy-demo --all-targets -- -D warnings
cargo +1.85.0 check -p toy-sqlx -p toy-sqlx-macros -p toy-demo
cargo fmt --all -- --check
}

all() {
"$0" 0
"$0" 1
"$0" 2
"$0" 3
"$0" 4
"$0" 5
"$0" 6
"$0" 7
"$0" fail-literal
"$0" fail-sql
"$0" fail-arity
"$0" fail-query-as-name
"$0" fail-query-as-type
"$0" fail-expression
"$0" fail-shape
"$0" fail-view
"$0" fail-subquery
"$0" offline
"$0" size
}

case "${1:-}" in
setup) setup ;;
0) run_stage 0 runtime_sql 'runtime SQLite error' ;;

  1. run_stage 1 literal_macro 'SELECT 40 + 2 AS answer' ;;
  2. run_stage 2 compile_time_prepare 'SQLite accepted during compilation' ;;
  3. run_stage 3 parameter_arity 'parameter type deliberately unchecked' ;;
  4. run_stage 4 typed_record 'grace@example.test None' ;;
  5. run_stage 5 query_as 'alan@example.test' ;;
  6. run_stage 6 nullability 'grace@example.test None' ;;
  7. run_stage 7 offline_metadata 'decoded 3 users using a compile-time description' ;;
    fail-literal) expect_failure "$1" 01_literal_macro fail-literal 'expected string literal' ;;
    fail-sql) expect_failure "$1" 02_compile_time_prepare fail-invalid-sql 'SQLite rejected this query' ;;
    fail-arity) expect_failure "$1" 03_parameter_arity fail-arity 'SQLite expects 2 parameter.*received 1' ;;
    fail-query-as-name) expect_failure "$1" 05_query_as fail-query-as-name 'has no field named.*email' ;;
    fail-query-as-type) expect_failure "$1" 05_query_as fail-query-as-type 'incompatible types|mismatched types' ;;
    fail-expression) expect_failure "$1" 06_nullability fail-expression-metadata 'direct columns from one table' ;;
    fail-shape) expect_failure "$1" 06_nullability fail-unsupported-shape 'joins and compound queries are out of scope' ;;
    fail-view)
    setup >/dev/null
    create_hidden_view
    expect_failure "$1" 06_nullability fail-view-source 'direct columns from one table' no
    ;;
    fail-subquery) expect_failure "$1" 06_nullability fail-subquery-source 'direct columns from one table' ;;
    offline) offline ;;
    size) size ;;
    workspace) workspace ;;
    all) all ;;
    )
    echo "usage: scripts/toy-demo.sh {setup|0|1|2|3|4|5|6|7|offline|fail-
    |all|workspace|size}" >&2
    exit 2
    ;;
    esac
    #+end_src
  • Recapitulação: montar o pipeline

A progressão foi uma cadeia de feedback anterior:

#+begin_example
runtime SQL error
-> compile-time literal
-> SQLite validation
-> arity validation
-> generated record
-> application struct
-> evidence-backed nullability
-> offline compilation
#+end_example

Nada aqui exigiu que a macro se tornasse um banco de dados. A macro apenas transporta
evidência do SQLite para o código Rust que o rustc já sabe como verificar.

| Reivindicação final | Evidência usada | Limite honesto |
|---+---+---|
| SQL é válido | SQLite preparou durante a expansão | apenas o esquema de compilação |
| Contagem de vinculação corresponde | contagem de parâmetros SQLite | sem tipos de vinculação SQLite |
| Campos de saída existem | metadados de coluna da instrução | apenas colunas diretas |
| Forma de saída Rust corresponde | registro gerado/literal de struct | valores dinâmicos podem decodificar mal |
| Coluna direta anulável | metadados de esquema =NOT NULL= | joins/expressões rejeitados |
| Compilação sem DB | =Description= em cache | frescor do cache é disciplina externa |

O modelo de engenharia reutilizável é: pergunte ao banco de dados, preserve sua evidência,
gerar Rust comum e deixe o rustc terminar a verificação
.

  • Perguntas e Respostas

O conteúdo principal para aqui no minuto 80. As próximas seções não são necessárias para a
história principal.

  • Apêndice

** Digitação de parâmetro dependente de backend
Backends estilo PostgreSQL podem retornar metadados de tipo de parâmetro e gerar código morto
asserções de tipo Rust. O SQLite retorna apenas aridade. Adicionar um analisador SQLite inventado
ao núcleo obscureceria esse limite.

** Substituições de expressão
Um brinquedo maior pode analisar aliases como ="count!: INTEGER"=. O núcleo rejeita
expressões para que cada tipo inferido tenha metadados diretos.

** Anulabilidade consciente de Join
Sistemas reais podem inspecionar planos ou bytecode SQLite. O legado avançado
=mini-sqlx-macros= demonstra por que isso expande rapidamente a implementação.

** Caches mais fortes
Discuta hashes criptográficos, identidade de esquema, gravações atômicas, bloqueios e frescor de cache de CI
sem emaranhá-los na implementação de ensino.

** Runtimes e pools assíncronos
Eles mudam a execução, não o pipeline de evidência/geração de código em tempo de compilação.

** Migrações e CI
A macro verifica o esquema presente durante a compilação. O CI deve criar um
DB isolado, aplicar migrações, compilar/preparar consultas e verificar evidências em cache.

** Mapeando o brinquedo para o SQLx
A =Description= do brinquedo corresponde a um resultado de descrição de backend; =load_description=
corresponde à seleção de dados de consulta online/offline; =expand_query= corresponde à
geração de código de argumento/saída. Os nomes são conceituais, não reivindicações de fidelidade de fonte.

** Github

[[https://github.com/chiefkemist/toy-sqlx][toy-sqlx -- Não é um clone do SQLx]]