Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add filter() fn, update pagination() fn #103

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ pub struct MainOptions {
/// Generate the "ConnectionType" type only once in a "common.rs" file
#[arg(long = "once-connection-type")]
pub once_connection_type: bool,

/// Set which diesel backend to use (something which implements `diesel::backend::Backend`)
/// Diesel provides the following backends:
/// - `diesel::pg::Pg`
/// - `diesel::sqlite::Sqlite`
/// - `diesel::mysql::Mysql`
///
/// See `crate::GenerationConfig::diesel_backend` for more details.
#[arg(short = 'b', long = "diesel-backend")]
pub diesel_backend: String,
}

#[derive(Debug, ValueEnum, Clone, PartialEq, Default)]
Expand Down Expand Up @@ -203,6 +213,7 @@ fn actual_main() -> dsync::Result<()> {
model_path: args.model_path,
once_common_structs: args.once_common_structs,
once_connection_type: args.once_connection_type,
diesel_backend: args.diesel_backend,
},
)?;

Expand Down
86 changes: 79 additions & 7 deletions src/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ impl<'a> Struct<'a> {
/// Render the full struct
fn render(&mut self) {
let ty = self.ty;
let table = &self.table;
let table = self.table;

let primary_keys: Vec<String> = table.primary_key_column_names();

Expand Down Expand Up @@ -423,12 +423,13 @@ impl {struct_name} {{

buffer.push_str(&format!(r##"
/// Paginates through the table where page is a 0-based index (i.e. page 0 is the first page)
pub{async_keyword} fn paginate(db: &mut ConnectionType, page: i64, page_size: i64) -> QueryResult<PaginationResult<Self>> {{
pub{async_keyword} fn paginate(db: &mut ConnectionType, page: i64, page_size: i64, filter: {struct_name}Filter) -> QueryResult<PaginationResult<Self>> {{
use {schema_path}{table_name}::dsl::*;

let page_size = if page_size < 1 {{ 1 }} else {{ page_size }};
let total_items = {table_name}.count().get_result(db){await_keyword}?;
let items = {table_name}.limit(page_size).offset(page * page_size).load::<Self>(db){await_keyword}?;
let page = page.max(0);
let page_size = page_size.max(1);
let total_items = Self::filter(filter.clone()).count().get_result(db)?;
let items = Self::filter(filter).limit(page_size).offset(page * page_size).load::<Self>(db){await_keyword}?;

Ok(PaginationResult {{
items,
Expand All @@ -441,6 +442,51 @@ impl {struct_name} {{
}}
"##));

// Table::filter() helper fn
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this block should likely be its own function to keep the functions smaller, but this can be delayed until we refactor this (see #105)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, I’ll take a look at this later and try to rebase 🙌

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unless you want to delay this PR, i will hold off on merging other stuff (even if approved) so you can get this (and possibly your other PR's) merged without having to rebase it often

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @hasezoey, I really appreciate your consideration 🤗

Please do approve this PR if you think it's generally acceptable. That way, I can rebase and merge later without having to stop any others.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do approve this PR if you think it's generally acceptable. That way, I can rebase and merge later without having to stop any others.

i take this as meaning you have not rebased yet, so i will merge the other standing (approved) PRs to not have to wait, after that i will approve

let diesel_backend = &config.diesel_backend;
let filters = table
.columns
.iter()
.map(|column| {
let column_name = column.name.to_string();
format!(
r##"
if let Some(filter_{column_name}) = filter.{column_name}.clone() {{
query = query.filter({schema_path}{table_name}::{column_name}.eq(filter_{column_name}));
}}"##
)
})
Comment on lines +451 to +459
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks quite redundant, is there not better option?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feel free to suggest changes. This new filter() API is an experiment I want to run and play with. We might want to remove it in the future as intellisense gets better and better around macros.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i had already looked, but there does not seem to be a diesel helper trait which would have a Update-like struct be the filter, something as simple as .filter(FilterStruct { someprop: Some(SomeValue), ..Default::default() }) similar to what .select(Struct::as_select()) would look like

.collect::<Vec<_>>()
.join("");
buffer.push_str(&format!(
r##"
/// A utility function to help build custom search queries
///
/// Example:
///
/// ```
/// // create a filter for completed todos
/// let query = Todo::filter(TodoFilter {{
/// completed: Some(true),
/// ..Default::default()
/// }});
///
/// // delete completed todos
/// diesel::delete(query).execute(db)?;
/// ```
pub fn filter<'a>(
filter: {struct_name}Filter,
) -> {schema_path}{table_name}::BoxedQuery<'a, {diesel_backend}> {{
let mut query = {schema_path}{table_name}::table.into_boxed();
{filters}

query
}}
"##
));
}

// TODO: If primary key columns are attached to the form struct (not optionally)
// then don't require item_id_params (otherwise it'll be duplicated)

Expand Down Expand Up @@ -470,10 +516,36 @@ impl {struct_name} {{
));

buffer.push_str(
r##"
}"##,
r#"
}"#,
);

// generate filter struct for filter() helper function
{
let filter_fields = table
.columns
.iter()
.map(|column| {
let struct_field = StructField::from(column);
format!(
"pub {column_name}: Option<{column_type}>,",
column_name = struct_field.name,
column_type = struct_field.to_rust_type()
)
})
.collect::<Vec<_>>()
.join("\n ");

buffer.push_str(&format!(
r##"
#[derive(Debug, Default, Clone)]
pub struct {struct_name}Filter {{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably be a new structtype

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's leave this for now as it's an experimental API that needs more iterations before we solidify it as a core struct.

{filter_fields}
}}
"##
));
}

buffer
}

Expand Down
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ pub enum ErrorEnum {
#[error("NoFileSignature: {0}")]
NoFileSignature(String),

/// Invalid generation config
#[error("InvalidGenerationConfig: {0}")]
InvalidGenerationConfig(String),

/// Variant for Other messages
#[error("Other: {0}")]
Other(String),
Expand Down
40 changes: 37 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use std::collections::HashMap;
use std::fmt::Display;
use std::path::{Path, PathBuf};

use crate::error::ErrorEnum;

#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum StringType {
/// Use "String"
Expand Down Expand Up @@ -214,14 +216,26 @@ impl<'a> Default for TableOptions<'a> {

#[derive(Debug, Clone)]
pub struct GenerationConfig<'a> {
/// Specific Table options for a given table
/// Specific code generation options for a particular table
pub table_options: HashMap<&'a str, TableOptions<'a>>,
/// Default table options, used when not in `table_options`
/// Default table options, can be overriden by `table_options`
pub default_table_options: TableOptions<'a>,
/// Connection type to insert
///
/// Example: `diesel::SqliteConnection`
/// For example:
/// - `diesel::pg::PgConnection` (default)
/// - `diesel::sqlite::SqliteConnection`
/// - `diesel::mysql::MysqlConnection`
/// - or, your custom diesel connection type (struct which implements `diesel::connection::Connection`)
pub connection_type: String,
/// Diesel backend
///
/// For example:
/// - `diesel::pg::Pg` (default)
/// - `diesel::sqlite::Sqlite`
/// - `diesel::mysql::Mysql`
/// - or, your custom diesel backend type (struct which implements `diesel::backend::Backend`)
pub diesel_backend: String,
/// Diesel schema import path
///
/// by default `crate::schema::`
Expand Down Expand Up @@ -309,6 +323,24 @@ impl From<&MarkedFile> for FileChange {
}
}

pub fn validate_config(config: &GenerationConfig) -> Result<()> {
const VALID_BACKENDS: [&str; 3] = [
"diesel::pg::Pg",
"diesel::sqlite::Sqlite",
"diesel::mysql::Mysql",
];

if config.diesel_backend.is_empty() {
return Err(Error::new(ErrorEnum::InvalidGenerationConfig(format!(
"Invalid diesel_backend '{}', please use one of the following: {:?}; or, a custom diesel backend type (a struct which implements `diesel::backend::Backend`).",
&config.diesel_backend,
VALID_BACKENDS.join(", ")
))));
}

Ok(())
}

/// Generate all Models for a given diesel schema file
///
/// Models are saved to disk
Expand All @@ -317,6 +349,8 @@ pub fn generate_files(
output_models_dir: &Path,
config: GenerationConfig,
) -> Result<Vec<FileChange>> {
validate_config(&config)?;

let generated = generate_code(
&std::fs::read_to_string(input_diesel_schema_file)
.attach_path_err(input_diesel_schema_file)?,
Expand Down
45 changes: 40 additions & 5 deletions test/autogenerated_all/models/todos/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ impl Todos {
}

/// Paginates through the table where page is a 0-based index (i.e. page 0 is the first page)
pub fn paginate(db: &mut ConnectionType, page: i64, page_size: i64) -> QueryResult<PaginationResult<Self>> {
pub fn paginate(db: &mut ConnectionType, page: i64, page_size: i64, filter: TodosFilter) -> QueryResult<PaginationResult<Self>> {
use crate::schema::todos::dsl::*;

let page_size = if page_size < 1 { 1 } else { page_size };
let total_items = todos.count().get_result(db)?;
let items = todos.limit(page_size).offset(page * page_size).load::<Self>(db)?;
let page = page.max(0);
let page_size = page_size.max(1);
let total_items = Self::filter(filter.clone()).count().get_result(db)?;
let items = Self::filter(filter).limit(page_size).offset(page * page_size).load::<Self>(db)?;

Ok(PaginationResult {
items,
Expand All @@ -65,6 +66,35 @@ impl Todos {
})
}

/// A utility function to help build custom search queries
///
/// Example:
///
/// ```
/// // create a filter for completed todos
/// let query = Todo::filter(TodoFilter {
/// completed: Some(true),
/// ..Default::default()
/// });
///
/// // delete completed todos
/// diesel::delete(query).execute(db)?;
/// ```
pub fn filter<'a>(
filter: TodosFilter,
) -> crate::schema::todos::BoxedQuery<'a, diesel::pg::Pg> {
let mut query = crate::schema::todos::table.into_boxed();

if let Some(filter_id) = filter.id.clone() {
query = query.filter(crate::schema::todos::id.eq(filter_id));
}
if let Some(filter_created_at) = filter.created_at.clone() {
query = query.filter(crate::schema::todos::created_at.eq(filter_created_at));
}

query
}

pub fn update(db: &mut ConnectionType, param_id: i32, item: &UpdateTodos) -> QueryResult<Self> {
use crate::schema::todos::dsl::*;

Expand All @@ -77,4 +107,9 @@ impl Todos {
diesel::delete(todos.filter(id.eq(param_id))).execute(db)
}

}
}
#[derive(Debug, Default, Clone)]
pub struct TodosFilter {
pub id: Option<i32>,
pub created_at: Option<chrono::NaiveDateTime>,
}
2 changes: 1 addition & 1 deletion test/autogenerated_all/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"

cd $SCRIPT_DIR

cargo run -- -i schema.rs -o models -g id -g created_at -c "diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>"
cargo run -- -i schema.rs -b diesel::pg::Pg -o models -g id -g created_at -c "diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>"
45 changes: 40 additions & 5 deletions test/autogenerated_attributes/models/todos/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ impl Todos {
}

/// Paginates through the table where page is a 0-based index (i.e. page 0 is the first page)
pub fn paginate(db: &mut ConnectionType, page: i64, page_size: i64) -> QueryResult<PaginationResult<Self>> {
pub fn paginate(db: &mut ConnectionType, page: i64, page_size: i64, filter: TodosFilter) -> QueryResult<PaginationResult<Self>> {
use crate::schema::todos::dsl::*;

let page_size = if page_size < 1 { 1 } else { page_size };
let total_items = todos.count().get_result(db)?;
let items = todos.limit(page_size).offset(page * page_size).load::<Self>(db)?;
let page = page.max(0);
let page_size = page_size.max(1);
let total_items = Self::filter(filter.clone()).count().get_result(db)?;
let items = Self::filter(filter).limit(page_size).offset(page * page_size).load::<Self>(db)?;

Ok(PaginationResult {
items,
Expand All @@ -70,6 +71,35 @@ impl Todos {
})
}

/// A utility function to help build custom search queries
///
/// Example:
///
/// ```
/// // create a filter for completed todos
/// let query = Todo::filter(TodoFilter {
/// completed: Some(true),
/// ..Default::default()
/// });
///
/// // delete completed todos
/// diesel::delete(query).execute(db)?;
/// ```
pub fn filter<'a>(
filter: TodosFilter,
) -> crate::schema::todos::BoxedQuery<'a, diesel::pg::Pg> {
let mut query = crate::schema::todos::table.into_boxed();

if let Some(filter_id) = filter.id.clone() {
query = query.filter(crate::schema::todos::id.eq(filter_id));
}
if let Some(filter_created_at) = filter.created_at.clone() {
query = query.filter(crate::schema::todos::created_at.eq(filter_created_at));
}

query
}

pub fn update(db: &mut ConnectionType, param_id: i32, item: &UpdateTodos) -> QueryResult<Self> {
use crate::schema::todos::dsl::*;

Expand All @@ -82,4 +112,9 @@ impl Todos {
diesel::delete(todos.filter(id.eq(param_id))).execute(db)
}

}
}
#[derive(Debug, Default, Clone)]
pub struct TodosFilter {
pub id: Option<i32>,
pub created_at: Option<chrono::NaiveDateTime>,
}
2 changes: 1 addition & 1 deletion test/autogenerated_attributes/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"

cd $SCRIPT_DIR

cargo run -- -i schema.rs -o models -g created_at -c "diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>"
cargo run -- -i schema.rs -b diesel::pg::Pg -o models -g created_at -c "diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>"
Loading