|
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 | +``` |
0 commit comments