ScalikeJDBC

ScalikeJDBC is a Scala-exclusive database library designed as "a tidy SQL-based database access library for Scala developers" that naturally wraps JDBC APIs and provides easy-to-use APIs. Based on the philosophy that "JDBC stands as a steadfast standard interface. Given its widespread support across most RDBMS, accessing databases remains consistent," it achieves robust and expressive database operations that leverage the full benefits of Scala's type system and functional programming through type-safe QueryDSL and string interpolation SQL queries.

ScalaDatabaseJDBCSQLORMType-safeQueryDSL

GitHub Overview

scalikejdbc/scalikejdbc

A tidy SQL-based DB access library for Scala developers. This library naturally wraps JDBC APIs and provides you easy-to-use APIs.

Stars1,272
Watchers57
Forks231
Created:November 18, 2011
Language:Scala
License:Apache License 2.0

Topics

databaseh2jdbcmysqlpostgresqlscala

Star History

scalikejdbc/scalikejdbc Star History
Data as of: 7/17/2025, 07:00 AM

Library

ScalikeJDBC

Overview

ScalikeJDBC is a Scala-exclusive database library designed as "a tidy SQL-based database access library for Scala developers" that naturally wraps JDBC APIs and provides easy-to-use APIs. Based on the philosophy that "JDBC stands as a steadfast standard interface. Given its widespread support across most RDBMS, accessing databases remains consistent," it achieves robust and expressive database operations that leverage the full benefits of Scala's type system and functional programming through type-safe QueryDSL and string interpolation SQL queries.

Details

ScalikeJDBC 2025 edition provides a more type-safe and functional programming-focused database access layer with complete support for the latest features of Scala 3 and Scala 2.13 (Union Types, Opaque Types, Given/Using, etc.). By adhering to JDBC standards, it enables consistent API usage across any RDBMS including Oracle, SQL Server, PostgreSQL, MySQL, and SQLite. Through type-safe query construction with QueryDSL, intuitive query writing with SQL interpolation, and simple annotation-free design, it strongly supports high-quality Scala application development in enterprise environments. Furthermore, it supports modern reactive application development through relationship management and N+1 problem resolution with scalikejdbc-orm, and asynchronous support (ScalikeJDBC-Async).

Key Features

  • JDBC Standard Compliance: Consistent connections with any JDBC-compatible database
  • Type-safe QueryDSL: Safe query construction leveraging Scala's type system
  • SQL Interpolation: Intuitive and type-safe SQL writing
  • Annotation-free: Simple design minimizing learning costs
  • Production Ready: Stability for reliable use in real projects
  • ORM Features: Advanced object mapping through scalikejdbc-orm

Pros and Cons

Pros

  • Consistent API usage across a wide range of databases through JDBC standards
  • Leverages benefits of Scala's type system and functional programming
  • Can utilize SQL knowledge while benefiting from type safety
  • Low learning cost due to annotation-free, simple design
  • High reliability with extensive production environment track record
  • N+1 problem resolution and relationship management through scalikejdbc-orm

Cons

  • Scala-exclusive, not usable for cross-platform development
  • Limited modern features compared to other Scala libraries (Doobie, Quill, etc.)
  • Asynchronous processing requires separate library (ScalikeJDBC-Async) still in alpha stage
  • No support for NoSQL databases
  • Advanced ORM features like Hibernate or Active Record not provided by default
  • Configuration management may become complex in large-scale Scala projects

Reference Pages

Code Examples

Setup

// build.sbt
ThisBuild / scalaVersion := "2.13.12"

