Skip to content

Commit 05cc534

Browse files
committed
draft of readme
1 parent b2bdf22 commit 05cc534

File tree

3 files changed

+96
-3
lines changed

3 files changed

+96
-3
lines changed

README.md

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,94 @@
1-
# tyql
1+
# Tyql
2+
3+
A Scala3 SQL query generator
4+
- based on [named tuples](https://scala-lang.org/api/3.x/docs/docs/reference/experimental/named-tuples.html) and not macros nor higher-kinded types,
5+
- checks query correctness at compile-time against selected backend,
6+
- generates SQL at runtime,
7+
- guides the user with nice error messages,
8+
- is usable (feature coverage, speed).
9+
10+
11+
### How do i use it?
12+
First, import a dialect (`postgres`, `mysql`, `mariadb`, `duckdb`, `sqlite`, `h2`) like this
13+
```scala
14+
import tyql.Dialect.postgres.given
15+
```
16+
Then define your tables as case classes or named tuples
17+
```scala
18+
case class Person(id: Long, name: String)
19+
val persons = Table[Person]()
20+
val orders = Table[(orderid: Long, person: Long, notes: Option[String])]("order")
21+
```
22+
Compose queries like this
23+
```scala
24+
val q = for (p <- persons ; if p.id > 10L ; o <- orders.joinOn(o => p.id == o.person))
25+
yield (name = p.name, orderNumber = o.orderid, specialNote = notes.getOrElse("NONE"))
26+
```
27+
You will sometimes have to wrap literals in `lit`, especially booleans: `lit(true)`.
28+
29+
You can then examine the SQL
30+
```scala
31+
println(q.toSQLString())
32+
```
33+
or run it against the database and receive Scala-native data structures back
34+
```
35+
val conn = java.sql.DriverManager.getConnection("jdbc:postgresql://localhost:5433/testdb", "testuser", "testpass")
36+
val db = tyql.DB(conn)
37+
db.run(q)
38+
/* List(
39+
* (name = "Adam", orderNumber = 12L, specialNote = "NONE"),
40+
* (name = "Eva", orderNumber = 3L, specialNote = "2nd floor")
41+
* )
42+
*/
43+
```
44+
45+
#### How do I configure it?
46+
```scala
47+
given tyql.Config = new tyql.Config(tyql.CaseConvention.Underscores, tyql.ParameterStyle.EscapedInline) {}
48+
```
49+
For case convention (how will the Scala identifiers be translated into SQL) you can pick
50+
```scala
51+
enum CaseConvention:
52+
case Exact
53+
case Underscores // three_letter_word
54+
case PascalCase // ThreeLetterWord
55+
case CamelCase // threeLetterWord
56+
case CapitalUnderscores // THREE_LETTER_WORD
57+
case Joined // threeletterword
58+
case JoinedCapital // THREELETTERWORD
59+
```
60+
For parameter style you can pick `EscapedInline` (literals will be pasted inside the SQL) or `DriverParametrized` (the SQL will be `?`-parametrized and the JDBC will be provided with the values).
61+
62+
### How fast is it in practice?
63+
We benchmarked against Quill (a prominent macro-based query generator) on a local MySQL instance.
64+
Quill computes and renders the query entirely at compile-time, we therefore compare to Tyql with caching enabled.
65+
We therefore are comparing the performance of the driver and fetching from cache.
66+
In our tests
67+
* time it takes to fetch 100k rows is almost identical (87-89µs),
68+
* time it takes for a round trip of a small query is almost identical (95-97µs).
69+
70+
Tyql generates queries usually in between 7µs and 25µs (depending on query complexity).
71+
72+
### What about caching queries with changing inputs?
73+
You can use `Var(thunk: => T)`. If you're using `?`-parametrization, the value will be fetched only inside `DB.run`, that is, when the parameters need to be passed to JDBC.
74+
```scala
75+
val q = persons.filter(p => p.age >= Var(getAge()))
76+
db.run(q)
77+
```
78+
The query will be computed and rendered only once, no matter how many times it is run with different parameters.
79+
80+
### What about transactions and other driver-specific functionality?
81+
We do not replace JDBC, only wrap its `Connection` with oue `DB`.
82+
```scala
83+
try {
84+
conn.setAutoCommit(false)
85+
val got = db.run(q1)
86+
// ...
87+
conn.commit()
88+
} catch { case e: Exception =>
89+
conn.rollback()
90+
} finally {
91+
conn.setAutoCommit(true)
92+
conn.close()
93+
}
94+
```

src/main/scala/tyql/ir/QueryIRNode.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ trait QueryIRNode:
1414
private var cached: java.util.concurrent.ConcurrentHashMap[(Dialect, Config), SQLRenderingContext] =
1515
null // do not allocate memory if unused
1616

17-
final def toSQLString(using d: Dialect)(using cnf: Config)(): String =
17+
final def toSQLString()(using d: Dialect)(using cnf: Config): String =
1818
val (sql, _) = toSQLQuery()
1919
sql
2020

src/main/scala/tyql/query/DatabaseAST.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package tyql
44
* @tparam Result
55
*/
66
trait DatabaseAST[Result](using val qTag: ResultTag[Result]):
7-
def toSQLString(using d: Dialect)(using cnf: Config): String = toQueryIR.toSQLString()
7+
def toSQLString()(using d: Dialect)(using cnf: Config): String = toQueryIR.toSQLString()
88

99
private var cached: java.util.concurrent.ConcurrentHashMap[Dialect, QueryIRNode] = null
1010

0 commit comments

Comments
 (0)