or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

database-io.mddatabase-profiles.mdindex.mdplain-sql.mdqueries.mdtable-definitions.mdtype-mappings.md
tile.json

type-mappings.mddocs/

Type Mappings

Column type mappings between Scala types and database types, including custom type mappings and implicit conversions.

Capabilities

Column Types

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]

Built-in Type Mappings

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]
}

Custom Type Mappings

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]
}

Database-Specific Types

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)
}

Type Conversions

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)
}

Validation and Constraints

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}")
    }
  )
}

Types

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]
}