added sql builder framework

This commit is contained in:
Maxime Duchene-Savard 2026-06-23 23:44:01 -04:00
parent b90e171977
commit 10a8622e7c
2 changed files with 1156 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View 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()
}
}
}