Merge branch 'master' into ss/actix-example

This commit is contained in:
Sam Samai 2021-09-17 20:17:54 +10:00
commit 1a4392dd0b
46 changed files with 837 additions and 73 deletions

View File

@ -81,9 +81,7 @@ let pear = fruit::ActiveModel {
};
// insert one
let res = Fruit::insert(pear).exec(db).await?;
println!("InsertResult: {}", res.last_insert_id);
let pear = pear.insert(db).await?;
// insert many
Fruit::insert_many(vec![apple, pear]).exec(db).await?;

View File

@ -26,7 +26,7 @@ type Result<T, E = rocket::response::Debug<sqlx::Error>> = std::result::Result<T
mod post;
pub use post::Entity as Post;
const DEFAULT_POSTS_PER_PAGE: usize = 25;
const DEFAULT_POSTS_PER_PAGE: usize = 5;
#[get("/new")]
async fn new() -> Template {
@ -97,6 +97,7 @@ async fn list(
posts: posts,
flash: flash,
page: page,
posts_per_page: posts_per_page,
num_pages: num_pages,
},
)

View File

@ -27,9 +27,9 @@
<td></td>
<td>
{% if page == 0 %} Previous {% else %}
<a href="/?page={{ page - 1 }}">Previous</a>
<a href="/?page={{ page - 1 }}&posts_per_page={{ posts_per_page }}">Previous</a>
{% endif %} | {% if page == num_pages - 1 %} Next {% else %}
<a href="/?page={{ page + 1 }}">Next</a>
<a href="/?page={{ page + 1 }}&posts_per_page={{ posts_per_page }}">Next</a>
{% endif %}
</td>
<td></td>

View File

@ -10,3 +10,5 @@ publish = false
[dependencies]
sea-orm = { path = "../../", features = [ "sqlx-all", "runtime-tokio-native-tls" ] }
tokio = { version = "1", features = ["full"] }
env_logger = { version = "^0.9" }
log = { version = "^0.4" }

View File

