Merge remote-tracking branch 'origin/master' into returning

This commit is contained in:
Billy Chan 2021-11-08 17:37:54 +08:00
commit 80c0d69733
No known key found for this signature in database
GPG Key ID: A2D690CAC7DF3CC7
43 changed files with 2539 additions and 72 deletions

View File

@ -290,7 +290,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
path: [basic, actix_example, actix4_example, rocket_example] path: [basic, actix_example, actix4_example, axum_example, rocket_example]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -19,9 +19,10 @@
#### SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust. #### SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust.
[![Getting Started](https://img.shields.io/badge/Getting%20Started-brightgreen)](https://www.sea-ql.org/SeaORM/docs/index) [![Getting Started](https://img.shields.io/badge/Getting%20Started-brightgreen)](https://www.sea-ql.org/SeaORM/docs/index)
[![Usage Example](https://img.shields.io/badge/Usage%20Example-yellow)](https://github.com/SeaQL/sea-orm/tree/master/examples/basic) [![Usage Example](https://img.shields.io/badge/Usage%20Example-green)](https://github.com/SeaQL/sea-orm/tree/master/examples/basic)
[![Rocket Example](https://img.shields.io/badge/Rocket%20Example-red)](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example)
[![Actix Example](https://img.shields.io/badge/Actix%20Example-blue)](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example) [![Actix Example](https://img.shields.io/badge/Actix%20Example-blue)](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example)
[![Rocket Example](https://img.shields.io/badge/Rocket%20Example-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) [![Axum Example](https://img.shields.io/badge/Axum%20Example-yellow)](https://github.com/SeaQL/sea-orm/tree/master/examples/axum_example)
[![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv)
## Features ## Features

View File

@ -0,0 +1,3 @@
HOST=127.0.0.1
PORT=8000
DATABASE_URL="postgres://postgres:password@localhost/axum_exmaple"

View File

@ -0,0 +1,33 @@
[package]
name = "sea-orm-axum-example"
version = "0.1.0"
authors = ["Yoshiera Huang <huangjasper@126.com>"]
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[workspace]
[dependencies]
tokio = { version = "1.5", features = ["full"] }
axum = { version = "0.3.0" }
tower = "0.4.10"
tower-http = { version = "0.1", features = ["fs"] }
tower-cookies = { git = "https://github.com/imbolc/tower-cookies" }
anyhow = "1"
dotenv = "0.15"
env_logger = "0.9"
serde = "1"
serde_json = "1"
tera = "1"
[dependencies.sea-orm]
path = "../../" # remove this line in your own project
version = "^0.3.0"
features = ["macros", "runtime-tokio-native-tls"]
default-features = false
[features]
default = ["sqlx-postgres"]
sqlx-mysql = ["sea-orm/sqlx-mysql"]
sqlx-postgres = ["sea-orm/sqlx-postgres"]

View File

@ -0,0 +1,10 @@
# Axum with SeaORM example app
Edit `Cargo.toml` to use `sqlx-mysql` or `sqlx-postgres`.
```toml
[features]
default = ["sqlx-$DATABASE"]
```
Edit `.env` to point to your database.

View File

@ -0,0 +1,51 @@
use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tower_cookies::{Cookie, Cookies};
#[derive(Deserialize)]
struct ValuedMessage<T> {
#[serde(rename = "_")]
value: T,
}
#[derive(Serialize)]
struct ValuedMessageRef<'a, T> {
#[serde(rename = "_")]
value: &'a T,
}
const FLASH_COOKIE_NAME: &str = "_flash";
pub fn get_flash_cookie<T>(cookies: &Cookies) -> Option<T>
where
T: DeserializeOwned,
{
cookies.get(FLASH_COOKIE_NAME).and_then(|flash_cookie| {
if let Ok(ValuedMessage::<T> { value }) = serde_json::from_str(flash_cookie.value()) {
Some(value)
} else {
None
}
})
}
pub type PostResponse = (StatusCode, HeaderMap);
pub fn post_response<T>(cookies: &mut Cookies, data: T) -> PostResponse
where
T: Serialize,
{
let valued_message_ref = ValuedMessageRef { value: &data };
let mut cookie = Cookie::new(
FLASH_COOKIE_NAME,
serde_json::to_string(&valued_message_ref).unwrap(),
);
cookie.set_path("/");
cookies.add(cookie);
let mut header = HeaderMap::new();
header.insert(header::LOCATION, HeaderValue::from_static("/"));
(StatusCode::SEE_OTHER, header)
}

View File

@ -0,0 +1,221 @@
mod flash;
mod post;
mod setup;
use axum::{
error_handling::HandleErrorExt,
extract::{Extension, Form, Path, Query},
http::StatusCode,
response::Html,
routing::{get, post, service_method_routing},
AddExtensionLayer, Router, Server,
};
use flash::{get_flash_cookie, post_response, PostResponse};
use post::Entity as Post;
use sea_orm::{prelude::*, Database, QueryOrder, Set};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::{env, net::SocketAddr};
use tera::Tera;
use tower::ServiceBuilder;
use tower_cookies::{CookieManagerLayer, Cookies};
use tower_http::services::ServeDir;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env::set_var("RUST_LOG", "debug");
env_logger::init();
dotenv::dotenv().ok();
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");
let host = env::var("HOST").expect("HOST is not set in .env file");
let port = env::var("PORT").expect("PORT is not set in .env file");
let server_url = format!("{}:{}", host, port);
let conn = Database::connect(db_url)
.await
.expect("Database connection failed");
let _ = setup::create_post_table(&conn).await;
let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*"))
.expect("Tera initialization failed");
// let state = AppState { templates, conn };
let app = Router::new()
.route("/", get(list_posts).post(create_post))
.route("/:id", get(edit_post).post(update_post))
.route("/new", get(new_post))
.route("/delete/:id", post(delete_post))
.nest(
"/static",
service_method_routing::get(ServeDir::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/static"
)))
.handle_error(|error: std::io::Error| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
}),
)
.layer(
ServiceBuilder::new()
.layer(CookieManagerLayer::new())
.layer(AddExtensionLayer::new(conn))
.layer(AddExtensionLayer::new(templates)),
);
let addr = SocketAddr::from_str(&server_url).unwrap();
Server::bind(&addr).serve(app.into_make_service()).await?;
Ok(())
}
#[derive(Deserialize)]
struct Params {
page: Option<usize>,
posts_per_page: Option<usize>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct FlashData {
kind: String,
message: String,
}
async fn list_posts(
Extension(ref templates): Extension<Tera>,
Extension(ref conn): Extension<DatabaseConnection>,
Query(params): Query<Params>,
cookies: Cookies,
) -> Result<Html<String>, (StatusCode, &'static str)> {
let page = params.page.unwrap_or(1);
let posts_per_page = params.posts_per_page.unwrap_or(5);
let paginator = Post::find()
.order_by_asc(post::Column::Id)
.paginate(conn, posts_per_page);
let num_pages = paginator.num_pages().await.ok().unwrap();
let posts = paginator
.fetch_page(page - 1)
.await
.expect("could not retrieve posts");
let mut ctx = tera::Context::new();
ctx.insert("posts", &posts);
ctx.insert("page", &page);
ctx.insert("posts_per_page", &posts_per_page);
ctx.insert("num_pages", &num_pages);
if let Some(value) = get_flash_cookie::<FlashData>(&cookies) {
ctx.insert("flash", &value);
}
let body = templates
.render("index.html.tera", &ctx)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
Ok(Html(body))
}
async fn new_post(
Extension(ref templates): Extension<Tera>,
) -> Result<Html<String>, (StatusCode, &'static str)> {
let ctx = tera::Context::new();
let body = templates
.render("new.html.tera", &ctx)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
Ok(Html(body))
}
async fn create_post(
Extension(ref conn): Extension<DatabaseConnection>,
form: Form<post::Model>,
mut cookies: Cookies,
) -> Result<PostResponse, (StatusCode, &'static str)> {
let model = form.0;
post::ActiveModel {
title: Set(model.title.to_owned()),
text: Set(model.text.to_owned()),
..Default::default()
}
.save(conn)
.await
.expect("could not insert post");
let data = FlashData {
kind: "success".to_owned(),
message: "Post succcessfully added".to_owned(),
};
Ok(post_response(&mut cookies, data))
}
async fn edit_post(
Extension(ref templates): Extension<Tera>,
Extension(ref conn): Extension<DatabaseConnection>,
Path(id): Path<i32>,
) -> Result<Html<String>, (StatusCode, &'static str)> {
let post: post::Model = Post::find_by_id(id)
.one(conn)
.await
.expect("could not find post")
.unwrap();
let mut ctx = tera::Context::new();
ctx.insert("post", &post);
let body = templates
.render("edit.html.tera", &ctx)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Template error"))?;
Ok(Html(body))
}
async fn update_post(
Extension(ref conn): Extension<DatabaseConnection>,
Path(id): Path<i32>,
form: Form<post::Model>,
mut cookies: Cookies,
) -> Result<PostResponse, (StatusCode, String)> {
let model = form.0;
post::ActiveModel {
id: Set(id),
title: Set(model.title.to_owned()),
text: Set(model.text.to_owned()),
}
.save(conn)
.await
.expect("could not edit post");
let data = FlashData {
kind: "success".to_owned(),
message: "Post succcessfully updated".to_owned(),
};
Ok(post_response(&mut cookies, data))
}
async fn delete_post(
Extension(ref conn): Extension<DatabaseConnection>,
Path(id): Path<i32>,
mut cookies: Cookies,
) -> Result<PostResponse, (StatusCode, &'static str)> {
let post: post::ActiveModel = Post::find_by_id(id)
.one(conn)
.await
.unwrap()
.unwrap()
.into();
post.delete(conn).await.unwrap();
let data = FlashData {
kind: "success".to_owned(),
message: "Post succcessfully deleted".to_owned(),
};
Ok(post_response(&mut cookies, data))
}

View File

@ -0,0 +1,26 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.3.2
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "posts")]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: i32,
pub title: String,
#[sea_orm(column_type = "Text")]
pub text: String,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,33 @@
use sea_orm::sea_query::{ColumnDef, TableCreateStatement};
use sea_orm::{error::*, sea_query, ConnectionTrait, DbConn, ExecResult};
async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result<ExecResult, DbErr> {
let builder = db.get_database_backend();
db.execute(builder.build(stmt)).await
}
pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
.table(super::post::Entity)
.if_not_exists()
.col(
ColumnDef::new(super::post::Column::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(
ColumnDef::new(super::post::Column::Title)
.string()
.not_null(),
)
.col(
ColumnDef::new(super::post::Column::Text)
.string()
.not_null(),
)
.to_owned();
create_table(db, &stmt).await
}

View File

@ -0,0 +1,427 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

View File

@ -0,0 +1,421 @@
/*
* Skeleton V2.0.4
* Copyright 2014, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* https://opensource.org/licenses/mit-license.php
* 12/29/2014
*/
/* Table of contents
- Grid
- Base Styles
- Typography
- Links
- Buttons
- Forms
- Lists
- Code
- Tables
- Spacing
- Utilities
- Clearing
- Media Queries
*/
/* Grid
*/
.container {
position: relative;
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box; }
.column,
.columns {
width: 100%;
float: left;
box-sizing: border-box; }
/* For devices larger than 400px */
@media (min-width: 400px) {
.container {
width: 85%;
padding: 0; }
}
/* For devices larger than 550px */
@media (min-width: 550px) {
.container {
width: 80%; }
.column,
.columns {
margin-left: 4%; }
.column:first-child,
.columns:first-child {
margin-left: 0; }
.one.column,
.one.columns { width: 4.66666666667%; }
.two.columns { width: 13.3333333333%; }
.three.columns { width: 22%; }
.four.columns { width: 30.6666666667%; }
.five.columns { width: 39.3333333333%; }
.six.columns { width: 48%; }
.seven.columns { width: 56.6666666667%; }
.eight.columns { width: 65.3333333333%; }
.nine.columns { width: 74.0%; }
.ten.columns { width: 82.6666666667%; }
.eleven.columns { width: 91.3333333333%; }
.twelve.columns { width: 100%; margin-left: 0; }
.one-third.column { width: 30.6666666667%; }
.two-thirds.column { width: 65.3333333333%; }
.one-half.column { width: 48%; }
/* Offsets */
.offset-by-one.column,
.offset-by-one.columns { margin-left: 8.66666666667%; }
.offset-by-two.column,
.offset-by-two.columns { margin-left: 17.3333333333%; }
.offset-by-three.column,
.offset-by-three.columns { margin-left: 26%; }
.offset-by-four.column,
.offset-by-four.columns { margin-left: 34.6666666667%; }
.offset-by-five.column,
.offset-by-five.columns { margin-left: 43.3333333333%; }
.offset-by-six.column,
.offset-by-six.columns { margin-left: 52%; }
.offset-by-seven.column,
.offset-by-seven.columns { margin-left: 60.6666666667%; }
.offset-by-eight.column,
.offset-by-eight.columns { margin-left: 69.3333333333%; }
.offset-by-nine.column,
.offset-by-nine.columns { margin-left: 78.0%; }
.offset-by-ten.column,
.offset-by-ten.columns { margin-left: 86.6666666667%; }
.offset-by-eleven.column,
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
.offset-by-one-third.column,
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
.offset-by-two-thirds.column,
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
.offset-by-one-half.column,
.offset-by-one-half.columns { margin-left: 52%; }
}
/* Base Styles
*/
/* NOTE
html is set to 62.5% so that all the REM measurements throughout Skeleton
are based on 10px sizing. So basically 1.5rem = 15px :) */
html {
font-size: 62.5%; }
body {
font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
line-height: 1.6;
font-weight: 400;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #222; }
/* Typography
*/
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 2rem;
font-weight: 300; }
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
/* Larger than phablet */
@media (min-width: 550px) {
h1 { font-size: 5.0rem; }
h2 { font-size: 4.2rem; }
h3 { font-size: 3.6rem; }
h4 { font-size: 3.0rem; }
h5 { font-size: 2.4rem; }
h6 { font-size: 1.5rem; }
}
p {
margin-top: 0; }
/* Links
*/
a {
color: #1EAEDB; }
a:hover {
color: #0FA0CE; }
/* Buttons
*/
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
display: inline-block;
height: 38px;
padding: 0 30px;
color: #555;
text-align: center;
font-size: 11px;
font-weight: 600;
line-height: 38px;
letter-spacing: .1rem;
text-transform: uppercase;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
box-sizing: border-box; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
color: #333;
border-color: #888;
outline: 0; }
.button.button-primary,
button.button-primary,
button.primary,
input[type="submit"].button-primary,
input[type="reset"].button-primary,
input[type="button"].button-primary {
color: #FFF;
background-color: #33C3F0;
border-color: #33C3F0; }
.button.button-primary:hover,
button.button-primary:hover,
button.primary:hover,
input[type="submit"].button-primary:hover,
input[type="reset"].button-primary:hover,
input[type="button"].button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus,
button.primary:focus,
input[type="submit"].button-primary:focus,
input[type="reset"].button-primary:focus,
input[type="button"].button-primary:focus {
color: #FFF;
background-color: #1EAEDB;
border-color: #1EAEDB; }
/* Forms
*/
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea,
select {
height: 38px;
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
background-color: #fff;
border: 1px solid #D1D1D1;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
/* Removes awkward default styles on some inputs for iOS */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none; }
textarea {
min-height: 65px;
padding-top: 6px;
padding-bottom: 6px; }
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="text"]:focus,
input[type="tel"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
border: 1px solid #33C3F0;
outline: 0; }
label,
legend {
display: block;
margin-bottom: .5rem;
font-weight: 600; }
fieldset {
padding: 0;
border-width: 0; }
input[type="checkbox"],
input[type="radio"] {
display: inline; }
label > .label-body {
display: inline-block;
margin-left: .5rem;
font-weight: normal; }
/* Lists
*/
ul {
list-style: circle inside; }
ol {
list-style: decimal inside; }
ol, ul {
padding-left: 0;
margin-top: 0; }
ul ul,
ul ol,
ol ol,
ol ul {
margin: 1.5rem 0 1.5rem 3rem;
font-size: 90%; }
li {
margin-bottom: 1rem; }
/* Code
*/
code {
padding: .2rem .5rem;
margin: 0 .2rem;
font-size: 90%;
white-space: nowrap;
background: #F1F1F1;
border: 1px solid #E1E1E1;
border-radius: 4px; }
pre > code {
display: block;
padding: 1rem 1.5rem;
white-space: pre; }
/* Tables
*/
th,
td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #E1E1E1; }
th:first-child,
td:first-child {
padding-left: 0; }
th:last-child,
td:last-child {
padding-right: 0; }
/* Spacing
*/
button,
.button {
margin-bottom: 1rem; }
input,
textarea,
select,
fieldset {
margin-bottom: 1.5rem; }
pre,
blockquote,
dl,
figure,
table,
p,
ul,
ol,
form {
margin-bottom: 2.5rem; }
/* Utilities
*/
.u-full-width {
width: 100%;
box-sizing: border-box; }
.u-max-full-width {
max-width: 100%;
box-sizing: border-box; }
.u-pull-right {
float: right; }
.u-pull-left {
float: left; }
/* Misc
*/
hr {
margin-top: 3rem;
margin-bottom: 3.5rem;
border-width: 0;
border-top: 1px solid #E1E1E1; }
/* Clearing
*/
/* Self Clearing Goodness */
.container:after,
.row:after,
.u-cf {
content: "";
display: table;
clear: both; }
/* Media Queries
*/
/*
Note: The best way to structure the use of media queries is to create the queries
near the relevant code. For example, if you wanted to change the styles for buttons
on small devices, paste the mobile query code up in the buttons section and style it
there.
*/
/* Larger than mobile */
@media (min-width: 400px) {}
/* Larger than phablet (also point when grid becomes active) */
@media (min-width: 550px) {}
/* Larger than tablet */
@media (min-width: 750px) {}
/* Larger than desktop */
@media (min-width: 1000px) {}
/* Larger than Desktop HD */
@media (min-width: 1200px) {}

View File

@ -0,0 +1,73 @@
.field-error {
border: 1px solid #ff0000 !important;
}
.field-error-flash {
color: #ff0000;
display: block;
margin: -10px 0 10px 0;
}
.field-success {
border: 1px solid #5ab953 !important;
}
.field-success-flash {
color: #5ab953;
display: block;
margin: -10px 0 10px 0;
}
span.completed {
text-decoration: line-through;
}
form.inline {
display: inline;
}
form.link,
button.link {
display: inline;
color: #1eaedb;
border: none;
outline: none;
background: none;
cursor: pointer;
padding: 0;
margin: 0 0 0 0;
height: inherit;
text-decoration: underline;
font-size: inherit;
text-transform: none;
font-weight: normal;
line-height: inherit;
letter-spacing: inherit;
}
form.link:hover,
button.link:hover {
color: #0fa0ce;
}
button.small {
height: 20px;
padding: 0 10px;
font-size: 10px;
line-height: 20px;
margin: 0 2.5px;
}
.post:hover {
background-color: #bce2ee;
}
.post td {
padding: 5px;
width: 150px;
}
#delete-button {
color: red;
border-color: red;
}

Binary file not shown.

View File

@ -0,0 +1,49 @@
{% extends "layout.html.tera" %} {% block content %}
<div class="row">
<h4>Edit Post</h4>
<div class="twelve columns">
<div class="ten columns">
<form action="/{{ post.id }}" method="post">
<div class="twelve columns">
<input
type="text"
placeholder="title"
name="title"
id="title"
value="{{ post.title }}"
autofocus
class="u-full-width"
/>
<input
type="text"
placeholder="content"
name="text"
id="text"
value="{{ post.text }}"
autofocus
class="u-full-width"
/>
</div>
<div class="twelve columns">
<div class="two columns">
<a href="/">
<input type="button" value="cancel" />
</a>
</div>
<div class="eight columns"></div>
<div class="two columns">
<input type="submit" value="save post" />
</div>
</div>
</form>
</div>
<div class="two columns">
<form action="/delete/{{ post.id }}" method="post">
<div class="two columns">
<input id="delete-button" type="submit" value="delete post" />
</div>
</form>
</div>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>404 - tera</title>
</head>
<body>
<h1>404: Hey! There's nothing here.</h1>
The page at {{ uri }} does not exist!
</body>
</html>

View File

@ -0,0 +1,52 @@
{% extends "layout.html.tera" %} {% block content %}
<div class="container">
<p><!--Nothing to see here --></p>
<h1>Posts</h1>
{% if flash %}
<small class="field-{{ flash.kind }}-flash">
{{ flash.message }}
</small>
{% endif %}
<table>
<tbody>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Text</th>
</tr>
</thead>
{% for post in posts %}
<tr class="post" onclick="window.location='/{{ post.id }}';">
<td>{{ post.id }}</td>
<td>{{ post.title }}</td>
<td>{{ post.text }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td></td>
<td>
{% if page == 1 %} Previous {% else %}
<a href="/?page={{ page - 1 }}&posts_per_page={{ posts_per_page }}"
>Previous</a
>
{% endif %} | {% if page == num_pages %} Next {% else %}
<a href="/?page={{ page + 1 }}&posts_per_page={{ posts_per_page }}"
>Next</a
>
{% endif %}
</td>
<td></td>
</tr>
</tfoot>
</table>
<div class="twelve columns">
<a href="/new">
<input type="button" value="add post" />
</a>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Axum Example</title>
<meta name="description" content="Axum - SeaOrm integration example" />
<meta name="author" content="Yoshiera Huang" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="//fonts.googleapis.com/css?family=Raleway:400,300,600"
rel="stylesheet"
type="text/css"
/>
<link rel="stylesheet" href="/static/css/normalize.css" />
<link rel="stylesheet" href="/static/css/skeleton.css" />
<link rel="stylesheet" href="/static/css/style.css" />
<link rel="icon" type="image/png" href="/static/images/favicon.png" />
</head>
<body>
<div class="container">
<p><!--Nothing to see here --></p>
{% block content %}{% endblock content %}
</div>
</body>
</html>

View File

@ -0,0 +1,38 @@
{% extends "layout.html.tera" %} {% block content %}
<div class="row">
<h4>New Post</h4>
<form action="/" method="post">
<div class="twelve columns">
<input
type="text"
placeholder="enter title"
name="title"
id="title"
value=""
autofocus
class="u-full-width"
/>
<input
type="text"
placeholder="enter content"
name="text"
id="text"
value=""
autofocus
class="u-full-width"
/>
</div>
<div class="twelve columns">
<div class="two columns">
<a href="/">
<input type="button" value="cancel" />
</a>
</div>
<div class="eight columns"></div>
<div class="two columns">
<input type="submit" value="save post" />
</div>
</div>
</form>
</div>
{% endblock content %}

View File

@ -328,23 +328,23 @@ impl EntityWriter {
let relation_defs = entity.get_relation_defs(); let relation_defs = entity.get_relation_defs();
let quoted = if relation_ref_tables_camel_case.is_empty() { let quoted = if relation_ref_tables_camel_case.is_empty() {
quote! { quote! {
_ => panic!("No RelationDef"), panic!("No RelationDef")
} }
} else { } else {
quote! { quote! {
match self {
#(Self::#relation_ref_tables_camel_case => #relation_defs,)* #(Self::#relation_ref_tables_camel_case => #relation_defs,)*
} }
}
}; };
quote! { quote! {
impl RelationTrait for Relation { impl RelationTrait for Relation {
fn def(&self) -> RelationDef { fn def(&self) -> RelationDef {
match self {
#quoted #quoted
} }
} }
} }
} }
}
pub fn gen_impl_related(entity: &Entity) -> Vec<TokenStream> { pub fn gen_impl_related(entity: &Entity) -> Vec<TokenStream> {
let camel = entity.get_relation_ref_tables_camel_case(); let camel = entity.get_relation_ref_tables_camel_case();

View File

@ -15,9 +15,7 @@ pub enum Relation {}
impl RelationTrait for Relation { impl RelationTrait for Relation {
fn def(&self) -> RelationDef { fn def(&self) -> RelationDef {
match self { panic!("No RelationDef")
_ => panic!("No RelationDef"),
}
} }
} }

View File

@ -21,9 +21,7 @@ pub enum Relation {}
impl RelationTrait for Relation { impl RelationTrait for Relation {
fn def(&self) -> RelationDef { fn def(&self) -> RelationDef {
match self { panic!("No RelationDef")
_ => panic!("No RelationDef"),
}
} }
} }

View File

@ -51,9 +51,7 @@ impl ColumnTrait for Column {
impl RelationTrait for Relation { impl RelationTrait for Relation {
fn def(&self) -> RelationDef { fn def(&self) -> RelationDef {
match self { panic!("No RelationDef")
_ => panic!("No RelationDef"),
}
} }
} }

View File

@ -70,9 +70,7 @@ impl ColumnTrait for Column {
impl RelationTrait for Relation { impl RelationTrait for Relation {
fn def(&self) -> RelationDef { fn def(&self) -> RelationDef {
match self { panic!("No RelationDef")
_ => panic!("No RelationDef"),
}
} }
} }

View File

@ -0,0 +1,286 @@
use heck::CamelCase;
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::{punctuated::Punctuated, token::Comma, Lit, LitInt, LitStr, Meta};
enum Error {
InputNotEnum,
Syn(syn::Error),
TT(TokenStream),
}
struct ActiveEnum {
ident: syn::Ident,
enum_name: String,
rs_type: TokenStream,
db_type: TokenStream,
is_string: bool,
variants: Vec<ActiveEnumVariant>,
}
struct ActiveEnumVariant {
ident: syn::Ident,
string_value: Option<LitStr>,
num_value: Option<LitInt>,
}
impl ActiveEnum {
fn new(input: syn::DeriveInput) -> Result<Self, Error> {
let ident_span = input.ident.span();
let ident = input.ident;
let mut enum_name = ident.to_string().to_camel_case();
let mut rs_type = Err(Error::TT(quote_spanned! {
ident_span => compile_error!("Missing macro attribute `rs_type`");
}));
let mut db_type = Err(Error::TT(quote_spanned! {
ident_span => compile_error!("Missing macro attribute `db_type`");
}));
for attr in input.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::NameValue(nv) = meta {
if let Some(name) = nv.path.get_ident() {
if name == "rs_type" {
if let Lit::Str(litstr) = &nv.lit {
rs_type = syn::parse_str::<TokenStream>(&litstr.value())
.map_err(Error::Syn);
}
} else if name == "db_type" {
if let Lit::Str(litstr) = &nv.lit {
let s = litstr.value();
match s.as_ref() {
"Enum" => {
db_type = Ok(quote! {
Enum(Self::name(), Self::values())
})
}
_ => {
db_type = syn::parse_str::<TokenStream>(&s)
.map_err(Error::Syn);
}
}
}
} else if name == "enum_name" {
if let Lit::Str(litstr) = &nv.lit {
enum_name = litstr.value();
}
}
}
}
}
}
}
let variant_vec = match input.data {
syn::Data::Enum(syn::DataEnum { variants, .. }) => variants,
_ => return Err(Error::InputNotEnum),
};
let mut is_string = false;
let mut is_int = false;
let mut variants = Vec::new();
for variant in variant_vec {
let variant_span = variant.ident.span();
let mut string_value = None;
let mut num_value = None;
for attr in variant.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 {
if let Meta::NameValue(nv) = meta {
if let Some(name) = nv.path.get_ident() {
if name == "string_value" {
if let Lit::Str(lit) = nv.lit {
is_string = true;
string_value = Some(lit);
}
} else if name == "num_value" {
if let Lit::Int(lit) = nv.lit {
is_int = true;
num_value = Some(lit);
}
}
}
}
}
}
}
if is_string && is_int {
return Err(Error::TT(quote_spanned! {
ident_span => compile_error!("All enum variants should specify the same `*_value` macro attribute, either `string_value` or `num_value` but not both");
}));
}
if string_value.is_none() && num_value.is_none() {
return Err(Error::TT(quote_spanned! {
variant_span => compile_error!("Missing macro attribute, either `string_value` or `num_value` should be specified");
}));
}
variants.push(ActiveEnumVariant {
ident: variant.ident,
string_value,
num_value,
});
}
Ok(ActiveEnum {
ident,
enum_name,
rs_type: rs_type?,
db_type: db_type?,
is_string,
variants,
})
}
fn expand(&self) -> syn::Result<TokenStream> {
let expanded_impl_active_enum = self.impl_active_enum();
Ok(expanded_impl_active_enum)
}
fn impl_active_enum(&self) -> TokenStream {
let Self {
ident,
enum_name,
rs_type,
db_type,
is_string,
variants,
} = self;
let variant_idents: Vec<syn::Ident> = variants
.iter()
.map(|variant| variant.ident.clone())
.collect();
let variant_values: Vec<TokenStream> = variants
.iter()
.map(|variant| {
let variant_span = variant.ident.span();
if let Some(string_value) = &variant.string_value {
let string = string_value.value();
quote! { #string }
} else if let Some(num_value) = &variant.num_value {
quote! { #num_value }
} else {
quote_spanned! {
variant_span => compile_error!("Missing macro attribute, either `string_value` or `num_value` should be specified");
}
}
})
.collect();
let val = if *is_string {
quote! { v.as_ref() }
} else {
quote! { v }
};
quote!(
#[automatically_derived]
impl sea_orm::ActiveEnum for #ident {
type Value = #rs_type;
fn name() -> String {
#enum_name.to_owned()
}
fn to_value(&self) -> Self::Value {
match self {
#( Self::#variant_idents => #variant_values, )*
}
.to_owned()
}
fn try_from_value(v: &Self::Value) -> Result<Self, sea_orm::DbErr> {
match #val {
#( #variant_values => Ok(Self::#variant_idents), )*
_ => Err(sea_orm::DbErr::Type(format!(
"unexpected value for {} enum: {}",
stringify!(#ident),
v
))),
}
}
fn db_type() -> sea_orm::ColumnDef {
sea_orm::ColumnType::#db_type.def()
}
}
#[automatically_derived]
#[allow(clippy::from_over_into)]
impl Into<sea_orm::sea_query::Value> for #ident {
fn into(self) -> sea_orm::sea_query::Value {
<Self as sea_orm::ActiveEnum>::to_value(&self).into()
}
}
#[automatically_derived]
impl sea_orm::TryGetable for #ident {
fn try_get(res: &sea_orm::QueryResult, pre: &str, col: &str) -> Result<Self, sea_orm::TryGetError> {
let value = <<Self as sea_orm::ActiveEnum>::Value as sea_orm::TryGetable>::try_get(res, pre, col)?;
<Self as sea_orm::ActiveEnum>::try_from_value(&value).map_err(sea_orm::TryGetError::DbErr)
}
}
#[automatically_derived]
impl sea_orm::sea_query::ValueType for #ident {
fn try_from(v: sea_orm::sea_query::Value) -> Result<Self, sea_orm::sea_query::ValueTypeErr> {
let value = <<Self as sea_orm::ActiveEnum>::Value as sea_orm::sea_query::ValueType>::try_from(v)?;
<Self as sea_orm::ActiveEnum>::try_from_value(&value).map_err(|_| sea_orm::sea_query::ValueTypeErr)
}
fn type_name() -> String {
<<Self as sea_orm::ActiveEnum>::Value as sea_orm::sea_query::ValueType>::type_name()
}
fn column_type() -> sea_orm::sea_query::ColumnType {
<Self as sea_orm::ActiveEnum>::db_type()
.get_column_type()
.to_owned()
.into()
}
}
#[automatically_derived]
impl sea_orm::sea_query::Nullable for #ident {
fn null() -> sea_orm::sea_query::Value {
<<Self as sea_orm::ActiveEnum>::Value as sea_orm::sea_query::Nullable>::null()
}
}
)
}
}
pub fn expand_derive_active_enum(input: syn::DeriveInput) -> syn::Result<TokenStream> {
let ident_span = input.ident.span();
match ActiveEnum::new(input) {
Ok(model) => model.expand(),
Err(Error::InputNotEnum) => Ok(quote_spanned! {
ident_span => compile_error!("you can only derive ActiveEnum on enums");
}),
Err(Error::TT(token_stream)) => Ok(token_stream),
Err(Error::Syn(e)) => Err(e),
}
}

View File

@ -1,7 +1,7 @@
use crate::util::{escape_rust_keyword, trim_starting_raw_identifier}; use crate::util::{escape_rust_keyword, trim_starting_raw_identifier};
use heck::CamelCase; use heck::CamelCase;
use proc_macro2::{Ident, Span, TokenStream}; use proc_macro2::{Ident, Span, TokenStream};
use quote::quote; use quote::{format_ident, quote, quote_spanned};
use syn::{ use syn::{
parse::Error, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, Data, Fields, parse::Error, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, Data, Fields,
Lit, Meta, Lit, Meta,
@ -193,8 +193,8 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec<Attribute>) -> syn::Res
primary_keys.push(quote! { #field_name }); primary_keys.push(quote! { #field_name });
} }
let field_type = match sql_type { let col_type = match sql_type {
Some(t) => t, Some(t) => quote! { sea_orm::prelude::ColumnType::#t.def() },
None => { None => {
let field_type = &field.ty; let field_type = &field.ty;
let temp = quote! { #field_type } let temp = quote! { #field_type }
@ -206,7 +206,7 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec<Attribute>) -> syn::Res
} else { } else {
temp.as_str() temp.as_str()
}; };
match temp { let col_type = match temp {
"char" => quote! { Char(None) }, "char" => quote! { Char(None) },
"String" | "&str" => quote! { String(None) }, "String" | "&str" => quote! { String(None) },
"u8" | "i8" => quote! { TinyInteger }, "u8" | "i8" => quote! { TinyInteger },
@ -229,16 +229,24 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec<Attribute>) -> syn::Res
"Decimal" => quote! { Decimal(None) }, "Decimal" => quote! { Decimal(None) },
"Vec<u8>" => quote! { Binary }, "Vec<u8>" => quote! { Binary },
_ => { _ => {
return Err(Error::new( // Assumed it's ActiveEnum if none of the above type matches
field.span(), quote! {}
format!("unrecognized type {}", temp),
))
} }
};
if col_type.is_empty() {
let field_span = field.span();
let ty = format_ident!("{}", temp);
let def = quote_spanned! { field_span => {
<#ty as ActiveEnum>::db_type()
}};
quote! { #def }
} else {
quote! { sea_orm::prelude::ColumnType::#col_type.def() }
} }
} }
}; };
let mut match_row = quote! { Self::#field_name => sea_orm::prelude::ColumnType::#field_type.def() }; let mut match_row = quote! { Self::#field_name => #col_type };
if nullable { if nullable {
match_row = quote! { #match_row.nullable() }; match_row = quote! { #match_row.nullable() };
} }

View File

@ -1,3 +1,4 @@
mod active_enum;
mod active_model; mod active_model;
mod active_model_behavior; mod active_model_behavior;
mod column; mod column;
@ -9,6 +10,7 @@ mod model;
mod primary_key; mod primary_key;
mod relation; mod relation;
pub use active_enum::*;
pub use active_model::*; pub use active_model::*;
pub use active_model_behavior::*; pub use active_model_behavior::*;
pub use column::*; pub use column::*;

View File

@ -492,6 +492,41 @@ pub fn derive_active_model_behavior(input: TokenStream) -> TokenStream {
} }
} }
/// A derive macro to implement `sea_orm::ActiveEnum` trait for enums.
///
/// # Limitations
///
/// This derive macros can only be used on enums.
///
/// # Macro Attributes
///
/// All macro attributes listed below have to be annotated in the form of `#[sea_orm(attr = value)]`.
///
/// - For enum
/// - `rs_type`: Define `ActiveEnum::Value`
/// - Possible values: `String`, `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`
/// - Note that value has to be passed as string, i.e. `rs_type = "i8"`
/// - `db_type`: Define `ColumnType` returned by `ActiveEnum::db_type()`
/// - Possible values: all available enum variants of `ColumnType`, e.g. `String(None)`, `String(Some(1))`, `Integer`
/// - Note that value has to be passed as string, i.e. `db_type = "Integer"`
/// - `enum_name`: Define `String` returned by `ActiveEnum::name()`
/// - This attribute is optional with default value being the name of enum in camel-case
/// - Note that value has to be passed as string, i.e. `db_type = "Integer"`
///
/// - For enum variant
/// - `string_value` or `num_value`:
/// - For `string_value`, value should be passed as string, i.e. `string_value = "A"`
/// - For `num_value`, value should be passed as integer, i.e. `num_value = 1` or `num_value = 1i32`
/// - Note that only one of it can be specified, and all variants of an enum have to annotate with the same `*_value` macro attribute
#[proc_macro_derive(DeriveActiveEnum, attributes(sea_orm))]
pub fn derive_active_enum(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match derives::expand_derive_active_enum(input) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
/// Convert a query result into the corresponding Model. /// Convert a query result into the corresponding Model.
/// ///
/// ### Usage /// ### Usage

View File

@ -82,6 +82,15 @@ macro_rules! build_any_stmt {
}; };
} }
macro_rules! build_postgres_stmt {
($stmt: expr, $db_backend: expr) => {
match $db_backend {
DbBackend::Postgres => $stmt.to_string(PostgresQueryBuilder),
DbBackend::MySql | DbBackend::Sqlite => unimplemented!(),
}
};
}
macro_rules! build_query_stmt { macro_rules! build_query_stmt {
($stmt: ty) => { ($stmt: ty) => {
impl StatementBuilder for $stmt { impl StatementBuilder for $stmt {
@ -114,3 +123,18 @@ build_schema_stmt!(sea_query::TableDropStatement);
build_schema_stmt!(sea_query::TableAlterStatement); build_schema_stmt!(sea_query::TableAlterStatement);
build_schema_stmt!(sea_query::TableRenameStatement); build_schema_stmt!(sea_query::TableRenameStatement);
build_schema_stmt!(sea_query::TableTruncateStatement); build_schema_stmt!(sea_query::TableTruncateStatement);
macro_rules! build_type_stmt {
($stmt: ty) => {
impl StatementBuilder for $stmt {
fn build(&self, db_backend: &DbBackend) -> Statement {
let stmt = build_postgres_stmt!(self, db_backend);
Statement::from_string(*db_backend, stmt)
}
}
};
}
build_type_stmt!(sea_query::extension::postgres::TypeAlterStatement);
build_type_stmt!(sea_query::extension::postgres::TypeCreateStatement);
build_type_stmt!(sea_query::extension::postgres::TypeDropStatement);

308
src/entity/active_enum.rs Normal file
View File

@ -0,0 +1,308 @@
use crate::{ColumnDef, DbErr, Iterable, TryGetable};
use sea_query::{Nullable, Value, ValueType};
/// A Rust representation of enum defined in database.
///
/// # Implementations
///
/// You can implement [ActiveEnum] manually by hand or use the derive macro [DeriveActiveEnum](sea_orm_macros::DeriveActiveEnum).
///
/// # Examples
///
/// Implementing it manually versus using the derive macro [DeriveActiveEnum](sea_orm_macros::DeriveActiveEnum).
///
/// > See [DeriveActiveEnum](sea_orm_macros::DeriveActiveEnum) for the full specification of macro attributes.
///
/// ```rust
/// use sea_orm::entity::prelude::*;
///
/// // Using the derive macro
/// #[derive(Debug, PartialEq, EnumIter, DeriveActiveEnum)]
/// #[sea_orm(
/// rs_type = "String",
/// db_type = "String(Some(1))",
/// enum_name = "category"
/// )]
/// pub enum DeriveCategory {
/// #[sea_orm(string_value = "B")]
/// Big,
/// #[sea_orm(string_value = "S")]
/// Small,
/// }
///
/// // Implementing it manually
/// #[derive(Debug, PartialEq, EnumIter)]
/// pub enum Category {
/// Big,
/// Small,
/// }
///
/// impl ActiveEnum for Category {
/// // The macro attribute `rs_type` is being pasted here
/// type Value = String;
///
/// // Will be atomically generated by `DeriveActiveEnum`
/// fn name() -> String {
/// "category".to_owned()
/// }
///
/// // Will be atomically generated by `DeriveActiveEnum`
/// fn to_value(&self) -> Self::Value {
/// match self {
/// Self::Big => "B",
/// Self::Small => "S",
/// }
/// .to_owned()
/// }
///
/// // Will be atomically generated by `DeriveActiveEnum`
/// fn try_from_value(v: &Self::Value) -> Result<Self, DbErr> {
/// match v.as_ref() {
/// "B" => Ok(Self::Big),
/// "S" => Ok(Self::Small),
/// _ => Err(DbErr::Type(format!(
/// "unexpected value for Category enum: {}",
/// v
/// ))),
/// }
/// }
///
/// fn db_type() -> ColumnDef {
/// // The macro attribute `db_type` is being pasted here
/// ColumnType::String(Some(1)).def()
/// }
/// }
/// ```
///
/// Using [ActiveEnum] on Model.
///
/// ```
/// use sea_orm::entity::prelude::*;
///
/// // Define the `Category` active enum
/// #[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum)]
/// #[sea_orm(rs_type = "String", db_type = "String(Some(1))")]
/// pub enum Category {
/// #[sea_orm(string_value = "B")]
/// Big,
/// #[sea_orm(string_value = "S")]
/// Small,
/// }
///
/// #[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
/// #[sea_orm(table_name = "active_enum")]
/// pub struct Model {
/// #[sea_orm(primary_key)]
/// pub id: i32,
/// // Represents a db column using `Category` active enum
/// pub category: Category,
/// pub category_opt: Option<Category>,
/// }
///
/// #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
/// pub enum Relation {}
///
/// impl ActiveModelBehavior for ActiveModel {}
/// ```
pub trait ActiveEnum: Sized + Iterable {
/// Define the Rust type that each enum variant represents.
type Value: Into<Value> + ValueType + Nullable + TryGetable;
/// Get the name of enum
fn name() -> String;
/// Convert enum variant into the corresponding value.
fn to_value(&self) -> Self::Value;
/// Try to convert the corresponding value into enum variant.
fn try_from_value(v: &Self::Value) -> Result<Self, DbErr>;
/// Get the database column definition of this active enum.
fn db_type() -> ColumnDef;
/// Convert an owned enum variant into the corresponding value.
fn into_value(self) -> Self::Value {
Self::to_value(&self)
}
/// Get the name of all enum variants
fn values() -> Vec<Self::Value> {
Self::iter().map(Self::into_value).collect()
}
}
#[cfg(test)]
mod tests {
use crate as sea_orm;
use crate::{entity::prelude::*, *};
use pretty_assertions::assert_eq;
#[test]
fn active_enum_string() {
#[derive(Debug, PartialEq, EnumIter)]
pub enum Category {
Big,
Small,
}
impl ActiveEnum for Category {
type Value = String;
fn name() -> String {
"category".to_owned()
}
fn to_value(&self) -> Self::Value {
match self {
Self::Big => "B",
Self::Small => "S",
}
.to_owned()
}
fn try_from_value(v: &Self::Value) -> Result<Self, DbErr> {
match v.as_ref() {
"B" => Ok(Self::Big),
"S" => Ok(Self::Small),
_ => Err(DbErr::Type(format!(
"unexpected value for Category enum: {}",
v
))),
}
}
fn db_type() -> ColumnDef {
ColumnType::String(Some(1)).def()
}
}
#[derive(Debug, PartialEq, EnumIter, DeriveActiveEnum)]
#[sea_orm(
rs_type = "String",
db_type = "String(Some(1))",
enum_name = "category"
)]
pub enum DeriveCategory {
#[sea_orm(string_value = "B")]
Big,
#[sea_orm(string_value = "S")]
Small,
}
assert_eq!(Category::Big.to_value(), "B".to_owned());
assert_eq!(Category::Small.to_value(), "S".to_owned());
assert_eq!(DeriveCategory::Big.to_value(), "B".to_owned());
assert_eq!(DeriveCategory::Small.to_value(), "S".to_owned());
assert_eq!(
Category::try_from_value(&"A".to_owned()).err(),
Some(DbErr::Type(
"unexpected value for Category enum: A".to_owned()
))
);
assert_eq!(
Category::try_from_value(&"B".to_owned()).ok(),
Some(Category::Big)
);
assert_eq!(
Category::try_from_value(&"S".to_owned()).ok(),
Some(Category::Small)
);
assert_eq!(
DeriveCategory::try_from_value(&"A".to_owned()).err(),
Some(DbErr::Type(
"unexpected value for DeriveCategory enum: A".to_owned()
))
);
assert_eq!(
DeriveCategory::try_from_value(&"B".to_owned()).ok(),
Some(DeriveCategory::Big)
);
assert_eq!(
DeriveCategory::try_from_value(&"S".to_owned()).ok(),
Some(DeriveCategory::Small)
);
assert_eq!(Category::db_type(), ColumnType::String(Some(1)).def());
assert_eq!(DeriveCategory::db_type(), ColumnType::String(Some(1)).def());
assert_eq!(Category::name(), DeriveCategory::name());
assert_eq!(Category::values(), DeriveCategory::values());
}
#[test]
fn active_enum_derive_signed_integers() {
macro_rules! test_int {
($ident: ident, $rs_type: expr, $db_type: expr, $col_def: ident) => {
#[derive(Debug, PartialEq, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = $rs_type, db_type = $db_type)]
pub enum $ident {
#[sea_orm(num_value = 1)]
Big,
#[sea_orm(num_value = 0)]
Small,
#[sea_orm(num_value = -10)]
Negative,
}
assert_eq!($ident::Big.to_value(), 1);
assert_eq!($ident::Small.to_value(), 0);
assert_eq!($ident::Negative.to_value(), -10);
assert_eq!($ident::try_from_value(&1).ok(), Some($ident::Big));
assert_eq!($ident::try_from_value(&0).ok(), Some($ident::Small));
assert_eq!($ident::try_from_value(&-10).ok(), Some($ident::Negative));
assert_eq!(
$ident::try_from_value(&2).err(),
Some(DbErr::Type(format!(
"unexpected value for {} enum: 2",
stringify!($ident)
)))
);
assert_eq!($ident::db_type(), ColumnType::$col_def.def());
};
}
test_int!(I8, "i8", "TinyInteger", TinyInteger);
test_int!(I16, "i16", "SmallInteger", SmallInteger);
test_int!(I32, "i32", "Integer", Integer);
test_int!(I64, "i64", "BigInteger", BigInteger);
}
#[test]
fn active_enum_derive_unsigned_integers() {
macro_rules! test_uint {
($ident: ident, $rs_type: expr, $db_type: expr, $col_def: ident) => {
#[derive(Debug, PartialEq, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = $rs_type, db_type = $db_type)]
pub enum $ident {
#[sea_orm(num_value = 1)]
Big,
#[sea_orm(num_value = 0)]
Small,
}
assert_eq!($ident::Big.to_value(), 1);
assert_eq!($ident::Small.to_value(), 0);
assert_eq!($ident::try_from_value(&1).ok(), Some($ident::Big));
assert_eq!($ident::try_from_value(&0).ok(), Some($ident::Small));
assert_eq!(
$ident::try_from_value(&2).err(),
Some(DbErr::Type(format!(
"unexpected value for {} enum: 2",
stringify!($ident)
)))
);
assert_eq!($ident::db_type(), ColumnType::$col_def.def());
};
}
test_uint!(U8, "u8", "TinyInteger", TinyInteger);
test_uint!(U16, "u16", "SmallInteger", SmallInteger);
test_uint!(U32, "u32", "Integer", Integer);
test_uint!(U64, "u64", "BigInteger", BigInteger);
}
}

View File

@ -1,5 +1,5 @@
use crate::{EntityName, IdenStatic, Iterable}; use crate::{EntityName, IdenStatic, Iterable};
use sea_query::{DynIden, Expr, SeaRc, SelectStatement, SimpleExpr, Value}; use sea_query::{Alias, BinOper, DynIden, Expr, SeaRc, SelectStatement, SimpleExpr, Value};
use std::str::FromStr; use std::str::FromStr;
/// Defines a Column for an Entity /// Defines a Column for an Entity
@ -62,6 +62,8 @@ pub enum ColumnType {
Custom(String), Custom(String),
/// A Universally Unique IDentifier that is specified in RFC 4122 /// A Universally Unique IDentifier that is specified in RFC 4122
Uuid, Uuid,
/// `ENUM` data type with name and variants
Enum(String, Vec<String>),
} }
macro_rules! bind_oper { macro_rules! bind_oper {
@ -76,6 +78,25 @@ macro_rules! bind_oper {
}; };
} }
macro_rules! bind_oper_with_enum_casting {
( $op: ident, $bin_op: ident ) => {
#[allow(missing_docs)]
fn $op<V>(&self, v: V) -> SimpleExpr
where
V: Into<Value>,
{
let val = Expr::val(v);
let col_def = self.def();
let col_type = col_def.get_column_type();
let expr = match col_type.get_enum_name() {
Some(enum_name) => val.as_enum(Alias::new(enum_name)),
None => val.into(),
};
Expr::tbl(self.entity_name(), *self).binary(BinOper::$bin_op, expr)
}
};
}
macro_rules! bind_func_no_params { macro_rules! bind_func_no_params {
( $func: ident ) => { ( $func: ident ) => {
/// See also SeaQuery's method with same name. /// See also SeaQuery's method with same name.
@ -128,8 +149,8 @@ pub trait ColumnTrait: IdenStatic + Iterable + FromStr {
(self.entity_name(), SeaRc::new(*self) as DynIden) (self.entity_name(), SeaRc::new(*self) as DynIden)
} }
bind_oper!(eq); bind_oper_with_enum_casting!(eq, Equal);
bind_oper!(ne); bind_oper_with_enum_casting!(ne, NotEqual);
bind_oper!(gt); bind_oper!(gt);
bind_oper!(gte); bind_oper!(gte);
bind_oper!(lt); bind_oper!(lt);
@ -281,6 +302,13 @@ impl ColumnType {
indexed: false, indexed: false,
} }
} }
pub(crate) fn get_enum_name(&self) -> Option<&String> {
match self {
ColumnType::Enum(s, _) => Some(s),
_ => None,
}
}
} }
impl ColumnDef { impl ColumnDef {
@ -306,6 +334,11 @@ impl ColumnDef {
self.indexed = true; self.indexed = true;
self self
} }
/// Get [ColumnType] as reference
pub fn get_column_type(&self) -> &ColumnType {
&self.col_type
}
} }
impl From<ColumnType> for sea_query::ColumnType { impl From<ColumnType> for sea_query::ColumnType {
@ -331,7 +364,7 @@ impl From<ColumnType> for sea_query::ColumnType {
ColumnType::Money(s) => sea_query::ColumnType::Money(s), ColumnType::Money(s) => sea_query::ColumnType::Money(s),
ColumnType::Json => sea_query::ColumnType::Json, ColumnType::Json => sea_query::ColumnType::Json,
ColumnType::JsonBinary => sea_query::ColumnType::JsonBinary, ColumnType::JsonBinary => sea_query::ColumnType::JsonBinary,
ColumnType::Custom(s) => { ColumnType::Custom(s) | ColumnType::Enum(s, _) => {
sea_query::ColumnType::Custom(sea_query::SeaRc::new(sea_query::Alias::new(&s))) sea_query::ColumnType::Custom(sea_query::SeaRc::new(sea_query::Alias::new(&s)))
} }
ColumnType::Uuid => sea_query::ColumnType::Uuid, ColumnType::Uuid => sea_query::ColumnType::Uuid,

View File

@ -95,6 +95,7 @@
/// // to create an ActiveModel using the [ActiveModelBehavior] /// // to create an ActiveModel using the [ActiveModelBehavior]
/// impl ActiveModelBehavior for ActiveModel {} /// impl ActiveModelBehavior for ActiveModel {}
/// ``` /// ```
mod active_enum;
mod active_model; mod active_model;
mod base_entity; mod base_entity;
mod column; mod column;
@ -106,6 +107,7 @@ pub mod prelude;
mod primary_key; mod primary_key;
mod relation; mod relation;
pub use active_enum::*;
pub use active_model::*; pub use active_model::*;
pub use base_entity::*; pub use base_entity::*;
pub use column::*; pub use column::*;

View File

@ -1,14 +1,15 @@
pub use crate::{ pub use crate::{
error::*, ActiveModelBehavior, ActiveModelTrait, ColumnDef, ColumnTrait, ColumnType, error::*, ActiveEnum, ActiveModelBehavior, ActiveModelTrait, ColumnDef, ColumnTrait,
DatabaseConnection, DbConn, EntityName, EntityTrait, EnumIter, ForeignKeyAction, Iden, ColumnType, DatabaseConnection, DbConn, EntityName, EntityTrait, EnumIter, ForeignKeyAction,
IdenStatic, Linked, ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult, Iden, IdenStatic, Linked, ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter,
Related, RelationDef, RelationTrait, Select, Value, QueryResult, Related, RelationDef, RelationTrait, Select, Value,
}; };
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
pub use crate::{ pub use crate::{
DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn,
DeriveEntityModel, DeriveIntoActiveModel, DeriveModel, DerivePrimaryKey, DeriveRelation, DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel, DeriveModel,
DerivePrimaryKey, DeriveRelation,
}; };
#[cfg(feature = "with-json")] #[cfg(feature = "with-json")]

View File

@ -11,6 +11,8 @@ pub enum DbErr {
RecordNotFound(String), RecordNotFound(String),
/// A custom error /// A custom error
Custom(String), Custom(String),
/// Error occurred while parsing value as target type
Type(String),
} }
impl std::error::Error for DbErr {} impl std::error::Error for DbErr {}
@ -23,6 +25,7 @@ impl std::fmt::Display for DbErr {
Self::Query(s) => write!(f, "Query Error: {}", s), Self::Query(s) => write!(f, "Query Error: {}", s),
Self::RecordNotFound(s) => write!(f, "RecordNotFound Error: {}", s), Self::RecordNotFound(s) => write!(f, "RecordNotFound Error: {}", s),
Self::Custom(s) => write!(f, "Custom Error: {}", s), Self::Custom(s) => write!(f, "Custom Error: {}", s),
Self::Type(s) => write!(f, "Type Error: {}", s),
} }
} }
} }

