diff --git a/Cargo.toml b/Cargo.toml index b5426c7..47d2f95 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" diff --git a/examples/one-to-many/Cargo.toml b/examples/one-to-many/Cargo.toml new file mode 100644 index 0000000..a016bf0 --- /dev/null +++ b/examples/one-to-many/Cargo.toml @@ -0,0 +1,15 @@ +[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] +geekorm = { path = "../geekorm", features = ["all", "libsql"] } + diff --git a/examples/one-to-many/src/main.rs b/examples/one-to-many/src/main.rs new file mode 100644 index 0000000..5938865 --- /dev/null +++ b/examples/one-to-many/src/main.rs @@ -0,0 +1,86 @@ +use anyhow::Result; + +use geekorm::prelude::*; +use geekorm::PrimaryKeyInteger; + +#[derive(GeekTable, Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +struct Users { + id: PrimaryKeyInteger, + username: String, + + #[geekorm(foreign_key = "Sessions.id")] + #[serde(skip)] + sessions: Vec, +} + +#[derive(GeekTable, Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +struct Sessions { + id: PrimaryKeyInteger, + + #[geekorm(rand, rand_prefix = "session")] + token: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + let conn = init().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"); + + 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 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(()) +} + +async fn init() -> Result { + 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(); + + // 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()?; + + // TODO: Make this better + let tables = tables!(); + + tables.create_all(&conn).await?; + + Ok(conn) +} 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 74c8a1a..a7ff3fe 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(), }) } } @@ -189,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 aaf83fd..bd77b48 100644 --- a/geekorm-core/src/builder/table.rs +++ b/geekorm-core/src/builder/table.rs @@ -1,7 +1,28 @@ -use crate::{Columns, QueryBuilder, ToSqlite, Values}; +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 { @@ -27,6 +48,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 +75,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 { @@ -80,6 +119,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-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-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/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..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::{ @@ -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, } @@ -116,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 { @@ -144,7 +153,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 +210,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 +231,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 +581,23 @@ impl From for geekorm_core::Column { } } -impl TryFrom<&Field> for ColumnDerive { - type Error = syn::Error; +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, + } + } +} - 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 +628,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/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 new file mode 100644 index 0000000..f65a130 --- /dev/null +++ b/geekorm-derive/src/derive/multitable.rs @@ -0,0 +1,101 @@ +//! MultiTable module + +use geekorm_core::{Column, ColumnType, ColumnTypeOptions, Table}; + +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; +/// +/// #[derive(GeekTable)] +/// struct Users { +/// id: PrimaryKey, +/// username: String, +/// +/// #[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, + 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, + auto_increment: true, + ..Default::default() + }), + ..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..4d0aac2 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 { @@ -60,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..e51252e 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! { + geekorm::Tables { tables: vec![ #tables_ast ] } + } + .into() +} 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/ 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 6b85642..0ffa08e 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) }); @@ -215,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! { @@ -287,15 +294,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( diff --git a/src/lib.rs b/src/lib.rs index 1cb9686..46eb183 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,6 +29,8 @@ pub mod utils { } // Derive Crate +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 @@ -70,6 +74,9 @@ pub mod prelude { // Backends Module pub use geekorm_core::{GeekConnection, GeekConnector}; + /// Public Re-exports + pub use geekorm_core::builder::table::Tables; + // Builder Modules pub use geekorm_core::builder::columns::{Column, Columns}; pub use geekorm_core::builder::columntypes::{ColumnType, ColumnTypeOptions};