added sql builder framework
This commit is contained in:
parent
b90e171977
commit
10a8622e7c
1021
src/main/kotlin/dev/mduchene/sql/Sql.kt
Normal file
1021
src/main/kotlin/dev/mduchene/sql/Sql.kt
Normal file
File diff suppressed because it is too large
Load Diff
135
src/test/kotlin/dev/mduchene/sql/SqlBuilderTest.kt
Normal file
135
src/test/kotlin/dev/mduchene/sql/SqlBuilderTest.kt
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package dev.mduchene.sql
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class SqlBuilderTest {
|
||||||
|
@Test
|
||||||
|
fun `builds select with joins count grouping and paging`() {
|
||||||
|
val statement = Sql
|
||||||
|
.select(
|
||||||
|
Sql.col("u.id"),
|
||||||
|
Sql.col("u.email"),
|
||||||
|
Sql.count(Sql.col("p.id")).asAlias("post_count"),
|
||||||
|
)
|
||||||
|
.from("users", "u")
|
||||||
|
.leftJoin("posts", "p", Sql.col("p.user_id").eq(Sql.col("u.id")))
|
||||||
|
.where(Sql.col("u.status").eq("active"))
|
||||||
|
.andWhere(Sql.col("u.deleted_at").isNull())
|
||||||
|
.groupBy("u.id", "u.email")
|
||||||
|
.having(Sql.count(Sql.col("p.id")).gt(0))
|
||||||
|
.orderBy(Sql.col("u.created_at").desc(NullsOrder.LAST))
|
||||||
|
.limit(25)
|
||||||
|
.offset(50)
|
||||||
|
.toSql()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"""SELECT "u"."id", "u"."email", COUNT("p"."id") AS "post_count" FROM "users" AS "u" LEFT JOIN "posts" AS "p" ON ("p"."user_id" = "u"."id") WHERE (("u"."status" = $1) AND ("u"."deleted_at" IS NULL)) GROUP BY "u"."id", "u"."email" HAVING (COUNT("p"."id") > $2) ORDER BY "u"."created_at" DESC NULLS LAST LIMIT 25 OFFSET 50""",
|
||||||
|
statement.sql,
|
||||||
|
)
|
||||||
|
assertEquals(listOf("active", 0), statement.parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `builds insert with multiple rows and returning`() {
|
||||||
|
val statement = Sql
|
||||||
|
.insertInto("users")
|
||||||
|
.columns("email", "display_name")
|
||||||
|
.values("sakura@example.com", "Sakura")
|
||||||
|
.values("naruto@example.com", "Naruto")
|
||||||
|
.returning("id")
|
||||||
|
.toSql()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"""INSERT INTO "users" ("email", "display_name") VALUES ($1, $2), ($3, $4) RETURNING "id"""",
|
||||||
|
statement.sql,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf("sakura@example.com", "Sakura", "naruto@example.com", "Naruto"),
|
||||||
|
statement.parameters,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `builds update with raw expression and where clause`() {
|
||||||
|
val statement = Sql
|
||||||
|
.update("users")
|
||||||
|
.set("display_name", "Hinata")
|
||||||
|
.set("updated_at", Sql.raw("CURRENT_TIMESTAMP"))
|
||||||
|
.where(Sql.col("id").eq(42))
|
||||||
|
.returning("id", "updated_at")
|
||||||
|
.toSql()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"""UPDATE "users" SET "display_name" = $1, "updated_at" = CURRENT_TIMESTAMP WHERE ("id" = $2) RETURNING "id", "updated_at"""",
|
||||||
|
statement.sql,
|
||||||
|
)
|
||||||
|
assertEquals(listOf("Hinata", 42), statement.parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `builds delete with cte and subquery`() {
|
||||||
|
val inactiveUsers = Sql
|
||||||
|
.select("id")
|
||||||
|
.from("users")
|
||||||
|
.where(Sql.col("last_seen_at").lt(Sql.raw("CURRENT_DATE - INTERVAL '1 year'")))
|
||||||
|
|
||||||
|
val statement = Sql
|
||||||
|
.deleteFrom("sessions")
|
||||||
|
.with("inactive_users", inactiveUsers)
|
||||||
|
.where(
|
||||||
|
Sql.col("user_id").inSubquery(
|
||||||
|
Sql.select("id").from("inactive_users"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toSql()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"""WITH "inactive_users" AS (SELECT "id" FROM "users" WHERE ("last_seen_at" < CURRENT_DATE - INTERVAL '1 year')) DELETE FROM "sessions" WHERE ("user_id" IN (SELECT "id" FROM "inactive_users"))""",
|
||||||
|
statement.sql,
|
||||||
|
)
|
||||||
|
assertEquals(emptyList(), statement.parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can render ansi placeholders from the same builder`() {
|
||||||
|
val builder = Sql
|
||||||
|
.select("id")
|
||||||
|
.from("users")
|
||||||
|
.where(Sql.col("email").eq("sakura@example.com"))
|
||||||
|
.andWhere(Sql.col("status").eq("active"))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"""SELECT "id" FROM "users" WHERE (("email" = $1) AND ("status" = $2))""",
|
||||||
|
builder.toSql(PostgresDialect).sql,
|
||||||
|
)
|
||||||
|
|
||||||
|
val ansi = builder.toSql(AnsiDialect)
|
||||||
|
assertEquals(
|
||||||
|
"""SELECT "id" FROM "users" WHERE (("email" = ?) AND ("status" = ?))""",
|
||||||
|
ansi.sql,
|
||||||
|
)
|
||||||
|
assertEquals(listOf("sakura@example.com", "active"), ansi.parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `rejects returning for dialects that do not support it`() {
|
||||||
|
val builder = Sql
|
||||||
|
.insertInto("users")
|
||||||
|
.columns("email")
|
||||||
|
.values("sakura@example.com")
|
||||||
|
.returning("id")
|
||||||
|
|
||||||
|
assertFailsWith<IllegalArgumentException> {
|
||||||
|
builder.toSql(AnsiDialect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `rejects unsafe identifiers unless raw sql is explicit`() {
|
||||||
|
assertFailsWith<IllegalArgumentException> {
|
||||||
|
Sql.select("id; DROP TABLE users").from("users").toSql()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user