Column type mappings between Scala types and database types, including custom type mappings and implicit conversions.
Built-in mappings between Scala types and database column types.
/**
* Base trait for typed database types
*/
trait TypedType[T] {
/** SQL type name for this type */
def sqlTypeName(sym: Option[FieldSymbol]): String
/** JDBC type code */
def sqlType: Int
/** Whether this type has a length parameter */
def hasLiteralLength: Boolean
}
/**
* Column type with conversion between database and Scala representation
*/
trait ColumnType[T] extends TypedType[T] {
/** Convert from JDBC ResultSet to Scala type */
def getValue(r: ResultSet, idx: Int): T
/** Convert from Scala type to PreparedStatement parameter */
def setValue(v: T, p: PreparedStatement, idx: Int): Unit
/** Whether this type can represent NULL values */
def nullable: Boolean
/** Create an Option version of this type */
def optionType: ColumnType[Option[T]]
}
/**
* Base column type without user-defined conversions
*/
trait BaseColumnType[T] extends ColumnType[T]Standard mappings for common Scala types to database column types.
/**
* Implicit column type instances for basic types
*/
object ColumnTypes {
implicit val booleanColumnType: BaseColumnType[Boolean]
implicit val byteColumnType: BaseColumnType[Byte]
implicit val shortColumnType: BaseColumnType[Short]
implicit val intColumnType: BaseColumnType[Int]
implicit val longColumnType: BaseColumnType[Long]
implicit val floatColumnType: BaseColumnType[Float]
implicit val doubleColumnType: BaseColumnType[Double]
implicit val stringColumnType: BaseColumnType[String]
implicit val bigDecimalColumnType: BaseColumnType[BigDecimal]
// Date/Time types
implicit val dateColumnType: BaseColumnType[Date]
implicit val timeColumnType: BaseColumnType[Time]
implicit val timestampColumnType: BaseColumnType[Timestamp]
implicit val localDateColumnType: BaseColumnType[LocalDate]
implicit val localTimeColumnType: BaseColumnType[LocalTime]
implicit val localDateTimeColumnType: BaseColumnType[LocalDateTime]
implicit val instantColumnType: BaseColumnType[Instant]
// Binary types
implicit val byteArrayColumnType: BaseColumnType[Array[Byte]]
implicit val blobColumnType: BaseColumnType[Blob]
implicit val clobColumnType: BaseColumnType[Clob]
// UUID type
implicit val uuidColumnType: BaseColumnType[UUID]
}
/**
* Optional type mapping
*/
implicit def optionColumnType[T](implicit base: ColumnType[T]): ColumnType[Option[T]]Usage Examples:
// Basic type usage in table definitions
class Products(tag: Tag) extends Table[Product](tag, "products") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def price = column[BigDecimal]("price")
def isActive = column[Boolean]("is_active", O.Default(true))
def createdAt = column[Timestamp]("created_at")
def description = column[Option[String]]("description") // Optional column
def metadata = column[Array[Byte]]("metadata")
def uuid = column[UUID]("uuid")
def * = (id, name, price, isActive, createdAt, description, metadata, uuid).mapTo[Product]
}
// Using Java 8 time types
class Events(tag: Tag) extends Table[Event](tag, "events") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def eventDate = column[LocalDate]("event_date")
def eventTime = column[LocalTime]("event_time")
def eventDateTime = column[LocalDateTime]("event_datetime")
def timestamp = column[Instant]("timestamp")
def * = (id, eventDate, eventTime, eventDateTime, timestamp).mapTo[Event]
}Create custom mappings between Scala types and database column types.
/**
* Create a custom column type mapping
*/
object MappedColumnType {
/**
* Create a mapped column type with bidirectional conversion
* @param tmap Function to convert from Scala type T to database type U
* @param tcomap Function to convert from database type U to Scala type T
*/
def apply[T, U](tmap: T => U, tcomap: U => T)(implicit tm: ColumnType[U]): ColumnType[T]
/**
* Create a mapped column type for base types
*/
def base[T, U](tmap: T => U, tcomap: U => T)(implicit tm: BaseColumnType[U]): BaseColumnType[T]
}Usage Examples:
// Custom enum mapping
sealed trait CoffeeSize
case object Small extends CoffeeSize
case object Medium extends CoffeeSize
case object Large extends CoffeeSize
object CoffeeSize {
implicit val coffeesSizeMapper = MappedColumnType.base[CoffeeSize, String](
{
case Small => "S"
case Medium => "M"
case Large => "L"
},
{
case "S" => Small
case "M" => Medium
case "L" => Large
}
)
}
// Use custom enum in table
class Orders(tag: Tag) extends Table[Order](tag, "orders") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def coffeeId = column[Int]("coffee_id")
def size = column[CoffeeSize]("size") // Uses custom mapping
def quantity = column[Int]("quantity")
def * = (id, coffeeId, size, quantity).mapTo[Order]
}
// JSON mapping example
case class ProductMetadata(tags: List[String], features: Map[String, String])
implicit val jsonMapper = MappedColumnType.base[ProductMetadata, String](
metadata => Json.toJson(metadata).toString(),
jsonString => Json.parse(jsonString).as[ProductMetadata]
)
class ProductsWithJson(tag: Tag) extends Table[(Int, String, ProductMetadata)](tag, "products") {
def id = column[Int]("id", O.PrimaryKey)
def name = column[String]("name")
def metadata = column[ProductMetadata]("metadata") // JSON column
def * = (id, name, metadata)
}
// Custom value class mapping
case class UserId(value: Long) extends AnyVal
case class ProductId(value: Long) extends AnyVal
implicit val userIdMapper = MappedColumnType.base[UserId, Long](_.value, UserId.apply)
implicit val productIdMapper = MappedColumnType.base[ProductId, Long](_.value, ProductId.apply)
class OrdersWithValueClasses(tag: Tag) extends Table[Order](tag, "orders") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def userId = column[UserId]("user_id") // Uses value class mapping
def productId = column[ProductId]("product_id") // Uses value class mapping
def quantity = column[Int]("quantity")
def * = (id, userId, productId, quantity).mapTo[Order]
}Handle database-specific column types and extensions.
/**
* PostgreSQL-specific types (example)
*/
object PostgresTypes {
/** PostgreSQL array type */
implicit def arrayColumnType[T](implicit base: ColumnType[T]): ColumnType[List[T]]
/** PostgreSQL JSON/JSONB type */
implicit val jsonColumnType: ColumnType[JsValue]
implicit val jsonbColumnType: ColumnType[JsValue]
/** PostgreSQL UUID type */
implicit val uuidColumnType: ColumnType[UUID]
/** PostgreSQL range types */
implicit val intRangeColumnType: ColumnType[Range[Int]]
implicit val timestampRangeColumnType: ColumnType[Range[Timestamp]]
}
/**
* MySQL-specific types (example)
*/
object MySQLTypes {
/** MySQL YEAR type */
implicit val yearColumnType: ColumnType[Year]
/** MySQL SET type */
implicit def setColumnType[T](implicit base: ColumnType[T]): ColumnType[Set[T]]
/** MySQL spatial types */
implicit val pointColumnType: ColumnType[Point]
implicit val geometryColumnType: ColumnType[Geometry]
}Usage Examples:
// PostgreSQL array columns
import slick.jdbc.PostgresProfile.api._
import PostgresTypes._
class TaggedItems(tag: Tag) extends Table[(Int, String, List[String])](tag, "tagged_items") {
def id = column[Int]("id", O.PrimaryKey)
def name = column[String]("name")
def tags = column[List[String]]("tags") // PostgreSQL array
def * = (id, name, tags)
}
// PostgreSQL JSON columns
case class UserPreferences(theme: String, language: String, notifications: Boolean)
implicit val preferencesMapper = MappedColumnType.base[UserPreferences, JsValue](
prefs => Json.toJson(prefs),
json => json.as[UserPreferences]
)
class UsersWithPreferences(tag: Tag) extends Table[(Int, String, UserPreferences)](tag, "users") {
def id = column[Int]("id", O.PrimaryKey)
def name = column[String]("name")
def preferences = column[UserPreferences]("preferences") // JSONB column
def * = (id, name, preferences)
}
// MySQL spatial types (hypothetical)
import slick.jdbc.MySQLProfile.api._
import MySQLTypes._
class Locations(tag: Tag) extends Table[(Int, String, Point)](tag, "locations") {
def id = column[Int]("id", O.PrimaryKey)
def name = column[String]("name")
def coordinates = column[Point]("coordinates") // MySQL POINT type
def * = (id, name, coordinates)
}Automatic conversions and lifting between related types.
/**
* Implicit conversions for lifted embedding
*/
object TypeConversions {
/** Convert literal values to column representations */
implicit def constColumnType[T](implicit tm: TypedType[T]): ColumnType[T]
/** Lift literal values to column expressions */
implicit def literalColumn[T : TypedType](v: T): LiteralColumn[T]
/** Convert between compatible numeric types */
implicit def numericConversion[T, U](c: Rep[T])(implicit tm: TypedType[U], conv: T => U): Rep[U]
/** Option lifting */
implicit def optionLift[T](c: Rep[T]): Rep[Option[T]]
}
/**
* Type-safe null handling
*/
trait NullHandling {
/** Check if optional column is defined */
def isDefined[T](col: Rep[Option[T]]): Rep[Boolean]
/** Check if optional column is empty */
def isEmpty[T](col: Rep[Option[T]]): Rep[Boolean]
/** Provide default value for null columns */
def ifNull[T](col: Rep[Option[T]], default: Rep[T]): Rep[T]
/** Get value from option or throw exception */
def get[T](col: Rep[Option[T]]): Rep[T]
}Usage Examples:
// Automatic type lifting
val coffees = TableQuery[Coffees]
// Literal values are automatically lifted to column expressions
val expensiveFilter = coffees.filter(_.price > 3.0) // 3.0 becomes LiteralColumn[Double]
val nameFilter = coffees.filter(_.name === "Latte") // "Latte" becomes LiteralColumn[String]
// Option handling
val activeUsers = users.filter(_.deletedAt.isEmpty)
val usersWithRoles = users.map(u => (u.name, u.role.ifNull("guest")))
// Type conversions in expressions
val pricesInCents = coffees.map(c => (c.name, c.price * 100)) // Double to Int conversion
val formattedPrices = coffees.map(c => c.name ++ ": $" ++ c.price.asColumnOf[String])
// Numeric conversions
class Statistics(tag: Tag) extends Table[(Int, Double, Float)](tag, "statistics") {
def id = column[Int]("id", O.PrimaryKey)
def average = column[Double]("average")
def percentage = column[Float]("percentage")
def * = (id, average, percentage)
// Automatic conversion between numeric types in queries
def highPercentage = this.filter(_.percentage > 0.8) // Double literal converted to Float
}
// Custom implicit conversions
case class Price(amount: BigDecimal, currency: String = "USD")
implicit val priceColumnType = MappedColumnType.base[Price, BigDecimal](
_.amount,
Price(_)
)
implicit def priceOrdering: Ordering[Rep[Price]] =
Ordering.by((_: Rep[Price]).asColumnOf[BigDecimal])
class ProductsWithPrices(tag: Tag) extends Table[(Int, String, Price)](tag, "products") {
def id = column[Int]("id", O.PrimaryKey)
def name = column[String]("name")
def price = column[Price]("price")
def * = (id, name, price)
// Can now sort by Price using custom ordering
def sortedByPrice = this.sortBy(_.price)
}Add validation and constraints to custom type mappings.
/**
* Validated column type with runtime validation
*/
case class ValidatedColumnType[T](
base: ColumnType[T],
validate: T => Either[String, T]
) extends ColumnType[T] {
override def getValue(r: ResultSet, idx: Int): T = {
val value = base.getValue(r, idx)
validate(value) match {
case Right(validated) => validated
case Left(error) => throw new SQLException(s"Validation failed: $error")
}
}
override def setValue(v: T, p: PreparedStatement, idx: Int): Unit = {
validate(v) match {
case Right(validated) => base.setValue(validated, p, idx)
case Left(error) => throw new SQLException(s"Validation failed: $error")
}
}
}Usage Examples:
// Email validation
case class Email(value: String) extends AnyVal
object Email {
private val emailRegex = """^[a-zA-Z0-9\._%+-]+@[a-zA-Z0-9\.-]+\.[a-zA-Z]{2,}$""".r
def apply(value: String): Either[String, Email] = {
if (emailRegex.matches(value)) Right(new Email(value))
else Left(s"Invalid email format: $value")
}
implicit val emailColumnType = ValidatedColumnType(
MappedColumnType.base[Email, String](_.value, new Email(_)),
(email: Email) => if (emailRegex.matches(email.value)) Right(email) else Left("Invalid email")
)
}
// Positive number validation
case class PositiveInt(value: Int) extends AnyVal
object PositiveInt {
implicit val positiveIntColumnType = ValidatedColumnType(
MappedColumnType.base[PositiveInt, Int](_.value, PositiveInt(_)),
(pi: PositiveInt) => if (pi.value > 0) Right(pi) else Left("Must be positive")
)
}
class ValidatedTable(tag: Tag) extends Table[(Int, Email, PositiveInt)](tag, "validated") {
def id = column[Int]("id", O.PrimaryKey)
def email = column[Email]("email") // Validated email format
def quantity = column[PositiveInt]("quantity") // Validated positive number
def * = (id, email, quantity)
}
// Range validation
case class Percentage(value: Double) extends AnyVal
object Percentage {
implicit val percentageColumnType = ValidatedColumnType(
MappedColumnType.base[Percentage, Double](_.value, Percentage(_)),
(p: Percentage) => {
if (p.value >= 0.0 && p.value <= 100.0) Right(p)
else Left(s"Percentage must be between 0 and 100, got ${p.value}")
}
)
}trait TypedType[T] {
def sqlTypeName(sym: Option[FieldSymbol]): String
def sqlType: Int
def hasLiteralLength: Boolean
}
trait ColumnType[T] extends TypedType[T] {
def getValue(r: ResultSet, idx: Int): T
def setValue(v: T, p: PreparedStatement, idx: Int): Unit
def nullable: Boolean
def optionType: ColumnType[Option[T]]
}
trait BaseColumnType[T] extends ColumnType[T]
object MappedColumnType {
def apply[T, U](tmap: T => U, tcomap: U => T)(implicit tm: ColumnType[U]): ColumnType[T]
def base[T, U](tmap: T => U, tcomap: U => T)(implicit tm: BaseColumnType[U]): BaseColumnType[T]
}
// Built-in type mappings
implicit val intColumnType: BaseColumnType[Int]
implicit val stringColumnType: BaseColumnType[String]
implicit val doubleColumnType: BaseColumnType[Double]
implicit val booleanColumnType: BaseColumnType[Boolean]
implicit val bigDecimalColumnType: BaseColumnType[BigDecimal]
implicit val timestampColumnType: BaseColumnType[Timestamp]
implicit val dateColumnType: BaseColumnType[Date]
implicit val uuidColumnType: BaseColumnType[UUID]
// Option types
implicit def optionColumnType[T](implicit base: ColumnType[T]): ColumnType[Option[T]]
// Database-specific extensions
trait DatabaseSpecificTypes {
implicit def arrayColumnType[T](implicit base: ColumnType[T]): ColumnType[List[T]]
implicit val jsonColumnType: ColumnType[JsValue]
}