Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

218 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JustSQL

Just write SQL as String and parse results into types.

A lightweight SQL library that adds type-safety to query results & parameters, allowing plain SQL queries.

Can be used in parallel with other libraries. Interop for Slick or HikariCP is provided.

Small: 407KB jar file. No external core dependency.

Why another SQL library?

  • ORMs, DSLs and custom string interpolation solutions are nice, but most are incomplete and restrictive, specially when writing complex SQL queries.
  • Debugging performance issues in ORM generated queries, translating back and forth between SQL and ORM types is time-consuming.
  • Many ORMs do not have any support for EXPLAIN ANALYZE statements.
  • IDEs have much better support/plugins for executing and analysing plain SQL queries versus custom DSLs.

Sponsors

Thank you JetBrains & JProfiler for full-featured open-source licences to their awesome development tools!

Jetbrains support JProfiler support [Become a sponsor]

NOTE: The following documentation and release is WIP

Quick start

See quick-start Example.scala.

Create JustSQL

I'm using Postgres and the default JavaSQLConnector here, but you should use a high-performance JDBC connection pool library. See interop for Slick or HikariCP.

A JustSQL instance is only required when executing a query i.e. when invoking runSync() or runAsync().

Everywhere else queries are declarative, so you define your queries without executing them so they can be embedded and composed.

import justsql._ //single import

implicit val db = JustSQL(datasource = JavaSQLConnector()) //create database instance

update()

Queries that mutate like CREATE, INSERT OR UPDATE queries are executed via update() function.

Let's create our example USERS table.

//create table
val create: Try[Int] = "CREATE TABLE USERS (id INT, name VARCHAR)".update().runSync()
//insert rows
val insert: Try[Int] =
  """
    |INSERT INTO USERS (id, name)
    |           VALUES (1, 'Harry'),
    |                  (2, 'Ayman')
    |""".stripMargin.update().runSync()

for-comprehension

Or execute the above queries using for-comprehension

val createAndInsert: Sql[(Int, Int)] =
  for {
    create <- "CREATE TABLE USERS (id INT, name VARCHAR)".update()
    insert <- "INSERT INTO USERS (id, name) VALUES (1, 'Harry'), (2, 'Ayman')".update()
  } yield (create, insert)

val result: Try[(Int, Int)] = createAndInsert.runSync()

Query parameters

SQL parameters are set with the suffix ?.

The above INSERT query can be written with parameters as following

//Or insert using parameters
val insertParametric: Try[Int] =
  UpdateSQL {
    implicit params =>
      s"""
         |INSERT INTO USERS (id, name)
         |           VALUES (${1.?}, ${"Harry".?}),
         |                  (${2.?}, ${"Ayman".?})
         |""".stripMargin
  }.runSync()

Transactionally

Being just SQL, transactions are written with the usual BEGIN; and COMMIT; statements.

val transaction: Try[Int] =
  UpdateSQL {
    implicit params =>
      s"""
         |BEGIN;
         |
         |CREATE TABLE USERS (id INT, name VARCHAR);
         |
         |INSERT INTO USERS (id, name)
         |           VALUES (${1.?}, ${"Harry".?}),
         |                  (${2.?}, ${"Ayman".?});
         |
         |COMMIT;
         |""".stripMargin
  }.recoverWith {
    _ =>
      "ROLLBACK".update() //if there was an error rollback
  }.runSync()

select()

First, we need to create a case class that represents a table row, which in our case is a User

//case class that represents a table row
case class User(id: Int, name: String)
//Build a row reader for User
implicit val userReader = RowReader(User.tupled)

Read all Users

val users: Try[ArraySeq[User]] = "SELECT * FROM USERS".select[User]().runSync()

Or if you want a List, provide it as a type argument

val usersCollected: Try[List[User]] = "SELECT * FROM USERS".select[User, List]().runSync()

Or with Parameters

val usersParametric: SelectSQL[String, ArraySeq] =
  SelectSQL[String] {
    implicit params: Params =>
      s"""
         |SELECT name from USERS where id = ${1.?}
         |""".stripMargin
  }

head(), headOption(), exactlyOne()

Returns Some(first element) from the query result or-else None if empty

val headOption: Try[Option[User]] = "SELECT * FROM USERS".select[User]().headOption().runSync()

First element from the query result

val head: Try[Int] = "SELECT max(id) FROM USERS".select[Int]().head().runSync()

Expects always one row in the result. If there are more than one, returns a failure.

val exactlyOne: Try[Int] = "SELECT count(*) FROM USERS".select[Int]().exactlyOne().runSync()

Embed queries - embed

Embed queries using embed function.

val query1: SelectSQL[Int, ArraySeq] =
  "SELECT max(id) from USERS".select[Int]()

//This query embeds query1 by calling `query1.embed`
val query2: Try[ArraySeq[String]] =
  SelectSQL[String] {
    implicit params: Params =>
      s"""
         |SELECT name from USERS
         | WHERE id = (${query1.embed})
         |""".stripMargin
  }.runSync()

Compose queries

TODO

Sequence

Run multiple queries in the same connection

val sequence: SQL[Seq[Int]] =
  SQL.sequence(
    "SELECT 1".select[Int](),
    "SELECT 2".select[Int](),
    "SELECT 3".select[Int]()
  ).map(_.flatten)

Failed or Success SQL[T]

Create a successful SQL

val success: SQL[Int] =
  SQL.success[Int](123)

Create a failed SQL

val failed: SQL[Int] =
  SQL.failure[Int](new Exception("Something went wrong"))

Custom ParamWriter

ParamWriter - Data types with single or multiple JDBC parameters

TODO

OneParamWriter - Data types with single JDBC parameter

//My custom data types
case class MyColumn(int: Int)

//Writer. See ParamWriter for more examples.
val paramWriter: OneParamWriter[MyColumn] =
  (statement: PositionedPreparedStatement, myColumn: MyColumn) =>
    statement setInt myColumn.int

Custom RowReader and ColReader

A SQL table is just a bunch of a rows and columns right. So we have a RowReader and a ColReader to represent those.

RowReader

A RowReader is just a collection of one or many ColReader(s).

TODO

ColReader

//custom column
case class MyColumn(int: Int)

//custom column reader
val colReader: ColReader[MyColumn] =
  (resultSet: ResultSet, index: Int) =>
    MyColumn(resultSet.getInt(1))

Slick interop

Make sure the dependency justsql-slick is in your build.

This allows JustSQL to borrow connections created by Slick.

//Your Slick database-config 
val dbConfig: DatabaseConfig[JdbcProfile] = ???
//Just pass it onto JustSQL
implicit val justSQL = JustSQL(SlickSQLConnector(dbConfig))

HikariCP interop

Make sure the dependency justsql-hikari is in your build.

//Pass HikariSQLConnector to JustSQL
implicit val justSQL = JustSQL(HikariSQLConnector())

Unsafe

Unsafe APIs give direct access to low level java.sql.ResultSet type.

unsafeSelect()

//read the names of all Users
val names: Try[Array[String]] = "SELECT * FROM USERS".unsafeSelect(_.getString("name")).runSync()

Releases

Packages

Used by

Contributors

Languages