Type-safe query composition with monadic operations, joins, aggregations, and filtering using Slick's lifted embedding DSL.
Build type-safe database queries using the Query class and its operations.
/**
* Core query representation with monadic operations
* @param E Element type (what you're selecting from)
* @param U Unpacked type (the Scala type you get)
* @param C Collection type (usually Seq)
*/
abstract class Query[+E, U, C[_]] extends Rep[C[U]] {
/** Apply a filter predicate (WHERE clause) */
def filter(f: E => Rep[Boolean]): Query[E, U, C]
/** Transform each element (SELECT clause) */
def map[F, G, T](f: E => F)(implicit shape: Shape[_ <: FlatShapeLevel, F, G, T]): Query[G, T, C]
/** Monadic composition for complex queries */
def flatMap[F, G, T](f: E => Query[F, T, Seq])(implicit shape: Shape[_ <: FlatShapeLevel, F, G, T]): Query[G, T, C]
/** Sort the results (ORDER BY clause) */
def sortBy[T](f: E => T)(implicit ord: slick.lifted.Ordering[T]): Query[E, U, C]
/** Group results by a key (GROUP BY clause) */
def groupBy[K](f: E => K): Query[(K, Query[E, U, C]), (K, C[U]), C]
/** Limit number of results */
def take(num: Int): Query[E, U, C]
/** Skip a number of results */
def drop(num: Int): Query[E, U, C]
/** Remove duplicate results */
def distinct: Query[E, U, C]
/** Check if any results exist */
def exists: Rep[Boolean]
/** Count the number of results */
def length: Rep[Int]
/** Check if the query is empty */
def isEmpty: Rep[Boolean]
/** Check if the query is non-empty */
def nonEmpty: Rep[Boolean]
/** Apply filter conditionally based on Option value */
def filterOpt[V, T](optValue: Option[V])(f: (E, V) => T)(implicit wt: CanBeQueryCondition[T]): Query[E, U, C]
/** Apply filter conditionally based on boolean condition */
def filterIf[T](p: Boolean)(f: E => T)(implicit wt: CanBeQueryCondition[T]): Query[E, U, C]
/** Apply filterNot conditionally based on Option value */
def filterNotOpt[V, T](optValue: Option[V])(f: (E, V) => T)(implicit wt: CanBeQueryCondition[T]): Query[E, U, C]
/** Apply filterNot conditionally based on boolean condition */
def filterNotIf[T](p: Boolean)(f: E => T)(implicit wt: CanBeQueryCondition[T]): Query[E, U, C]
}Usage Examples:
// Basic filtering and mapping
val expensiveCoffees = coffees.filter(_.price > 3.0)
val coffeeNames = coffees.map(_.name)
val coffeePricesInCents = coffees.map(c => (c.name, c.price * 100))
// Sorting
val coffeesByName = coffees.sortBy(_.name)
val coffeesByPriceDesc = coffees.sortBy(_.price.desc)
// Combining operations
val cheapCoffeeNames = coffees
.filter(_.price < 2.0)
.sortBy(_.name)
.map(_.name)
// Limiting results
val first5Coffees = coffees.take(5)
val skip10AndTake5 = coffees.drop(10).take(5)
// Existence checks
val hasExpensiveCoffees = coffees.filter(_.price > 5.0).exists
val coffeeCount = coffees.length
// Conditional filtering
val optionalCategory: Option[String] = Some("espresso")
val coffeesByOptionalCategory = coffees.filterOpt(optionalCategory) { (coffee, category) =>
coffee.category === category
}
val includeExpensive = true
val filteredCoffees = coffees.filterIf(includeExpensive)(_.price > 4.0)
// Conditional negative filtering
val excludeCategory: Option[String] = Some("decaf")
val nonDecafCoffees = coffees.filterNotOpt(excludeCategory) { (coffee, category) =>
coffee.category === category
}Combine data from multiple tables using various join types.
/**
* Inner join between two queries
*/
def join[E2, U2, D[_]](q2: Query[E2, U2, D]): Query[(E, E2), (U, U2), C]
/**
* Left outer join between two queries
*/
def joinLeft[E2, U2, D[_]](q2: Query[E2, U2, D]): Query[(E, Rep[Option[E2]]), (U, Option[U2]), C]
/**
* Right outer join between two queries
*/
def joinRight[E2, U2, D[_]](q2: Query[E2, U2, D]): Query[(Rep[Option[E]], E2), (Option[U], U2), C]
/**
* Full outer join between two queries
*/
def joinFull[E2, U2, D[_]](q2: Query[E2, U2, D]): Query[(Rep[Option[E]], Rep[Option[E2]]), (Option[U], Option[U2]), C]
/**
* Cross join (cartesian product) between two queries
*/
def zip[E2, U2, D[_]](q2: Query[E2, U2, D]): Query[(E, E2), (U, U2), C]Usage Examples:
// Setup table queries
val users = TableQuery[Users]
val orders = TableQuery[Orders]
val products = TableQuery[Products]
// Inner join with explicit condition
val usersWithOrders = for {
user <- users
order <- orders if user.id === order.userId
} yield (user.name, order.id)
// Using join method
val userOrderJoin = users.join(orders).on(_.id === _.userId)
// Left join to include users without orders
val allUsersWithOptionalOrders = users.joinLeft(orders).on(_.id === _.userId)
// Complex join with multiple tables
val orderDetails = for {
order <- orders
user <- users if order.userId === user.id
product <- products if order.productId === product.id
} yield (user.name, product.name, order.quantity, product.price)
// Join with filtering
val recentOrdersWithUsers = for {
order <- orders if order.orderDate > Date.valueOf("2023-01-01")
user <- users if order.userId === user.id
} yield (user.name, order.orderDate)Perform aggregate calculations on query results.
/**
* Count all rows
*/
def length: Rep[Int]
/**
* Maximum value of a column
*/
def max[B >: U](implicit ord: slick.lifted.Ordering[Option[B]]): Rep[Option[B]]
/**
* Minimum value of a column
*/
def min[B >: U](implicit ord: slick.lifted.Ordering[Option[B]]): Rep[Option[B]]
/**
* Sum of numeric column values
*/
def sum[B >: U](implicit num: Numeric[B]): Rep[Option[B]]
/**
* Average of numeric column values
*/
def avg[B >: U](implicit num: Numeric[B]): Rep[Option[B]]Additional aggregate functions available on columns:
/**
* Count non-null values in a column
*/
def count: Rep[Int]
/**
* Count distinct values in a column
*/
def countDistinct: Rep[Int]Usage Examples:
// Basic aggregations
val totalCoffees = coffees.length
val maxPrice = coffees.map(_.price).max
val minPrice = coffees.map(_.price).min
val avgPrice = coffees.map(_.price).avg
val totalValue = coffees.map(_.price).sum
// Group by with aggregations
val pricesByCategory = coffees
.groupBy(_.category)
.map { case (category, group) =>
(category, group.map(_.price).avg, group.length)
}
// Count distinct
val distinctCategories = coffees.map(_.category).countDistinct
// Complex aggregation query
val userOrderStats = orders
.groupBy(_.userId)
.map { case (userId, group) =>
(userId, group.length, group.map(_.quantity).sum, group.map(_.quantity).avg)
}Combine query results using set operations.
/**
* Union of two queries (removes duplicates)
*/
def union[O >: E, R, D[_]](other: Query[O, R, D]): Query[O, R, C]
/**
* Union all of two queries (keeps duplicates)
*/
def unionAll[O >: E, R, D[_]](other: Query[O, R, D]): Query[O, R, C]
/**
* Intersection of two queries
*/
def intersect[O >: E, R, D[_]](other: Query[O, R, D]): Query[O, R, C]
/**
* Difference between two queries (elements in first but not second)
*/
def diff[O >: E, R, D[_]](other: Query[O, R, D]): Query[O, R, C]Usage Examples:
// Union queries
val cheapOrExpensive = coffees.filter(_.price < 2.0).union(coffees.filter(_.price > 4.0))
// All coffee names and tea names
val allBeverageNames = coffees.map(_.name).unionAll(teas.map(_.name))
// Coffee categories that are also tea categories
val commonCategories = coffees.map(_.category).intersect(teas.map(_.category))
// Coffee names that aren't tea names
val coffeeOnlyNames = coffees.map(_.name).diff(teas.map(_.name))Operations available on individual columns and expressions.
/**
* Comparison operations
*/
def ===[P2](e: P2): Rep[Boolean] // Equality
def =!=[P2](e: P2): Rep[Boolean] // Inequality
def <[P2](e: P2): Rep[Boolean] // Less than
def <=[P2](e: P2): Rep[Boolean] // Less than or equal
def >[P2](e: P2): Rep[Boolean] // Greater than
def >=[P2](e: P2): Rep[Boolean] // Greater than or equal
/**
* Set membership operations
*/
def in[P2](e: Query[P2, _, Seq]): Rep[Boolean]
def inSet[P2](set: Traversable[P2]): Rep[Boolean]
def between[P2, P3](start: P2, end: P3): Rep[Boolean]
/**
* Null handling
*/
def ifNull[B](alt: B): Rep[B]
def isDefined: Rep[Boolean] // For Option columns
def isEmpty: Rep[Boolean] // For Option columns
/**
* Boolean operations
*/
def &&(e: Rep[Boolean]): Rep[Boolean] // Logical AND
def ||(e: Rep[Boolean]): Rep[Boolean] // Logical OR
def unary_!(): Rep[Boolean] // Logical NOT
/**
* Arithmetic operations (for numeric types)
*/
def +[P2, R](e: P2): Rep[R] // Addition
def -[P2, R](e: P2): Rep[R] // Subtraction
def *[P2, R](e: P2): Rep[R] // Multiplication
def /[P2, R](e: P2): Rep[R] // Division
def %[P2, R](e: P2): Rep[R] // Modulo
/**
* String operations
*/
def ++[P2](e: P2): Rep[String] // String concatenation
def like(pattern: Rep[String]): Rep[Boolean] // Pattern matching
def startsWith(prefix: Rep[String]): Rep[Boolean]
def endsWith(suffix: Rep[String]): Rep[Boolean]Usage Examples:
// Comparison operations
val expensiveCoffees = coffees.filter(_.price > 3.0)
val specificCoffee = coffees.filter(_.name === "Latte")
val priceRange = coffees.filter(_.price.between(2.0, 4.0))
// Set membership
val popularCoffees = coffees.filter(_.name.inSet(Set("Latte", "Cappuccino", "Espresso")))
// String operations
val latteVariants = coffees.filter(_.name.like("%Latte%"))
val coffeeDescriptions = coffees.map(c => c.name ++ " - $" ++ c.price.asColumnOf[String])
// Null handling
val activeUsers = users.filter(_.deletedAt.isEmpty)
val usersWithDefaultRole = users.map(u => (u.name, u.role.ifNull("user")))
// Boolean logic
val affordablePopularCoffees = coffees.filter(c => c.price < 3.0 && c.name.like("%Latte%"))
// Arithmetic
val pricesWithTax = coffees.map(c => (c.name, c.price * 1.08))
val discountedPrices = coffees.map(c => c.price - (c.price * 0.1))Use subqueries for complex filtering and selection.
/**
* Check if any rows exist in a subquery
*/
def exists: Rep[Boolean]
/**
* Filter based on subquery results
*/
def in[P2](subquery: Query[P2, _, Seq]): Rep[Boolean]
/**
* Use subquery in SELECT clause
*/
def map[F](f: E => F): Query[F, _, C]Usage Examples:
// Exists subquery
val usersWithOrders = users.filter(user =>
orders.filter(_.userId === user.id).exists
)
// In subquery
val usersWhoOrderedExpensiveItems = users.filter(_.id.in(
orders.filter(_.total > 100.0).map(_.userId)
))
// Correlated subquery
val usersWithOrderCount = users.map { user =>
(user.name, orders.filter(_.userId === user.id).length)
}
// Scalar subquery
val avgOrderValue = orders.map(_.total).avg
val aboveAverageOrders = orders.filter(_.total > avgOrderValue)Use Scala's for comprehension syntax for complex queries.
Usage Examples:
// Basic for comprehension
val coffeeNamesAndPrices = for {
coffee <- coffees
if coffee.price > 2.0
} yield (coffee.name, coffee.price)
// Multiple table join
val userOrderDetails = for {
user <- users
order <- orders if order.userId === user.id
if order.total > 50.0
} yield (user.name, order.id, order.total)
// Complex filtering and joining
val recentOrderSummary = for {
order <- orders if order.orderDate > Date.valueOf("2023-01-01")
user <- users if order.userId === user.id
product <- products if order.productId === product.id
} yield (user.name, product.name, order.quantity, order.total)
// Nested conditions
val premiumUserExpensiveOrders = for {
user <- users if user.accountType === "premium"
order <- orders if order.userId === user.id && order.total > 100.0
} yield (user.name, order.total)abstract class Query[+E, U, C[_]] extends Rep[C[U]]
trait Rep[T]
// Query result types
type SimpleQuery[E, U] = Query[E, U, Seq]
type UnpackQuery[E] = Query[E, E, Seq]
// Shape types for query mapping
trait Shape[Level <: ShapeLevel, -Mixed_, Unpacked_, Packed_]
trait ProvenShape[U] extends Shape[FlatShapeLevel, U, U, U]
// Ordering for sortBy operations
trait Ordering[T]
object Ordering {
def apply[T](implicit ord: scala.math.Ordering[T]): Ordering[T]
implicit def columnOrdering[T](implicit tm: TypedType[T], ord: scala.math.Ordering[T]): Ordering[Rep[T]]
}