Skip to content

Slick Seeker

Type-safe, high-performance cursor-based pagination for Slick 3.5+.

Features

  • Keyset Pagination - O(1) performance regardless of page depth
  • Bidirectional - Navigate forward and backward through result sets
  • Type-Safe - Compile-time verification of cursor/column matching
  • Profile Agnostic - Works with any Slick JDBC profile (PostgreSQL, MySQL, H2, SQLite, Oracle, etc.)
  • Flexible Ordering - Support for nulls first/last, custom enum orders
  • Modular - Core + optional Play JSON integration
  • Composable - Chain decorators for Base64, compression, encryption

Why Cursor-Based Pagination?

Traditional offset-based pagination (OFFSET + LIMIT) has serious performance issues:

-- Page 1000: Database must scan and skip 19,900 rows!
SELECT * FROM users ORDER BY name LIMIT 100 OFFSET 19900;

Problems:

  • Slow for deep pages (O(n) where n = offset)
  • Unstable with concurrent writes (items shift between pages)
  • Memory intensive for large offsets

Cursor-based pagination (keyset pagination) solves this:

-- Any page: Fast index-based lookup!
SELECT * FROM users
WHERE name > 'last_name' OR (name = 'last_name' AND id > last_id)
ORDER BY name, id 
LIMIT 100;

Benefits:

  • Constant O(1) performance for any page
  • Stable with concurrent writes
  • Efficient index usage

Installation

Add to your build.sbt:

libraryDependencies ++= Seq(
  "io.github.devnico" %% "slick-seeker" % "0.3.3",
  "io.github.devnico" %% "slick-seeker-play-json" % "0.3.3"  // Optional, but you need some kind of cursor encoder
)

Quick Example

// Step 1: Create a custom profile
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

// Step 2: Import your profile API
import MyPostgresProfile.api._

// Step 3: Define your table
case class User(id: Int, name: String, email: String)

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 * = (id, name, email).mapTo[User]
}

val users = TableQuery[Users]

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

// Paginate!
val page1 = db.run(seeker.page(limit = 20, cursor = None))
// PaginatedResult(total=100, items=[...], nextCursor=Some("..."), prevCursor=None)

val page2 = db.run(seeker.page(limit = 20, cursor = page1.nextCursor))
// Continue pagination...

Learn More

Requirements

  • Scala 2.13.14+, 3.3.4+, 3.5.2+
  • Slick 3.5.0+
  • Your Slick profile API must be imported before slick-seeker

License

Apache License 2.0 - Copyright © 2025 Nicolas Schlecker