libraryDependencies ++= Seq(
  // ScalikeJDBC core
  "org.scalikejdbc" %% "scalikejdbc" % "4.3.0",
  "org.scalikejdbc" %% "scalikejdbc-config" % "4.3.0",
  "org.scalikejdbc" %% "scalikejdbc-streams" % "4.3.0",
  
  // ORM functionality
  "org.scalikejdbc" %% "scalikejdbc-orm" % "4.3.0",
  
  // Database drivers
  "org.postgresql" % "postgresql" % "42.7.1",
  "mysql" % "mysql-connector-java" % "8.0.33",
  "com.h2database" % "h2" % "2.2.224",
  
  // Logging
  "ch.qos.logback" % "logback-classic" % "1.4.14",
  
  // Testing
  "org.scalikejdbc" %% "scalikejdbc-test" % "4.3.0" % Test,
  "org.scalatest" %% "scalatest" % "3.2.17" % Test
)
// src/main/resources/application.conf
db {
  default {
    driver = "org.postgresql.Driver"
    url = "jdbc:postgresql://localhost:5432/myapp"
    user = "myuser"
    password = "mypassword"
    
    # Connection pool settings
    poolInitialSize = 10
    poolMaxSize = 20
    poolConnectionTimeoutMillis = 1000
    poolValidationQuery = "select 1 as one"
    poolFactoryName = "commons-dbcp2"
  }
  
  test {
    driver = "org.h2.Driver"
    url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"
    user = "sa"
    password = ""
  }
}
// src/main/scala/config/DBConfig.scala
package config

import scalikejdbc._
import scalikejdbc.config._

object DBConfig {
  def initialize(): Unit = {
    // Automatic configuration from config file
    DBs.setupAll()
    
    // Global settings
    GlobalSettings.loggingSQLErrors = true
    GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(
      enabled = true,
      singleLineMode = false,
      printUnprocessedStackTrace = false,
      stackTraceDepth = 15,
      logLevel = Symbol("info"),
      warningEnabled = true,
      warningThresholdMillis = 3000L,
      warningLogLevel = Symbol("warn")
    )
  }
  
  def close(): Unit = {
    DBs.closeAll()
  }
}

Basic CRUD Operations

// src/main/scala/models/User.scala
package models

import scalikejdbc._
import java.time.{LocalDateTime, ZonedDateTime}
import java.util.UUID

case class User(
  id: Option[Long] = None,
  username: String,
  email: String,
  hashedPassword: String,
  isActive: Boolean = true,
  createdAt: LocalDateTime = LocalDateTime.now(),
  updatedAt: Option[LocalDateTime] = None
)

object User extends SQLSyntaxSupport[User] {
  override val tableName = "users"
  override val columns = Seq("id", "username", "email", "hashed_password", "is_active", "created_at", "updated_at")
  
  // Column aliases
  val u = User.syntax("u")
  
  // Convert ResultSet to User object
  def apply(rs: WrappedResultSet): User = User(
    id = rs.longOpt("id"),
    username = rs.string("username"),
    email = rs.string("email"),
    hashedPassword = rs.string("hashed_password"),
    isActive = rs.boolean("is_active"),
    createdAt = rs.localDateTime("created_at"),
    updatedAt = rs.localDateTimeOpt("updated_at")
  )
  
  def apply(u: SyntaxProvider[User])(rs: WrappedResultSet): User = apply(rs)
  
  // CRUD operations
  def create(username: String, email: String, hashedPassword: String)(implicit session: DBSession = autoSession): User = {
    val now = LocalDateTime.now()
    val id = withSQL {
      insert.into(User).namedValues(
        column.username -> username,
        column.email -> email,
        column.hashedPassword -> hashedPassword,
        column.isActive -> true,
        column.createdAt -> now
      )
    }.updateAndReturnGeneratedKey.apply()
    
    User(
      id = Some(id),
      username = username,
      email = email,
      hashedPassword = hashedPassword,
      isActive = true,
      createdAt = now
    )
  }
  
  def findById(id: Long)(implicit session: DBSession = readOnlySession): Option[User] = {
    withSQL {
      select.from(User as u).where.eq(u.id, id)
    }.map(User(u)).single.apply()
  }
  