View File

@ -27,9 +27,10 @@
//! #### SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust. //! #### SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust.
//! //!
//! [![Getting Started](https://img.shields.io/badge/Getting%20Started-brightgreen)](https://www.sea-ql.org/SeaORM/docs/index) //! [![Getting Started](https://img.shields.io/badge/Getting%20Started-brightgreen)](https://www.sea-ql.org/SeaORM/docs/index)
//! [![Usage Example](https://img.shields.io/badge/Usage%20Example-yellow)](https://github.com/SeaQL/sea-orm/tree/master/examples/basic) //! [![Usage Example](https://img.shields.io/badge/Usage%20Example-green)](https://github.com/SeaQL/sea-orm/tree/master/examples/basic)
//! [![Rocket Example](https://img.shields.io/badge/Rocket%20Example-red)](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example)
//! [![Actix Example](https://img.shields.io/badge/Actix%20Example-blue)](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example) //! [![Actix Example](https://img.shields.io/badge/Actix%20Example-blue)](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example)
//! [![Rocket Example](https://img.shields.io/badge/Rocket%20Example-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) //! [![Axum Example](https://img.shields.io/badge/Axum%20Example-yellow)](https://github.com/SeaQL/sea-orm/tree/master/examples/axum_example)
//! [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) //! [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv)
//! //!
//! ## Features //! ## Features
@ -292,9 +293,9 @@ pub use schema::*;
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
pub use sea_orm_macros::{ pub use sea_orm_macros::{
DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, DeriveActiveEnum, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn,
DeriveEntityModel, DeriveIntoActiveModel, DeriveModel, DerivePrimaryKey, DeriveRelation, DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveIntoActiveModel, DeriveModel,
FromQueryResult, DerivePrimaryKey, DeriveRelation, FromQueryResult,
}; };
pub use sea_query; pub use sea_query;

