Base implementation and overrides

Hey!

Coming from OOP languages I'm struggling to find the correct approach for code structuring related to db filters. The problem is I need to override some methods for some dbs (e.g. how the like filter is built).

I came up to something like this, but I'm not sure if it's a good solution:

  1. I will have smth like 20 different db dialects and will have to duplicate BuildFilter impl to support all db dialects
  2. I have to define structs for each db with the same fields
pub struct MysqlBinaryFilter {
    filter: BinaryFilter,
    param_allocator: ParamAllocator,
}

pub struct OtherlBinaryFilter {
    filter: BinaryFilter,
    param_allocator: ParamAllocator,
}

I also thought not to define these fields but rather pass them as props, e.g.

fn equals(&self, filter: &BinaryFilter, column: &String, param_allocator: &ParamAllocator) -> String {
        // default equals
        format!("{} = {}", column, self.filter().member)
}

Here's the full code

pub struct BinaryFilter {
    pub operator: BinaryOperator,
    pub member: String,
    pub values: Vec<String>,
}

pub trait BuildFilter {
    fn filter(&self) -> &BinaryFilter;
    
    fn param_allocator(&self) -> &ParamAllocator;

    fn equals(&self, column: &String) -> String {
        // default equals
        format!("{} = {}", column, self.filter().member)
    }
    
    fn like(&self, column: &String) -> String {
        // default like
    }
}


pub struct GenericBinaryFilter<'a> {
    filter: &'a BinaryFilter,
    param_allocator: ParamAllocator,
}
    
impl<'a> BuildFilter for GenericBinaryFilter<'a> {
    fn filter(&self) -> &BinaryFilter {
        &self.filter
    }

    fn param_allocator(&self) -> &ParamAllocator {
        &self.param_allocator
    }
}


pub struct PostgresBinaryFilter {
    filter: BinaryFilter,
    param_allocator: ParamAllocator,
}

impl BuildFilter for PostgresBinaryFilter {
    fn filter(&self) -> &BinaryFilter {
        &self.filter
    }

    fn param_allocator(&self) -> &ParamAllocator {
        &self.param_allocator
    }
}

pub struct MysqlBinaryFilter {
    filter: BinaryFilter,
    param_allocator: ParamAllocator,
}

impl BuildFilter for MysqlBinaryFilter {
    fn filter(&self) -> &BinaryFilter {
        &self.filter
    }

    fn param_allocator(&self) -> &ParamAllocator {
        &self.param_allocator
    }
}

What do you think? Are there better ways to sort it out?

I don't know if it's a good fit for everything you want to do, but one approach is to split the customizable and not-customizable functionality...

pub trait SharedBuildFilter {
    fn filter(&self) -> &BinaryFilter;
    fn param_allocator(&self) -> &ParamAllocator;
}

pub trait BuildFilter: SharedBuildFilter {
    fn equals(&self, column: &String) -> String {
        // default equals
        format!("{} = {}", column, self.filter().member)
    }
    
    fn like(&self, column: &String) -> String {
        // default like
        column.into()
    }
}

...make the similar types share a generic type constructor...

pub struct FlavoredBinaryFilter<Flavor> {
    // See: https://doc.rust-lang.org/core/marker/struct.PhantomData.html
    // Alternatively just store `Flavor`, which could be zero-sized or
    // could hold flavor-specific data
    flavor: PhantomData<Flavor>,
    filter: BinaryFilter,
    param_allocator: ParamAllocator,
}

impl<Flavor> SharedBuildFilter for FlavoredBinaryFilter<Flavor> {
    fn filter(&self) -> &BinaryFilter {
        &self.filter
    }

    fn param_allocator(&self) -> &ParamAllocator {
        &self.param_allocator
    }
}

...and override the default trait method bodies on a flavor-per-flavor basis.

pub enum MySql {}
impl BuildFilter for FlavoredBinaryFilter<MySql> {
    fn like(&self, column: &String) -> String {
        column.chars().rev().collect()
    }
}

Bonus tip: Always take &str as an input instead of &String (and &[T] instead of &Vec<T>).

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.