Skip to main content

Getting Started

Installation

criteria4s module dependency graph

Add the core library and the dialect modules you need to your build.sbt:

// Core DSL (always required)
libraryDependencies += "com.eff3ct" %% "criteria4s-core" % "1.0.0"

// Dialect modules (pick the ones you need)
libraryDependencies += "com.eff3ct" %% "criteria4s-sql" % "1.0.0"
libraryDependencies += "com.eff3ct" %% "criteria4s-mongodb" % "1.0.0"
libraryDependencies += "com.eff3ct" %% "criteria4s-elasticsearch" % "1.0.0"
libraryDependencies += "com.eff3ct" %% "criteria4s-postgresql" % "1.0.0"
libraryDependencies += "com.eff3ct" %% "criteria4s-mysql" % "1.0.0"
libraryDependencies += "com.eff3ct" %% "criteria4s-sparksql" % "1.0.0"

// Integration modules (for driver-level interop)
libraryDependencies += "com.eff3ct" %% "criteria4s-sql-jdbc" % "1.0.0"
libraryDependencies += "com.eff3ct" %% "criteria4s-mongodb-driver" % "1.0.0"
libraryDependencies += "com.eff3ct" %% "criteria4s-elasticsearch-client" % "1.0.0"

criteria4s targets Scala 3.6.4 LTS and above.

Your First Expression

Let's build a simple SQL filter: age > 18. You need the core import for the DSL types, the SQL dialect for rendering, and functions for the expression builders:

import com.eff3ct.criteria4s.core.*
import com.eff3ct.criteria4s.functions as F
import com.eff3ct.criteria4s.dialect.sql.{*, given}
val criteria: Criteria[SQL] = F.gt(F.col[SQL]("age"), F.lit[SQL, Int](18))
// criteria: Criteria[SQL] = age > 18
criteria.value
// res0: String = "age > 18"

F.col creates a column reference, F.lit creates a literal value, and F.gt produces a Criteria[SQL] tagged with the SQL dialect. Calling .value renders the expression to a string.

The Two API Styles

criteria4s provides two ways to build expressions: function-style and extension-style. They produce identical results, so it comes down to whichever reads more naturally in your context.

Function-style with functions as F

The function-style API uses top-level functions from the functions package, accessed through the F namespace:

import com.eff3ct.criteria4s.core.*
import com.eff3ct.criteria4s.functions as F
import com.eff3ct.criteria4s.dialect.sql.{*, given}

val expr = F.and(
F.===(F.col[SQL]("status"), F.lit[SQL, String]("active")),
F.gt(F.col[SQL]("age"), F.lit[SQL, Int](18))
)
// expr: Criteria[SQL] = (status = 'active') AND (age > 18)
expr.value
// res1: String = "(status = 'active') AND (age > 18)"

Extension-style with extensions.*

The extension-style API adds methods directly to Ref and Criteria values, letting you chain predicates in a more natural left-to-right flow:

import com.eff3ct.criteria4s.core.*
import com.eff3ct.criteria4s.functions as F
import com.eff3ct.criteria4s.extensions.*
import com.eff3ct.criteria4s.dialect.sql.{*, given}

val expr = (F.col[SQL]("status") === F.lit("active")) and
(F.col[SQL]("age") :> F.lit(18))
// expr: Criteria[SQL] = (status = 'active') AND (age > 18)
expr.value
// res2: String = "(status = 'active') AND (age > 18)"

The extension style reads more naturally for chained expressions. It also provides symbolic aliases: :> for gt, :< for lt, :>= for geq, :<= for leq, :& for and, and :| for or.

Why functions as F Instead of functions.*

You may wonder why the examples import functions as F rather than using a wildcard import. The functions package defines names like and, or, not, in, and lt — common enough to collide with identifiers in your own code. Using a qualified import via as F keeps everything under a clear namespace (F.and, F.or, F.gt) without polluting your scope. If you prefer the wildcard import, it works fine; just be aware of potential name clashes.

Composing Expressions

Expressions compose naturally. Every predicate returns a Criteria[T], and conjunctions combine two Criteria[T] values into a new Criteria[T]. You can build complex filters incrementally by naming intermediate values:

import com.eff3ct.criteria4s.core.*
import com.eff3ct.criteria4s.functions as F
import com.eff3ct.criteria4s.extensions.*
import com.eff3ct.criteria4s.dialect.sql.{*, given}

val isAdult = F.col[SQL]("age") :>= F.lit(18)
// isAdult: Criteria[SQL] = age >= 18
val isActive = F.col[SQL]("status") === F.lit("active")
// isActive: Criteria[SQL] = status = 'active'
val hasEmail = F.col[SQL]("email").isNotNull
// hasEmail: Criteria[SQL] = email IS NOT NULL

val filter = (isAdult and isActive) or hasEmail
// filter: Criteria[SQL] = ((age >= 18) AND (status = 'active')) OR (email IS NOT NULL)
filter.value
// res3: String = "((age >= 18) AND (status = 'active')) OR (email IS NOT NULL)"

What's Next

  • Type Classes: Understand why criteria4s uses type classes and how this solves the Expression Problem.
  • Tagless Final: Learn how phantom types and polymorphic functions let you write backend-agnostic criteria.
  • Architecture: Explore the full type hierarchy from CriteriaTag to CaseExpr.