Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ object `scalasql-simple` extends CommonBase {
object scalasql extends Cross[ScalaSql](scalaVersions)
trait ScalaSql extends Common { common =>
def moduleDeps = Seq(query, operations)
def ivyDeps = Agg.empty[Dep] ++ Option.when(scalaVersion().startsWith("2."))(
ivy"org.scala-lang:scala-reflect:${scalaVersion()}"
)
def ivyDeps = Agg(ivy"com.lihaoyi::upickle:3.1.3") ++ Option.when(scalaVersion().startsWith("2."))(
ivy"org.scala-lang:scala-reflect:${scalaVersion()}"
)

override def consoleScalacOptions: T[Seq[String]] = Seq("-Xprint:typer")

Expand Down
64 changes: 64 additions & 0 deletions scalasql/src/dialects/PostgresDialect.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,26 @@ trait PostgresDialect extends Dialect with ReturningDialect with OnConflictOps {
override implicit def StringType: TypeMapper[String] = new PostgresStringType
class PostgresStringType extends StringType { override def castTypeString = "VARCHAR" }

implicit def UjsonValueType: TypeMapper[ujson.Value] = new UjsonValueType
class UjsonValueType extends TypeMapper[ujson.Value] {
def jdbcType = java.sql.JDBCType.OTHER
def get(r: java.sql.ResultSet, idx: Int) = {
val str = r.getString(idx)
if (str == null) ujson.Null
else ujson.read(str)
}
def put(r: java.sql.PreparedStatement, idx: Int, v: ujson.Value) =
r.setObject(idx, v.toString(), java.sql.Types.OTHER)
}

override implicit def ExprStringOpsConv(v: Expr[String]): PostgresDialect.ExprStringOps[String] =
new PostgresDialect.ExprStringOps(v)

implicit def ExprJsonOpsConv(v: Expr[ujson.Value]): PostgresDialect.ExprJsonOps =
new PostgresDialect.ExprJsonOps(v)

implicit def UjsonQueryable: Queryable.Row[Expr[ujson.Value], ujson.Value] = ExprQueryable(UjsonValueType)

override implicit def ExprBlobOpsConv(
v: Expr[geny.Bytes]
): PostgresDialect.ExprStringLikeOps[geny.Bytes] =
Expand Down Expand Up @@ -115,6 +132,53 @@ object PostgresDialect extends PostgresDialect {
def random: Expr[Double] = Expr { _ => sql"RANDOM()" }
}

class ExprJsonOps(val v: Expr[ujson.Value]) extends scalasql.operations.ExprOps(v) {
// -> integer
def ->(n: Expr[Int]): Expr[ujson.Value] = Expr { implicit ctx => sql"$v -> $n" }
// -> text
def ->(k: Expr[String])(implicit d: DummyImplicit): Expr[ujson.Value] = Expr { implicit ctx =>
sql"$v -> $k"
}
// ->> integer
def ->>(n: Expr[Int]): Expr[String] = Expr { implicit ctx => sql"$v ->> $n" }
// ->> text
def ->>(k: Expr[String])(implicit d: DummyImplicit): Expr[String] = Expr { implicit ctx =>
sql"$v ->> $k"
}

def #>(path: Expr[String]*): Expr[ujson.Value] = Expr { implicit ctx =>
val array = sql"ARRAY[${SqlStr.join(path.map(p => sql"$p"), sql", ")}]"
sql"$v #> $array"
}

def #>>(path: Expr[String]*): Expr[String] = Expr { implicit ctx =>
val array = sql"ARRAY[${SqlStr.join(path.map(p => sql"$p"), sql", ")}]"
sql"$v #>> $array"
}

def @>(other: Expr[ujson.Value]): Expr[Boolean] = Expr { implicit ctx => sql"$v @> $other" }
def <@(other: Expr[ujson.Value]): Expr[Boolean] = Expr { implicit ctx => sql"$v <@ $other" }

def ?(key: Expr[String]): Expr[Boolean] = Expr { implicit ctx => sql"jsonb_exists($v, $key)" }

def ?|(keys: Expr[String]*): Expr[Boolean] = Expr { implicit ctx =>
val array = sql"ARRAY[${SqlStr.join(keys.map(p => sql"$p"), sql", ")}]"
sql"jsonb_exists_any($v, $array)"
}

def ?&(keys: Expr[String]*): Expr[Boolean] = Expr { implicit ctx =>
val array = sql"ARRAY[${SqlStr.join(keys.map(p => sql"$p"), sql", ")}]"
sql"jsonb_exists_all($v, $array)"
}

def ||(other: Expr[ujson.Value]): Expr[ujson.Value] = Expr { implicit ctx => sql"$v || $other" }

def -(key: Expr[String]): Expr[ujson.Value] = Expr { implicit ctx => sql"$v - $key" }
def -(index: Expr[Int])(implicit d: DummyImplicit): Expr[ujson.Value] = Expr { implicit ctx =>
sql"$v - $index"
}
}

class ExprAggOps[T](v: Aggregatable[Expr[T]]) extends scalasql.operations.ExprAggOps[T](v) {
def mkString(sep: Expr[String] = null)(implicit tm: TypeMapper[T]): Expr[String] = {
val sepRender = Option(sep).getOrElse(sql"''")
Expand Down
1 change: 1 addition & 0 deletions scalasql/test/src/ConcreteTestSuites.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ package postgres {
object OptionalTests extends datatypes.OptionalTests with PostgresSuite

object PostgresDialectTests extends PostgresDialectTests
object PostgresJsonTests extends scalasql.dialects.PostgresJsonTests

}

Expand Down
1 change: 1 addition & 0 deletions scalasql/test/src/ExampleTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import utest._
object ExampleTests extends TestSuite {
def tests = Tests {
test("postgres") - example.PostgresExample.main(Array())
test("postgresJson") - example.PostgresJsonExample.main(Array())
test("mysql") - example.MySqlExample.main(Array())
test("h2") - example.H2Example.main(Array())
test("sqlite") - example.SqliteExample.main(Array())
Expand Down
139 changes: 139 additions & 0 deletions scalasql/test/src/dialects/PostgresJsonTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package scalasql.dialects

import scalasql._
import scalasql.core.Expr
import scalasql.dialects.PostgresDialect._
import utest._
import utils.ScalaSqlSuite
import ujson.Value

trait PostgresJsonTests extends ScalaSqlSuite with PostgresDialect {
def description = "JSON operations"

// Mock DB for SQL generation checks only
lazy val mockDb = new DbClient.Connection(null, new Config {})

def check[Q, R](query: Q, sql: String)(implicit qr: Queryable[Q, R]) = {
val result = mockDb.renderSql(query)
val expected = sql.trim.replaceAll("\\s+", " ")
assert(result == expected)
}

// Placeholder for checker required by ScalaSqlSuite, but we won't use it
def checker: scalasql.utils.TestChecker = ???

override def utestBeforeEach(path: Seq[String]): Unit = {}
override def utestAfterEach(path: Seq[String]): Unit = {}

override implicit def UjsonQueryable: Queryable.Row[Expr[ujson.Value], ujson.Value] = new Expr.ExprQueryable()(UjsonValueType)

case class JsonTable[T[_]](id: T[Int], data: T[ujson.Value], dataJson: T[ujson.Value])
object JsonTable extends Table[JsonTable] {
override def tableName = "json_table"
override def tableColumnNameOverride(s: String) = s match {
case "dataJson" => "data_json"
case s => s
}
}

def tests = Tests {
test("access") - {
test("key") - check(
query = JsonTable.select.map(t => t.data -> "a"),
sql = "SELECT json_table0.data -> ? AS res FROM json_table json_table0"
)
test("index") - check(
query = JsonTable.select.map(t => t.data -> 0),
sql = "SELECT json_table0.data -> ? AS res FROM json_table json_table0"
)
test("keyText") - check(
query = JsonTable.select.map(t => t.data ->> "b"),
sql = "SELECT json_table0.data ->> ? AS res FROM json_table json_table0"
)
test("indexText") - check(
query = JsonTable.select.map(t => t.data ->> 1),
sql = "SELECT json_table0.data ->> ? AS res FROM json_table json_table0"
)
test("path") - check(
query = JsonTable.select.map(t => t.data #> "a"),
sql = "SELECT json_table0.data #> ARRAY[?] AS res FROM json_table json_table0"
)
test("pathText") - check(
query = JsonTable.select.map(t => t.data #>> "b"),
sql = "SELECT json_table0.data #>> ARRAY[?] AS res FROM json_table json_table0"
)
}

test("contains") - {
test("right") - check(
query = JsonTable.select.filter(t => t.data @> ujson.Obj("a" -> 1)).map(_.id),
sql = "SELECT json_table0.id AS res FROM json_table json_table0 WHERE json_table0.data @> ?"
)
test("left") - check(
query = JsonTable.select.filter(t => t.data <@ ujson.Arr(1, 2, 3)).map(_.id),
sql = "SELECT json_table0.id AS res FROM json_table json_table0 WHERE json_table0.data <@ ?"
)
}

test("exists") - {
test("key") - check(
query = JsonTable.select.filter(t => t.data ? "a").map(_.id),
sql =
"SELECT json_table0.id AS res FROM json_table json_table0 WHERE jsonb_exists(json_table0.data, ?)"
)
test("any") - check(
query = JsonTable.select.filter(t => t.data ?| ("a", "z")).map(_.id),
sql =
"SELECT json_table0.id AS res FROM json_table json_table0 WHERE jsonb_exists_any(json_table0.data, ARRAY[?, ?])"
)
test("all") - check(
query = JsonTable.select.filter(t => t.data ?& ("a", "b")).map(_.id),
sql =
"SELECT json_table0.id AS res FROM json_table json_table0 WHERE jsonb_exists_all(json_table0.data, ARRAY[?, ?])"
)
}

test("concat") - check(
query = JsonTable.select.filter(_.id === 1).map(t => t.data || ujson.Obj("c" -> 3)),
sql =
"SELECT json_table0.data || ? AS res FROM json_table json_table0 WHERE (json_table0.id = ?)"
)

test("delete") - {
test("key") - check(
query = JsonTable.select.filter(_.id === 1).map(t => t.data - "b"),
sql =
"SELECT json_table0.data - ? AS res FROM json_table json_table0 WHERE (json_table0.id = ?)"
)
test("index") - check(
query = JsonTable.select.filter(_.id === 2).map(t => t.data - 1),
sql =
"SELECT json_table0.data - ? AS res FROM json_table json_table0 WHERE (json_table0.id = ?)"
)
}

test("json") - {
// Testing JSON type (non-binary)
test("access") - check(
query = JsonTable.select.map(t => t.dataJson -> "a"),
sql = "SELECT json_table0.data_json -> ? AS res FROM json_table json_table0"
)
}

test("insert") - {
test("simple") - {
val query = JsonTable.insert.values(
JsonTable[Sc](
id = 3,
data = ujson.Obj("x" -> 10),
dataJson = ujson.Obj("y" -> 20)
)
)
check(
query,
"INSERT INTO json_table (id, data, data_json) VALUES (?, ?, ?)"
)
}
}
}
}
123 changes: 123 additions & 0 deletions scalasql/test/src/example/PostgresJsonExample.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package scalasql.example

import org.testcontainers.containers.PostgreSQLContainer
import scalasql.Table
import scalasql.PostgresDialect._
import scalasql.core.Expr
import ujson.Value

object PostgresJsonExample {

case class Person[T[_]](
id: T[Int],
name: T[String],
info: T[ujson.Value]
)

object Person extends Table[Person]

lazy val postgres = {
println("Initializing Postgres")
val pg = new PostgreSQLContainer("postgres:15-alpine")
pg.start()
pg
}

val dataSource = new org.postgresql.ds.PGSimpleDataSource
dataSource.setURL(postgres.getJdbcUrl)
dataSource.setDatabaseName(postgres.getDatabaseName);
dataSource.setUser(postgres.getUsername);
dataSource.setPassword(postgres.getPassword);

lazy val postgresClient = new scalasql.DbClient.DataSource(
dataSource,
config = new scalasql.Config {}
)

def main(args: Array[String]): Unit = {
postgresClient.transaction { db =>
db.updateRaw("""
CREATE TABLE person (
id SERIAL PRIMARY KEY,
name VARCHAR(256),
info JSONB
);
""")

val inserted = db.run(
Person.insert.batched(_.name, _.info)(
("John", ujson.Obj("age" -> 30, "pets" -> ujson.Arr("cat", "dog"), "active" -> true)),
("Jane", ujson.Obj("age" -> 25, "pets" -> ujson.Arr(), "active" -> false)),
("Bob", ujson.Obj("age" -> 40, "pets" -> ujson.Arr("fish"), "active" -> true))
)
)

assert(inserted == 3)

// Select with filter using JSON operator -> and casting
// Find people older than 28
// Note: -> returns JSON, so we cast to Int for comparison if we extracted as text,
// but here we can rely on ujson comparison if we implement it,
// or easier: extract as text and cast, or use @> for containment

// Using ->> to get text and cast to integer
val seniors = db.run(
Person.select
.filter(p => (p.info ->> "age").cast[Int] > 28)
.map(_.name)
)
assert(seniors.toSet == Set("John", "Bob"))

// Using @> (contains)
// Find people who are active
val active = db.run(
Person.select
.filter(p => p.info @> ujson.Obj("active" -> true))
.map(_.name)
)
assert(active.toSet == Set("John", "Bob"))

// Using ? (exists key)
// Find people who have "pets" key (all of them)
val hasPets = db.run(
Person.select.filter(p => p.info ? "pets").size
)
assert(hasPets == 3)

// Using -> and index access
// Find people whose first pet is "cat"
// p.info -> "pets" gives the array. -> 0 gives the first element.
// We compare it to ujson.Str("cat")
val catLovers = db.run(
Person.select
.filter(p => (p.info -> "pets" -> 0) === Expr[ujson.Value](ujson.Str("cat")))
.map(_.name)
)
assert(catLovers == Seq("John"))

// Update
// Add a new field "city": "New York" to John
db.run(
Person
.update(_.name === "John")
.set(p => p.info := (p.info || ujson.Obj("city" -> "New York")))
)

val johnInfo = db.run(Person.select.filter(_.name === "John").single).info
assert(johnInfo("city").str == "New York")
assert(johnInfo("age").num == 30)

// Delete key
// Remove "active" field from Jane
db.run(
Person
.update(_.name === "Jane")
.set(p => p.info := (p.info - "active"))
)

val janeInfo = db.run(Person.select.filter(_.name === "Jane").single).info
assert(!janeInfo.obj.contains("active"))
assert(janeInfo("age").num == 25)
}
}
}
Loading