Skip to content

Quick Start

Get started with Slick Seeker in 5 minutes.

Installation

Add to your build.sbt:

libraryDependencies ++= Seq(
  "io.github.devnico" %% "slick-seeker" % "0.4.0",
  "io.github.devnico" %% "slick-seeker-play-json" % "0.4.0"  // Optional
)

Basic Usage

Step 1: Create Your Profile

Create a custom profile that extends your database profile and mixes in SlickSeekerSupport:

import slick.jdbc.PostgresProfile
import io.github.devnico.slickseeker.SlickSeekerSupport
import io.github.devnico.slickseeker.playjson.PlayJsonSeekerSupport

trait MyPostgresProfile extends PostgresProfile 
  with SlickSeekerSupport 
  with PlayJsonSeekerSupport {

  object MyApi extends API with SeekImplicits with JsonSeekerImplicits

  override val api: MyApi.type = MyApi
}

object MyPostgresProfile extends MyPostgresProfile

Why? This pattern allows Slick Seeker to work with any JDBC profile (PostgreSQL, MySQL, H2, SQLite, Oracle, etc.) without being tied to a specific database. By defining the cursor environment inside your profile, it's available wherever you import the profile API.

Step 2: Import Your Profile API

// Import your custom profile API
import MyPostgresProfile.api._

This provides:

  • All Slick query methods (filter, sortBy, map, etc.)
  • Slick Seeker's .toSeeker extension method
  • All necessary type classes and implicits
  • Your cursor environment

Step 3: Define Your Table

case class User(id: Int, name: String, email: String, active: Boolean)

class Users(tag: Tag) extends Table[User](tag, "users") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
  def name = column[String]("name")
  def email = column[String]("email")
  def active = column[Boolean]("active")

  def * = (id, name, email, active).mapTo[User]
}

val users = TableQuery[Users]

Step 4: Create a Seeker

val seeker = users.toSeeker
  .seek(_.name.asc)      // Primary sort: name ascending
  .seek(_.id.asc)        // Tiebreaker: id ascending

Step 5: Paginate

import scala.concurrent.ExecutionContext.Implicits.global

// First page
val page1: Future[PaginatedResult[User]] = 
  db.run(seeker.page(limit = 20, cursor = None))

// Next page (use cursor from previous page)
val page2: Future[PaginatedResult[User]] = 
  page1.flatMap { p1 =>
    db.run(seeker.page(limit = 20, cursor = p1.nextCursor))
  }

// Previous page (bidirectional navigation)
val backToPage1: Future[PaginatedResult[User]] =
  page2.flatMap { p2 =>
    db.run(seeker.page(limit = 20, cursor = p2.prevCursor))
  }

PaginatedResult

The page() method returns a PaginatedResult[T]:

case class PaginatedResult[T](
  total: Int,              // Total number of items
  items: Seq[T],           // Current page items
  nextCursor: Option[String],  // Cursor for next page (None if last page)
  prevCursor: Option[String]   // Cursor for previous page (None if first page)
)

Using with Play JSON

For REST APIs, you can serialize PaginatedResult to JSON:

import io.github.devnico.slickseeker.playjson._
import play.api.libs.json.Json

implicit val userFormat = Json.format[User]

val result: PaginatedResult[User] = ???
val json = Json.toJson(result)
// {
//   "total": 100,
//   "items": [...],
//   "nextCursor": "eyJkaXJlY3Rpb24iOiJmb3J3YXJkIiwiLi4u",
//   "prevCursor": null
// }

Common Patterns

Sort by Multiple Columns

val seeker = users.toSeeker
  .seek(_.active.desc)    // Active users first
  .seek(_.name.asc)       // Then by name
  .seek(_.id.asc)         // Tiebreaker

Handle Nullable Columns

case class Person(id: Int, firstName: String, lastName: Option[String])

val seeker = persons.toSeeker
  .seek(_.lastName.nullsLast.asc)  // NULLs at the end
  .seek(_.firstName.asc)
  .seek(_.id.asc)

Reverse Sort Direction

// Sort descending
val seeker = users.toSeeker
  .seek(_.name.desc)
  .seek(_.id.desc)

// Or use seekDirection
val seeker2 = users.toSeeker
  .seek(_.name)
  .seek(_.id)
  .seekDirection(SortDirection.Desc)

PostgreSQL? Use Type-Safe Tuple Seeker!

If you're using PostgreSQL and all your seek columns are non-nullable with uniform direction, use SlickPgTupleSeeker for compile-time safety and better performance:

// Standard SlickSeeker (works on any database)
val standardSeeker = users.toSeeker
  .seek(_.name.asc)
  .seek(_.id.asc)

// PostgreSQL-optimized with compile-time safety
val pgSeeker = users.toPgTupleSeekerAsc
  .seek(_.name)   // No .asc needed - enforced by type
  .seek(_.id)

// Generates optimized SQL:
// WHERE (name, id) > (?, ?)
// vs standard: WHERE (name > ?) OR (name = ? AND id > ?)

Benefits: - ✅ Compile-time safety - rejects Option[T] columns at compilation - ✅ Simpler SQL - single tuple comparison instead of OR clauses - ✅ Better performance - easier for PostgreSQL to optimize - ✅ Type-enforced direction - impossible to mix ASC/DESC

Use when: - Database is PostgreSQL 8.2+ or H2 in PostgreSQL mode - All columns are non-nullable (no Option[T]) - All columns have same direction (all ASC or all DESC)

See Cookbook - PostgreSQL Tuple Optimization for details.

Quick Reference

Choose Your Seeker

Feature Standard SlickSeeker SlickPgTupleSeeker
Databases All (PostgreSQL, MySQL, H2, SQLite, etc.) PostgreSQL 8.2+, H2 (PG mode)
Nullable columns ✅ Yes ❌ No - compile error
Mixed directions ✅ Yes ❌ No - type enforced
Type safety Runtime Compile-time
SQL generation OR-based clauses Tuple comparison
Performance Good Better (PostgreSQL)

API Quick Reference

// Standard seeker
users.toSeeker
  .seek(_.col.asc)           // Ascending
  .seek(_.col.desc)          // Descending
  .seek(_.col.nullsLast.asc) // Nulls handling
  .seekDirection(...)        // Batch direction change

// PostgreSQL tuple seeker (type-safe)
users.toPgTupleSeekerAsc     // All columns ASC
  .seek(_.col)               // No .asc/.desc needed

users.toPgTupleSeekerDesc    // All columns DESC
  .seek(_.col)

// Pagination (same for both)
seeker.page(limit = 20, cursor = None)           // First page
seeker.page(limit = 20, cursor = Some("..."))    // Next/prev page

What's Next?

  • Core Concepts - Deep dive into cursor pagination and decorators
  • Cookbook - Real-world examples and PostgreSQL optimization