From 93fdf91c6e0eecee75a75b3e907e1bef1e8f8d78 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Wed, 22 May 2024 18:43:23 +0100 Subject: [PATCH 1/6] feat: Initial One-to-Many support --- geekorm-core/src/builder/columntypes.rs | 5 ++ geekorm-core/src/builder/table.rs | 1 + geekorm-core/src/builder/values.rs | 56 +++++++++++--- geekorm-derive/README.md | 27 ++++++- geekorm-derive/build.rs | 7 +- geekorm-derive/src/derive.rs | 1 + geekorm-derive/src/derive/column.rs | 30 ++++++-- geekorm-derive/src/derive/multitable.rs | 88 ++++++++++++++++++++++ geekorm-derive/src/derive/table.rs | 9 +++ geekorm-derive/src/parsers.rs | 19 ++--- geekorm-derive/src/parsers/tablebuilder.rs | 20 +++-- 11 files changed, 220 insertions(+), 43 deletions(-) create mode 100644 geekorm-derive/src/derive/multitable.rs diff --git a/geekorm-core/src/builder/columntypes.rs b/geekorm-core/src/builder/columntypes.rs index 74c8a1a..623a7b2 100644 --- a/geekorm-core/src/builder/columntypes.rs +++ b/geekorm-core/src/builder/columntypes.rs @@ -11,6 +11,8 @@ pub enum ColumnType { Identifier(ColumnTypeOptions), /// Foreign Key column type with the table name ForeignKey(ColumnTypeOptions), + /// One to many column type with options + OneToMany(ColumnTypeOptions), /// Text column type with options Text(ColumnTypeOptions), /// Integer column type with options @@ -26,6 +28,7 @@ impl Display for ColumnType { match self { ColumnType::Identifier(_) => write!(f, "PrimaryKey"), ColumnType::ForeignKey(fk) => write!(f, "ForeignKey<{}>", fk), + ColumnType::OneToMany(_) => write!(f, "OneToMany"), ColumnType::Text(_) => write!(f, "Text"), ColumnType::Integer(_) => write!(f, "Integer"), ColumnType::Boolean(_) => write!(f, "Boolean"), @@ -76,6 +79,8 @@ impl ToSqlite for ColumnType { } format!("BLOB {}", options.on_create(query)?) } + // Blank + ColumnType::OneToMany(_) => String::new(), }) } } diff --git a/geekorm-core/src/builder/table.rs b/geekorm-core/src/builder/table.rs index aaf83fd..50fee96 100644 --- a/geekorm-core/src/builder/table.rs +++ b/geekorm-core/src/builder/table.rs @@ -80,6 +80,7 @@ impl ToSqlite for Table { self.columns .columns .iter() + .filter(|col| !col.skip) .map(|col| col.name.clone()) .collect() }; diff --git a/geekorm-core/src/builder/values.rs b/geekorm-core/src/builder/values.rs index 0bf1a8b..e5407dd 100644 --- a/geekorm-core/src/builder/values.rs +++ b/geekorm-core/src/builder/values.rs @@ -240,18 +240,6 @@ impl From for Value { } } -impl From> for Value { - fn from(value: Vec) -> Self { - Value::Blob(serde_json::to_vec(&value).unwrap()) - } -} - -impl From<&Vec> for Value { - fn from(value: &Vec) -> Self { - Value::Blob(serde_json::to_vec(value).unwrap()) - } -} - impl From> for Value { fn from(value: Vec) -> Self { Value::Blob(value) @@ -264,6 +252,50 @@ impl From<&Vec> for Value { } } +impl From> for Value +where + T: Into, +{ + fn from(value: std::vec::Vec) -> Self { + Value::Blob( + value + .into_iter() + .map(|value| value.into()) + .flat_map(|value| match value { + Value::Text(value) => value.into_bytes(), + Value::Integer(value) => value.to_string().into_bytes(), + Value::Boolean(value) => value.to_string().into_bytes(), + Value::Identifier(value) => value.into_bytes(), + Value::Blob(value) => value, + Value::Null => Vec::new(), + }) + .collect(), + ) + } +} + +impl From<&std::vec::Vec> for Value +where + T: Into + Clone, +{ + fn from(value: &std::vec::Vec) -> Self { + Value::Blob( + value + .iter() + .map(|value| value.clone().into()) + .flat_map(|value| match value { + Value::Text(value) => value.into_bytes(), + Value::Integer(value) => value.to_string().into_bytes(), + Value::Boolean(value) => value.to_string().into_bytes(), + Value::Identifier(value) => value.into_bytes(), + Value::Blob(value) => value, + Value::Null => Vec::new(), + }) + .collect(), + ) + } +} + /// Serialize a Value impl Serialize for Value { fn serialize(&self, serializer: S) -> Result diff --git a/geekorm-derive/README.md b/geekorm-derive/README.md index 39a728a..32349df 100644 --- a/geekorm-derive/README.md +++ b/geekorm-derive/README.md @@ -50,6 +50,29 @@ user.name = String::from("42ByteLabs"); let update = Users::update(&user); ``` +### One-to-Many + +```rust +use geekorm::prelude::*; +use geekorm::{GeekTable, PrimaryKeyInteger}; + +#[derive(GeekTable, Default)] +struct Users { + id: PrimaryKeyInteger, + + #[geekorm(foreign_key = "Sessions.id")] + sessions: Vec, +} + +#[derive(GeekTable, Default)] +struct Sessions { + id: PrimaryKeyInteger, + + #[geekorm(rand, rand_length = 42)] + session: String, +} +``` + ## Feature - Automatic New Struct Function When the `new` feature is enabled, the following methods are generated for the struct: @@ -177,5 +200,7 @@ if user.check_password("newpassword")? { - `hash` or `password`: Sets the String field as a hashable value - `hash_algorithm`: Set the algorithm to use - - Default: `Pbkdf2` + - `Pbkdf2` (default) + - `Argon2` + - `Sha512` diff --git a/geekorm-derive/build.rs b/geekorm-derive/build.rs index 94a7cb8..2ade666 100644 --- a/geekorm-derive/build.rs +++ b/geekorm-derive/build.rs @@ -1,13 +1,8 @@ fn main() { - let compile_time = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos(); - let state_dir = std::env::var("OUT_DIR").unwrap(); let cargo_bin_name = std::env::var("CARGO_BIN_NAME").unwrap_or_else(|_| "geekorm".to_string()); - let state_file = format!("geekorm-{}-{}.json", cargo_bin_name, compile_time); + let state_file = format!("geekorm-{}.json", cargo_bin_name); println!( "cargo:rustc-env=GEEKORM_STATE_FILE={}", diff --git a/geekorm-derive/src/derive.rs b/geekorm-derive/src/derive.rs index 4bcd093..6c5e43a 100644 --- a/geekorm-derive/src/derive.rs +++ b/geekorm-derive/src/derive.rs @@ -13,6 +13,7 @@ use uuid::Uuid; mod column; mod columntypes; +mod multitable; mod table; use crate::{ diff --git a/geekorm-derive/src/derive/column.rs b/geekorm-derive/src/derive/column.rs index 8d733d8..eb231d3 100644 --- a/geekorm-derive/src/derive/column.rs +++ b/geekorm-derive/src/derive/column.rs @@ -17,7 +17,9 @@ use crate::{ internal::TableState, }; -#[derive(Debug, Clone)] +use super::{multitable::one_to_many, TableDerive}; + +#[derive(Debug, Clone, Default)] pub(crate) struct ColumnsDerive { pub(crate) columns: Vec, } @@ -144,7 +146,10 @@ pub(crate) struct ColumnDerive { impl ColumnDerive { #[allow(irrefutable_let_patterns, clippy::collapsible_match)] - pub(crate) fn apply_attributes(&mut self) -> Result<(), syn::Error> { + pub(crate) fn apply_attributes( + &mut self, + table_derive: &TableDerive, + ) -> Result<(), syn::Error> { let attributes = &self.attributes; for attr in attributes { @@ -198,6 +203,17 @@ impl ColumnDerive { foreign_key: format!("{}.{}", table, column), ..Default::default() }); + + if let Type::Path(TypePath { path, .. }) = &self.itype { + if let Some(segment) = path.segments.first() { + if segment.ident.to_string().as_str() == "Vec" { + // Skip, this isn't a column in the parent table + self.skip = true; + // This is One-to-Many + one_to_many(table_derive, self, table)?; + } + } + } } } } @@ -208,7 +224,7 @@ impl ColumnDerive { .map(|a| { if let Some(value) = &a.value { if let GeekAttributeValue::Int(len) = value { - len.clone() as usize + *len as usize } else { 32 } @@ -558,10 +574,8 @@ impl From for geekorm_core::Column { } } -impl TryFrom<&Field> for ColumnDerive { - type Error = syn::Error; - - fn try_from(value: &Field) -> Result { +impl ColumnDerive { + pub(crate) fn parse(value: &Field, table: &TableDerive) -> Result { let name: Ident = match &value.ident { Some(ident) => ident.clone(), None => { @@ -592,7 +606,7 @@ impl TryFrom<&Field> for ColumnDerive { skip: false, mode: None, }; - col.apply_attributes()?; + col.apply_attributes(table)?; // TODO(geekmasher): Check if the column is public // if let Some(ref mode) = col.mode { diff --git a/geekorm-derive/src/derive/multitable.rs b/geekorm-derive/src/derive/multitable.rs new file mode 100644 index 0000000..266098e --- /dev/null +++ b/geekorm-derive/src/derive/multitable.rs @@ -0,0 +1,88 @@ +//! MultiTable module + +use geekorm_core::{Column, ColumnType, ColumnTypeOptions, Table}; + +use crate::internal::TableState; + +use super::{ColumnDerive, ColumnTypeDerive, ColumnTypeOptionsDerive, ColumnsDerive, TableDerive}; + +/// +/// +/// # Table +/// +/// UsersSession { +/// id: u64, +/// user_id: u64, +/// session_id: u64, +/// } +pub(crate) fn one_to_many( + table: &TableDerive, + column: &ColumnDerive, + foreign_table: &str, +) -> Result { + let table_name = format!("{}_{}", table.name, foreign_table); + + let mut new_table = Table { + name: table_name, + columns: Default::default(), + }; + + new_table.columns.columns.push(Column { + name: String::from("id"), + column_type: ColumnType::Identifier(ColumnTypeOptions { + primary_key: true, + unique: true, + not_null: true, + auto_increment: true, + foreign_key: String::new(), + }), + ..Default::default() + }); + + let table_pk = match table.get_primary_key() { + Some(pk) => pk, + None => { + return Err(syn::Error::new_spanned( + table, + format!( + "Table `{}` must have a primary key to create a one-to-many relationship", + table.name + ), + )) + } + }; + + new_table.columns.columns.push(Column { + name: format!("{}_id", table.name.to_lowercase()), + column_type: ColumnType::ForeignKey(ColumnTypeOptions { + foreign_key: format!("{}.{}", table.name, table_pk.name), + not_null: true, + ..Default::default() + }), + ..Default::default() + }); + + let foreign_table_key = match column.coltype { + ColumnTypeDerive::ForeignKey(ref key) => key.foreign_key.clone(), + _ => { + return Err(syn::Error::new_spanned( + column, + "Column must be a foreign key to create a one-to-many relationship", + )) + } + }; + + new_table.columns.columns.push(Column { + name: format!("{}_id", foreign_table.to_lowercase()), + column_type: ColumnType::ForeignKey(ColumnTypeOptions { + foreign_key: foreign_table_key, + not_null: true, + ..Default::default() + }), + ..Default::default() + }); + + TableState::add(new_table.clone()); + + Ok(new_table) +} diff --git a/geekorm-derive/src/derive/table.rs b/geekorm-derive/src/derive/table.rs index 4de4773..26d54c3 100644 --- a/geekorm-derive/src/derive/table.rs +++ b/geekorm-derive/src/derive/table.rs @@ -18,6 +18,15 @@ pub(crate) struct TableDerive { } impl TableDerive { + pub(crate) fn get_primary_key(&self) -> Option { + for column in &self.columns.columns { + if column.is_primary_key() { + return Some(column.clone()); + } + } + None + } + #[allow(irrefutable_let_patterns)] pub(crate) fn apply_attributes(&mut self, attributes: &Vec) { for attr in attributes { diff --git a/geekorm-derive/src/parsers.rs b/geekorm-derive/src/parsers.rs index 95f8f54..6d72c39 100644 --- a/geekorm-derive/src/parsers.rs +++ b/geekorm-derive/src/parsers.rs @@ -34,25 +34,26 @@ pub(crate) fn derive_parser(ast: &DeriveInput) -> Result { let mut errors: Vec = Vec::new(); - let mut columns: Vec = Vec::new(); + + let mut table = TableDerive { + name: name.to_string(), + columns: ColumnsDerive::default(), + }; for field in fields.named.iter() { - match ColumnDerive::try_from(field) { - Ok(column) => columns.push(column), + match ColumnDerive::parse(field, &table) { + Ok(column) => { + table.columns.columns.push(column); + } Err(err) => errors.push(err), } } - - let mut table = TableDerive { - name: name.to_string(), - columns: ColumnsDerive::from(columns), - }; table.apply_attributes(&attributes); TableState::add(table.clone().into()); // Generate for the whole table - let mut tokens = generate_struct(name, &fields, &ast.generics, table)?; + let mut tokens = generate_struct(name, fields, &ast.generics, table)?; if !errors.is_empty() { for error in errors { diff --git a/geekorm-derive/src/parsers/tablebuilder.rs b/geekorm-derive/src/parsers/tablebuilder.rs index 6b85642..7577632 100644 --- a/geekorm-derive/src/parsers/tablebuilder.rs +++ b/geekorm-derive/src/parsers/tablebuilder.rs @@ -100,6 +100,9 @@ pub fn generate_query_builder( for column in table.columns.columns.iter() { let name = &column.name; let ident = syn::Ident::new(name.as_str(), name.span()); + if column.skip { + continue; + } insert_values.extend(quote! { .add_value(#name, &item.#ident) }); @@ -287,15 +290,18 @@ pub fn generate_table_fetch( syn::GenericArgument::Type(Type::Path(path)) => { let fident = path.path.segments.first().unwrap().ident.clone(); - stream.extend(column.get_fetcher(ident, &fident)); + // TODO check other types? + if field_type.ident.to_string().as_str() == "ForeignKey" { + stream.extend(column.get_fetcher(ident, &fident)); - // Add fetch function to the list of fetch functions - let func_name = format!("fetch_{}", column.identifier); - let func = Ident::new(&func_name, Span::call_site()); + // Add fetch function to the list of fetch functions + let func_name = format!("fetch_{}", column.identifier); + let func = Ident::new(&func_name, Span::call_site()); - fetch_functions.extend(quote! { - Self::#func(self, connection).await?; - }); + fetch_functions.extend(quote! { + Self::#func(self, connection).await?; + }); + } } _ => { return Err(syn::Error::new( From 8bd034eb3e8c2d585fffa894329b0fb381219592 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Wed, 22 May 2024 20:45:58 +0100 Subject: [PATCH 2/6] feat(example): Add One-to-Many example --- examples/one-to-many/Cargo.toml | 13 +++++++ examples/one-to-many/src/main.rs | 58 ++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 examples/one-to-many/Cargo.toml create mode 100644 examples/one-to-many/src/main.rs diff --git a/examples/one-to-many/Cargo.toml b/examples/one-to-many/Cargo.toml new file mode 100644 index 0000000..1b963f9 --- /dev/null +++ b/examples/one-to-many/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "one-to-many" +version.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true +keywords.workspace = true +categories.workspace = true +documentation.workspace = true +repository.workspace = true +authors.workspace = true + +[dependencies] diff --git a/examples/one-to-many/src/main.rs b/examples/one-to-many/src/main.rs new file mode 100644 index 0000000..bc0ae2b --- /dev/null +++ b/examples/one-to-many/src/main.rs @@ -0,0 +1,58 @@ +use anyhow::Result; + +use geekorm::prelude::*; +use geekorm::PrimaryKey; + +#[derive(GeekTable, Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +struct Users { + id: PrimaryKey, + username: String, + + #[geekorm(foreign_key = "Sessions.id")] + sessions: Vec, +} + +#[derive(GeekTable, Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +struct Sessions { + id: PrimaryKey, + + #[geekorm(rand, rand_prefix = "session")] + token: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + init(); + // Initialize an in-memory database + let db = libsql::Builder::new_local(":memory:").build().await?; + // let db = libsql::Builder::new_local("/tmp/turso-testing.sqlite").build().await?; + let conn = db.connect()?; + + let tables = tables!(); + + let mut user = Users::new("geekmasher"); + let session = Sessions::new(); + + user.sessions.push(session); + + println!("{:?}", user); + + Ok(()) +} + +fn init() { + println!( + "{} - v{}\n", + geekorm::GEEKORM_BANNER, + geekorm::GEEKORM_VERSION + ); + println!("Turso LibSQL Example\n{:=<40}\n", "="); + let debug_env: bool = std::env::var("DEBUG").is_ok(); + env_logger::builder() + .filter_level(if debug_env { + log::LevelFilter::Debug + } else { + log::LevelFilter::Info + }) + .init(); +} From 6e61a5f1e8f4724365263fde78fd9293bafaff6d Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Wed, 22 May 2024 20:46:20 +0100 Subject: [PATCH 3/6] feat(examples): Update Cargo --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 21b876a..d546b7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,11 @@ name = "geekorm-example-foreignkeys" path = "./examples/foreignkeys/src/main.rs" required-features = ["new", "helpers"] +[[example]] +name = "geekorm-example-one-to-many" +path = "./examples/one-to-many/src/main.rs" +required-features = ["new", "helpers", "rand", "libsql"] + [[example]] name = "geekorm-example-turso-libsql" path = "./examples/turso-libsql/src/main.rs" From 027be3ee910342442b9907ff53b244d8eca3d932 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Wed, 22 May 2024 20:46:59 +0100 Subject: [PATCH 4/6] feat: Add tables macro and improve builders --- geekorm-core/src/builder/columns.rs | 20 +++++++++ geekorm-core/src/builder/columntypes.rs | 6 +++ geekorm-core/src/builder/table.rs | 30 +++++++++++--- geekorm-core/src/queries/builder.rs | 2 + geekorm-derive/src/derive/column.rs | 24 ++++++++++- geekorm-derive/src/derive/columntypes.rs | 42 ++++++++++++++++++- geekorm-derive/src/derive/multitable.rs | 25 +++++++++--- geekorm-derive/src/derive/table.rs | 9 ++++ geekorm-derive/src/lib.rs | 52 ++++++++++++++++++++++++ src/lib.rs | 4 ++ 10 files changed, 201 insertions(+), 13 deletions(-) diff --git a/geekorm-core/src/builder/columns.rs b/geekorm-core/src/builder/columns.rs index 5cc1d8e..43a88db 100644 --- a/geekorm-core/src/builder/columns.rs +++ b/geekorm-core/src/builder/columns.rs @@ -38,6 +38,7 @@ impl Columns { pub fn get_foreign_keys(&self) -> Vec<&Column> { self.columns .iter() + .filter(|col| !col.skip) .filter(|col| matches!(col.column_type, ColumnType::ForeignKey(_))) .collect() } @@ -165,6 +166,25 @@ impl Column { pub fn is_primary_key(&self) -> bool { self.column_type.is_primary_key() } + + /// Get the foreign key of a column + pub fn foreign_key(&self) -> Option { + match &self.column_type { + ColumnType::ForeignKey(opts) => Some(opts.foreign_key.clone()), + _ => None, + } + } + + /// Get the foreign key table name + pub fn foreign_key_table(&self) -> Option { + match &self.column_type { + ColumnType::ForeignKey(opts) => { + let (table, _) = opts.foreign_key.split_once('.').unwrap(); + Some(table.to_string()) + } + _ => None, + } + } } impl Default for Column { diff --git a/geekorm-core/src/builder/columntypes.rs b/geekorm-core/src/builder/columntypes.rs index 623a7b2..a7ff3fe 100644 --- a/geekorm-core/src/builder/columntypes.rs +++ b/geekorm-core/src/builder/columntypes.rs @@ -194,6 +194,12 @@ impl ToSqlite for ColumnTypeOptions { } } +impl From<&ColumnTypeOptions> for String { + fn from(options: &ColumnTypeOptions) -> Self { + options.to_string() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/geekorm-core/src/builder/table.rs b/geekorm-core/src/builder/table.rs index 50fee96..bde0f1d 100644 --- a/geekorm-core/src/builder/table.rs +++ b/geekorm-core/src/builder/table.rs @@ -1,4 +1,4 @@ -use crate::{Columns, QueryBuilder, ToSqlite, Values}; +use crate::{Columns, Query, QueryBuilder, ToSqlite, Values}; use serde::{Deserialize, Serialize}; use std::fmt::Display; @@ -27,6 +27,11 @@ impl Table { .unwrap_or_else(|| String::from("id")) } + /// Get all foreign keys in the table + pub fn get_foreign_keys(&self) -> Vec<&crate::Column> { + self.columns.get_foreign_keys() + } + /// Get the foreign key by table name pub fn get_foreign_key(&self, table_name: String) -> &crate::Column { for column in self.columns.get_foreign_keys() { @@ -49,15 +54,28 @@ impl Table { }; Ok(format!("{}.{}", self.name, name)) } + + /// Create Query + pub fn create(&self) -> Result { + QueryBuilder::create().table(self.clone()).build() + } } impl ToSqlite for Table { fn on_create(&self, query: &QueryBuilder) -> Result { - Ok(format!( - "CREATE TABLE IF NOT EXISTS {} {};", - self.name, - self.columns.on_create(query)? - )) + let mut queries = Vec::new(); + + if query.pivot_tables.is_empty() { + queries.push(format!( + "CREATE TABLE IF NOT EXISTS {} {};", + self.name, + self.columns.on_create(query)? + )); + } else { + todo!("Pivot tables are not yet supported"); + } + + Ok(queries.join("\n")) } fn on_select(&self, qb: &QueryBuilder) -> Result { diff --git a/geekorm-core/src/queries/builder.rs b/geekorm-core/src/queries/builder.rs index 92fe0b1..8bdce93 100644 --- a/geekorm-core/src/queries/builder.rs +++ b/geekorm-core/src/queries/builder.rs @@ -64,6 +64,8 @@ use crate::{ #[derive(Debug, Clone, Default)] pub struct QueryBuilder { pub(crate) table: Table, + /// Vector of tables to pivot + pub(crate) pivot_tables: Vec, pub(crate) query_type: QueryType, /// If a query should use aliases pub(crate) aliases: bool, diff --git a/geekorm-derive/src/derive/column.rs b/geekorm-derive/src/derive/column.rs index eb231d3..7eb8ba4 100644 --- a/geekorm-derive/src/derive/column.rs +++ b/geekorm-derive/src/derive/column.rs @@ -1,4 +1,4 @@ -use geekorm_core::{utils::crypto::HashingAlgorithm, ColumnType}; +use geekorm_core::{utils::crypto::HashingAlgorithm, Column, ColumnType, Columns}; use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; use std::{ @@ -118,6 +118,13 @@ impl From> for ColumnsDerive { } } +impl From<&Columns> for ColumnsDerive { + fn from(value: &Columns) -> Self { + let columns = value.columns.iter().map(|c| c.into()).collect(); + ColumnsDerive { columns } + } +} + #[derive(Debug, Clone)] pub(crate) enum ColumnMode { Rand { @@ -574,6 +581,21 @@ impl From for geekorm_core::Column { } } +impl From<&Column> for ColumnDerive { + fn from(value: &Column) -> Self { + ColumnDerive { + name: value.name.clone(), + coltype: (&value.column_type).into(), + alias: value.alias.clone(), + skip: value.skip, + attributes: Vec::new(), + identifier: Ident::new(&value.name, Span::call_site()), + itype: syn::parse_quote! { String }, + mode: None, + } + } +} + impl ColumnDerive { pub(crate) fn parse(value: &Field, table: &TableDerive) -> Result { let name: Ident = match &value.ident { diff --git a/geekorm-derive/src/derive/columntypes.rs b/geekorm-derive/src/derive/columntypes.rs index 3567dc2..5abc07a 100644 --- a/geekorm-derive/src/derive/columntypes.rs +++ b/geekorm-derive/src/derive/columntypes.rs @@ -1,3 +1,4 @@ +use geekorm_core::{ColumnType, ColumnTypeOptions}; use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; use std::{ @@ -9,11 +10,12 @@ use syn::{GenericArgument, Ident, Type, TypePath}; #[derive(Debug, Clone)] pub(crate) enum ColumnTypeDerive { Identifier(ColumnTypeOptionsDerive), + ForeignKey(ColumnTypeOptionsDerive), + OneToMany(ColumnTypeOptionsDerive), Text(ColumnTypeOptionsDerive), Integer(ColumnTypeOptionsDerive), Boolean(ColumnTypeOptionsDerive), Blob(ColumnTypeOptionsDerive), - ForeignKey(ColumnTypeOptionsDerive), } impl ToTokens for ColumnTypeDerive { @@ -47,6 +49,9 @@ impl ToTokens for ColumnTypeDerive { ColumnTypeDerive::ForeignKey(options) => tokens.extend(quote! { geekorm::ColumnType::ForeignKey(#options) }), + ColumnTypeDerive::OneToMany(options) => tokens.extend(quote! { + geekorm::ColumnType::OneToMany(#options) + }), } } } @@ -57,6 +62,9 @@ impl From for geekorm_core::ColumnType { ColumnTypeDerive::Identifier(options) => { geekorm_core::ColumnType::Identifier(options.into()) } + ColumnTypeDerive::OneToMany(options) => { + geekorm_core::ColumnType::OneToMany(options.into()) + } ColumnTypeDerive::Text(options) => geekorm_core::ColumnType::Text(options.into()), ColumnTypeDerive::Integer(options) => geekorm_core::ColumnType::Integer(options.into()), ColumnTypeDerive::Boolean(options) => geekorm_core::ColumnType::Boolean(options.into()), @@ -68,6 +76,26 @@ impl From for geekorm_core::ColumnType { } } +impl From<&ColumnType> for ColumnTypeDerive { + fn from(coltype: &ColumnType) -> Self { + match coltype { + geekorm_core::ColumnType::Identifier(options) => { + ColumnTypeDerive::Identifier(options.into()) + } + geekorm_core::ColumnType::ForeignKey(options) => { + ColumnTypeDerive::ForeignKey(options.into()) + } + geekorm_core::ColumnType::OneToMany(options) => { + ColumnTypeDerive::OneToMany(options.into()) + } + geekorm_core::ColumnType::Text(options) => ColumnTypeDerive::Text(options.into()), + geekorm_core::ColumnType::Integer(options) => ColumnTypeDerive::Integer(options.into()), + geekorm_core::ColumnType::Boolean(options) => ColumnTypeDerive::Boolean(options.into()), + geekorm_core::ColumnType::Blob(options) => ColumnTypeDerive::Blob(options.into()), + } + } +} + impl TryFrom<&Type> for ColumnTypeDerive { type Error = syn::Error; @@ -226,3 +254,15 @@ impl From for geekorm_core::ColumnTypeOptions { } } } + +impl From<&ColumnTypeOptions> for ColumnTypeOptionsDerive { + fn from(opts: &ColumnTypeOptions) -> ColumnTypeOptionsDerive { + ColumnTypeOptionsDerive { + primary_key: opts.primary_key, + foreign_key: opts.foreign_key.clone(), + unique: opts.unique, + not_null: opts.not_null, + auto_increment: opts.auto_increment, + } + } +} diff --git a/geekorm-derive/src/derive/multitable.rs b/geekorm-derive/src/derive/multitable.rs index 266098e..64c3d61 100644 --- a/geekorm-derive/src/derive/multitable.rs +++ b/geekorm-derive/src/derive/multitable.rs @@ -6,15 +6,30 @@ use crate::internal::TableState; use super::{ColumnDerive, ColumnTypeDerive, ColumnTypeOptionsDerive, ColumnsDerive, TableDerive}; +/// Create a One-to-Many relationship between two tables by creating a new table +/// with the primary key of the first and second tables are foreign keys in the third table. /// +/// ```rust +/// use geekorm::prelude::*; +/// use geekorm::PrimaryKey; /// -/// # Table +/// #[derive(GeekTable)] +/// struct Users { +/// id: PrimaryKey, +/// username: String, /// -/// UsersSession { -/// id: u64, -/// user_id: u64, -/// session_id: u64, +/// #[geekorm(foreign_key = "Sessions.id")] +/// sessions: Vec, /// } +/// +/// #[derive(GeekTable)] +/// struct Sessions { +/// id: PrimaryKey, +/// #[geekorm(rand)] +/// token: String +/// } +/// +/// ``` pub(crate) fn one_to_many( table: &TableDerive, column: &ColumnDerive, diff --git a/geekorm-derive/src/derive/table.rs b/geekorm-derive/src/derive/table.rs index 26d54c3..4d0aac2 100644 --- a/geekorm-derive/src/derive/table.rs +++ b/geekorm-derive/src/derive/table.rs @@ -69,3 +69,12 @@ impl From for Table { } } } + +impl From<&Table> for TableDerive { + fn from(value: &Table) -> Self { + TableDerive { + name: value.name.clone(), + columns: (&value.columns).into(), + } + } +} diff --git a/geekorm-derive/src/lib.rs b/geekorm-derive/src/lib.rs index 5415f2a..2795ec1 100644 --- a/geekorm-derive/src/lib.rs +++ b/geekorm-derive/src/lib.rs @@ -19,11 +19,14 @@ mod errors; mod internal; mod parsers; +use geekorm_core::Table; use parsers::derive_parser; use proc_macro::TokenStream; use quote::{quote, ToTokens}; use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Fields}; +use crate::derive::TableDerive; + /// Derive macro for `GeekTable` trait. /// /// This macro will generate the implementation of `GeekTable` trait for the given struct. @@ -55,3 +58,52 @@ pub fn table_derive(input: TokenStream) -> TokenStream { derive_parser(&ast).unwrap().into() } + +#[proc_macro] +pub fn tables(_input: TokenStream) -> TokenStream { + let state = internal::TableState::load_state_file(); + + let mut table_names: Vec = Vec::new(); + let mut tables: Vec
= Vec::new(); + + // TODO: Maybe this could be better? + let mut index = 0; + while tables.len() != state.tables.len() { + let table = state.tables.get(index).unwrap(); + + if !table_names.contains(&table.name) { + let fkeys = table.get_foreign_keys(); + + if fkeys.is_empty() { + tables.push(table.clone()); + table_names.push(table.name.clone()); + } else if fkeys + .iter() + .all(|fkey| table_names.contains(&fkey.foreign_key_table().unwrap())) + { + tables.push(table.clone()); + table_names.push(table.name.clone()); + } + } + + if index == state.tables.len() - 1 { + index = 0; + } else { + index += 1; + } + } + + let mut tables_ast = proc_macro2::TokenStream::new(); + + tables.iter().for_each(|table| { + let derive_table: TableDerive = TableDerive::from(table); + tables_ast.extend(quote! { + #derive_table , + }); + }); + + quote! { + vec![ #tables_ast ] + } + .into() +} diff --git a/src/lib.rs b/src/lib.rs index 347472e..0c05e25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,8 @@ pub mod utils { } // Derive Crate +/// Tables Proc Macro +pub use geekorm_derive::tables; /// GeekTable Derive Macro pub use geekorm_derive::GeekTable; @@ -56,6 +58,8 @@ pub mod prelude { /// GeekTable pub use crate::GeekTable; + // Tables + pub use geekorm_derive::tables; // Traits From f961ef219157f009b820fa237d93519a6c4500a9 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Thu, 23 May 2024 23:12:55 +0100 Subject: [PATCH 5/6] feat: Update multitable and one-to-many examples --- examples/one-to-many/src/main.rs | 30 ++++++++++++++++++------- geekorm-derive/src/derive/multitable.rs | 4 +--- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/examples/one-to-many/src/main.rs b/examples/one-to-many/src/main.rs index bc0ae2b..8ab741d 100644 --- a/examples/one-to-many/src/main.rs +++ b/examples/one-to-many/src/main.rs @@ -9,6 +9,7 @@ struct Users { username: String, #[geekorm(foreign_key = "Sessions.id")] + #[serde(skip)] sessions: Vec, } @@ -22,25 +23,25 @@ struct Sessions { #[tokio::main] async fn main() -> Result<()> { - init(); - // Initialize an in-memory database - let db = libsql::Builder::new_local(":memory:").build().await?; - // let db = libsql::Builder::new_local("/tmp/turso-testing.sqlite").build().await?; - let conn = db.connect()?; - - let tables = tables!(); + let conn = init().await?; let mut user = Users::new("geekmasher"); + user.execute_insert(&conn).await?; + let session = Sessions::new(); user.sessions.push(session); + user.execute_update(&conn).await?; println!("{:?}", user); + let query_user = Users::query_first(&conn, Users::select_by_primary_key(user.id)).await?; + println!("{:?}", query_user); + Ok(()) } -fn init() { +async fn init() -> Result { println!( "{} - v{}\n", geekorm::GEEKORM_BANNER, @@ -55,4 +56,17 @@ fn init() { log::LevelFilter::Info }) .init(); + + // Initialize an in-memory database + let db = libsql::Builder::new_local(":memory:").build().await?; + // let db = libsql::Builder::new_local("/tmp/turso-testing.sqlite").build().await?; + let conn = db.connect()?; + + let tables = tables!(); + for table in tables { + let query = table.create()?; + conn.execute(query.to_str(), ()).await?; + } + + Ok(conn) } diff --git a/geekorm-derive/src/derive/multitable.rs b/geekorm-derive/src/derive/multitable.rs index 64c3d61..f65a130 100644 --- a/geekorm-derive/src/derive/multitable.rs +++ b/geekorm-derive/src/derive/multitable.rs @@ -46,10 +46,8 @@ pub(crate) fn one_to_many( name: String::from("id"), column_type: ColumnType::Identifier(ColumnTypeOptions { primary_key: true, - unique: true, - not_null: true, auto_increment: true, - foreign_key: String::new(), + ..Default::default() }), ..Default::default() }); From ae246f38aa419a07e3f403592f27c2e1fcc6b908 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Wed, 29 May 2024 22:00:25 +0100 Subject: [PATCH 6/6] feat: Updates to tables and One-to-Many --- examples/one-to-many/Cargo.toml | 2 ++ examples/one-to-many/src/main.rs | 40 +++++++++++++++------- geekorm-core/src/builder/table.rs | 21 ++++++++++++ geekorm-core/src/utils/mod.rs | 2 ++ geekorm-core/src/utils/tables.rs | 1 + geekorm-derive/src/lib.rs | 2 +- geekorm-derive/src/parsers/ | 0 geekorm-derive/src/parsers/tablebuilder.rs | 4 +++ src/lib.rs | 9 +++-- 9 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 geekorm-core/src/utils/tables.rs create mode 100644 geekorm-derive/src/parsers/ diff --git a/examples/one-to-many/Cargo.toml b/examples/one-to-many/Cargo.toml index 1b963f9..a016bf0 100644 --- a/examples/one-to-many/Cargo.toml +++ b/examples/one-to-many/Cargo.toml @@ -11,3 +11,5 @@ repository.workspace = true authors.workspace = true [dependencies] +geekorm = { path = "../geekorm", features = ["all", "libsql"] } + diff --git a/examples/one-to-many/src/main.rs b/examples/one-to-many/src/main.rs index 8ab741d..5938865 100644 --- a/examples/one-to-many/src/main.rs +++ b/examples/one-to-many/src/main.rs @@ -1,11 +1,11 @@ use anyhow::Result; use geekorm::prelude::*; -use geekorm::PrimaryKey; +use geekorm::PrimaryKeyInteger; #[derive(GeekTable, Debug, Default, Clone, serde::Serialize, serde::Deserialize)] struct Users { - id: PrimaryKey, + id: PrimaryKeyInteger, username: String, #[geekorm(foreign_key = "Sessions.id")] @@ -15,7 +15,7 @@ struct Users { #[derive(GeekTable, Debug, Default, Clone, serde::Serialize, serde::Deserialize)] struct Sessions { - id: PrimaryKey, + id: PrimaryKeyInteger, #[geekorm(rand, rand_prefix = "session")] token: String, @@ -25,17 +25,30 @@ struct Sessions { async fn main() -> Result<()> { let conn = init().await?; - let mut user = Users::new("geekmasher"); - user.execute_insert(&conn).await?; + let mut user = match Users::query_first(&conn, Users::select_by_username("geekmasher")).await { + Ok(user) => user, + Err(_) => { + let mut user = Users::new("geekmasher"); - let session = Sessions::new(); + user.execute_insert(&conn).await?; + user + } + }; + // New session + let mut session = Sessions::new(); + session.execute_insert(&conn).await?; + + // Add session to user user.sessions.push(session); + // user.execute_update_session(&conn).await?; user.execute_update(&conn).await?; println!("{:?}", user); - let query_user = Users::query_first(&conn, Users::select_by_primary_key(user.id)).await?; + let mut query_user = Users::query_first(&conn, Users::select_by_primary_key(user.id)).await?; + query_user.fetch_all(&conn).await?; + println!("{:?}", query_user); Ok(()) @@ -58,15 +71,16 @@ async fn init() -> Result { .init(); // Initialize an in-memory database - let db = libsql::Builder::new_local(":memory:").build().await?; - // let db = libsql::Builder::new_local("/tmp/turso-testing.sqlite").build().await?; + // let db = libsql::Builder::new_local(":memory:").build().await?; + let db = libsql::Builder::new_local("/tmp/turso-testing.sqlite") + .build() + .await?; let conn = db.connect()?; + // TODO: Make this better let tables = tables!(); - for table in tables { - let query = table.create()?; - conn.execute(query.to_str(), ()).await?; - } + + tables.create_all(&conn).await?; Ok(conn) } diff --git a/geekorm-core/src/builder/table.rs b/geekorm-core/src/builder/table.rs index bde0f1d..bd77b48 100644 --- a/geekorm-core/src/builder/table.rs +++ b/geekorm-core/src/builder/table.rs @@ -2,6 +2,27 @@ use crate::{Columns, Query, QueryBuilder, ToSqlite, Values}; use serde::{Deserialize, Serialize}; use std::fmt::Display; +/// The Table struct for defining a table +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Tables { + /// Tables in the database + pub tables: Vec
, +} + +impl Tables { + /// Create all tables in the database + #[cfg(feature = "libsql")] + pub async fn create_all(&self, conn: &libsql::Connection) -> Result<(), crate::Error> { + for table in &self.tables { + let query = table.create()?; + conn.execute(query.to_str(), ()) + .await + .map_err(|e| crate::Error::LibSQLError(format!("Error creating table: {}", e)))?; + } + Ok(()) + } +} + /// The Table struct for defining a table #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Table { diff --git a/geekorm-core/src/utils/mod.rs b/geekorm-core/src/utils/mod.rs index f6ab03e..2d17961 100644 --- a/geekorm-core/src/utils/mod.rs +++ b/geekorm-core/src/utils/mod.rs @@ -11,6 +11,8 @@ /// The Cryptography module pub mod crypto; +pub mod tables; + #[cfg(feature = "rand")] pub use crypto::rand::generate_random_string; diff --git a/geekorm-core/src/utils/tables.rs b/geekorm-core/src/utils/tables.rs new file mode 100644 index 0000000..88ede8b --- /dev/null +++ b/geekorm-core/src/utils/tables.rs @@ -0,0 +1 @@ +//! Tables diff --git a/geekorm-derive/src/lib.rs b/geekorm-derive/src/lib.rs index 2795ec1..e51252e 100644 --- a/geekorm-derive/src/lib.rs +++ b/geekorm-derive/src/lib.rs @@ -103,7 +103,7 @@ pub fn tables(_input: TokenStream) -> TokenStream { }); quote! { - vec![ #tables_ast ] + geekorm::Tables { tables: vec![ #tables_ast ] } } .into() } diff --git a/geekorm-derive/src/parsers/ b/geekorm-derive/src/parsers/ new file mode 100644 index 0000000..e69de29 diff --git a/geekorm-derive/src/parsers/tablebuilder.rs b/geekorm-derive/src/parsers/tablebuilder.rs index 7577632..0ffa08e 100644 --- a/geekorm-derive/src/parsers/tablebuilder.rs +++ b/geekorm-derive/src/parsers/tablebuilder.rs @@ -218,6 +218,10 @@ pub fn generate_table_execute( }); } + // table.columns.columns.iter().filter_map(|column| { + // if column.is_ + // }) + // TODO(geekmasher): The execute_insert method might have an issue as we don't have a lock and // the last inserted item might not be the one we inserted. Ok(quote! { diff --git a/src/lib.rs b/src/lib.rs index 0c05e25..a0dac52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ // Builder Modules pub use geekorm_core::builder::columns::{Column, Columns}; pub use geekorm_core::builder::columntypes::{ColumnType, ColumnTypeOptions}; -pub use geekorm_core::builder::table::Table; +pub use geekorm_core::builder::table::{Table, Tables}; pub use geekorm_core::Error; // Keys Modules pub use geekorm_core::builder::keys::foreign::{ForeignKey, ForeignKeyInteger}; @@ -29,8 +29,8 @@ pub mod utils { } // Derive Crate -/// Tables Proc Macro pub use geekorm_derive::tables; + /// GeekTable Derive Macro pub use geekorm_derive::GeekTable; @@ -58,7 +58,7 @@ pub mod prelude { /// GeekTable pub use crate::GeekTable; - // Tables + /// Tables pub use geekorm_derive::tables; // Traits @@ -73,4 +73,7 @@ pub mod prelude { pub use geekorm_core::ToSqlite; // Backends Module pub use geekorm_core::{GeekConnection, GeekConnector}; + + /// Public Re-exports + pub use geekorm_core::builder::table::Tables; }