#+title: SQLx के अंदर — लेगो ईंटों से कंपाइल-टाइम चेक की गई SQL बनाना
#+author: ChiefKemist
#+date: <2026-07-25 Sat>
- परिचय
हम इसकी कुछ मुख्य विशेषताओं को समझने और उन्हें कैसे बनाया जा सकता है, यह जानने के लिए एक खिलौना (toy) SQLx बना रहे हैं। हम साथ ही साथ कुछ Rust + इकोसिस्टम सुविधाओं जैसे macros और Cargo वर्कस्पेस का भी पता लगाएंगे। अंतिम लक्ष्य यह समझना है कि Rust और SQL के मामले में कंपाइल-टाइम क्वेरी गारंटी वास्तव में कहाँ से आती है।
हम उस SQL से शुरुआत करते हैं जिसे Rust एक अनचेक स्ट्रिंग के रूप में मानता है, और हम यहाँ समाप्त करते हैं:
#+begin_src rust :tangle no
let users = toy_sqlx::query!(
&connection,
"SELECT id, email FROM users WHERE active = ?1",
true,
)?;
#+end_src
जहाँ एक मैक्रो कॉल तीन प्रणालियों को शुद्धता में योगदान करने के लिए प्रेरित करेगा:
- SQLite साबित करता है कि SQL बिल्ड स्कीमा के लिए मान्य है।
- मैक्रो SQLite के साक्ष्य को Rust कोड में बदल देता है।
- rustc उत्पन्न फ़ील्ड और एप्लिकेशन स्ट्रक्ट्स की जाँच करता है।
हम SQLx को क्लोन नहीं करेंगे, इसके बजाय हम Rust कोड में सही SQL लागू करने के लिए सबसे छोटी उपयोगी कंपाइल-टाइम पाइपलाइन को उजागर कर रहे हैं।
- मैक्रोज़ के बारे में
#+begin_quote
“पूरी भाषा हर समय वहाँ मौजूद है। रीड-टाइम, कंपाइल-टाइम और रनटाइम के बीच कोई वास्तविक अंतर नहीं है। आप पढ़ते समय कोड को कंपाइल या रन कर सकते हैं, कंपाइल करते समय कोड को पढ़ या रन कर सकते हैं, और रनटाइम पर कोड को पढ़ या कंपाइल कर सकते हैं।
रीड-टाइम पर कोड चलाने से उपयोगकर्ता Lisp के सिंटैक्स को फिर से प्रोग्राम कर सकते हैं; कंपाइल-टाइम पर कोड चलाना मैक्रोज़ का आधार है; रनटाइम पर कंपाइल करना Emacs जैसे प्रोग्रामों में एक एक्सटेंशन भाषा के रूप में Lisp के उपयोग का आधार है; और रनटाइम पर पढ़ना प्रोग्रामों को s-expressions का उपयोग करके संवाद करने में सक्षम बनाता है, एक विचार जिसे हाल ही में XML के रूप में फिर से खोजा गया है।”
#+end_quote
— पॉल ग्राहम, /Revenge of the Nerds/, /Hackers & Painters/ में,
अनुभाग “What Made Lisp Different,” आइटम 9.
संदर्भ: [[https://paulgraham.com/icad.html][Paul Graham — Revenge of the Nerds]]
- मचान (Scaffolding): तीन क्रेट्स
कार्गो वर्कस्पेस क्रेट्स: 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
- चरण 0: रनटाइम विफलता को उजागर करना
:PROPERTIES:
:MINUTES: 5
:END:
#+begin_quote
समस्या: Rust SQL को सामान्य टेक्स्ट के रूप में देखता है, इसलिए यह एक खराब कॉलम को अस्वीकार नहीं कर सकता है।
क्यों परवाह करें: पहली उपयोगी प्रतिक्रिया केवल प्रोग्राम के निर्माण, तैनाती और क्वेरी पथ के चलने के बाद ही आती है।
यह चरण: एक छोटा डेटाबेस बनाएं और जानबूझकर रनटाइम पर एक अमान्य क्वेरी तैयार करें। यह वह आधार रेखा है जिसे हर बाद के चरण में सुधारना होगा।
#+end_quote
फिक्स्चर में केवल वही स्कीमा साक्ष्य हैं जिनकी हमें बाद में आवश्यकता होगी: घोषित कॉलम प्रकार और एक नलेबल कॉलम। सेटअप प्रोग्राम किसी भी चेक किए गए मैक्रो का उपयोग नहीं करता है।
#+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
- चरण चलाएं:
#+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
- अवलोकन: Rust कंपाइल होता है; SQLite रनटाइम पर =definitely_missing= की रिपोर्ट करता है।
- हमने प्राप्त किया: जल्दी आगे बढ़ने के लिए एक ठोस विफलता।
- अभी भी टूटा हुआ है: कंपाइलर को कभी भी निरीक्षण योग्य इनपुट के रूप में SQL प्राप्त नहीं होता है। चरण 1 SQL को एक प्रक्रियात्मक मैक्रो को देता है।
- चरण 1: कंपाइल-टाइम पर SQL को दृश्यमान बनाना
:PROPERTIES:
:MINUTES: 6
:END:
#+begin_quote
समस्या: एक प्रक्रियात्मक मैक्रो को Rust टोकन प्राप्त होते हैं, न कि रनटाइम वेरिएबल के अंदर छिपा हुआ मान।
क्यों परवाह करें: कंपाइल-टाइम चेकिंग असंभव है जब तक कि मैक्रो विस्तार के दौरान SQL स्वयं उपलब्ध न हो।
यह चरण: एक स्ट्रिंग लिटरल की आवश्यकता है। पहला मैक्रो केवल लिटरल को पार्स और रिटर्न करता है, ताकि हम डेटाबेस लॉजिक को मिलाए बिना इस सीमा को देख सकें।
#+end_quote
नीचे दिया गया रनटाइम क्रेट प्लंबिंग का समर्थन कर रहा है: SQLite से कनेक्ट करें और पंक्तियों को मैप करें। यह कोई कंपाइल-टाइम विश्लेषण नहीं करता है।
#+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
//! SQLx के कंपाइल-टाइम क्वेरी मैक्रोज़ का जानबूझकर छोटा, केवल-SQLite मॉडल।
//!
//! # केंद्रीय विचार
//!
//! एक प्रक्रियात्मक मैक्रो तब चलता है जब उसे कॉल करने वाला क्रेट कंपाइल किया जा रहा होता है। यह
//! Rust टोकन प्राप्त करता है, SQLite से पूछता है कि SQL स्ट्रिंग का क्या अर्थ है, और नए Rust
//! टोकन उत्सर्जित करता है। उत्सर्जित Rust फिर सामान्य Rust कंपाइलर से गुजरता है।
//!
//! पाइपलाइन है:
//!
//! 1. [syn] मैक्रो इनपुट को Rust डेटा संरचनाओं में पार्स करता है।
//! 2. [rusqlite] SQL तैयार करता है और पैरामीटर/कॉलम मेटाडेटा लौटाता है।
//! 3. यह क्रेट जानबूझकर छोटे शिक्षण सबसेट को मान्य करता है।
//! 4. [quote!] उस मेटाडेटा से सामान्य Rust कोड बनाता है।
//! 5. rustc उत्पन्न फ़ील्ड, प्रकार और स्ट्रक्ट लिटरल की जाँच करता है।
//!
//! यह शिक्षण कोड है, सामान्य SQL विश्लेषक नहीं। टाइप की गई क्वेरी जानबूझकर केवल एक वास्तविक SQLite तालिका से सीधे कॉलम स्वीकार करती है।
// नामों, पर्यावरण चर, कैश फ़ाइलों और पथों के लिए उपयोग किए जाने वाले मानक-पुस्तकालय उपकरण।
use std::{collections::HashSet, env, fs, path::PathBuf};
// proc_macro::TokenStream कंपाइलर-सामना करने वाला इनपुट और आउटपुट प्रकार है।
use proc_macro::TokenStream;
// proc_macro2 टोकन प्रकार प्रदान करता है जिन्हें बनाना और परीक्षण करना आसान है।
use proc_macro2::{Ident, Span, TokenStream as Tokens};
// quote! Rust-जैसे सिंटैक्स को टोकन में बदल देता है; format_ident! पहचानकर्ता बनाता है।
use quote::{format_ident, quote};
// SQLite रनटाइम डेटाबेस और हमारा कंपाइल-टाइम SQL प्राधिकरण दोनों है।
use rusqlite::{Connection, OpenFlags};
// विवरणों को क्रमबद्ध किया जाता है ताकि उसी साक्ष्य का उपयोग ऑफ़लाइन किया जा सके।
use serde::{Deserialize, Serialize};
// syn लिटरल, एक्सप्रेशन, प्रकार, अल्पविराम और कस्टम मैक्रो व्याकरण को पार्स करता है।
#[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
/// मैक्रो इनपुट को स्ट्रिंग लिटरल होने की आवश्यकता है, फिर उस लिटरल को अपरिवर्तित उत्सर्जित करें।
///
/// यह पहला शिक्षण मैक्रो SQL को नहीं समझता है। इसका एकमात्र काम यह दिखाना है
/// कि कंपाइल-टाइम चेकिंग को कंपाइल-टाइम इनपुट की आवश्यकता होती है: sql_literal!(variable)
/// विफल हो जाता है क्योंकि variable एक [LitStr] नहीं है।
#[proc_macro]
pub fn sql_literal(input: TokenStream) -> TokenStream {
// parse_macro_input! विस्तार को रोकता है और विफलता पर कंपाइलर डायग्नोस्टिक उत्सर्जित करता है।
let sql = parse_macro_input!(input as LitStr);
// #sql पार्स किए गए लिटरल को quote! द्वारा निर्मित टोकन स्ट्रीम में इंटरपोलेट करता है।
quote!(#sql).into()
}
/// हमारे आंतरिक syn::Result को rustc द्वारा आवश्यक टोकन स्ट्रीम में बदलें।
///
/// प्रक्रियात्मक मैक्रो Result वापस नहीं कर सकते। इसलिए एक syn::Error सबसे उपयोगी स्रोत स्पैन पर रखे गए compile_error! इनवोकेशन बन जाता है।
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
यह साबित करने के लिए =cargo expand= का उपयोग करें कि मैक्रो लिटरल को अपरिवर्तित उत्सर्जित करता है। पहला
कमांड पुष्टि करता है कि =cargo-expand= स्थापित है। यदि यह गायब है, तो
=cargo install cargo-expand --locked= को एक बार चलाएं।
#+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"));
: };
: }
उपयोगी विस्तार जानबूझकर उबाऊ है:
#+begin_src rust :tangle no
"SELECT 40 + 2 AS answer"
#+end_src
- चरण और उसकी अपेक्षित विफलता चलाएं:
#+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
- अवलोकन: एक लिटरल कंपाइल होता है; एक रनटाइम =String= को मैक्रो पार्सर द्वारा अस्वीकार कर दिया जाता है।
- हमने प्राप्त किया: मैक्रो संकलन के दौरान SQL टेक्स्ट का मालिक है।
- अभी भी टूटा हुआ है: टेक्स्ट का मालिक होना मान्य SQL साबित नहीं करता है। चरण 2 उस सिस्टम से पूछता है
जो वास्तव में SQLite को समझता है।
- चरण 2: SQLite को संकलन के दौरान खराब SQL को अस्वीकार करने दें
:PROPERTIES:
:MINUTES: 12
:END:
#+begin_quote
समस्या: मैक्रो अब SQL देख सकता है, लेकिन यह अभी भी नहीं बता सकता कि कोई तालिका
या कॉलम मौजूद है या नहीं।
क्यों परवाह करें: मैक्रो के अंदर दूसरा SQLite पार्सर लिखना बड़ा, अधूरा और SQLite से कम भरोसेमंद होगा।
यह चरण: मैक्रो विस्तार के दौरान, बिल्ड डेटाबेस को केवल-पढ़ने के लिए खोलें और
SQLite से क्वेरी तैयार करने के लिए कहें। SQLite के उत्तर को एक छोटे =Description=
में बदलें जिसे बाद के चरण पुन: उपयोग कर सकें।
#+end_quote
केवल तीन गतिशील टुकड़े हैं: विवरण, मैक्रो प्रविष्टि बिंदु, और
ऑनलाइन लोडर। इस चरण में, =connection.prepare= पर कॉल देखें। अन्य विवरण फ़ील्ड चरण 3, 4 और 6 में उपयोगी हो जाते हैं।
#+name: description-ir
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
// जब भी कैश किए गए मेटाडेटा का अर्थ या आकार बदलता है तो इसे बढ़ाएं। पुरानी कैश
// प्रविष्टियाँ तब नए नियमों के तहत व्याख्या किए जाने के बजाय स्पष्ट रूप से विफल हो जाती हैं।
const CACHE_VERSION: u8 = 3;
/// ऑनलाइन और ऑफ़लाइन चेकिंग द्वारा साझा किया गया छोटा मध्यवर्ती प्रतिनिधित्व।
///
/// Description को एक SQL स्ट्रिंग के बारे में फैक्ट शीट के रूप में सोचें:
///
/// - parameter_count बताता है कि SQLite कितने मानों की अपेक्षा करता है;
/// - columns परिणाम सेट का वर्णन करता है;
/// - source_is_table रिकॉर्ड करता है कि क्या हमारा संकीर्ण टाइप-क्वेरी नियम सिद्ध हुआ था;
/// - version, database, और sql ऑफ़लाइन कैश लोडिंग की रक्षा करते हैं।
///
/// एक प्रतिनिधित्व रखना महत्वपूर्ण है: कोड निर्माण को यह जानने की आवश्यकता नहीं है
/// कि ये तथ्य लाइव SQLite से आए हैं या कैश फ़ाइल से।
#[rustfmt::skip]
#[derive(Debug, Serialize, Deserialize)]
struct Description { version: u8, database: String, sql: String, parameter_count: usize, source_is_table: bool, columns: Vec }
/// एक Rust आउटपुट फ़ील्ड उत्पन्न करने के लिए आवश्यक SQLite साक्ष्य।
///
/// declared_type वह प्रकार है जो CREATE TABLE में लिखा गया है; यह उस प्रकार का नहीं है
/// जो SQLite गतिशील रूप से संग्रहीत कर सकता है। nullable उत्पन्न Rust में T
/// और Option<T> के बीच निर्णय लेता है।
#[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
/// मैक्रो विस्तार के दौरान SQL लिटरल को मान्य करने के लिए SQLite से पूछें।
///
/// उत्सर्जित मान अभी भी केवल मूल स्ट्रिंग लिटरल है। सुधार
/// समय है: अमान्य SQL रनटाइम त्रुटि के बजाय कंपाइलर त्रुटि बन जाता है।
#[proc_macro]
pub fn checked_sql(input: TokenStream) -> TokenStream {
let sql = parse_macro_input!(input as LitStr);
// विवरण लोड करने से सत्यापन होता है; यह मैक्रो तथ्यों को त्याग देता है।
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
/// ठीक एक स्रोत से क्वेरी साक्ष्य प्राप्त करें।
///
/// ऑफ़लाइन मोड पहले से सहेजे गए विवरण को पढ़ता है। ऑनलाइन मोड SQLite से पूछता है
/// सीधे और वैकल्पिक रूप से बाद के ऑफ़लाइन बिल्ड के लिए उत्तर को सहेजता है।
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)
}
/// टाइप की गई क्वेरी द्वारा समर्थित जानबूझकर छोटे SQL आकार को पहचानें।
///
/// यह एक स्कोप गार्ड है, न कि SQL पार्सर। यह केवल एक साधारण तालिका से सीधे कॉलम के प्रोजेक्शन के लिए तालिका का नाम लौटाता है। कोई भी अनिश्चितता None लौटाती है, जो टाइप किए गए मैक्रो को तथ्यों का आविष्कार करने के बजाय क्वेरी को अस्वीकार करने के लिए मजबूर करती है।
#[rustfmt::skip]
fn direct_source(sql: &str) -> Option<&str> {
// टिप्पणियाँ अतिरिक्त FROM क्लॉज को छिपा सकती हैं, इसलिए यह खिलौना उन्हें सीधे अस्वीकार कर देता है।
if ["--", "/", "/"].iter().any(|marker| sql.contains(marker)) { return None; }
// व्हाइटस्पेस टोकनाइज़ेशन पर्याप्त है क्योंकि हर स्वीकृत आकार सरल है।
let words = sql.split_whitespace().collect::<Vec<>>();
// ठीक एक FROM सामान्य SELECT सबक्वेरी और अस्पष्ट स्रोतों को बाहर करता है।
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 };
// FROM के बाद पहला शब्द एक अनकोटेड, अनक्वालिफाइड तालिका पहचानकर्ता होना चाहिए।
let table = *words.get(from + 1)?;
// स्रोत क्लॉज को तब रोकें जब सामान्य फ़िल्टरिंग/ऑर्डरिंग शुरू हो।
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 == '');
// प्रत्येक चयनित फ़ील्ड column, table.column, या 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])))) };
// तालिका के बाद, कोई उपनाम, alias, या AS alias की अनुमति न दें—कभी भी दूसरा स्रोत नहीं।
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)
}
/// संकलन के दौरान क्वेरी का वर्णन करने के लिए एक लाइव SQLite डेटाबेस से पूछें।
///
/// तैयारी क्वेरी को निष्पादित किए बिना सिंटैक्स, तालिका नामों और कॉलम नामों को मान्य करती है। span डायग्नोस्टिक्स को कॉलर के कोड में SQL लिटरल पर वापस इंगित करता है।
#[rustfmt::skip]
fn describe_online(sql: &str, span: Span) -> syn::Result {
// कार्गो इस फ़ंक्शन को proc-macro प्रक्रिया के अंदर चलाता है, न कि अंतिम प्रोग्राम में।
let url = env::var("TOY_DATABASE_URL")
.map_err(|| syn::Error::new(span, "TOY_DATABASE_URL is required for online checking"))?;
// रनटाइम क्रेट के समान SQLite URL रूपों के छोटे सेट को स्वीकार करें।
let path = url
.strip_prefix("sqlite://")
.or_else(|| url.strip_prefix("sqlite:"))
.unwrap_or(&url);
// केवल-पढ़ने का मोड संकलन को शिक्षण डेटाबेस को संशोधित करने से रोकता है।
let connection = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)
.map_err(|error| syn::Error::new(span, format!("cannot open SQLite: {error}")))?;
// यह मुख्य कंपाइल-टाइम चेक है: SQLite को SQLite SQL को मान्य करने दें।
let statement = connection.prepare(sql).map_err(|error| {
syn::Error::new(
span,
format!("SQLite rejected this query during compilation: {error}"),
)
})?;
// केवल बाद के Rust कोड निर्माण के लिए आवश्यक आउटपुट तथ्यों की प्रतिलिपि बनाएँ।
let columns = (0..statement.column_count())
.map(|index| {
let name = statement.column_name(index)?.to_owned();
let metadata = statement.column_metadata(index)?;
// rusqlite कई मूल फ़ील्ड उजागर करता है; इस खिलौने को घोषणा और NOT NULL की आवश्यकता है।
let (declared_type, nullable) = match metadata {
Some((, _, _, declared, _, not_null, _, )) => (
declared.map(|value| value.to_string_lossy().into_owned()),
!not_null,
),
// गायब मूल साक्ष्य को कभी भी गैर-शून्य के रूप में नहीं माना जाना चाहिए।
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}")))?;
// लेक्सिकल पहचान पर्याप्त नहीं है: sqlite_schema को एक वास्तविक तालिका की पुष्टि करनी चाहिए, न कि एक दृश्य की।
#[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));
// तैयार कथन स्वयं 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
- चरण और उसकी अपेक्षित विफलता चलाएं:
#+begin_src bash :results output
./scripts/toy-demo.sh 2
./scripts/toy-demo.sh fail-sql
#+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.57s
: Running target/debug/02_compile_time_prepare
: SQLite accepted during compilation: SELECT id, email FROM users
: fail-sql: failed as intended
- अवलोकन: =definitely_missing= अब एक कंपाइलर त्रुटि है।
- हमने प्राप्त किया: SQLite ने बिल्ड डेटाबेस के खिलाफ क्वेरी को स्वीकार कर लिया।
- सीमा: रनटाइम डेटाबेस में अभी भी एक अलग स्कीमा हो सकता है।
- अभी भी टूटा हुआ है: मान्य क्वेरी अभी भी अनटाइप्ड पंक्तियाँ लौटा सकती हैं। चरण 3
तर्कों के खिलाफ प्लेसहोल्डर्स की जाँच करता है।
- चरण 3: तर्कों की गलत संख्या को अस्वीकार करना
:PROPERTIES:
:MINUTES: 8
:END:
#+begin_quote
समस्या: एक क्वेरी मान्य हो सकती है जबकि इसके =?1=, =?2= प्लेसहोल्डर्स Rust द्वारा आपूर्ति किए गए मानों की संख्या से मेल नहीं खाते हैं।
क्यों परवाह करें: वह बेमेल अन्यथा एक और रनटाइम तैयारी/बाइंड विफलता बन जाता है।
यह चरण: SQLite की पैरामीटर गणना की तुलना मैक्रो तर्कों की संख्या से करें।
Rust एक्सप्रेशन को उनका मूल्यांकन किए बिना rustc के लिए दृश्यमान रखें।
#+end_quote
यह जानबूझकर केवल-arity है। SQLite स्थिर बाइंड प्रकार प्रदान नहीं करता है।
#+name: checked-input
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// checked_query!("SQL", arg1, arg2, ...) के लिए पार्स किया गया इनपुट।
///
/// Punctuated अल्पविराम द्वारा अलग किए गए शून्य या अधिक एक्सप्रेशन का syn का प्रतिनिधित्व है।
/// एक्सप्रेशन को सिंटैक्स के रूप में रखने से हम उन्हें चलाए बिना गिन सकते हैं।
#[rustfmt::skip]
struct CheckedInput { sql: LitStr, args: Punctuated<Expr, Token![,]> }
/// checked_query! द्वारा स्वीकार किए गए छोटे व्याकरण को syn को सिखाएं।
impl Parse for CheckedInput {
fn parse(input: ParseStream<'_>) -> syn::Result {
// पहला टोकन SQL स्ट्रिंग लिटरल होना चाहिए।
let sql = input.parse()?;
let args = if input.is_empty() {
// प्लेसहोल्डर के बिना क्वेरी को किसी अल्पविराम और किसी तर्क की आवश्यकता नहीं होती है।
Punctuated::new()
} else {
// अन्यथा SQL के बाद अल्पविराम का उपभोग करें, फिर सभी अल्पविराम-पृथक एक्सप्रेशन।
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
/// SQL और पैरामीटर arity को मान्य करें, लेकिन क्वेरी को निष्पादित न करें।
///
/// यह संक्रमणकालीन मैक्रो एक सबक को अलग करता है: SQLite हमें बता सकता है कि यह कितने
/// पैरामीटर की अपेक्षा करता है, लेकिन स्थिर SQLite पैरामीटर प्रकार नहीं।
#[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
/// टोकन पार्स होने के बाद checked_query! को लागू करें।
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! {{
// यह शाखा कभी नहीं चलती है, लेकिन rustc अभी भी प्रत्येक एक्सप्रेशन को हल और टाइप-चेक करता है।
// #(...)* quote का पुनरावृत्ति सिंटैक्स है: प्रति तर्क एक बार बॉडी उत्सर्जित करें।
if false {
#(let _ = &(#args);)*
}
// मैक्रो का रनटाइम मान मूल SQL स्ट्रिंग बना रहता है।
#sql
}})
}
#+end_src
यह देखने के लिए डेमो का विस्तार करें कि बिना मूल्यांकन किया गया तर्क =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
- चरण और उसकी अपेक्षित विफलता चलाएं:
#+begin_src bash :results output
./scripts/toy-demo.sh 3
./scripts/toy-demo.sh fail-arity
#+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.19s
: Running target/debug/03_parameter_arity
: arity checked, parameter type deliberately unchecked: SELECT id FROM users WHERE active = ?1
: fail-arity: failed as intended
- अवलोकन: दो प्लेसहोल्डर्स के लिए एक मान विफल हो जाता है; एक प्लेसहोल्डर के लिए एक मान
पास हो जाता है—भले ही वह मान जानबूझकर एक अनुपयुक्त स्ट्रिंग हो। - हमने प्राप्त किया: बाइंड गणना मेल खाती है।
- सीमा: कोई स्थिर SQLite बाइंड-प्रकार का दावा नहीं किया गया है।
- अभी भी टूटा हुआ है: सफल क्वेरी अभी भी अनटाइप्ड पंक्तियाँ लौटाती हैं। चरण 4 Rust आउटपुट आकार और डिकोडर उत्पन्न करता है।
- चरण 4: एक टाइप किया गया रिकॉर्ड और डिकोडर उत्पन्न करें
:PROPERTIES:
:MINUTES: 17
:END:
#+begin_quote
समस्या: यहाँ तक कि चेक की गई SQL को भी दोहरावदार =row.get(0)=,
=row.get(1)= कॉल और हस्तलिखित Rust प्रकारों की आवश्यकता होती है।
क्यों परवाह करें: स्थितिजन्य डिकोडिंग नाजुक है, और SQL परिणाम आकार को Rust में हाथ से दोहराया जाता है।
यह चरण: =Description= में कॉलम नामों और घोषित प्रकारों को एक स्थानीय
=Record= में बदलें, फिर स्थितिजन्य डिकोडर उत्पन्न करें जो इसे बनाता है।
#+end_quote
घोषित-प्रकार मैपिंग जानबूझकर दृश्यमान और छोटी है:
| SQLite घोषणा में शामिल है | उत्पन्न Rust आधार प्रकार |
|---+---|
| =BOOL= या =BOOLEAN= | =bool= |
| =INT= | =i64= |
| =REAL=, =FLOA=, या =DOUB= | =f64= |
| =CHAR=, =CLOB=, या =TEXT= | =String= |
| =BLOB= | =Vec= |
| कुछ और | कंपाइल-टाइम त्रुटि |
चरण 6 बताता है कि आधार प्रकार कब =T= रहता है और कब नलेबिलिटी इसे =Option= में लपेटती है।
#+name: query-input
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// query!(&connection, "SQL", arg1, arg2, ...) के लिए पार्स किया गया इनपुट।
///
/// कनेक्शन और प्रत्येक तर्क पूर्ण Rust एक्सप्रेशन हैं। उन्हें तब तक सिंटैक्स के रूप में रखा जाता है
/// जब तक विस्तार ऐसा कोड उत्सर्जित नहीं करता जो प्रत्येक का ठीक एक बार मूल्यांकन करता है।
#[rustfmt::skip]
struct QueryInput { connection: Expr, sql: LitStr, args: Punctuated<Expr, Token![,]> }
/// पहले कनेक्शन को पार्स करें, फिर SQL और तर्कों के लिए CheckedInput का पुन: उपयोग करें।
#[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
/// एक क्वेरी को मान्य और निष्पादित करें जिसका आउटपुट रिकॉर्ड मैक्रो द्वारा उत्पन्न होता है।
///
/// स्थानीय उत्पन्न प्रकार में प्रति चयनित SQL कॉलम एक Rust फ़ील्ड होता है।
#[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
/// चुनें कि पंक्ति डिकोडर कौन सा Rust मान बनाता है।
enum Output {
/// query! हमसे एक स्थानीय Record प्रकार परिभाषित करने के लिए कहता है।
Generated,
/// query_as! हमें निर्माण करने के लिए एक एप्लिकेशन-स्वामित्व वाला प्रकार देता है।
Given(Box),
}
/// एक Rust फ़ील्ड नाम और प्रकार टोकन में अनुवादित एक चेक किया गया SQL आउटपुट कॉलम।
#[rustfmt::skip]
struct RustColumn { ident: Ident, ty: Tokens }
/// query! और query_as! द्वारा उत्सर्जित Rust प्रोग्राम बनाएं।
///
/// अंतिम quote! से पहले सब कुछ संकलन के दौरान चलता है। उस quote! के अंदर का कोड
/// वह है जिसे कॉलर का प्रोग्राम रनटाइम पर निष्पादित करेगा।
#[rustfmt::skip]
fn expand_query(input: QueryInput, output: Output) -> syn::Result {
// कंपाइल-टाइम चरण: साक्ष्य इकट्ठा करें और असमर्थित इनपुट को अस्वीकार करें।
let description = load_description(&input.sql)?;
validate_arity(input.args.len(), &description, input.sql.span())?;
let columns = typed_columns(&description, input.sql.span())?;
// प्रत्येक चेक किए गए कॉलम को `field_name: RustType` टोकन में बदलें।
let fields = columns.iter().map(|column| {
let ident = &column.ident;
let ty = &column.ty;
quote!(#ident: #ty)
});
// प्रत्येक चेक किए गए कॉलम को `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();
// दोनों सार्वजनिक मैक्रो एक डिकोडर साझा करते हैं; केवल उनका अंतिम निर्माता भिन्न होता है।
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;
// उत्पन्न स्थानीय नाम हमें प्रत्येक कॉलर एक्सप्रेशन का ठीक एक बार मूल्यांकन करने देते हैं।
let names: Vec<_> = (0..args.len())
.map(|index| format_ident!("__toy_arg_{index}"))
.collect();
let args = args.into_iter().collect::<Vec<_>>();
// रनटाइम चरण: यह पूरा ब्लॉक मैक्रो कॉल साइट पर डाला जाता है।
Ok(quote! {{
#definition
// टुपल राइट-हैंड साइड का मूल्यांकन उत्पन्न नामों के बंधने से पहले किया जाता है।
let (__toy_connection, #(#names,)*) = (#connection, #(&(#args),)*);
// rusqlite `ToSql` लागू करने वाले मानों के संदर्भों की एक स्लाइस स्वीकार करता है।
let __toy_parameters: &[&dyn ::toy_sqlx::rusqlite::ToSql] = &[#(#names),*];
::toy_sqlx::map_rows(
__toy_connection,
#sql,
__toy_parameters,
// एक SQLite पंक्ति को उत्पन्न या कॉलर-प्रदान किए गए स्ट्रक्ट में डिकोड करें।
|__toy_row| ::core::result::Result::Ok(#expression),
)
}})
}
/// Rust तर्कों की संख्या की तुलना SQLite की प्लेसहोल्डर गणना से करें।
///
/// यह जानबूझकर केवल कितने मान मौजूद हैं, इसकी जाँच करता है। SQLite इस खिलौने को स्थिर स्थिर पैरामीटर प्रकार नहीं देता है, इसलिए अधिक दावा करना भ्रामक होगा।
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}"),
))
}
}
/// SQLite कॉलम को सुरक्षित Rust फ़ील्ड नामों और प्रकारों में बदलें।
///
/// फ़ंक्शन पहले साक्ष्य सीमा को लागू करता है, फिर नामों की जाँच करता है, डुप्लिकेट फ़ील्ड को अस्वीकार करता है, SQLite घोषणाओं को मैप करता है, और नलेबिलिटी लागू करता है।
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| {
// एक SQL आउटपुट नाम का उपयोग उत्पन्न Rust स्ट्रक्ट लिटरल में किया जाना चाहिए।
let ident = rust_ident(&column.name, span)?;
if !names.insert(ident.to_string()) {
return Err(syn::Error::new(span, "duplicate output field name"));
}
// एक्सप्रेशन में अक्सर कोई तालिका घोषणा नहीं होती है; टाइप किया गया आउटपुट उन्हें अस्वीकार करता है।
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 एक संभावित-NULL SQL मान को Option<T> के रूप में दर्शाता है।
let ty = if column.nullable {
quote!(::core::option::Option<#base>)
} else {
base
};
Ok(RustColumn { ident, ty })
})
.collect()
}
/// टाइप किया गया Rust उत्पन्न करने से पहले व्यापक SQL आकारों को अस्वीकार करें।
///
/// जॉइन और कंपाउंड क्वेरी को नलेबिलिटी विश्लेषण की आवश्यकता होती है जिसे यह कार्यशाला लागू नहीं करती है। रूढ़िवादी अस्वीकृति छोटी गारंटी को ईमानदार रखती है।
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(())
}
}
/// एक SQLite आउटपुट नाम को Rust फ़ील्ड पहचानकर्ता में बदलें।
///
/// कच्चा रूप (r#type, उदाहरण के लिए) कानूनी होने पर Rust कीवर्ड की अनुमति देता है। फॉलबैक अलग कच्चे-पहचानकर्ता पार्सिंग व्यवहार वाले कंपाइलर संस्करणों पर सामान्य पहचानकर्ताओं को संभालता है।
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")))
}
/// SQLite घोषित प्रकारों के एक छोटे सेट को Rust प्रकार टोकन में मैप करें।
///
/// SQLite प्रकार आत्मीयता का उपयोग करता है और गतिशील संग्रहीत मानों की अनुमति देता है। यह तालिका एक शिक्षण सबसेट है, न कि यह दावा कि प्रत्येक संग्रहीत मान का यह Rust प्रकार होना चाहिए।
///
/// | घोषणा में शामिल है | उत्पन्न आधार प्रकार |
/// | --- | --- |
/// | BOOL या BOOLEAN | bool |
/// | INT | i64 |
/// | REAL, FLOA, या DOUB | f64 |
/// | CHAR, CLOB, या TEXT | String |
/// | BLOB | Vec<u8> |
/// | कुछ और | कंपाइलर त्रुटि |
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
डेमो का विस्तार करें और उत्पन्न =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 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
महत्वपूर्ण उत्पन्न आकार है:
#+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
- चरण चलाएं:
#+begin_src bash :results output
./scripts/toy-demo.sh 4
#+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.34s
: Running target/debug/04_typed_record
: 1 ada@example.test Some("Ada")
: 2 grace@example.test None
- अवलोकन: लूप =user.id= और =user.email= का उपयोग करता है; कोई हस्तलिखित =row.get=
कॉल एप्लिकेशन कोड में नहीं रहती है। - हमने प्राप्त किया: आउटपुट नाम और समर्थित घोषणाएं Rust फ़ील्ड बन जाते हैं।
- चरण 6 के लिए पार्क किया गया प्रश्न: क्या साक्ष्य =email= के लिए =String= लेकिन
=display_name= के लिए =Option= को सही ठहराता है? - अगला टूटा हुआ: =Record= मैक्रो विस्तार के लिए स्थानीय है। चरण 5 उसी साक्ष्य को एक एप्लिकेशन-स्वामित्व वाले स्ट्रक्ट में मैप करता है।
- चरण 5: एप्लिकेशन-स्वामित्व वाले स्ट्रक्ट को पॉप्युलेट करें
:PROPERTIES:
:MINUTES: 10
:END:
#+begin_quote
समस्या: उत्पन्न स्थानीय =Record= एक एक्सप्रेशन के अंदर सुविधाजनक है, लेकिन
एप्लिकेशन में पहले से ही फ़ंक्शन और मॉड्यूल में उपयोग किए जाने वाले नामित डोमेन प्रकार हैं।
क्यों परवाह करें: क्वेरी परिणामों को दूसरे मैनुअल डिकोडर को जोड़े बिना उन प्रकारों में फिट होना चाहिए।
यह चरण: कॉलर के स्ट्रक्ट का एक सामान्य लिटरल उत्पन्न करें। rustc को—न कि
मैक्रो को—फ़ील्ड नामों और Rust प्रकारों को सत्यापित करने दें।
#+end_quote
=query_as!= उसी विस्तार का पुन: उपयोग करता है और केवल अंतिम निर्माता को बदलता है। यह
स्ट्रक्ट परिभाषा पर निरीक्षण या चिंतन नहीं करता है।
#+name: query-as-input
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// query_as!(OutputType, &connection, "SQL", args...) के लिए पार्स किया गया इनपुट।
#[rustfmt::skip]
struct QueryAsInput { output: Type, query: QueryInput }
/// कॉलर के Rust प्रकार को पार्स करें, फिर पूर्ण 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
/// कॉलर-प्रदान किए गए Rust स्ट्रक्ट में क्वेरी को मान्य और निष्पादित करें।
///
/// मैक्रो एक सामान्य स्ट्रक्ट लिटरल उत्सर्जित करता है। rustc इसलिए कस्टम रिफ्लेक्शन मशीनरी के बिना गायब, अतिरिक्त, या गलत तरीके से टाइप किए गए फ़ील्ड की रिपोर्ट करता है।
#[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
=query_as!= का विस्तार करें यह देखने के लिए कि यह =map_rows= का पुन: उपयोग करता है लेकिन =Record= को परिभाषित करने के बजाय =UserSummary= बनाता है:
#+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
#+RESULTS:
#+begin_example
::toy_sqlx::map_rows(
__toy_connection,
"SELECT id, email FROM users ORDER BY id",
__toy_parameters,
|__toy_row| ::core::result::Result::Ok(UserSummary {
id: __toy_row.get::<usize, i64>(0usize)?,
email: __toy_row.get::<usize, ::std::string::String>(1usize)?,
}),
)
}? {
{
::std::io::_print(format_args!("{0} {1}\n", user.id, user.email));
};
#+end_example
उत्पन्न एक्सप्रेशन एक सामान्य स्ट्रक्ट लिटरल है:
#+begin_src rust :tangle no
UserSummary {
id: row.get::<usize, i64>(0)?,
email: row.get::<usize, String>(1)?,
}
#+end_src
- चरण और दोनों अपेक्षित विफलताएं चलाएं:
#+begin_src bash :results output
./scripts/toy-demo.sh 5
./scripts/toy-demo.sh fail-query-as-name
./scripts/toy-demo.sh fail-query-as-type
#+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.31s
: Running target/debug/05_query_as
: 1 ada@example.test
: 2 grace@example.test
: 3 alan@example.test
: fail-query-as-name: failed as intended
: fail-query-as-type: failed as intended
- अवलोकन: सही =UserSummary= कंपाइल होता है; एक गलत फ़ील्ड या फ़ील्ड प्रकार एक सामान्य rustc त्रुटि उत्पन्न करता है।
- हमने प्राप्त किया: चेक की गई पंक्तियाँ बिना किसी दूसरे डिकोडिंग सिस्टम के एप्लिकेशन-स्वामित्व वाले प्रकारों में प्रवेश कर सकती हैं।
- अभी भी टूटा हुआ है: उत्पन्न Rust केवल तभी भरोसेमंद है जब SQL मेटाडेटा इसे सही ठहराता है। चरण 6 ईमानदार साक्ष्य सीमा को परिभाषित करता है।
- चरण 6: प्रकार के दावे को ईमानदार बनाएं
:PROPERTIES:
:MINUTES: 8
:END:
#+begin_quote
समस्या: एक ऐसे मान के लिए =String= उत्पन्न करना जो वास्तव में =NULL= हो सकता है, एक कंपाइल-टाइम सुविधा को आत्मविश्वास से गलत बनाता है।
क्यों परवाह करें: जॉइन, व्यू, सबक्वेरी और एक्सप्रेशन नलेबिलिटी को बदल सकते हैं, भले ही अंतर्निहित तालिका कॉलम =NOT NULL= कहता हो।
यह चरण: केवल एक वास्तविक तालिका से सीधे कॉलम के लिए टाइप किए गए आउटपुट को स्वीकार करें। उस तालिका के स्कीमा को पढ़ें: =NOT NULL= =T= बन जाता है; सब कुछ नलेबल =Option= बन जाता है। उन आकारों को अस्वीकार करें जिनके साक्ष्य को हम सही नहीं ठहरा सकते।
#+end_quote
यह हर क्वेरी को समझने का नाटक करने वाला एक छोटा SQL पार्सर नहीं है। अस्वीकृति शुद्धता तंत्र है।
#+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
- चरण और उसकी साक्ष्य-सीमा विफलताओं को चलाएं:
#+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
#+RESULTS:
#+begin_example
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.76s
Running target/debug/06_nullability
ada@example.test Some("Ada")
grace@example.test None
alan@example.test Some("Alan")
fail-expression: failed as intended
fail-shape: failed as intended
fail-view: failed as intended
fail-subquery: failed as intended
#+end_example
- अवलोकन: सीधा =email= =String= है; नलेबल =display_name= =Option= है; असमर्थित आकार संकलन के दौरान विफल हो जाते हैं।
- हमने प्राप्त किया: सीधे स्कीमा साक्ष्य द्वारा समर्थित एक संकीर्ण टाइप-क्वेरी दावा।
- अभी भी टूटा हुआ है: ऑनलाइन विस्तार को हर बार डेटाबेस की आवश्यकता होती है। चरण 7 साक्ष्य को पोर्टेबल बनाता है।
- चरण 7: बिल्ड डेटाबेस के बिना कंपाइल करें
:PROPERTIES:
:MINUTES: 7
:END:
#+begin_quote
समस्या: कंपाइल-टाइम चेकिंग अब बिल्ड डेटाबेस को खोलने पर निर्भर करती है।
क्यों परवाह करें: डेवलपर्स और CI को पुनरुत्पादनीय बिल्ड की आवश्यकता हो सकती है जहाँ वह डेटाबेस अनुपलब्ध हो।
यह चरण: पहले से मान्य =Description= को JSON के रूप में सहेजें, फिर ऑफ़लाइन मोड में उसी आकार को लोड करें। लोडर के बाद हर सत्यापन और कोड-निर्माण चरण को अपरिवर्तित रखें।
#+end_quote
कैश जानबूझकर दृश्यमान है: एक छोटा हैश फ़ाइल नाम चुनता है, जबकि संस्करण, बैकएंड और सटीक SQL को कैश किए गए साक्ष्य पर भरोसा करने से पहले चेक किया जाता है।
#+name: offline-cache
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
/// कार्यशाला के बूलियन पर्यावरण झंडे पढ़ें।
fn flag(name: &str) -> bool {
env::var(name)
.ok()
.is_some_and(|value| matches!(value.as_str(), "1" | "true" | "TRUE"))
}
/// सटीक SQL टेक्स्ट से एक छोटा, नियतात्मक कैश फ़ाइल नाम तैयार करें।
///
/// FNV-1a का उपयोग किया जाता है क्योंकि इसका लूप सिखाना आसान है। एक उत्पादन कैश
/// सामान्य रूप से एक टक्कर-प्रतिरोधी हैश और मजबूत समवर्ती गारंटी का उपयोग करेगा।
fn hash(sql: &str) -> u64 {
sql.as_bytes()
.iter()
.fold(0xcbf29ce484222325, |hash, byte| {
(hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3)
})
}
/// इस क्वेरी की कैश फ़ाइल को उस क्रेट के अंदर खोजें जिसने मैक्रो को कॉल किया था।
///
/// CARGO_MANIFEST_DIR कॉलर का है, इसलिए प्रत्येक क्रेट को अपनी
/// .toy-sqlx निर्देशिका मिलती है, न कि 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))))
}
/// ऑफ़लाइन संकलन के लिए पठनीय JSON के रूप में एक लाइव SQLite विवरण को क्रमबद्ध करें।
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())))
}
/// कैश किए गए साक्ष्य लोड करें और कुछ भी अस्वीकार करें जो इस क्वेरी से मेल नहीं खाता है।
///
/// संस्करण, बैकएंड और सटीक SQL की जाँच करना अलग-अलग मान्यताओं के तहत बनाई गई कैश प्रविष्टि को चुपचाप 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}")))?;
// तीनों जाँच कैश की विश्वास सीमा का हिस्सा हैं।
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
- ऑनलाइन डेमो चलाएं, फिर ऑफ़लाइन बिल्ड साबित करें:
#+begin_src bash :results output
./scripts/toy-demo.sh 7
./scripts/toy-demo.sh offline
#+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.85s
: Running target/debug/07_offline_metadata
: decoded 3 users using a compile-time description
: offline: success plus missing, malformed, version, backend, and SQL mismatch checks passed
- अवलोकन: मेटाडेटा ऑनलाइन तैयार करें, डेटाबेस हटाएं, विस्तार को मजबूर करें, और
कैश से सफलतापूर्वक कंपाइल करें। - हमने प्राप्त किया: कोड निर्माण एक =Description= का उपभोग करता है, चाहे
इसका साक्ष्य लाइव SQLite से आया हो या चेक किए गए कैश से। - सीमा: कैश ताजगी, टक्कर प्रतिरोध, परमाणु लेखन, और समवर्ती
लेखक उत्पादन संबंधी चिंताएं हैं। खिलौना गायब, विकृत, गलत-संस्करण, गलत-बैकएंड, और गलत-SQL मेटाडेटा को सत्यापित करता है।
** टेंगल्ड सत्यापन और प्रस्तुति धावक :noexport:
मुख्य कहानी ऊपर समाप्त होती है। शेष ब्लॉक परीक्षणों, प्रदर्शनों,
अपेक्षित विफलताओं और आकार की जाँचों को स्वचालित करते हैं; वे अतिरिक्त शिक्षण चरण नहीं हैं।
#+name: focused-macro-tests
#+begin_src rust :tangle toy-sqlx-macros/src/lib.rs :mkdirp yes :comments no
// ये परीक्षण उन छोटी नीतियों को लक्षित करते हैं जिन्हें संपादन के दौरान तोड़ना सबसे आसान है
// कार्यशाला: प्रकार मैपिंग, क्वेरी-आकार अस्वीकृति, फ़ील्ड नाम, और कैश हैश।
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[rustfmt::skip]
fn maps_the_teaching_types() {
// उत्सर्जित टोकन की तुलना टेक्स्ट के रूप में करें क्योंकि ये सहायक कोड उत्पन्न करते हैं, मान नहीं।
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() {
// हर अनिश्चित आकार को एक अस्वस्थ प्रकार उत्पन्न करने के बजाय बंद विफल होना चाहिए।
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() {
// अमान्य या दोहराए गए SQL नाम एक मान्य Rust स्ट्रक्ट नहीं बना सकते।
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() {
// इस मान को बदलने से हर कैश फ़ाइल स्थानांतरित हो जाएगी और इसके लिए एक स्पष्ट निर्णय की आवश्यकता होगी।
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' ;;
- run_stage 1 literal_macro 'SELECT 40 + 2 AS answer' ;;
- run_stage 2 compile_time_prepare 'SQLite accepted during compilation' ;;
- run_stage 3 parameter_arity 'parameter type deliberately unchecked' ;;
- run_stage 4 typed_record 'grace@example.test None' ;;
- run_stage 5 query_as 'alan@example.test' ;;
- run_stage 6 nullability 'grace@example.test None' ;;
- 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
- पुनर्कथन: पाइपलाइन को असेंबल करें
प्रगति पहले की प्रतिक्रियाओं की एक श्रृंखला थी:
#+begin_example
runtime SQL error
-> compile-time literal
-> SQLite validation
-> arity validation
-> generated record
-> application struct
-> evidence-backed nullability
-> offline compilation
#+end_example
यहाँ किसी भी चीज़ के लिए मैक्रो को डेटाबेस बनने की आवश्यकता नहीं थी। मैक्रो केवल
SQLite से Rust कोड में साक्ष्य ले जाता है जिसे rustc पहले से ही जानता है कि कैसे चेक करना है।
| अंतिम दावा | उपयोग किया गया साक्ष्य | ईमानदार सीमा |
|---+---+---|
| SQL मान्य है | SQLite ने विस्तार के दौरान इसे तैयार किया | केवल बिल्ड स्कीमा |
| बाइंड गणना मेल खाती है | SQLite पैरामीटर गणना | कोई SQLite बाइंड प्रकार नहीं |
| आउटपुट फ़ील्ड मौजूद हैं | स्टेटमेंट कॉलम मेटाडेटा | केवल सीधे कॉलम |
| Rust आउटपुट आकार मेल खाता है | उत्पन्न रिकॉर्ड/स्ट्रक्ट लिटरल | गतिशील मान खराब डिकोड हो सकते हैं |
| नलेबल सीधा कॉलम | स्कीमा =NOT NULL= मेटाडेटा | जॉइन/एक्सप्रेशन अस्वीकार कर दिए गए |
| DB-मुक्त बिल्ड | कैश किया गया =Description= | कैश ताजगी बाहरी अनुशासन है |
पुन: प्रयोज्य इंजीनियरिंग मॉडल है: डेटाबेस से पूछें, इसके साक्ष्य को संरक्षित करें,
सामान्य Rust उत्पन्न करें, और rustc को चेक पूरा करने दें।
- प्रश्नोत्तर
मुख्य सामग्री यहाँ 80वें मिनट तक रुक जाती है। अगले अनुभाग मुख्य कहानी के लिए आवश्यक नहीं हैं।
- परिशिष्ट
** बैकएंड-निर्भर पैरामीटर टाइपिंग
PostgreSQL-जैसे बैकएंड पैरामीटर प्रकार मेटाडेटा लौटा सकते हैं और मृत-कोड
Rust प्रकार दावे उत्पन्न कर सकते हैं। SQLite केवल arity लौटाता है। कोर में एक बनावटी SQLite पार्सर जोड़ने से यह सीमा अस्पष्ट हो जाएगी।
** एक्सप्रेशन ओवरराइड
एक बड़ा खिलौना ="count!: INTEGER"= जैसे उपनामों को पार्स कर सकता है। कोर एक्सप्रेशन को अस्वीकार करता है ताकि प्रत्येक अनुमानित प्रकार में सीधा मेटाडेटा हो।
** जॉइन-जागरूक नलेबिलिटी
वास्तविक सिस्टम योजनाओं या SQLite बाइटकोड का निरीक्षण कर सकते हैं। उन्नत विरासत
=mini-sqlx-macros= प्रदर्शित करता है कि यह कार्यान्वयन का तेजी से विस्तार क्यों करता है।
** मजबूत कैश
क्रिप्टोग्राफिक हैश, स्कीमा पहचान, परमाणु लेखन, लॉक और CI कैश ताजगी पर चर्चा करें, उन्हें शिक्षण कार्यान्वयन में उलझाए बिना।
** एसिंक रनटाइम और पूल
वे निष्पादन बदलते हैं, कंपाइल-टाइम साक्ष्य/कोड-निर्माण पाइपलाइन नहीं।
** माइग्रेशन और CI
मैक्रो बिल्ड के दौरान मौजूद स्कीमा की जाँच करता है। CI को एक अलग DB बनाना चाहिए, माइग्रेशन लागू करना चाहिए, क्वेरी को कंपाइल/तैयार करना चाहिए, और कैश किए गए साक्ष्य को सत्यापित करना चाहिए।
** खिलौने को SQLx से मैप करना
खिलौना =Description= एक बैकएंड डिस्क्राइब परिणाम के अनुरूप है; =load_description=
ऑनलाइन/ऑफ़लाइन क्वेरी डेटा चयन के अनुरूप है; =expand_query= तर्क/आउटपुट कोड निर्माण के अनुरूप है। नाम वैचारिक हैं, स्रोत-निष्ठा दावे नहीं।
** Github
[[https://github.com/chiefkemist/toy-sqlx][toy-sqlx -- SQLx का क्लोन नहीं]]

