initial commit
This commit is contained in:
commit
12d131ded0
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
9
inline-postgres-impl/Cargo.toml
Normal file
9
inline-postgres-impl/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "inline-postgres-impl"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
postgres = "0.19"
|
||||||
42
inline-postgres-impl/src/lib.rs
Normal file
42
inline-postgres-impl/src/lib.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
|
pub use postgres::*;
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
pub use super::{Fetch, Exec};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Query<'a, O> {
|
||||||
|
pub code: &'static str,
|
||||||
|
pub vals: &'a [&'a (dyn types::ToSql + Sync)],
|
||||||
|
pub _phantom: PhantomData<O>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<O> Query<'_, O> {
|
||||||
|
pub fn execute_on(self, client: &mut Client) -> Result<u64, Error> {
|
||||||
|
client.execute(self.code, self.vals)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub trait Fetch {
|
||||||
|
fn fetch<'a, O: From<row::Row>>(&mut self, query: Query<'a, O>) -> Result<Vec<O>, Error>;
|
||||||
|
}
|
||||||
|
impl Fetch for Client {
|
||||||
|
fn fetch<'a, O: From<Row>>(&mut self, query: Query<'a, O>) -> Result<Vec<O>, Error> {
|
||||||
|
let res = self
|
||||||
|
.query(query.code, query.vals)?
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect();
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Exec {
|
||||||
|
fn exec<'a, O: From<row::Row>>(&mut self, query: Query<'a, O>) -> Result<(), Error>;
|
||||||
|
}
|
||||||
|
impl Exec for Client {
|
||||||
|
fn exec<'a, O: From<row::Row>>(&mut self, query: Query<'a, O>) -> Result<(), Error> {
|
||||||
|
self.query(query.code, query.vals)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
11
inline-postgres-macros/Cargo.toml
Normal file
11
inline-postgres-macros/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "inline-postgres-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "1.0"
|
||||||
|
proc-macro2 = "1.0"
|
||||||
32
inline-postgres-macros/src/dim.rs
Normal file
32
inline-postgres-macros/src/dim.rs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
use proc_macro::{TokenStream, Span, TokenTree};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Dimensions {
|
||||||
|
pub first_line: usize,
|
||||||
|
pub last_line: usize,
|
||||||
|
pub indent: usize,
|
||||||
|
}
|
||||||
|
impl Dimensions {
|
||||||
|
pub fn from_tokens(tokens: TokenStream) -> Self {
|
||||||
|
let mut dim = Self {
|
||||||
|
first_line: usize::MAX,
|
||||||
|
last_line: usize::MIN,
|
||||||
|
indent: usize::MAX,
|
||||||
|
};
|
||||||
|
dim.visit_tokens(tokens);
|
||||||
|
dim
|
||||||
|
}
|
||||||
|
fn adjust(&mut self, span: Span) {
|
||||||
|
self.first_line = self.first_line.min(span.start().line);
|
||||||
|
self.last_line = self.last_line.max(span.end().line);
|
||||||
|
self.indent = self.indent.min(span.start().column);
|
||||||
|
}
|
||||||
|
fn visit_tokens(&mut self, tokens: TokenStream) {
|
||||||
|
for token in tokens {
|
||||||
|
self.adjust(token.span());
|
||||||
|
if let TokenTree::Group(g) = token {
|
||||||
|
self.visit_tokens(g.stream());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
inline-postgres-macros/src/lib.rs
Normal file
73
inline-postgres-macros/src/lib.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
#![feature(proc_macro_span)]
|
||||||
|
|
||||||
|
use proc_macro::{TokenStream, TokenTree};
|
||||||
|
use quote::{quote};
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
|
||||||
|
mod dim;
|
||||||
|
mod visit;
|
||||||
|
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn pg(tokens: TokenStream) -> TokenStream {
|
||||||
|
let dimensions = dim::Dimensions::from_tokens(tokens.clone());
|
||||||
|
let query_data = visit::Visitor::process_sql(dimensions, tokens);
|
||||||
|
|
||||||
|
let values: TokenStream2 = query_data
|
||||||
|
.captured_values()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|tt| {
|
||||||
|
let tt: TokenStream2 = tt.into();
|
||||||
|
quote!(&(#tt),)
|
||||||
|
})
|
||||||
|
.fold(quote!{}, |a, b| quote!{#a #b});
|
||||||
|
let values = quote!{&[#values]};
|
||||||
|
|
||||||
|
let struct_content_def: TokenStream2 = query_data
|
||||||
|
.returned_values()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|o| {
|
||||||
|
let ident: TokenStream2 = Some(TokenTree::Ident(o.ident)).into_iter().collect::<TokenStream>().into();
|
||||||
|
let typ: TokenStream2 = o.typ.into();
|
||||||
|
quote!(
|
||||||
|
#[allow(unused)]
|
||||||
|
#ident : #typ,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.fold(quote!{}, |a, b| quote!{#a #b});
|
||||||
|
|
||||||
|
let struct_content_use: TokenStream2 = query_data
|
||||||
|
.returned_values()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|o| {
|
||||||
|
let ident: TokenStream2 = Some(TokenTree::Ident(o.ident)).into_iter().collect::<TokenStream>().into();
|
||||||
|
let typ: TokenStream2 = o.typ.into();
|
||||||
|
let ident_str: TokenStream2 = format!("\"{}\"", ident).parse().unwrap();
|
||||||
|
quote!(#ident : record.get::<&'static str, #typ>(#ident_str),)
|
||||||
|
})
|
||||||
|
.fold(quote!{}, |a, b| quote!{#a #b});
|
||||||
|
|
||||||
|
|
||||||
|
let query = query_data.code();
|
||||||
|
|
||||||
|
quote!{{
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct r#struct {
|
||||||
|
#struct_content_def
|
||||||
|
}
|
||||||
|
impl From<::inline_postgres::row::Row> for r#struct {
|
||||||
|
fn from(record: ::inline_postgres::row::Row) -> Self {
|
||||||
|
r#struct {
|
||||||
|
#struct_content_use
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::inline_postgres::Query {
|
||||||
|
code: #query,
|
||||||
|
vals: #values,
|
||||||
|
_phantom: ::core::marker::PhantomData::<r#struct>,
|
||||||
|
}
|
||||||
|
}}.into()
|
||||||
|
}
|
||||||
94
inline-postgres-macros/src/visit.rs
Normal file
94
inline-postgres-macros/src/visit.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
use proc_macro::{Delimiter, Ident, Span, TokenStream, TokenTree};
|
||||||
|
|
||||||
|
use crate::dim::Dimensions;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Output {
|
||||||
|
pub ident: Ident,
|
||||||
|
pub typ: TokenStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Visitor {
|
||||||
|
current_line: usize,
|
||||||
|
current_column: usize,
|
||||||
|
dims: Dimensions,
|
||||||
|
buffer: String,
|
||||||
|
values: Vec<TokenStream>,
|
||||||
|
outs: Vec<Output>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Visitor {
|
||||||
|
pub fn code(&self) -> &str {
|
||||||
|
&self.buffer
|
||||||
|
}
|
||||||
|
pub fn captured_values(&self) -> &[TokenStream] {
|
||||||
|
&self.values
|
||||||
|
}
|
||||||
|
pub fn returned_values(&self) -> &[Output] {
|
||||||
|
&self.outs
|
||||||
|
}
|
||||||
|
pub fn process_sql(dims: Dimensions, tokens: TokenStream) -> Self {
|
||||||
|
let mut visitor = Visitor {
|
||||||
|
current_line: dims.first_line,
|
||||||
|
current_column: dims.indent,
|
||||||
|
dims,
|
||||||
|
buffer: String::new(),
|
||||||
|
values: Vec::new(),
|
||||||
|
outs: Vec::new(),
|
||||||
|
};
|
||||||
|
visitor.visit(tokens);
|
||||||
|
visitor
|
||||||
|
}
|
||||||
|
fn print(&mut self, object: &str, span: Span) {
|
||||||
|
while self.current_line < span.start().line {
|
||||||
|
self.buffer += "\n";
|
||||||
|
self.current_line += 1;
|
||||||
|
self.current_column = self.dims.indent;
|
||||||
|
}
|
||||||
|
while self.current_column < span.start().column {
|
||||||
|
self.buffer += " ";
|
||||||
|
self.current_column += 1;
|
||||||
|
}
|
||||||
|
self.buffer += object;
|
||||||
|
self.current_line = span.end().line;
|
||||||
|
self.current_column = span.end().column;
|
||||||
|
}
|
||||||
|
fn visit(&mut self, tokens: TokenStream) {
|
||||||
|
for token in tokens {
|
||||||
|
if let TokenTree::Group(group) = token {
|
||||||
|
if group.delimiter() == Delimiter::Brace {
|
||||||
|
self.values.push(group.stream());
|
||||||
|
let marker = format!("${}", self.values.len());
|
||||||
|
self.print(&marker, group.span_open());
|
||||||
|
} else if group.delimiter() == Delimiter::Bracket {
|
||||||
|
let tokens = group.stream().into_iter().collect::<Vec<TokenTree>>();
|
||||||
|
assert!(tokens.len() >= 3);
|
||||||
|
let ident = match &tokens[0] {
|
||||||
|
TokenTree::Ident(i) => i.clone(),
|
||||||
|
_ => panic!("we need an identifier here"),
|
||||||
|
};
|
||||||
|
match &tokens[1] {
|
||||||
|
TokenTree::Punct(p) => {
|
||||||
|
assert_eq!(':', p.as_char());
|
||||||
|
}
|
||||||
|
_ => panic!("expected a colon to separate the variable name and the type"),
|
||||||
|
}
|
||||||
|
let typ = tokens.into_iter().skip(2).collect::<TokenStream>();
|
||||||
|
self.print(&ident.to_string(), group.span());
|
||||||
|
self.outs.push(Output { ident, typ });
|
||||||
|
} else {
|
||||||
|
let (open, close) = match group.delimiter() {
|
||||||
|
Delimiter::Parenthesis => ("(", ")"),
|
||||||
|
Delimiter::None => ("", ""),
|
||||||
|
Delimiter::Bracket | Delimiter::Brace => unreachable!(),
|
||||||
|
};
|
||||||
|
self.print(open, group.span_open());
|
||||||
|
self.visit(group.stream());
|
||||||
|
self.print(close, group.span_close());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.print(&token.to_string(), token.span());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
inline-postgres/Cargo.toml
Normal file
10
inline-postgres/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "inline-postgres"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
inline-postgres-impl = { path = "../inline-postgres-impl" }
|
||||||
|
inline-postgres-macros = { path = "../inline-postgres-macros" }
|
||||||
18
inline-postgres/examples/output.rs
Normal file
18
inline-postgres/examples/output.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
use inline_postgres as pg;
|
||||||
|
use inline_postgres::prelude::*;
|
||||||
|
|
||||||
|
fn main() -> Result<(), pg::Error> {
|
||||||
|
let mut client = pg::Client::connect("host=localhost, user=postgres", pg::NoTls)?;
|
||||||
|
|
||||||
|
let x = 5;
|
||||||
|
|
||||||
|
let rows = client.fetch(pg! {
|
||||||
|
select 1 as [a:i32], generate_series(1, {x}) as [b: i32]
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
println!("{row:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
14
inline-postgres/examples/parametrized.rs
Normal file
14
inline-postgres/examples/parametrized.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
use inline_postgres as pg;
|
||||||
|
use inline_postgres::prelude::*;
|
||||||
|
|
||||||
|
fn main() -> Result<(), pg::Error> {
|
||||||
|
let mut client = pg::Client::connect("host=localhost, user=postgres", pg::NoTls)?;
|
||||||
|
|
||||||
|
let x = 10;
|
||||||
|
|
||||||
|
client.exec(pg! {
|
||||||
|
select 1, generate_series(1, {x}) as number
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
12
inline-postgres/examples/plain.rs
Normal file
12
inline-postgres/examples/plain.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
use inline_postgres as pg;
|
||||||
|
use inline_postgres::prelude::*;
|
||||||
|
|
||||||
|
fn main() -> Result<(), pg::Error> {
|
||||||
|
let mut client = pg::Client::connect("host=localhost, user=postgres", pg::NoTls)?;
|
||||||
|
|
||||||
|
client.exec(pg! {
|
||||||
|
select 1, generate_series(1, 10) as x
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
6
inline-postgres/src/lib.rs
Normal file
6
inline-postgres/src/lib.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
pub use inline_postgres_impl::*;
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
pub use inline_postgres_impl::prelude::*;
|
||||||
|
pub use inline_postgres_macros::*;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user