  def findByUsername(username: String)(implicit session: DBSession = readOnlySession): Option[User] = {
    withSQL {
      select.from(User as u).where.eq(u.username, username)
    }.map(User(u)).single.apply()
  }
  
  def findByEmail(email: String)(implicit session: DBSession = readOnlySession): Option[User] = {
    withSQL {
      select.from(User as u).where.eq(u.email, email)
    }.map(User(u)).single.apply()
  }
  
  def findAll(limit: Int = 100, offset: Int = 0)(implicit session: DBSession = readOnlySession): List[User] = {
    withSQL {
      select.from(User as u)
        .orderBy(u.createdAt.desc)
        .limit(limit).offset(offset)
    }.map(User(u)).list.apply()
  }
  
  def findActiveUsers()(implicit session: DBSession = readOnlySession): List[User] = {
    withSQL {
      select.from(User as u)
        .where.eq(u.isActive, true)
        .orderBy(u.username.asc)
    }.map(User(u)).list.apply()
  }
  
  def update(user: User)(implicit session: DBSession = autoSession): Int = {
    withSQL {
      scalikejdbc.update(User).set(
        column.username -> user.username,
        column.email -> user.email,
        column.hashedPassword -> user.hashedPassword,
        column.isActive -> user.isActive,
        column.updatedAt -> LocalDateTime.now()
      ).where.eq(column.id, user.id)
    }.update.apply()
  }
  
  def delete(id: Long)(implicit session: DBSession = autoSession): Int = {
    withSQL {
      deleteFrom(User).where.eq(column.id, id)
    }.update.apply()
  }
  
  def deactivate(id: Long)(implicit session: DBSession = autoSession): Int = {
    withSQL {
      scalikejdbc.update(User).set(
        column.isActive -> false,
        column.updatedAt -> LocalDateTime.now()
      ).where.eq(column.id, id)
    }.update.apply()
  }
  
  // Search functionality
  def search(query: String, limit: Int = 20)(implicit session: DBSession = readOnlySession): List[User] = {
    val pattern = s"%$query%"
    withSQL {
      select.from(User as u)
        .where(
          sqls.like(u.username, pattern)
            .or.like(u.email, pattern)
        )
        .and.eq(u.isActive, true)
        .orderBy(u.username.asc)
        .limit(limit)
    }.map(User(u)).list.apply()
  }
  
  // Statistics
  def countActiveUsers()(implicit session: DBSession = readOnlySession): Long = {
    withSQL {
      select(sqls.count).from(User as u).where.eq(u.isActive, true)
    }.map(_.long(1)).single.apply().getOrElse(0L)
  }
}

Related Entities and JOIN Operations

// src/main/scala/models/Post.scala
package models

import scalikejdbc._
import java.time.LocalDateTime

case class Post(
  id: Option[Long] = None,
  userId: Long,
  title: String,
  content: String,
  published: Boolean = false,
  createdAt: LocalDateTime = LocalDateTime.now(),
  updatedAt: Option[LocalDateTime] = None
)

object Post extends SQLSyntaxSupport[Post] {
  override val tableName = "posts"
  override val columns = Seq("id", "user_id", "title", "content", "published", "created_at", "updated_at")
  
  val p = Post.syntax("p")
  
  def apply(rs: WrappedResultSet): Post = Post(
    id = rs.longOpt("id"),
    userId = rs.long("user_id"),
    title = rs.string("title"),
    content = rs.string("content"),
    published = rs.boolean("published"),
    createdAt = rs.localDateTime("created_at"),
    updatedAt = rs.localDateTimeOpt("updated_at")
  )
  
  def apply(p: SyntaxProvider[Post])(rs: WrappedResultSet): Post = apply(rs)
  
  // Related object including joined data
  case class PostWithUser(post: Post, user: User)
  