View File

@ -1,9 +1,9 @@
use crate::{ use crate::{
ActiveModelTrait, EntityName, EntityTrait, IntoActiveModel, Iterable, PrimaryKeyTrait, ActiveModelTrait, ColumnTrait, EntityName, EntityTrait, IntoActiveModel, Iterable,
QueryTrait, PrimaryKeyTrait, QueryTrait,
}; };
use core::marker::PhantomData; use core::marker::PhantomData;
use sea_query::{InsertStatement, ValueTuple}; use sea_query::{Alias, Expr, InsertStatement, ValueTuple};
/// Performs INSERT operations on a ActiveModel /// Performs INSERT operations on a ActiveModel
#[derive(Debug)] #[derive(Debug)]
@ -133,11 +133,18 @@ where
} }
if av_has_val { if av_has_val {
columns.push(col); columns.push(col);
values.push(av.into_value().unwrap()); let val = Expr::val(av.into_value().unwrap());
let col_def = col.def();
let col_type = col_def.get_column_type();
let expr = match col_type.get_enum_name() {
Some(enum_name) => val.as_enum(Alias::new(enum_name)),
None => val.into(),
};
values.push(expr);
} }
} }
self.query.columns(columns); self.query.columns(columns);
self.query.values_panic(values); self.query.exprs_panic(values);
self self
} }

