I answer my question with what I finally created.
Define a simple case and mapperper class
case class User( id: String = java.util.UUID.randomUUID().toString, companyScopeId: String, firstName: Option[String] = None, lastName: Option[String] = None ) class UserTable(tag: Tag) extends Table[User](tag,"USER") { override def id = column[String]("id", O.PrimaryKey) def companyScopeId = column[String]("company_scope_id", O.NotNull) def firstName = column[Option[String]]("first_name", O.Nullable) def lastName = column[Option[String]]("last_name", O.Nullable) def * = (id, companyScopeId, firstName, lastName) <> (User.tupled,User.unapply) }
Slick predicate concept
I assume that the concept of "predicate" is what can be put inside TableQuery.filter . But this type is quite complex, because it performs the Table function and returns a type that has an implicit CanBeQueryCondition
Unfortunately for me there are 3 different types that have CanBeQueryCondition and putting them in a list that needs to be folded into a single predicate seems to be not easy (i.e. filter is easy to use, but && and || hard to apply (as far as I tried)) . But, fortunately, we can easily convert Boolean to Colunm[Boolean] to Column[Option[Boolean]] using the extension method .? .
So, let's define our predicate type:
type TablePredicate[Item, T <: Table[Item]] = T => Column[Option[Boolean]]
Adding a list of predicates (i.e. using conjunctions / disjunctions, i.e. making AND and OR sentences)
Now we have only one type, so we can easily collapse the list of predicates into one
// A predicate that never filter the result def matchAll[Item, T <: Table[Item]]: TablePredicate[Item,T] = { table: T => LiteralColumn(1) === LiteralColumn(1) } // A predicate that always filter the result def matchNone[Item, T <: Table[Item]]: TablePredicate[Item,T] = { table: T => LiteralColumn(1) =!= LiteralColumn(1) } def conjunction[Item, T <: Table[Item]](predicates: TraversableOnce[TablePredicate[Item, T]]): TablePredicate[Item,T] = { if ( predicates.isEmpty ) matchAll[Item,T] else { predicates.reduce { (predicate1, predicate2) => table: T => predicate1(table) && predicate2(table) } } } def disjunction[Item, T <: Table[Item]](predicates: TraversableOnce[TablePredicate[Item, T]]): TablePredicate[Item,T] = { if ( predicates.isEmpty ) matchNone[Item,T] else { predicates.reduce { (predicate1, predicate2) => table: T => predicate1(table) || predicate2(table) } } }
Dynamic filter class class
From these predicate primitives, we can begin to create our dynamic, composite, and typical DSL queries based on the case class.
case class UserFilters( companyScopeIds: Option[Set[String]] = None, firstNames: Option[Set[String]] = None, lastNames: Option[Set[String]] = None ) { type UserPredicate = TablePredicate[User,UserTable] def withFirstNames(firstNames: Set[String]): UserFilters = this.copy(firstNames = Some(firstNames)) def withFirstNames(firstNames: String*): UserFilters = withFirstNames(firstNames.toSet) def withLastNames(lastNames: Set[String]): UserFilters = this.copy(lastNames = Some(lastNames)) def withLastNames(lastNames: String*): UserFilters = withLastNames(lastNames.toSet) def withCompanyScopeIds(companyScopeIds: Set[String]): UserFilters = this.copy(companyScopeIds = Some(companyScopeIds)) def withCompanyScopeIds(companyScopeIds: String*): UserFilters = withCompanyScopeIds(companyScopeIds.toSet) private def filterByFirstNames(firstNames: Set[String]): UserPredicate = { table: UserTable => table.firstName inSet firstNames } private def filterByLastNames(lastNames: Set[String]): UserPredicate = { table: UserTable => table.lastName inSet lastNames } private def filterByCompanyScopeIds(companyScopeIds: Set[String]): UserPredicate = { table: UserTable => (table.companyScopeId.? inSet companyScopeIds) } def predicate: UserPredicate = { // Build the list of predicate options (because filters are actually optional) val optionalPredicates: List[Option[UserPredicate]] = List( firstNames.map(filterByFirstNames(_)), lastNames.map(filterByLastNames(_)), companyScopeIds.map(filterByCompanyScopeIds(_)) ) // Filter the list to remove None's val predicates: List[UserPredicate] = optionalPredicates.flatten // By default, create a conjunction (AND) of the predicates of the represented by this case class conjunction[User,UserTable](predicates) } }
Pay attention to use .? for the companyScopeId field, which allows you to put an optional column in our Slick predicate definition
Using DSL
val Users = TableQuery(new UserTable(_)) val filter1 = UserFilters().withLastNames("lorber","silhol").withFirstName("robert") val filter2 = UserFilters().withFirstName("sebastien") val filter = disjunction[User,UserTable](Set(filter1.predicate,filter2.predicate)) val users = Users.filter(filter.predicate).list // results in // ( last_name in ("lorber","silhol") AND first_name in ("robert") ) // OR // ( first_name in ("sebastien") )
Conclusion
This is far from ideal, but the first project and, at least, can give you some inspiration :) I would like Slick to simplify the creation of things that are very common in other DSL requests (for example, the Hibernate / JPA Criteria API)