  def create(userId: Long, title: String, content: String, published: Boolean = false)(implicit session: DBSession = autoSession): Post = {
    val now = LocalDateTime.now()
    val id = withSQL {
      insert.into(Post).namedValues(
        column.userId -> userId,
        column.title -> title,
        column.content -> content,
        column.published -> published,
        column.createdAt -> now
      )
    }.updateAndReturnGeneratedKey.apply()
    
    Post(
      id = Some(id),
      userId = userId,
      title = title,
      content = content,
      published = published,
      createdAt = now
    )
  }
  
  def findById(id: Long)(implicit session: DBSession = readOnlySession): Option[Post] = {
    withSQL {
      select.from(Post as p).where.eq(p.id, id)
    }.map(Post(p)).single.apply()
  }
  
  def findByUserId(userId: Long, limit: Int = 50)(implicit session: DBSession = readOnlySession): List[Post] = {
    withSQL {
      select.from(Post as p)
        .where.eq(p.userId, userId)
        .orderBy(p.createdAt.desc)
        .limit(limit)
    }.map(Post(p)).list.apply()
  }
  
  def findPublishedPosts(limit: Int = 20, offset: Int = 0)(implicit session: DBSession = readOnlySession): List[Post] = {
    withSQL {
      select.from(Post as p)
        .where.eq(p.published, true)
        .orderBy(p.createdAt.desc)
        .limit(limit).offset(offset)
    }.map(Post(p)).list.apply()
  }
  
  // JOIN query: Join posts with user information
  def findPostsWithUsers(limit: Int = 20)(implicit session: DBSession = readOnlySession): List[PostWithUser] = {
    withSQL {
      select
        .from(Post as p)
        .innerJoin(User as User.u).on(p.userId, User.u.id)
        .where.eq(p.published, true)
        .and.eq(User.u.isActive, true)
        .orderBy(p.createdAt.desc)
        .limit(limit)
    }.map { rs =>
      PostWithUser(
        post = Post(p)(rs),
        user = User(User.u)(rs)
      )
    }.list.apply()
  }
  
  // Search functionality (full content search)
  def searchPosts(query: String, publishedOnly: Boolean = true, limit: Int = 20)(implicit session: DBSession = readOnlySession): List[PostWithUser] = {
    val pattern = s"%$query%"
    withSQL {
      select
        .from(Post as p)
        .innerJoin(User as User.u).on(p.userId, User.u.id)
        .where(
          sqls.like(p.title, pattern)
            .or.like(p.content, pattern)
        )
        .and(if (publishedOnly) sqls.eq(p.published, true) else sqls.empty)
        .and.eq(User.u.isActive, true)
        .orderBy(p.createdAt.desc)
        .limit(limit)
    }.map { rs =>
      PostWithUser(
        post = Post(p)(rs),
        user = User(User.u)(rs)
      )
    }.list.apply()
  }
  
  def update(post: Post)(implicit session: DBSession = autoSession): Int = {
    withSQL {
      scalikejdbc.update(Post).set(
        column.title -> post.title,
        column.content -> post.content,
        column.published -> post.published,
        column.updatedAt -> LocalDateTime.now()
      ).where.eq(column.id, post.id)
    }.update.apply()
  }
  
  def publish(id: Long)(implicit session: DBSession = autoSession): Int = {
    withSQL {
      scalikejdbc.update(Post).set(
        column.published -> true,
        column.updatedAt -> LocalDateTime.now()
      ).where.eq(column.id, id)
    }.update.apply()
  }
  
  def delete(id: Long)(implicit session: DBSession = autoSession): Int = {
    withSQL {
      deleteFrom(Post).where.eq(column.id, id)
    }.update.apply()
  }
}

Aggregate Queries and Statistics

// src/main/scala/models/Analytics.scala
package models

import scalikejdbc._

object Analytics {
  case class UserPostCount(user: User, postCount: Long)
  case class PostStats(totalPosts: Long, publishedPosts: Long, unpublishedPosts: Long)
  case class MonthlyPostCount(year: Int, month: Int, count: Long)
  