@ -3,7 +3,12 @@ use sea_orm::*;
#[tokio::main]
pub async fn main() {
let db = Database::connect("sql://sea:sea@localhost/bakery")
env_logger::builder()
.filter_level(log::LevelFilter::Debug)
.is_test(true)
.init();
let db = Database::connect("mysql://sea:sea@localhost/bakery")
.await
.unwrap();

View File

@ -21,15 +21,17 @@ path = "src/main.rs"
clap = { version = "^2.33.3" }
dotenv = { version = "^0.15" }
async-std = { version = "^1.9", features = [ "attributes" ] }
sea-orm = { version = "^0.1.2", features = [ "sqlx-all" ] }
sea-orm-codegen = { version = "^0.2.0" }
sea-schema = { version = "^0.2.7", default-features = false, features = [
sea-orm-codegen = { version = "^0.2.0", path = "../sea-orm-codegen" }
sea-schema = { version = "^0.2.8", git = "https://github.com/SeaQL/sea-schema.git", default-features = false, features = [
"debug-print",
"sqlx-mysql",
"sqlx-postgres",
"discovery",
"writer",
] }
sqlx = { version = "^0.5", default-features = false, features = [ "mysql", "postgres" ] }
env_logger = { version = "^0.9" }
log = { version = "^0.4" }
[features]
default = [ "runtime-async-std-native-tls" ]

View File

@ -40,6 +40,20 @@ pub fn build_cli() -> App<'static, 'static> {
.long("include-hidden-tables")
.help("Generate entity file for hidden tables (i.e. table name starts with an underscore)")
.takes_value(false),
)
.arg(
Arg::with_name("EXPANDED_FORMAT")
.long("expanded-format")
.help("Generate entity file of expanded format")
.takes_value(false)
.conflicts_with("COMPACT_FORMAT"),
)
.arg(
Arg::with_name("COMPACT_FORMAT")
.long("compact-format")
.help("Generate entity file of compact format")
.takes_value(false)
.conflicts_with("EXPANDED_FORMAT"),
),
)
.setting(AppSettings::SubcommandRequiredElseHelp);
@ -48,5 +62,13 @@ pub fn build_cli() -> App<'static, 'static> {
.version(env!("CARGO_PKG_VERSION"))
.setting(AppSettings::VersionlessSubcommands)
.subcommand(entity_subcommand)
.arg(
Arg::with_name("VERBOSE")
.long("verbose")
.short("v")
.help("Show debug messages")
.takes_value(false)
.global(true),
)
.setting(AppSettings::SubcommandRequiredElseHelp)
}

View File

@ -1,5 +1,6 @@
use clap::ArgMatches;
use dotenv::dotenv;
use log::LevelFilter;
use sea_orm_codegen::{EntityTransformer, OutputFile};
use std::{error::Error, fmt::Display, fs, io::Write, path::Path, process::Command};
@ -25,6 +26,7 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Er
let url = args.value_of("DATABASE_URL").unwrap();
let output_dir = args.value_of("OUTPUT_DIR").unwrap();
let include_hidden_tables = args.is_present("INCLUDE_HIDDEN_TABLES");
let expanded_format = args.is_present("EXPANDED_FORMAT");
let filter_hidden_tables = |table: &str| -> bool {
if include_hidden_tables {
true
@ -32,6 +34,12 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Er
!table.starts_with("_")
}
};
if args.is_present("VERBOSE") {
let _ = ::env_logger::builder()
.filter_level(LevelFilter::Debug)
.is_test(true)
.try_init();
}
let table_stmts = if url.starts_with("mysql://") {
use sea_schema::mysql::discovery::SchemaDiscovery;
@ -66,7 +74,7 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Er
panic!("This database is not supported ({})", url)
};
let output = EntityTransformer::transform(table_stmts)?.generate();
let output = EntityTransformer::transform(table_stmts)?.generate(expanded_format);
let dir = Path::new(output_dir);
fs::create_dir_all(dir)?;

View File

@ -15,7 +15,7 @@ name = "sea_orm_codegen"
path = "src/lib.rs"
[dependencies]
sea-query = { version = "^0.15" }
sea-query = { version = "^0.16.2" }
syn = { version = "^1", default-features = false, features = [
"derive",
"parsing",
@ -25,3 +25,6 @@ syn = { version = "^1", default-features = false, features = [
quote = "^1"
heck = "^0.3"
proc-macro2 = "^1"
[dev-dependencies]
pretty_assertions = { version = "^0.7" }

View File

@ -1,3 +1,4 @@
ignore = [
"tests/entity/*.rs",
"tests/compact/*.rs",
"tests/expanded/*.rs",
]

View File

@ -91,6 +91,10 @@ impl Entity {
self.relations.iter().map(|rel| rel.get_def()).collect()
}
pub fn get_relation_attrs(&self) -> Vec<TokenStream> {
self.relations.iter().map(|rel| rel.get_attrs()).collect()
}
pub fn get_relation_rel_types(&self) -> Vec<Ident> {
self.relations
.iter()
@ -168,7 +172,7 @@ impl Entity {
mod tests {
use crate::{Column, Entity, PrimaryKey, Relation, RelationType};
use quote::format_ident;
use sea_query::ColumnType;
use sea_query::{ColumnType, ForeignKeyAction};
fn setup() -> Entity {
Entity {
@ -195,12 +199,16 @@ mod tests {
columns: vec!["id".to_owned()],
ref_columns: vec!["cake_id".to_owned()],
rel_type: RelationType::HasOne,
on_delete: Some(ForeignKeyAction::Cascade),
on_update: Some(ForeignKeyAction::Cascade),
},
Relation {
ref_table: "filling".to_owned(),
columns: vec!["id".to_owned()],
ref_columns: vec!["cake_id".to_owned()],
rel_type: RelationType::HasOne,
on_delete: Some(ForeignKeyAction::Cascade),
on_update: Some(ForeignKeyAction::Cascade),
},
],
conjunct_relations: vec![],
@ -347,6 +355,18 @@ mod tests {
}
}
#[test]
fn test_get_relation_attrs() {
let entity = setup();
for (i, elem) in entity.get_relation_attrs().into_iter().enumerate() {
assert_eq!(
elem.to_string(),
entity.relations[i].get_attrs().to_string()
);
}
}
#[test]
fn test_get_relation_rel_types() {
let entity = setup();

View File

@ -52,6 +52,21 @@ impl Column {
}
}
pub fn get_col_type_attrs(&self) -> Option<TokenStream> {
let col_type = match &self.col_type {
ColumnType::Float(Some(l)) => Some(format!("Float(Some({}))", l)),
ColumnType::Double(Some(l)) => Some(format!("Double(Some({}))", l)),
ColumnType::Decimal(Some((p, s))) => Some(format!("Decimal(Some(({}, {})))", p, s)),
ColumnType::Money(Some((p, s))) => Some(format!("Money(Some({}, {}))", p, s)),
ColumnType::Text => Some("Text".to_owned()),
ColumnType::Custom(iden) => {
Some(format!("Custom(\"{}\".to_owned())", iden.to_string()))
}
_ => None,
};
col_type.map(|ty| quote! { column_type = #ty })
}
pub fn get_def(&self) -> TokenStream {
let mut col_def = match &self.col_type {
ColumnType::Char(s) => match s {

View File

@ -1,7 +1,7 @@
use heck::{CamelCase, SnakeCase};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use sea_query::TableForeignKey;
use sea_query::{ForeignKeyAction, TableForeignKey};
#[derive(Clone, Debug)]
pub enum RelationType {
@ -16,6 +16,8 @@ pub struct Relation {
pub(crate) columns: Vec<String>,
pub(crate) ref_columns: Vec<String>,
pub(crate) rel_type: RelationType,
pub(crate) on_update: Option<ForeignKeyAction>,
pub(crate) on_delete: Option<ForeignKeyAction>,
}
impl Relation {
@ -49,6 +51,53 @@ impl Relation {
}
}
pub fn get_attrs(&self) -> TokenStream {
let rel_type = self.get_rel_type();
let ref_table_snake_case = self.get_ref_table_snake_case();
let ref_entity = format!("super::{}::Entity", ref_table_snake_case);
match self.rel_type {
RelationType::HasOne | RelationType::HasMany => {
quote! {
#[sea_orm(#rel_type = #ref_entity)]
}
}
RelationType::BelongsTo => {
let column_camel_case = self.get_column_camel_case();
let ref_column_camel_case = self.get_ref_column_camel_case();
let from = format!("Column::{}", column_camel_case);
let to = format!(
"super::{}::Column::{}",
ref_table_snake_case, ref_column_camel_case
);
let on_update = if let Some(action) = &self.on_update {
let action = Self::get_foreign_key_action(action);
quote! {
on_update = #action,
}
} else {
TokenStream::new()
};
let on_delete = if let Some(action) = &self.on_delete {
let action = Self::get_foreign_key_action(action);
quote! {
on_delete = #action,
}
} else {
TokenStream::new()
};
quote! {
#[sea_orm(
#rel_type = #ref_entity,
from = #from,
to = #to,
#on_update
#on_delete
)]
}
}
}
}
pub fn get_rel_type(&self) -> Ident {
match self.rel_type {
RelationType::HasOne => format_ident!("has_one"),
@ -64,6 +113,17 @@ impl Relation {
pub fn get_ref_column_camel_case(&self) -> Ident {
format_ident!("{}", self.ref_columns[0].to_camel_case())
}
pub fn get_foreign_key_action(action: &ForeignKeyAction) -> String {
match action {
ForeignKeyAction::Restrict => "Restrict",
ForeignKeyAction::Cascade => "Cascade",
ForeignKeyAction::SetNull => "SetNull",
ForeignKeyAction::NoAction => "NoAction",
ForeignKeyAction::SetDefault => "SetDefault",
}
.to_owned()
}
}
impl From<&TableForeignKey> for Relation {
@ -75,11 +135,15 @@ impl From<&TableForeignKey> for Relation {
let columns = tbl_fk.get_columns();
let ref_columns = tbl_fk.get_ref_columns();
let rel_type = RelationType::BelongsTo;
let on_delete = tbl_fk.get_on_delete();
let on_update = tbl_fk.get_on_update();
Self {
ref_table,
columns,
ref_columns,
rel_type,
on_delete,
on_update,
}
}
}
@ -88,6 +152,7 @@ impl From<&TableForeignKey> for Relation {
mod tests {
use crate::{Relation, RelationType};
use proc_macro2::TokenStream;
use sea_query::ForeignKeyAction;
fn setup() -> Vec<Relation> {
vec![
@ -96,18 +161,24 @@ mod tests {
columns: vec!["id".to_owned()],
ref_columns: vec!["cake_id".to_owned()],
rel_type: RelationType::HasOne,
on_delete: None,
on_update: None,
},
Relation {
ref_table: "filling".to_owned(),
columns: vec!["filling_id".to_owned()],
ref_columns: vec!["id".to_owned()],
rel_type: RelationType::BelongsTo,
on_delete: Some(ForeignKeyAction::Cascade),
on_update: Some(ForeignKeyAction::Cascade),
},
Relation {
ref_table: "filling".to_owned(),
columns: vec!["filling_id".to_owned()],
ref_columns: vec!["id".to_owned()],
rel_type: RelationType::HasMany,
on_delete: Some(ForeignKeyAction::Cascade),
on_update: None,
},
]
}

View File

@ -1,6 +1,7 @@
use crate::Entity;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{punctuated::Punctuated, token::Comma};
#[derive(Clone, Debug)]
pub struct EntityWriter {
@ -17,21 +18,25 @@ pub struct OutputFile {
}
impl EntityWriter {
pub fn generate(self) -> WriterOutput {
pub fn generate(self, expanded_format: bool) -> WriterOutput {
let mut files = Vec::new();
files.extend(self.write_entities());
files.extend(self.write_entities(expanded_format));
files.push(self.write_mod());
files.push(self.write_prelude());
WriterOutput { files }
}
pub fn write_entities(&self) -> Vec<OutputFile> {
pub fn write_entities(&self, expanded_format: bool) -> Vec<OutputFile> {
self.entities
.iter()
.map(|entity| {
let mut lines = Vec::new();
Self::write_doc_comment(&mut lines);
let code_blocks = Self::gen_code_blocks(entity);
let code_blocks = if expanded_format {
Self::gen_expanded_code_blocks(entity)
} else {
Self::gen_compact_code_blocks(entity)
};
Self::write(&mut lines, code_blocks);
OutputFile {
name: format!("{}.rs", entity.get_table_name_snake_case()),
@ -97,7 +102,7 @@ impl EntityWriter {
lines.push("".to_owned());
}
pub fn gen_code_blocks(entity: &Entity) -> Vec<TokenStream> {
pub fn gen_expanded_code_blocks(entity: &Entity) -> Vec<TokenStream> {
let mut code_blocks = vec![
Self::gen_import(),
Self::gen_entity_struct(),
@ -116,6 +121,23 @@ impl EntityWriter {
code_blocks
}
pub fn gen_compact_code_blocks(entity: &Entity) -> Vec<TokenStream> {
let mut code_blocks = vec![Self::gen_import(), Self::gen_compact_model_struct(entity)];
let relation_defs = if entity.get_relation_ref_tables_camel_case().is_empty() {
vec![
Self::gen_relation_enum(entity),
Self::gen_impl_relation_trait(entity),
]
} else {
vec![Self::gen_compact_relation_enum(entity)]
};
code_blocks.extend(relation_defs);
code_blocks.extend(Self::gen_impl_related(entity));
code_blocks.extend(Self::gen_impl_conjunct_related(entity));
code_blocks.extend(vec![Self::gen_impl_active_model_behavior()]);
code_blocks
}
pub fn gen_import() -> TokenStream {
quote! {
use sea_orm::entity::prelude::*;
@ -297,6 +319,74 @@ impl EntityWriter {
pub use super::#table_name_snake_case_ident::Entity as #table_name_camel_case_ident;
}
}
pub fn gen_compact_model_struct(entity: &Entity) -> TokenStream {
let table_name = entity.table_name.as_str();
let column_names_snake_case = entity.get_column_names_snake_case();
let column_rs_types = entity.get_column_rs_types();
let primary_keys: Vec<String> = entity
.primary_keys
.iter()
.map(|pk| pk.name.clone())
.collect();
let attrs: Vec<TokenStream> = entity
.columns
.iter()
.map(|col| {
let mut attrs: Punctuated<_, Comma> = Punctuated::new();
if primary_keys.contains(&col.name) {
attrs.push(quote! { primary_key });
if !col.auto_increment {
attrs.push(quote! { auto_increment = false });
}
}
if let Some(ts) = col.get_col_type_attrs() {
attrs.extend(vec![ts]);
if !col.not_null {
attrs.push(quote! { nullable });
}
};
if !attrs.is_empty() {
let mut ts = TokenStream::new();
for (i, attr) in attrs.into_iter().enumerate() {
if i > 0 {
ts = quote! { #ts, };
}
ts = quote! { #ts #attr };
}
quote! {
#[sea_orm(#ts)]
}
} else {
TokenStream::new()
}
})
.collect();
quote! {
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = #table_name)]
pub struct Model {
#(
#attrs
pub #column_names_snake_case: #column_rs_types,
)*
}
}
}
pub fn gen_compact_relation_enum(entity: &Entity) -> TokenStream {
let relation_ref_tables_camel_case = entity.get_relation_ref_tables_camel_case();
let attrs = entity.get_relation_attrs();
quote! {
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#(
#attrs
#relation_ref_tables_camel_case,
)*
}
}
}
}
#[cfg(test)]
@ -304,18 +394,11 @@ mod tests {
use crate::{
Column, ConjunctRelation, Entity, EntityWriter, PrimaryKey, Relation, RelationType,
};
use pretty_assertions::assert_eq;
use proc_macro2::TokenStream;
use sea_query::ColumnType;
use sea_query::{ColumnType, ForeignKeyAction};
use std::io::{self, BufRead, BufReader};
const ENTITY_FILES: [&str; 5] = [
include_str!("../../tests/entity/cake.rs"),
include_str!("../../tests/entity/cake_filling.rs"),
include_str!("../../tests/entity/filling.rs"),
include_str!("../../tests/entity/fruit.rs"),
include_str!("../../tests/entity/vendor.rs"),
];
fn setup() -> Vec<Entity> {
vec![
Entity {
@ -330,9 +413,9 @@ mod tests {
},
Column {
name: "name".to_owned(),
col_type: ColumnType::String(Some(255)),
col_type: ColumnType::Text,
auto_increment: false,
not_null: true,
not_null: false,
unique: false,
},
],
@ -341,6 +424,8 @@ mod tests {
columns: vec![],
ref_columns: vec![],
rel_type: RelationType::HasMany,
on_delete: None,
on_update: None,
}],
conjunct_relations: vec![ConjunctRelation {
via: "cake_filling".to_owned(),
@ -374,12 +459,16 @@ mod tests {
columns: vec!["cake_id".to_owned()],
ref_columns: vec!["id".to_owned()],
rel_type: RelationType::BelongsTo,
on_delete: Some(ForeignKeyAction::Cascade),
on_update: Some(ForeignKeyAction::Cascade),
},
Relation {
ref_table: "filling".to_owned(),
columns: vec!["filling_id".to_owned()],
ref_columns: vec!["id".to_owned()],
rel_type: RelationType::BelongsTo,
on_delete: Some(ForeignKeyAction::Cascade),
on_update: Some(ForeignKeyAction::Cascade),
},
],
conjunct_relations: vec![],
@ -450,12 +539,16 @@ mod tests {
columns: vec!["cake_id".to_owned()],
ref_columns: vec!["id".to_owned()],
rel_type: RelationType::BelongsTo,
on_delete: None,
on_update: None,
},
Relation {
ref_table: "vendor".to_owned(),
columns: vec![],
ref_columns: vec![],
rel_type: RelationType::HasMany,
on_delete: None,
on_update: None,
},
],
conjunct_relations: vec![],
@ -493,6 +586,8 @@ mod tests {
columns: vec!["fruit_id".to_owned()],
ref_columns: vec!["id".to_owned()],
rel_type: RelationType::BelongsTo,
on_delete: None,
on_update: None,
}],
conjunct_relations: vec![],
primary_keys: vec![PrimaryKey {
@ -503,8 +598,15 @@ mod tests {
}
#[test]
fn test_gen_code_blocks() -> io::Result<()> {
fn test_gen_expanded_code_blocks() -> io::Result<()> {
let entities = setup();
const ENTITY_FILES: [&str; 5] = [
include_str!("../../tests/expanded/cake.rs"),
include_str!("../../tests/expanded/cake_filling.rs"),
include_str!("../../tests/expanded/filling.rs"),
include_str!("../../tests/expanded/fruit.rs"),
include_str!("../../tests/expanded/vendor.rs"),
];
assert_eq!(entities.len(), ENTITY_FILES.len());
@ -521,7 +623,46 @@ mod tests {
}
let content = lines.join("");
let expected: TokenStream = content.parse().unwrap();
let generated = EntityWriter::gen_code_blocks(entity)
let generated = EntityWriter::gen_expanded_code_blocks(entity)
.into_iter()
.skip(1)
.fold(TokenStream::new(), |mut acc, tok| {
acc.extend(tok);
acc
});
assert_eq!(expected.to_string(), generated.to_string());
}
Ok(())
}
#[test]
fn test_gen_compact_code_blocks() -> io::Result<()> {
let entities = setup();
const ENTITY_FILES: [&str; 5] = [
include_str!("../../tests/compact/cake.rs"),
include_str!("../../tests/compact/cake_filling.rs"),
include_str!("../../tests/compact/filling.rs"),
include_str!("../../tests/compact/fruit.rs"),
include_str!("../../tests/compact/vendor.rs"),
];
assert_eq!(entities.len(), ENTITY_FILES.len());
for (i, entity) in entities.iter().enumerate() {
let mut reader = BufReader::new(ENTITY_FILES[i].as_bytes());
let mut lines: Vec<String> = Vec::new();
reader.read_until(b';', &mut Vec::new())?;
let mut line = String::new();
while reader.read_line(&mut line)? > 0 {
lines.push(line.to_owned());
line.clear();
}
let content = lines.join("");
let expected: TokenStream = content.parse().unwrap();
let generated = EntityWriter::gen_compact_code_blocks(entity)
.into_iter()
.skip(1)
.fold(TokenStream::new(), |mut acc, tok| {

View File

@ -0,0 +1,35 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "cake")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(column_type = "Text", nullable)]
pub name: Option<String> ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::fruit::Entity")]
Fruit,
}
impl Related<super::fruit::Entity> for Entity {
fn to() -> RelationDef {
Relation::Fruit.def()
}
}
impl Related<super::filling::Entity> for Entity {
fn to() -> RelationDef {
super::cake_filling::Relation::Filling.def()
}
fn via() -> Option<RelationDef> {
Some(super::cake_filling::Relation::Cake.def().rev())
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,46 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "_cake_filling_")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub cake_id: i32,
#[sea_orm(primary_key, auto_increment = false)]
pub filling_id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::cake::Entity",
from = "Column::CakeId",
to = "super::cake::Column::Id",
on_update = "Cascade",
on_delete = "Cascade",
)]
Cake,
#[sea_orm(
belongs_to = "super::filling::Entity",
from = "Column::FillingId",
to = "super::filling::Column::Id",
on_update = "Cascade",
on_delete = "Cascade",
)]
Filling,
}
impl Related<super::cake::Entity> for Entity {
fn to() -> RelationDef {
Relation::Cake.def()
}
}
impl Related<super::filling::Entity> for Entity {
fn to() -> RelationDef {
Relation::Filling.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,33 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "filling")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
_ => panic!("No RelationDef"),
}
}
}
impl Related<super::cake::Entity> for Entity {
fn to() -> RelationDef {
super::cake_filling::Relation::Cake.def()
}
fn via() -> Option<RelationDef> {
Some(super::cake_filling::Relation::Filling.def().rev())
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,38 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "fruit")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub cake_id: Option<i32> ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::cake::Entity",
from = "Column::CakeId",
to = "super::cake::Column::Id",
)]
Cake,
#[sea_orm(has_many = "super::vendor::Entity")]
Vendor,
}
impl Related<super::cake::Entity> for Entity {
fn to() -> RelationDef {
Relation::Cake.def()
}
}
impl Related<super::vendor::Entity> for Entity {
fn to() -> RelationDef {
Relation::Vendor.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,30 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "vendor")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub fruit_id: Option<i32> ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::fruit::Entity",
from = "Column::FruitId",
to = "super::fruit::Column::Id",
)]
Fruit,
}
impl Related<super::fruit::Entity> for Entity {
fn to() -> RelationDef {
Relation::Fruit.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -14,7 +14,7 @@ impl EntityName for Entity {
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
pub id: i32,
pub name: String,
pub name: Option<String> ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
@ -46,7 +46,7 @@ impl ColumnTrait for Column {
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Name => ColumnType::String(Some(255u32)).def(),
Self::Name => ColumnType::Text.def().null(),
}
}
}

View File

@ -0,0 +1,9 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
pub mod prelude;
pub mod cake;
pub mod cake_filling;
pub mod filling;
pub mod fruit;
pub mod vendor;

View File

@ -0,0 +1,7 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0
pub use super::cake::Entity as Cake;
pub use super::cake_filling::Entity as CakeFilling;
pub use super::filling::Entity as Filling;
pub use super::fruit::Entity as Fruit;
pub use super::vendor::Entity as Vendor;

View File

@ -1,3 +1,4 @@
use crate::util::field_not_ignored;
use heck::CamelCase;
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote, quote_spanned};
@ -14,7 +15,9 @@ pub fn expand_derive_active_model(ident: Ident, data: Data) -> syn::Result<Token
ident.span() => compile_error!("you can only derive DeriveActiveModel on structs");
})
}
};
}
.into_iter()
.filter(field_not_ignored);
let field: Vec<Ident> = fields
.clone()

View File

@ -68,6 +68,7 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec<Attribute>) -> syn::Res
let mut default_value = None;
let mut default_expr = None;
let mut indexed = false;
let mut ignore = false;
let mut unique = false;
let mut sql_type = None;
// search for #[sea_orm(primary_key, auto_increment = false, column_type = "String(Some(255))", default_value = "new user", default_expr = "gen_random_uuid()", nullable, indexed, unique)]
@ -120,7 +121,10 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec<Attribute>) -> syn::Res
}
Meta::Path(p) => {
if let Some(name) = p.get_ident() {
if name == "primary_key" {
if name == "ignore" {
ignore = true;
break;
} else if name == "primary_key" {
primary_keys.push(quote! { #field_name });
primary_key_types.push(field.ty.clone());
} else if name == "nullable" {
@ -138,6 +142,11 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec<Attribute>) -> syn::Res
}
}
if ignore {
columns_enum.pop();
continue;
}
let field_type = match sql_type {
Some(t) => t,
None => {
@ -163,7 +172,9 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec<Attribute>) -> syn::Res
"bool" => quote! { Boolean },
"NaiveDate" => quote! { Date },
"NaiveTime" => quote! { Time },
"DateTime" | "NaiveDateTime" => quote! { DateTime },
"DateTime" | "NaiveDateTime" | "DateTimeWithTimeZone" => {
quote! { DateTime }
}
"Uuid" => quote! { Uuid },
"Json" => quote! { Json },
"Decimal" => quote! { Decimal },

View File

@ -1,8 +1,9 @@
use crate::attributes::derive_attr;
use crate::{attributes::derive_attr, util::field_not_ignored};
use heck::CamelCase;
use proc_macro2::TokenStream;
use quote::{format_ident, quote, quote_spanned};
use std::iter::FromIterator;
use syn::Ident;
enum Error {
InputNotStruct,
@ -14,6 +15,7 @@ struct DeriveModel {
entity_ident: syn::Ident,
field_idents: Vec<syn::Ident>,
ident: syn::Ident,
ignore_attrs: Vec<bool>,
}
impl DeriveModel {
@ -48,11 +50,17 @@ impl DeriveModel {
})
.collect();
let ignore_attrs = fields
.iter()
.map(|field| !field_not_ignored(field))
.collect();
Ok(DeriveModel {
column_idents,
entity_ident,
field_idents,
ident,
ignore_attrs,
})
}
@ -70,23 +78,56 @@ impl DeriveModel {
let ident = &self.ident;
let field_idents = &self.field_idents;
let column_idents = &self.column_idents;
let field_values: Vec<TokenStream> = column_idents
.iter()
.zip(&self.ignore_attrs)
.map(|(column_ident, ignore)| {
if *ignore {
quote! {
Default::default()
}
} else {
quote! {
row.try_get(pre, sea_orm::IdenStatic::as_str(&<<Self as sea_orm::ModelTrait>::Entity as sea_orm::entity::EntityTrait>::Column::#column_ident).into())?
}
}
})
.collect();
quote!(
impl sea_orm::FromQueryResult for #ident {
fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> Result<Self, sea_orm::DbErr> {
Ok(Self {
#(#field_idents: row.try_get(pre, sea_orm::IdenStatic::as_str(&<<Self as sea_orm::ModelTrait>::Entity as sea_orm::entity::EntityTrait>::Column::#column_idents).into())?),*
#(#field_idents: #field_values),*
})
}
}
)
}
fn impl_model_trait(&self) -> TokenStream {
fn impl_model_trait<'a>(&'a self) -> TokenStream {
let ident = &self.ident;
let entity_ident = &self.entity_ident;
let field_idents = &self.field_idents;
let column_idents = &self.column_idents;
let ignore_attrs = &self.ignore_attrs;
let ignore = |(ident, ignore): (&'a Ident, &bool)| -> Option<&'a Ident> {
if *ignore {
None
} else {
Some(ident)
}
};
let field_idents: Vec<&Ident> = self
.field_idents
.iter()
.zip(ignore_attrs)
.filter_map(ignore)
.collect();
let column_idents: Vec<&Ident> = self
.column_idents
.iter()
.zip(ignore_attrs)
.filter_map(ignore)
.collect();
let missing_field_msg = format!("field does not exist on {}", ident);

View File

@ -5,6 +5,7 @@ use syn::{parse_macro_input, DeriveInput, Error};
mod attributes;
mod derives;
mod util;
#[proc_macro_derive(DeriveEntity, attributes(sea_orm))]
pub fn derive_entity(input: TokenStream) -> TokenStream {
@ -73,7 +74,7 @@ pub fn derive_model(input: TokenStream) -> TokenStream {
.into()
}
#[proc_macro_derive(DeriveActiveModel)]
#[proc_macro_derive(DeriveActiveModel, attributes(sea_orm))]
pub fn derive_active_model(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);

View File

@ -0,0 +1,26 @@
use syn::{punctuated::Punctuated, token::Comma, Field, Meta};
pub(crate) fn field_not_ignored(field: &Field) -> bool {
for attr in field.attrs.iter() {
if let Some(ident) = attr.path.get_ident() {
if ident != "sea_orm" {
continue;
}
} else {
continue;
}
if let Ok(list) = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated) {
for meta in list.iter() {
if let Meta::Path(path) = meta {
if let Some(name) = path.get_ident() {
if name == "ignore" {
return false;
}
}
}
}
}
}
true
}

20
src/entity/link.rs Normal file
View File

@ -0,0 +1,20 @@
use crate::{EntityTrait, QuerySelect, RelationDef, Select};
use sea_query::JoinType;
pub type LinkDef = RelationDef;
pub trait Linked {
type FromEntity: EntityTrait;
type ToEntity: EntityTrait;
fn link(&self) -> Vec<LinkDef>;
fn find_linked(&self) -> Select<Self::ToEntity> {
let mut select = Select::new();
for rel in self.link().into_iter().rev() {
select = select.join_rev(JoinType::InnerJoin, rel);
}
select
}
}

View File

@ -2,6 +2,7 @@ mod active_model;
mod base_entity;
mod column;
mod identity;
mod link;
mod model;
pub mod prelude;
mod primary_key;
@ -11,6 +12,7 @@ pub use active_model::*;
pub use base_entity::*;
pub use column::*;
pub use identity::*;
pub use link::*;
pub use model::*;
// pub use prelude::*;
pub use primary_key::*;

View File

@ -1,4 +1,7 @@
use crate::{DbErr, EntityTrait, Linked, QueryFilter, QueryResult, Related, Select};
use crate::{
DbErr, EntityTrait, Linked, QueryFilter, QueryResult, Related, Select, SelectModel,
SelectorRaw, Statement,
};
pub use sea_query::Value;
use std::fmt::Debug;
@ -25,15 +28,75 @@ pub trait ModelTrait: Clone + Send + Debug {
}
}
pub trait FromQueryResult {
fn from_query_result(res: &QueryResult, pre: &str) -> Result<Self, DbErr>
where
Self: Sized;
pub trait FromQueryResult: Sized {
fn from_query_result(res: &QueryResult, pre: &str) -> Result<Self, DbErr>;
fn from_query_result_optional(res: &QueryResult, pre: &str) -> Result<Option<Self>, DbErr>
where
Self: Sized,
{
fn from_query_result_optional(res: &QueryResult, pre: &str) -> Result<Option<Self>, DbErr> {
Ok(Self::from_query_result(res, pre).ok())
}
/// ```
/// # #[cfg(feature = "mock")]
/// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend};
/// #
/// # let db = MockDatabase::new(DbBackend::Postgres)
/// # .append_query_results(vec![vec![
/// # maplit::btreemap! {
/// # "name" => Into::<Value>::into("Chocolate Forest"),
/// # "num_of_cakes" => Into::<Value>::into(1),
/// # },
/// # maplit::btreemap! {
/// # "name" => Into::<Value>::into("New York Cheese"),
/// # "num_of_cakes" => Into::<Value>::into(1),
/// # },
/// # ]])
/// # .into_connection();
/// #
/// use sea_orm::{entity::*, query::*, tests_cfg::cake, FromQueryResult};
///
/// #[derive(Debug, PartialEq, FromQueryResult)]
/// struct SelectResult {
/// name: String,
/// num_of_cakes: i32,
/// }
///
/// # let _: Result<(), DbErr> = smol::block_on(async {
/// #
/// let res: Vec<SelectResult> = SelectResult::find_by_statement(Statement::from_sql_and_values(
/// DbBackend::Postgres,
/// r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#,
/// vec![],
/// ))
/// .all(&db)
/// .await?;
///
/// assert_eq!(
/// res,
/// vec![
/// SelectResult {
/// name: "Chocolate Forest".to_owned(),
/// num_of_cakes: 1,
/// },
/// SelectResult {
/// name: "New York Cheese".to_owned(),
/// num_of_cakes: 1,
/// },
/// ]
/// );
/// #
/// # Ok(())
/// # });
///
/// assert_eq!(
/// db.into_transaction_log(),
/// vec![Transaction::from_sql_and_values(
/// DbBackend::Postgres,
/// r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#,
/// vec![]
/// ),]
/// );
/// ```
fn find_by_statement(stmt: Statement) -> SelectorRaw<SelectModel<Self>> {
SelectorRaw::<SelectModel<Self>>::from_statement(stmt)
}
}

View File

@ -30,22 +30,6 @@ where
}
}
pub trait Linked {
type FromEntity: EntityTrait;
type ToEntity: EntityTrait;
fn link(&self) -> Vec<RelationDef>;
fn find_linked(&self) -> Select<Self::ToEntity> {
let mut select = Select::new();
for rel in self.link().into_iter().rev() {
select = select.join_rev(JoinType::InnerJoin, rel);
}
select
}
}
#[derive(Debug)]
pub struct RelationDef {
pub rel_type: RelationType,

View File

@ -34,6 +34,7 @@ where
where
A: 'a,
{
// TODO: extract primary key's value from query
// so that self is dropped before entering await
let mut query = self.query;
#[cfg(feature = "sqlx-postgres")]
@ -48,6 +49,7 @@ where
}
}
Inserter::<A>::new(query).exec(db)
// TODO: return primary key if extracted before, otherwise use InsertResult
}
}

View File

@ -1,4 +1,6 @@
use crate::{debug_print, DbErr};
#[cfg(feature = "mock")]
use crate::debug_print;
use crate::DbErr;
use std::fmt;
#[derive(Debug)]

View File

@ -263,6 +263,18 @@ impl<S> SelectorRaw<S>
where
S: SelectorTrait,
{
/// Create `SelectorRaw` from Statment. Executing this `SelectorRaw` will
/// return a type `M` which implement `FromQueryResult`.
pub fn from_statement<M>(stmt: Statement) -> SelectorRaw<SelectModel<M>>
where
M: FromQueryResult,
{
SelectorRaw {
stmt,
selector: SelectModel { model: PhantomData },
}
}
/// ```
/// # #[cfg(feature = "mock")]
/// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend};

View File

@ -94,9 +94,7 @@
//! };
//!
//! // insert one
//! let res = Fruit::insert(pear).exec(db).await?;
//!
//! println!("InsertResult: {}", res.last_insert_id);
//! let pear = pear.insert(db).await?;
//! # Ok(())
//! # }
//! # async fn function2(db: &DbConn) -> Result<(), DbErr> {

View File

@ -0,0 +1,94 @@
use crate as sea_orm;
use crate::entity::prelude::*;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl EntityName for Entity {
fn table_name(&self) -> &str {
"cake"
}
}
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
pub id: i32,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Name,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Fruit,
}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Name => ColumnType::String(None).def(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Fruit => Entity::has_many(super::fruit::Entity).into(),
}
}
}
impl Related<super::fruit::Entity> for Entity {
fn to() -> RelationDef {
Relation::Fruit.def()
}
}
impl Related<super::filling::Entity> for Entity {
fn to() -> RelationDef {
super::cake_filling::Relation::Filling.def()
}
fn via() -> Option<RelationDef> {
Some(super::cake_filling::Relation::Cake.def().rev())
}
}
#[derive(Debug)]
pub struct CakeToFilling;
impl Linked for CakeToFilling {
type FromEntity = Entity;
type ToEntity = super::filling::Entity;
fn link(&self) -> Vec<RelationDef> {
vec![
super::cake_filling::Relation::Cake.def().rev(),
super::cake_filling::Relation::Filling.def(),
]
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -19,6 +19,8 @@ pub struct Model {
pub cake_id: i32,
pub filling_id: i32,
pub price: Decimal,
#[sea_orm(ignore)]
pub ignored_attr: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]

View File

@ -9,6 +9,8 @@ pub struct Entity;
pub struct Model {
pub id: i32,
pub name: String,
#[sea_orm(ignore)]
pub ignored_attr: i32,
}
// If your column names are not in snake-case, derive `DeriveCustomColumn` here.

View File

@ -18,6 +18,12 @@ pub enum Relation {
to = "super::cake::Column::Id"
)]
Cake,
#[sea_orm(
belongs_to = "super::cake_expanded::Entity",
from = "Column::CakeId",
to = "super::cake_expanded::Column::Id"
)]
CakeExpanded,
}
impl Related<super::cake::Entity> for Entity {
@ -26,4 +32,10 @@ impl Related<super::cake::Entity> for Entity {
}
}
impl Related<super::cake_expanded::Entity> for Entity {
fn to() -> RelationDef {
Relation::CakeExpanded.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,12 +1,14 @@
//! Configurations for test cases and examples. Not intended for actual use.
pub mod cake;
pub mod cake_expanded;
pub mod cake_filling;
pub mod cake_filling_price;
pub mod filling;
pub mod fruit;
pub use cake::Entity as Cake;
pub use cake_expanded::Entity as CakeExpanded;
pub use cake_filling::Entity as CakeFilling;
pub use cake_filling_price::Entity as CakeFillingPrice;
pub use filling::Entity as Filling;