View File

@ -2,7 +2,7 @@ use crate::{ColumnTrait, EntityTrait, Iterable, QueryFilter, QueryOrder, QuerySe
use core::fmt::Debug; use core::fmt::Debug;
use core::marker::PhantomData; use core::marker::PhantomData;
pub use sea_query::JoinType; pub use sea_query::JoinType;
use sea_query::{DynIden, IntoColumnRef, SeaRc, SelectStatement, SimpleExpr}; use sea_query::{Alias, DynIden, Expr, IntoColumnRef, SeaRc, SelectStatement, SimpleExpr};
/// Defines a structure to perform select operations /// Defines a structure to perform select operations
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -114,13 +114,24 @@ where
} }
fn prepare_select(mut self) -> Self { fn prepare_select(mut self) -> Self {
self.query.columns(self.column_list()); self.query.exprs(self.column_list());
self self
} }
fn column_list(&self) -> Vec<(DynIden, E::Column)> { fn column_list(&self) -> Vec<SimpleExpr> {
let table = SeaRc::new(E::default()) as DynIden; let table = SeaRc::new(E::default()) as DynIden;
E::Column::iter().map(|col| (table.clone(), col)).collect() let text_type = SeaRc::new(Alias::new("text")) as DynIden;
E::Column::iter()
.map(|col| {
let expr = Expr::tbl(table.clone(), col);
let col_def = col.def();
let col_type = col_def.get_column_type();
match col_type.get_enum_name() {
Some(_) => expr.as_enum(text_type.clone()),
None => expr.into(),
}
})
.collect()
} }
fn prepare_from(mut self) -> Self { fn prepare_from(mut self) -> Self {

View File

@ -3,7 +3,7 @@ use crate::{
QueryTrait, QueryTrait,
}; };
use core::marker::PhantomData; use core::marker::PhantomData;
use sea_query::{IntoIden, SimpleExpr, UpdateStatement}; use sea_query::{Alias, Expr, IntoIden, SimpleExpr, UpdateStatement};
/// Defines a structure to perform UPDATE query operations on a ActiveModel /// Defines a structure to perform UPDATE query operations on a ActiveModel
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -109,7 +109,14 @@ where
} }
let av = self.model.get(col); let av = self.model.get(col);
if av.is_set() { if av.is_set() {
self.query.value(col, av.unwrap()); let val = Expr::val(av.into_value().unwrap());
let col_def = col.def();
let col_type = col_def.get_column_type();
let expr = match col_type.get_enum_name() {
Some(enum_name) => val.as_enum(Alias::new(enum_name)),
None => val.into(),
};
self.query.value_expr(col, expr);
} }
} }
self self