  // Get post count by user
  def getUserPostCounts(limit: Int = 50)(implicit session: DBSession = readOnlySession): List[UserPostCount] = {
    withSQL {
      select(
        User.u.result.*,
        sqls.count(Post.p.id).as("post_count")
      )
      .from(User as User.u)
      .leftJoin(Post as Post.p).on(User.u.id, Post.p.userId)
      .where.eq(User.u.isActive, true)
      .groupBy(User.u.id)
      .orderBy(sqls"post_count".desc)
      .limit(limit)
    }.map { rs =>
      UserPostCount(
        user = User(User.u)(rs),
        postCount = rs.long("post_count")
      )
    }.list.apply()
  }
  
  // Post statistics
  def getPostStats()(implicit session: DBSession = readOnlySession): PostStats = {
    val totalPosts = withSQL {
      select(sqls.count).from(Post as Post.p)
    }.map(_.long(1)).single.apply().getOrElse(0L)
    
    val publishedPosts = withSQL {
      select(sqls.count).from(Post as Post.p).where.eq(Post.p.published, true)
    }.map(_.long(1)).single.apply().getOrElse(0L)
    
    PostStats(
      totalPosts = totalPosts,
      publishedPosts = publishedPosts,
      unpublishedPosts = totalPosts - publishedPosts
    )
  }
  
  // Monthly post counts
  def getMonthlyPostCounts(months: Int = 12)(implicit session: DBSession = readOnlySession): List[MonthlyPostCount] = {
    withSQL {
      select(
        sqls"EXTRACT(YEAR FROM created_at)".as("year"),
        sqls"EXTRACT(MONTH FROM created_at)".as("month"),
        sqls.count(Post.p.id).as("post_count")
      )
      .from(Post as Post.p)
      .where.ge(Post.p.createdAt, sqls"NOW() - INTERVAL '$months months'")
      .groupBy(sqls"EXTRACT(YEAR FROM created_at)", sqls"EXTRACT(MONTH FROM created_at)")
      .orderBy(sqls"year".desc, sqls"month".desc)
    }.map { rs =>
      MonthlyPostCount(
        year = rs.int("year"),
        month = rs.int("month"),
        count = rs.long("post_count")
      )
    }.list.apply()
  }
  
  // Get active users (with many recent posts)
  def getActiveUsers(days: Int = 30, limit: Int = 10)(implicit session: DBSession = readOnlySession): List[UserPostCount] = {
    withSQL {
      select(
        User.u.result.*,
        sqls.count(Post.p.id).as("recent_post_count")
      )
      .from(User as User.u)
      .innerJoin(Post as Post.p).on(User.u.id, Post.p.userId)
      .where.ge(Post.p.createdAt, sqls"NOW() - INTERVAL '$days days'")
      .and.eq(User.u.isActive, true)
      .groupBy(User.u.id)
      .having(sqls.gt(sqls.count(Post.p.id), 0))
      .orderBy(sqls"recent_post_count".desc)
      .limit(limit)
    }.map { rs =>
      UserPostCount(
        user = User(User.u)(rs),
        postCount = rs.long("recent_post_count")
      )
    }.list.apply()
  }
}

Transaction Processing

// src/main/scala/services/UserService.scala
package services

import scalikejdbc._
import models.{User, Post}
import java.time.LocalDateTime

class UserService {
  
  // User creation with welcome post in transaction
  def createUserWithWelcomePost(
    username: String,
    email: String,
    hashedPassword: String
  ): Either[String, (User, Post)] = {
    
    DB localTx { implicit session =>
      try {
        // Check for user duplication
        User.findByUsername(username) match {
          case Some(_) => return Left(s"Username '$username' already exists")
          case None =>
        }
        
        User.findByEmail(email) match {
          case Some(_) => return Left(s"Email '$email' already exists")
          case None =>
        }
        
        // Create user
        val user = User.create(username, email, hashedPassword)
        
        // Create welcome post
        val welcomePost = Post.create(
          userId = user.id.get,
          title = s"Welcome ${user.username}!",
          content = "Thank you for joining our platform. This is your first post!",
          published = true
        )
        
        Right((user, welcomePost))
        
      } catch {
        case ex: Exception =>
          session.rollback()
          Left(s"Failed to create user: ${ex.getMessage}")
      }
    }
  }
  
