Skip to main content

Adding a New Dialect

This guide walks through creating a new criteria4s dialect step by step. We will use a fictional "CouchDB" dialect as an example.

Step 1: Create the Module Directory

Create a new directory at the project root:

couchdb/
└── src/
├── main/scala/com/eff3ct/criteria4s/dialect/couchdb/
└── test/scala/com/eff3ct/criteria4s/dialect/couchdb/

Step 2: Define the Dialect Trait

Create a trait that extends CriteriaTag. If your dialect is SQL-based, extend SQL instead:

// couchdb/src/main/scala/com/eff3ct/criteria4s/dialect/couchdb/CouchDB.scala
package com.eff3ct.criteria4s.dialect.couchdb

import com.eff3ct.criteria4s.core.*
import com.eff3ct.criteria4s.instances.*

// For a non-SQL dialect, extend CriteriaTag directly:
trait CouchDB extends CriteriaTag

// For a SQL-based dialect, extend SQL instead:
// trait CouchDB extends com.eff3ct.criteria4s.dialect.sql.SQL

Step 3: Create the Companion Object with Given Instances

Define Show instances for your dialect (how columns, strings, sequences, and tuples are rendered) and an inner trait with all the predicate/conjunction given instances. The build function from com.eff3ct.criteria4s.instances.* constructs type class instances from simple render functions:

object CouchDB {

// How column names are rendered
given showColumn: Show[Column, CouchDB] =
Show.create(col => s""""${col.colName}"""")

// How sequences are rendered (for IN/NOT IN)
given showSeq[V](using show: Show[V, CouchDB]): Show[Seq[V], CouchDB] =
Show.create(_.map(show.show).mkString("[", ", ", "]"))

// How range tuples are rendered (for BETWEEN)
given showTuple[V](using show: Show[V, CouchDB]): Show[(V, V), CouchDB] =
Show.create { case (l, r) =>
s"""{"$$gte": ${show.show(l)}, "$$lt": ${show.show(r)}}"""
}

// Inner trait with all predicate given instances
trait CouchDBExpr[T <: CouchDB] {

// Define how each predicate renders for your dialect.
// Use `build[T, PredicateType](renderFunction)` from the instances package.

given eqPred: EQ[T] = build[T, EQ] { (field, value) =>
s"""{"selector": {$field: {"$$eq": $value}}}"""
}

given neqPred: NEQ[T] = build[T, NEQ] { (field, value) =>
s"""{"selector": {$field: {"$$ne": $value}}}"""
}

given gtPred: GT[T] = build[T, GT] { (field, value) =>
s"""{"selector": {$field: {"$$gt": $value}}}"""
}

// ... define all other predicates: LT, GEQ, LEQ, IN, NOTIN,
// LIKE, ISNULL, ISNOTNULL, BETWEEN, NOTBETWEEN,
// STARTSWITH, ENDSWITH, CONTAINS, ISTRUE, ISFALSE

given andConj: AND[T] = build[T, AND] { (left, right) =>
s"""{"$$and": [$left, $right]}"""
}

given orConj: OR[T] = build[T, OR] { (left, right) =>
s"""{"$$or": [$left, $right]}"""
}

given notConj: NOT[T] = build[T, NOT] { expr =>
s"""{"$$not": $expr}"""
}
}
}

Step 4: Create the Package Object

The package object extends your inner Expr trait to export all given instances. This is what users import to get all dialect-specific instances into scope:

// couchdb/src/main/scala/com/eff3ct/criteria4s/dialect/couchdb/package.scala
package com.eff3ct.criteria4s.dialect

package object couchdb extends CouchDB.CouchDBExpr[CouchDB]

Step 5: Add to build.sbt

Add the module to the root build.sbt:

lazy val couchdb: Project =
(project in file("couchdb"))
.settings(
name := "criteria4s-couchdb",
publish / skip := false,
libraryDependencies += munit % Test,
testFrameworks += new TestFramework("munit.Framework")
)
.dependsOn(core) // or .dependsOn(sql) for SQL-based dialects

Then add it to the root aggregate so it is included in sbt compile and sbt test:

lazy val criteria4s: Project =
project
.in(file("."))
.aggregate(core, sql, mongodb, postgresql, mysql, sparksql,
elasticsearch, couchdb, /* ... */)

If you want the docs project to compile mdoc blocks for this dialect, also add it to the docs project's .dependsOn(...).

Step 6: Write Tests

Create a test suite that verifies every predicate renders correctly. Testing the full surface area upfront makes it easy to catch regressions later:

// couchdb/src/test/scala/com/eff3ct/criteria4s/dialect/couchdb/CouchDBExprSpec.scala
package com.eff3ct.criteria4s.dialect.couchdb

import com.eff3ct.criteria4s.core.*
import com.eff3ct.criteria4s.functions as F

class CouchDBExprSpec extends munit.FunSuite {

test("CouchDB EQ renders correctly") {
val result = F.===[CouchDB, Column, Int](F.col("age"), F.lit(30))
assertEquals(result.value, """{"selector": {"age": {"$eq": 30}}}""")
}

test("CouchDB AND renders correctly") {
val left = F.===[CouchDB, Column, Int](F.col("a"), F.lit(1))
val right = F.===[CouchDB, Column, Int](F.col("b"), F.lit(2))
val result = F.and[CouchDB](left, right)
assertEquals(result.value, """{"$and": [...]}""")
}

// Test ALL predicates: EQ, NEQ, GT, LT, GEQ, LEQ, IN, NOTIN,
// LIKE, ISNULL, ISNOTNULL, BETWEEN, NOTBETWEEN,
// STARTSWITH, ENDSWITH, CONTAINS, ISTRUE, ISFALSE
// AND, OR, NOT
}

Step 7: For SQL-Based Dialects

If your dialect is SQL-based, the process is simpler. You only need to override the Show[Column, T] instance and optionally the Show[Seq[V], T] and Show[(V, V), T] instances. All predicates, conjunctions, and transforms are inherited from SQL.SQLExpr:

trait MyNewSQL extends SQL

object MyNewSQL extends SQL.SQLExpr[MyNewSQL] {
// Override column quoting
given showColumn: Show[Column, MyNewSQL] =
Show.create(col => s"[${col.colName}]") // e.g., SQL Server bracket quoting

given showSeq[V](using show: Show[V, MyNewSQL]): Show[Seq[V], MyNewSQL] =
Show.create(_.map(show.show).mkString("(", ", ", ")"))

given showTuple[V](using show: Show[V, MyNewSQL]): Show[(V, V), MyNewSQL] =
Show.create { case (l, r) => s"${show.show(l)} AND ${show.show(r)}" }
}

Complete Minimal Example

Here is the minimal set of files needed for a new SQL-based dialect with bracket quoting:

newsql/src/main/scala/.../NewSQL.scala:

package com.eff3ct.criteria4s.dialect.newsql

import com.eff3ct.criteria4s.core.{Column, Show}
import com.eff3ct.criteria4s.dialect.sql.SQL

trait NewSQL extends SQL

object NewSQL extends SQL.SQLExpr[NewSQL] {
given showColumn: Show[Column, NewSQL] =
Show.create(col => s"[${col.colName}]")

given showSeq[V](using show: Show[V, NewSQL]): Show[Seq[V], NewSQL] =
Show.create(_.map(show.show).mkString("(", ", ", ")"))

given showTuple[V](using show: Show[V, NewSQL]): Show[(V, V), NewSQL] =
Show.create { case (l, r) => s"${show.show(l)} AND ${show.show(r)}" }
}

newsql/src/main/scala/.../package.scala:

package com.eff3ct.criteria4s.dialect

package object newsql extends sql.SQL.SQLExpr[NewSQL]

That is all you need — the SQL base provides every predicate, conjunction, and transform automatically.