commit 12d131ded08996df1dd87c20c9a125c8089dbb9b Author: Jonas Maier <> Date: Sat Mar 11 13:55:34 2023 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6985cf1 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/inline-postgres-impl/Cargo.toml b/inline-postgres-impl/Cargo.toml new file mode 100644 index 0000000..64e043b --- /dev/null +++ b/inline-postgres-impl/Cargo.toml @@ -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" \ No newline at end of file diff --git a/inline-postgres-impl/src/lib.rs b/inline-postgres-impl/src/lib.rs new file mode 100644 index 0000000..07b4068 --- /dev/null +++ b/inline-postgres-impl/src/lib.rs @@ -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, +} + +impl Query<'_, O> { + pub fn execute_on(self, client: &mut Client) -> Result { + client.execute(self.code, self.vals) + } +} +pub trait Fetch { + fn fetch<'a, O: From>(&mut self, query: Query<'a, O>) -> Result, Error>; +} +impl Fetch for Client { + fn fetch<'a, O: From>(&mut self, query: Query<'a, O>) -> Result, 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>(&mut self, query: Query<'a, O>) -> Result<(), Error>; +} +impl Exec for Client { + fn exec<'a, O: From>(&mut self, query: Query<'a, O>) -> Result<(), Error> { + self.query(query.code, query.vals)?; + Ok(()) + } +} diff --git a/inline-postgres-macros/Cargo.toml b/inline-postgres-macros/Cargo.toml new file mode 100644 index 0000000..97582ba --- /dev/null +++ b/inline-postgres-macros/Cargo.toml @@ -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" \ No newline at end of file diff --git a/inline-postgres-macros/src/dim.rs b/inline-postgres-macros/src/dim.rs new file mode 100644 index 0000000..a2df844 --- /dev/null +++ b/inline-postgres-macros/src/dim.rs @@ -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()); + } + } + } +} \ No newline at end of file diff --git a/inline-postgres-macros/src/lib.rs b/inline-postgres-macros/src/lib.rs new file mode 100644 index 0000000..88cfac5 --- /dev/null +++ b/inline-postgres-macros/src/lib.rs @@ -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::().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::().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::, + } + }}.into() +} \ No newline at end of file diff --git a/inline-postgres-macros/src/visit.rs b/inline-postgres-macros/src/visit.rs new file mode 100644 index 0000000..3bf7202 --- /dev/null +++ b/inline-postgres-macros/src/visit.rs @@ -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, + outs: Vec, +} + +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::>(); + 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::(); + 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()); + } + } + } +} \ No newline at end of file diff --git a/inline-postgres/Cargo.toml b/inline-postgres/Cargo.toml new file mode 100644 index 0000000..50d4e47 --- /dev/null +++ b/inline-postgres/Cargo.toml @@ -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" } \ No newline at end of file diff --git a/inline-postgres/examples/output.rs b/inline-postgres/examples/output.rs new file mode 100644 index 0000000..ec47ebe --- /dev/null +++ b/inline-postgres/examples/output.rs @@ -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(()) +} diff --git a/inline-postgres/examples/parametrized.rs b/inline-postgres/examples/parametrized.rs new file mode 100644 index 0000000..b3e2c15 --- /dev/null +++ b/inline-postgres/examples/parametrized.rs @@ -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(()) +} \ No newline at end of file diff --git a/inline-postgres/examples/plain.rs b/inline-postgres/examples/plain.rs new file mode 100644 index 0000000..c98c701 --- /dev/null +++ b/inline-postgres/examples/plain.rs @@ -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(()) +} \ No newline at end of file diff --git a/inline-postgres/src/lib.rs b/inline-postgres/src/lib.rs new file mode 100644 index 0000000..a0a2d39 --- /dev/null +++ b/inline-postgres/src/lib.rs @@ -0,0 +1,6 @@ +pub use inline_postgres_impl::*; + +pub mod prelude { + pub use inline_postgres_impl::prelude::*; + pub use inline_postgres_macros::*; +} \ No newline at end of file