  // Batch user operations
  def batchUpdateUserStatus(userIds: List[Long], isActive: Boolean): Either[String, Int] = {
    DB localTx { implicit session =>
      try {
        val updatedCount = userIds.map { userId =>
          if (isActive) {
            User.update(
              User.findById(userId).getOrElse(return Left(s"User $userId not found"))
                .copy(isActive = true, updatedAt = Some(LocalDateTime.now()))
            )
          } else {
            User.deactivate(userId)
          }
        }.sum
        
        Right(updatedCount)
        
      } catch {
        case ex: Exception =>
          session.rollback()
          Left(s"Batch update failed: ${ex.getMessage}")
      }
    }
  }
  
  // Complex business logic: User deletion and post archiving
  def deactivateUserAndArchivePosts(userId: Long): Either[String, (User, Int)] = {
    DB localTx { implicit session =>
      try {
        val user = User.findById(userId).getOrElse(return Left("User not found"))
        
        if (!user.isActive) {
          return Left("User is already inactive")
        }
        
        // Unpublish all user posts
        val archivedPostsCount = withSQL {
          scalikejdbc.update(Post).set(
            Post.column.published -> false,
            Post.column.updatedAt -> LocalDateTime.now()
          ).where.eq(Post.column.userId, userId)
        }.update.apply()
        
        // Deactivate user
        User.deactivate(userId)
        val updatedUser = User.findById(userId).get
        
        Right((updatedUser, archivedPostsCount))
        
      } catch {
        case ex: Exception =>
          session.rollback()
          Left(s"User deactivation failed: ${ex.getMessage}")
      }
    }
  }
}

Test Implementation

// src/test/scala/models/UserSpec.scala
package models

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.BeforeAndAfterEach
import scalikejdbc._
import scalikejdbc.scalatest.AutoRollback

class UserSpec extends AnyFlatSpec with Matchers with BeforeAndAfterEach with AutoRollback {
  
  override def db(): DB = NamedDB("test").toDB()
  
  override def beforeEach(): Unit = {
    super.beforeEach()
    // Test data setup
    DB localTx { implicit session =>
      sql"""
        CREATE TABLE IF NOT EXISTS users (
          id BIGINT AUTO_INCREMENT PRIMARY KEY,
          username VARCHAR(255) NOT NULL UNIQUE,
          email VARCHAR(255) NOT NULL UNIQUE,
          hashed_password VARCHAR(255) NOT NULL,
          is_active BOOLEAN DEFAULT TRUE,
          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
          updated_at TIMESTAMP NULL
        )
      """.execute.apply()
      
      sql"""
        CREATE TABLE IF NOT EXISTS posts (
          id BIGINT AUTO_INCREMENT PRIMARY KEY,
          user_id BIGINT NOT NULL,
          title VARCHAR(255) NOT NULL,
          content TEXT NOT NULL,
          published BOOLEAN DEFAULT FALSE,
          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
          updated_at TIMESTAMP NULL,
          FOREIGN KEY (user_id) REFERENCES users(id)
        )
      """.execute.apply()
    }
  }
  
  "User.create" should "create a new user successfully" in { implicit session =>
    val username = "testuser"
    val email = "[email protected]"
    val password = "hashedpassword123"
    
    val user = User.create(username, email, password)
    
    user.id should not be None
    user.username should be(username)
    user.email should be(email)
    user.hashedPassword should be(password)
    user.isActive should be(true)
    user.createdAt should not be null
  }
  
  "User.findById" should "return user when exists" in { implicit session =>
    val user = User.create("findtest", "[email protected]", "password")
    val foundUser = User.findById(user.id.get)
    
    foundUser should not be None
    foundUser.get.username should be("findtest")
    foundUser.get.email should be("[email protected]")
  }
  
