Skip to content

Commit

Permalink
feat: Advanced queries flag (BREAKING CHANGE) (#126)
Browse files Browse the repository at this point in the history
* feat: Advanced queries flag

BREAKING CHANGE

* fix(advanced-queries): Fix filters for nullable columns

* Update src/code.rs

Co-authored-by: hasezoey <[email protected]>

* Update src/code.rs

Co-authored-by: hasezoey <[email protected]>

* update tests

---------

Co-authored-by: hasezoey <[email protected]>
  • Loading branch information
Wulf and hasezoey authored Jan 15, 2024
1 parent bfb1c8e commit fd5862d
Show file tree
Hide file tree
Showing 46 changed files with 373 additions and 639 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ async = []
backtrace = []
# enable derive "QueryableByName"
derive-queryablebyname = []
# enable *experimental* queries
advanced-queries = []

[dependencies]
clap = { version = "4.4", features = ["derive", "wrap_help"] }
Expand Down
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ async fn demo(db: Connection) {
text: "Create a demo",
completed: false,
})?;

let todos_list = todos::paginate(&mut db, 1, 10)?;


let updated_todo = todos::update(&mut db, created_todo.id, UpdateTodo {
text: created_todo.text,
completed: true,
Expand Down Expand Up @@ -120,6 +118,7 @@ cargo install dsync
* `--single-model-file`: (optional) Generate only a single model file, instead of a directory with `mod.rs` and `generated.rs`
* `--readonly-prefix`: (optional, repeatable) A prefix to treat a table matching this as readonly *2
* `--readonly-suffix`: (optional, repeatable) A suffix to treat a table matching this as readonly *2
* `--diesel-backend`: (when the "advanced-queries" feature is enabled) The diesel backend in use (possible values include `diesel::pg::Pg`, `diesel::sqlite::Sqlite`, `diesel::mysql::Mysql`, or your custom backend type)
* note: the CLI has fail-safes to prevent accidental file overwriting
```sh
Expand All @@ -131,6 +130,14 @@ Notes:
- *2: "readonly" tables dont have `Update*` & `Create*` structs, only `*`(no suffix / prefix) structs.
For example this is useful for Sqlite views, which are read-only (cannot be written to, but can be read)
## Experimental API
We're currently experimenting with advanced query generation. This includes pagination, filtering/searching, and the like. Enable the `advanced-queries` feature flag to see some of it in action.
Alternatively, you can see what gets generated in the advanced queries test here: [`test/advanced_queries/models`](test/advanced_queries/models)
Feel free to open an issue to discuss these API and provide your feeedback.
## Docs
See `dsync --help` for more information.
Expand Down
13 changes: 13 additions & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ pub struct MainOptions {
/// A Suffix to treat a table matching this as readonly (only generate the Read struct)
#[arg(long = "readonly-suffix")]
pub readonly_suffixes: Vec<String>,

#[cfg(feature = "advanced-queries")]
/// 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 @@ -251,6 +262,8 @@ fn actual_main() -> dsync::Result<()> {
once_connection_type: args.once_connection_type,
readonly_prefixes: args.readonly_prefixes,
readonly_suffixes: args.readonly_suffixes,
#[cfg(feature = "advanced-queries")]
diesel_backend: args.diesel_backend,
},
)?;

Expand Down
98 changes: 94 additions & 4 deletions src/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,14 +535,16 @@ fn build_table_fns(
"##
));

#[cfg(feature = "advanced-queries")]
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) -> diesel::QueryResult<PaginationResult<Self>> {{
pub{async_keyword} fn paginate(db: &mut ConnectionType, page: i64, page_size: i64, filter: {struct_name}Filter) -> diesel::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 @@ -555,6 +557,67 @@ fn build_table_fns(
}}
"##));

#[cfg(feature = "advanced-queries")]
// Table::filter() helper fn
{
let diesel_backend = &config.diesel_backend;
let filters = table
.columns
.iter()
.map(|column| {
let column_name = column.name.to_string();

if column.is_nullable {
// "Option::None" will never match anything, and "is_null" is required to be used, see https://docs.diesel.rs/master/diesel/expression_methods/trait.ExpressionMethods.html#method.eq
format!(
r##"
if let Some(filter_{column_name}) = filter.{column_name}.clone() {{
query = if filter_{column_name}.is_some() {{
query.filter({schema_path}{table_name}::{column_name}.eq(filter_{column_name}))
}} else {{
query.filter({schema_path}{table_name}::{column_name}.is_null())
}};
}}"##
)
} else {
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}));
}}"##
)
}
})
.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 @@ -589,6 +652,33 @@ fn build_table_fns(

buffer.push_str("}\n");

#[cfg(feature = "advanced-queries")]
// 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(&formatdoc!(
r##"
#[derive(Debug, Default, Clone)]
pub struct {struct_name}Filter {{
{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
38 changes: 36 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ use std::collections::HashMap;
use std::fmt::Display;
use std::path::{Path, PathBuf};

use crate::error::ErrorEnum;

/// Available options for string types
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum StringType {
Expand Down Expand Up @@ -306,9 +308,9 @@ impl<'a> Default for TableOptions<'a> {
/// Global config, not table specific
#[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
///
Expand All @@ -335,6 +337,16 @@ pub struct GenerationConfig<'a> {
pub readonly_prefixes: Vec<String>,
/// Suffixes to treat tables as readonly
pub readonly_suffixes: Vec<String>,

#[cfg(feature = "advanced-queries")]
/// 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,
}

impl GenerationConfig<'_> {
Expand All @@ -356,6 +368,25 @@ impl GenerationConfig<'_> {
}
}

#[cfg(feature = "advanced-queries")]
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 a model for the given schema contents
///
/// Model is returned and not saved to disk yet
Expand Down Expand Up @@ -436,6 +467,9 @@ pub fn generate_files(
output_models_dir: &Path,
config: GenerationConfig,
) -> Result<Vec<FileChange>> {
#[cfg(feature = "advanced-queries")]
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
1 change: 1 addition & 0 deletions test/advanced_queries/models/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod todos;
Loading

0 comments on commit fd5862d

Please sign in to comment.