View File

@ -1,20 +1,58 @@
use crate::{ use crate::{
unpack_table_ref, ColumnTrait, EntityTrait, Identity, Iterable, PrimaryKeyToColumn, unpack_table_ref, ColumnTrait, ColumnType, DbBackend, EntityTrait, Identity, Iterable,
PrimaryKeyTrait, RelationTrait, Schema, PrimaryKeyToColumn, PrimaryKeyTrait, RelationTrait, Schema,
};
use sea_query::{
extension::postgres::{Type, TypeCreateStatement},
Alias, ColumnDef, ForeignKeyCreateStatement, Iden, Index, TableCreateStatement,
}; };
use sea_query::{ColumnDef, ForeignKeyCreateStatement, Iden, Index, TableCreateStatement};
impl Schema { impl Schema {
/// Creates a table from an Entity. See [TableCreateStatement] for more details /// Creates Postgres enums from an Entity. See [TypeCreateStatement] for more details
pub fn create_table_from_entity<E>(entity: E) -> TableCreateStatement pub fn create_enum_from_entity<E>(entity: E, db_backend: DbBackend) -> Vec<TypeCreateStatement>
where where
E: EntityTrait, E: EntityTrait,
{ {
create_table_from_entity(entity) create_enum_from_entity(entity, db_backend)
}
/// Creates a table from an Entity. See [TableCreateStatement] for more details
pub fn create_table_from_entity<E>(entity: E, db_backend: DbBackend) -> TableCreateStatement
where
E: EntityTrait,
{
create_table_from_entity(entity, db_backend)
} }
} }
pub(crate) fn create_table_from_entity<E>(entity: E) -> TableCreateStatement pub(crate) fn create_enum_from_entity<E>(_: E, db_backend: DbBackend) -> Vec<TypeCreateStatement>
where
E: EntityTrait,
{
if matches!(db_backend, DbBackend::MySql | DbBackend::Sqlite) {
return Vec::new();
}
let mut vec = Vec::new();
for col in E::Column::iter() {
let col_def = col.def();
let col_type = col_def.get_column_type();
if !matches!(col_type, ColumnType::Enum(_, _)) {
continue;
}
let (name, values) = match col_type {
ColumnType::Enum(s, v) => (s.as_str(), v),
_ => unreachable!(),
};
let stmt = Type::create()
.as_enum(Alias::new(name))
.values(values.iter().map(|val| Alias::new(val.as_str())))
.to_owned();
vec.push(stmt);
}
vec
}
pub(crate) fn create_table_from_entity<E>(entity: E, db_backend: DbBackend) -> TableCreateStatement
where where
E: EntityTrait, E: EntityTrait,
{ {
@ -22,7 +60,17 @@ where
for column in E::Column::iter() { for column in E::Column::iter() {
let orm_column_def = column.def(); let orm_column_def = column.def();
let types = orm_column_def.col_type.into(); let types = match orm_column_def.col_type {
ColumnType::Enum(s, variants) => match db_backend {
DbBackend::MySql => {
ColumnType::Custom(format!("ENUM('{}')", variants.join("', '")))
}
DbBackend::Postgres => ColumnType::Custom(s),
DbBackend::Sqlite => ColumnType::Text,
}
.into(),
_ => orm_column_def.col_type.into(),
};
let mut column_def = ColumnDef::new_with_type(column, types); let mut column_def = ColumnDef::new_with_type(column, types);
if !orm_column_def.null { if !orm_column_def.null {
column_def.not_null(); column_def.not_null();
@ -122,13 +170,14 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{sea_query::*, tests_cfg::*, Schema}; use crate::{sea_query::*, tests_cfg::*, DbBackend, Schema};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
fn test_create_table_from_entity() { fn test_create_table_from_entity() {
assert_eq!( assert_eq!(
Schema::create_table_from_entity(CakeFillingPrice).to_string(MysqlQueryBuilder), Schema::create_table_from_entity(CakeFillingPrice, DbBackend::MySql)
.to_string(MysqlQueryBuilder),
Table::create() Table::create()
.table(CakeFillingPrice) .table(CakeFillingPrice)
.col( .col(

View File

@ -0,0 +1,92 @@
pub mod common;
pub use common::{features::*, setup::*, TestContext};
use sea_orm::{entity::prelude::*, entity::*, DatabaseConnection};
#[sea_orm_macros::test]
#[cfg(any(
feature = "sqlx-mysql",
feature = "sqlx-sqlite",
feature = "sqlx-postgres"
))]
async fn main() -> Result<(), DbErr> {
let ctx = TestContext::new("active_enum_tests").await;
create_tables(&ctx.db).await?;
insert_active_enum(&ctx.db).await?;
ctx.delete().await;
Ok(())
}
pub async fn insert_active_enum(db: &DatabaseConnection) -> Result<(), DbErr> {
use active_enum::*;
let am = ActiveModel {
category: Set(None),
color: Set(None),
tea: Set(None),
..Default::default()
}
.insert(db)
.await?;
let model = Entity::find().one(db).await?.unwrap();
assert_eq!(
model,
Model {
id: 1,
category: None,
color: None,
tea: None,
}
);
assert_eq!(
model,
Entity::find()
.filter(Column::Id.is_not_null())
.filter(Column::Category.is_null())
.filter(Column::Color.is_null())
.filter(Column::Tea.is_null())
.one(db)
.await?
.unwrap()
);
let am = ActiveModel {
category: Set(Some(Category::Big)),
color: Set(Some(Color::Black)),
tea: Set(Some(Tea::EverydayTea)),
..am
}
.save(db)
.await?;
let model = Entity::find().one(db).await?.unwrap();
assert_eq!(
model,
Model {
id: 1,
category: Some(Category::Big),
color: Some(Color::Black),
tea: Some(Tea::EverydayTea),
}
);
assert_eq!(
model,
Entity::find()
.filter(Column::Id.eq(1))
.filter(Column::Category.eq(Category::Big))
.filter(Column::Color.eq(Color::Black))
.filter(Column::Tea.eq(Tea::EverydayTea))
.one(db)
.await?
.unwrap()
);
let res = am.delete(db).await?;
assert_eq!(res.rows_affected, 1);
assert_eq!(Entity::find().one(db).await?, None);
Ok(())
}

View File

@ -0,0 +1,43 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "active_enum")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub category: Option<Category>,
pub color: Option<Color>,
pub tea: Option<Tea>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "String(Some(1))")]
pub enum Category {
#[sea_orm(string_value = "B")]
Big,
#[sea_orm(string_value = "S")]
Small,
}
#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "i32", db_type = r#"Integer"#)]
pub enum Color {
#[sea_orm(num_value = 0)]
Black,
#[sea_orm(num_value = 1)]
White,
}
#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "tea")]
pub enum Tea {
#[sea_orm(string_value = "EverydayTea")]
EverydayTea,
#[sea_orm(string_value = "BreakfastTea")]
BreakfastTea,
}

View File

@ -1,3 +1,4 @@
pub mod active_enum;
pub mod applog; pub mod applog;
pub mod byte_primary_key; pub mod byte_primary_key;
pub mod metadata; pub mod metadata;
@ -5,6 +6,7 @@ pub mod repository;
pub mod schema; pub mod schema;
pub mod self_join; pub mod self_join;
pub use active_enum::Entity as ActiveEnum;
pub use applog::Entity as Applog; pub use applog::Entity as Applog;
pub use byte_primary_key::Entity as BytePrimaryKey; pub use byte_primary_key::Entity as BytePrimaryKey;
pub use metadata::Entity as Metadata; pub use metadata::Entity as Metadata;

View File

@ -1,11 +1,11 @@
pub use super::super::bakery_chain::*; pub use super::super::bakery_chain::*;
use super::*; use super::*;
use crate::common::setup::{create_table, create_table_without_asserts}; use crate::common::setup::{create_enum, create_table, create_table_without_asserts};
use sea_orm::{ use sea_orm::{
error::*, sea_query, ConnectionTrait, DatabaseConnection, DbBackend, DbConn, ExecResult, error::*, sea_query, ConnectionTrait, DatabaseConnection, DbBackend, DbConn, ExecResult,
}; };
use sea_query::{ColumnDef, ForeignKeyCreateStatement}; use sea_query::{extension::postgres::Type, Alias, ColumnDef, ForeignKeyCreateStatement};
pub async fn create_tables(db: &DatabaseConnection) -> Result<(), DbErr> { pub async fn create_tables(db: &DatabaseConnection) -> Result<(), DbErr> {
create_log_table(db).await?; create_log_table(db).await?;
@ -13,6 +13,7 @@ pub async fn create_tables(db: &DatabaseConnection) -> Result<(), DbErr> {
create_repository_table(db).await?; create_repository_table(db).await?;
create_self_join_table(db).await?; create_self_join_table(db).await?;
create_byte_primary_key_table(db).await?; create_byte_primary_key_table(db).await?;
create_active_enum_table(db).await?;
Ok(()) Ok(())
} }
@ -123,3 +124,40 @@ pub async fn create_byte_primary_key_table(db: &DbConn) -> Result<ExecResult, Db
create_table_without_asserts(db, &stmt).await create_table_without_asserts(db, &stmt).await
} }
pub async fn create_active_enum_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let db_backend = db.get_database_backend();
let tea_enum = Alias::new("tea");
let create_enum_stmts = match db_backend {
DbBackend::MySql | DbBackend::Sqlite => Vec::new(),
DbBackend::Postgres => vec![Type::create()
.as_enum(tea_enum.clone())
.values(vec![Alias::new("EverydayTea"), Alias::new("BreakfastTea")])
.to_owned()],
};
create_enum(db, &create_enum_stmts, ActiveEnum).await?;
let mut tea_col = ColumnDef::new(active_enum::Column::Tea);
match db_backend {
DbBackend::MySql => tea_col.custom(Alias::new("ENUM('EverydayTea', 'BreakfastTea')")),
DbBackend::Sqlite => tea_col.text(),
DbBackend::Postgres => tea_col.custom(tea_enum),
};
let create_table_stmt = sea_query::Table::create()
.table(active_enum::Entity)
.col(
ColumnDef::new(active_enum::Column::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(active_enum::Column::Category).string_len(1))
.col(ColumnDef::new(active_enum::Column::Color).integer())
.col(&mut tea_col)
.to_owned();
create_table(db, &create_table_stmt, ActiveEnum).await
}

View File

@ -1,9 +1,12 @@
use pretty_assertions::assert_eq;
use sea_orm::{ use sea_orm::{
ConnectionTrait, Database, DatabaseBackend, DatabaseConnection, DbBackend, DbConn, DbErr, ColumnTrait, ColumnType, ConnectionTrait, Database, DatabaseBackend, DatabaseConnection,
EntityTrait, ExecResult, Schema, Statement, DbBackend, DbConn, DbErr, EntityTrait, ExecResult, Iterable, Schema, Statement,
};
use sea_query::{
extension::postgres::{Type, TypeCreateStatement},
Alias, Table, TableCreateStatement,
}; };
use sea_query::{Alias, Table, TableCreateStatement};
pub async fn setup(base_url: &str, db_name: &str) -> DatabaseConnection { pub async fn setup(base_url: &str, db_name: &str) -> DatabaseConnection {
let db = if cfg!(feature = "sqlx-mysql") { let db = if cfg!(feature = "sqlx-mysql") {
@ -74,6 +77,51 @@ pub async fn tear_down(base_url: &str, db_name: &str) {
}; };
} }
pub async fn create_enum<E>(
db: &DbConn,
creates: &[TypeCreateStatement],
entity: E,
) -> Result<(), DbErr>
where
E: EntityTrait,
{
let builder = db.get_database_backend();
if builder == DbBackend::Postgres {
for col in E::Column::iter() {
let col_def = col.def();
let col_type = col_def.get_column_type();
if !matches!(col_type, ColumnType::Enum(_, _)) {
continue;
}
let name = match col_type {
ColumnType::Enum(s, _) => s.as_str(),
_ => unreachable!(),
};
let drop_type_stmt = Type::drop()
.name(Alias::new(name))
.if_exists()
.cascade()
.to_owned();
let stmt = builder.build(&drop_type_stmt);
db.execute(stmt).await?;
}
}
let expect_stmts: Vec<Statement> = creates.iter().map(|stmt| builder.build(stmt)).collect();
let create_from_entity_stmts: Vec<Statement> = Schema::create_enum_from_entity(entity, builder)
.iter()
.map(|stmt| builder.build(stmt))
.collect();
assert_eq!(expect_stmts, create_from_entity_stmts);
for stmt in expect_stmts {
db.execute(stmt).await.map(|_| ())?;
}
Ok(())
}
pub async fn create_table<E>( pub async fn create_table<E>(
db: &DbConn, db: &DbConn,
create: &TableCreateStatement, create: &TableCreateStatement,
@ -84,7 +132,7 @@ where
{ {
let builder = db.get_database_backend(); let builder = db.get_database_backend();
assert_eq!( assert_eq!(
builder.build(&Schema::create_table_from_entity(entity)), builder.build(&Schema::create_table_from_entity(entity, builder)),
builder.build(create) builder.build(create)
); );