  it should "return None when user does not exist" in { implicit session =>
    val foundUser = User.findById(999999L)
    foundUser should be(None)
  }
  
  "User.findByUsername" should "return user when username exists" in { implicit session =>
    User.create("uniqueuser", "[email protected]", "password")
    val foundUser = User.findByUsername("uniqueuser")
    
    foundUser should not be None
    foundUser.get.username should be("uniqueuser")
  }
  
  "User.update" should "update user information correctly" in { implicit session =>
    val user = User.create("updatetest", "[email protected]", "password")
    val updatedUser = user.copy(
      username = "updatedname",
      email = "[email protected]"
    )
    
    val updateCount = User.update(updatedUser)
    updateCount should be(1)
    
    val retrieved = User.findById(user.id.get).get
    retrieved.username should be("updatedname")
    retrieved.email should be("[email protected]")
    retrieved.updatedAt should not be None
  }
  
  "User.search" should "find users by username and email" in { implicit session =>
    User.create("alice", "[email protected]", "password")
    User.create("bob", "[email protected]", "password")
    User.create("charlie", "[email protected]", "password")
    
    val searchResults = User.search("alice")
    searchResults should have length 1
    searchResults.head.username should be("alice")
    
    val emailSearchResults = User.search("example.com")
    emailSearchResults should have length 2
  }
  
  "User.countActiveUsers" should "return correct count" in { implicit session =>
    User.create("active1", "[email protected]", "password")
    User.create("active2", "[email protected]", "password")
    val inactiveUser = User.create("inactive", "[email protected]", "password")
    User.deactivate(inactiveUser.id.get)
    
    val activeCount = User.countActiveUsers()
    activeCount should be(2)
  }
}

// src/test/scala/services/UserServiceSpec.scala
package services

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.BeforeAndAfterEach
import scalikejdbc._
import scalikejdbc.scalatest.AutoRollback
import models.{User, Post}

class UserServiceSpec extends AnyFlatSpec with Matchers with BeforeAndAfterEach with AutoRollback {
  
  override def db(): DB = NamedDB("test").toDB()
  val userService = new UserService()
  
  override def beforeEach(): Unit = {
    super.beforeEach()
    // Table creation (same as User test)
  }
  
  "UserService.createUserWithWelcomePost" should "create user and welcome post in transaction" in { implicit session =>
    val result = userService.createUserWithWelcomePost(
      "newuser", 
      "[email protected]", 
      "hashedpassword"
    )
    
    result should be a 'right
    val (user, post) = result.right.get
    
    user.username should be("newuser")
    user.email should be("[email protected]")
    
    post.userId should be(user.id.get)
    post.title should include("Welcome newuser!")
    post.published should be(true)
  }
  
  it should "fail when username already exists" in { implicit session =>
    User.create("existinguser", "[email protected]", "password")
    
    val result = userService.createUserWithWelcomePost(
      "existinguser", 
      "[email protected]", 
      "password"
    )
    
    result should be a 'left
    result.left.get should include("Username 'existinguser' already exists")
  }
  
  "UserService.batchUpdateUserStatus" should "update multiple users" in { implicit session =>
    val user1 = User.create("batch1", "[email protected]", "password")
    val user2 = User.create("batch2", "[email protected]", "password")
    val user3 = User.create("batch3", "[email protected]", "password")
    
    User.deactivate(user1.id.get)
    User.deactivate(user2.id.get)
    
    val result = userService.batchUpdateUserStatus(
      List(user1.id.get, user2.id.get),
      isActive = true
    )
    
    result should be a 'right
    result.right.get should be(2)
    
    User.findById(user1.id.get).get.isActive should be(true)
    User.findById(user2.id.get).get.isActive should be(true)
    User.findById(user3.id.get).get.isActive should be(true)
  }
}