Skip to content

Commit 0de7f5c

Browse files
committed
wip: tutorial.md
1 parent 487ce9e commit 0de7f5c

File tree

8 files changed

+1968
-35
lines changed

8 files changed

+1968
-35
lines changed

docs/generateDocs.mill

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ def generateTutorial(sourcePath: os.Path, destPath: os.Path) = {
55
var isDocs = Option.empty[Int]
66
var isCode = false
77
val outputLines = collection.mutable.Buffer.empty[String]
8+
val snippets = collection.mutable.HashMap.empty[String, scala.collection.BufferedIterator[String]]
89
outputLines.append(generatedCodeHeader)
910
for (line <- os.read.lines(sourcePath)) {
1011
val isDocsIndex = line.indexOf("// +DOCS")
@@ -25,6 +26,24 @@ def generateTutorial(sourcePath: os.Path, destPath: os.Path) = {
2526
(suffix, isCode) match{
2627
case ("", _) => outputLines.append("")
2728

29+
case (s"// +INCLUDE SNIPPET [$key] $rest", _) =>
30+
// reuse the iterator each time,
31+
// basically assume snippets are requested in order.
32+
val sublines: scala.collection.BufferedIterator[String] = snippets.getOrElseUpdate(rest, os.read.lines(mill.api.WorkspaceRoot.workspaceRoot / os.SubPath(rest)).iterator.buffered)
33+
val start = s"// +SNIPPET [$key]"
34+
val end = s"// -SNIPPET [$key]"
35+
while (sublines.hasNext && !sublines.head.contains(start)) {
36+
sublines.next() // drop lines until we find the start
37+
}
38+
val indent = sublines.headOption.map(_.indexOf(start)).getOrElse(-1)
39+
if (indent != -1) {
40+
sublines.next() // skip the start line
41+
while (sublines.hasNext && !sublines.head.contains(end)) {
42+
outputLines.append(sublines.next().drop(indent))
43+
}
44+
} else {
45+
outputLines.append("")
46+
}
2847
case (s"// +INCLUDE $rest", _) =>
2948
os.read.lines(mill.api.WorkspaceRoot.workspaceRoot / os.SubPath(rest)).foreach(outputLines.append)
3049

@@ -124,6 +143,10 @@ def generateReference(dest: os.Path, scalafmtCallback: (Seq[os.Path], os.Path) =
124143
|databases, due to differences in how each database parses SQL. These differences
125144
|are typically minor, and as long as you use the right `Dialect` for your database
126145
|ScalaSql should do the right thing for you.
146+
|
147+
|>**A note for users of `SimpleTable`**: The examples in this document assume usage of
148+
|>`Table`, with a higher kinded type parameter on a case class. If you are using
149+
|>`SimpleTable`, then the same code snippets should work by dropping `[Sc]`.
127150
|""".stripMargin
128151
)
129152
val recordsWithoutDuplicateSuites = records

docs/reference.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ databases, due to differences in how each database parses SQL. These differences
1616
are typically minor, and as long as you use the right `Dialect` for your database
1717
ScalaSql should do the right thing for you.
1818

19+
>**A note for users of `SimpleTable`**: The examples in this document assume usage of
20+
>`Table`, with a higher kinded type parameter on a case class. If you are using
21+
>`SimpleTable`, then the same code snippets should work by dropping `[Sc]`.
22+
1923
## DbApi
2024
Basic usage of `db.*` operations such as `db.run`
2125
### DbApi.renderSql

docs/tutorial.md

Lines changed: 147 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[//]: # (GENERATED SOURCES, DO NOT EDIT DIRECTLY)
22

33

4-
This tutorials is a tour of how to use ScalaSql, from the most basic concepts
4+
This tutorial is a tour of how to use ScalaSql, from the most basic concepts
55
to writing some realistic queries. If you are browsing this on Github, you
66
can open the `Outline` pane on the right to browse the section headers to
77
see what we will cover and find anything specific of interest to you.
@@ -71,8 +71,14 @@ supported databases, to see what kind of set up is necessary for each one
7171
### Modeling Your Schema
7272

7373
Next, you need to define your data model classes. In ScalaSql, your data model
74-
is defined using `case class`es with each field wrapped in the wrapper type
75-
parameter `T[_]`. This allows us to re-use the same case class to represent
74+
is defined using `case class`es with each field representing a column in an database table.
75+
76+
There are two flavors to consider: `Table` (available for Scala 2.13+), and `SimpleTable` (Scala 3.7+).
77+
78+
**Using `Table`**
79+
80+
Declare your case class with a type parameter `T[_]`, which is used to wrap the type of each
81+
field. This allows us to re-use the same case class to represent
7682
both database values (when `T` is `scalasql.Expr`) as well as Scala values
7783
(when `T` is `scalasql.Sc`).
7884

@@ -120,6 +126,59 @@ case class CountryLanguage[T[_]](
120126
object CountryLanguage extends Table[CountryLanguage]()
121127
```
122128

129+
**Using `SimpleTable`**
130+
> Note: only available in the `com.lihaoyi::scalasql-namedtuples` library, which supports Scala 3.7.0+
131+
132+
Declare your case class as usual. Inside of queries, the class will be represented by a `Record` with the same fields, but wrapped in `scalasql.Expr`.
133+
134+
Here, we define three classes `Country` `City` and `CountryLanguage`, modeling
135+
the database tables we saw above.
136+
137+
Also included is the necessary import statement to include the `SimpleTable` definition.
138+
139+
```scala
140+
import scalasql.simple.{*, given}
141+
142+
case class Country(
143+
code: String,
144+
name: String,
145+
continent: String,
146+
region: String,
147+
surfaceArea: Int,
148+
indepYear: Option[Int],
149+
population: Long,
150+
lifeExpectancy: Option[Double],
151+
gnp: Option[scala.math.BigDecimal],
152+
gnpOld: Option[scala.math.BigDecimal],
153+
localName: String,
154+
governmentForm: String,
155+
headOfState: Option[String],
156+
capital: Option[Int],
157+
code2: String
158+
)
159+
160+
object Country extends SimpleTable[Country]
161+
162+
case class City(
163+
id: Int,
164+
name: String,
165+
countryCode: String,
166+
district: String,
167+
population: Long
168+
)
169+
170+
object City extends SimpleTable[City]
171+
172+
case class CountryLanguage(
173+
countryCode: String,
174+
language: String,
175+
isOfficial: Boolean,
176+
percentage: Double
177+
)
178+
179+
object CountryLanguage extends SimpleTable[CountryLanguage]
180+
```
181+
123182
### Creating Your Database Client
124183
Lastly, we need to initialize our `scalasql.DbClient`. This requires
125184
passing in a `java.sql.Connection`, a `scalasql.Config` object, and the SQL dialect
@@ -201,11 +260,16 @@ db.run(query).take(3) ==> Seq(
201260
)
202261

203262
```
204-
Notice that `db.run` returns instances of type `City[Sc]`. `Sc` is `scalasql.Sc`,
263+
Notice that `db.run` returns instances of type `City[Sc]` (or `City` if using `SimpleTable`).
264+
265+
`Sc` is `scalasql.Sc`,
205266
short for the "Scala" type, representing a `City` object containing normal Scala
206267
values. The `[Sc]` type parameter must be provided explicitly whenever creating,
207268
type-annotating, or otherwise working with these `City` values.
208269

270+
> In this tutorial, unless otherwise specified, we will assume usage of the `Table` encoding.
271+
> If you are using `SimpleTable`, the same code will work, but drop `[Sc]` type arguments.
272+
209273
In this example, we do `.take(3)` after running the query to show only the first
210274
3 table entries for brevity, but by that point the `City.select` query had already
211275
fetched the entire database table into memory. This can be a problem with non-trivial
@@ -235,8 +299,12 @@ db.run(query) ==> City[Sc](3208, "Singapore", "SGP", district = "", population =
235299
```
236300
Note that we use `===` rather than `==` for the equality comparison. The
237301
function literal passed to `.filter` is given a `City[Expr]` as its parameter,
238-
representing a `City` that is part of the database query, in contrast to the
239-
`City[Sc]`s that `db.run` returns , and so `_.name` is of type `Expr[String]`
302+
(or `Record[City, Expr]` with the `SimpleTable` encoding) representing a `City`
303+
that is part of the database query, in contrast to the
304+
`City[Sc]`s that `db.run` returns.
305+
306+
Within a query therefore `_.name` is a field selection on the function parameter,
307+
resulting in `Expr[String]`,
240308
rather than just `String` or `Sc[String]`. You can use your IDE's
241309
auto-complete to see what operations are available on `Expr[String]`: typically
242310
they will represent SQL string functions rather than Scala string functions and
@@ -309,7 +377,8 @@ db.run(query).take(2) ==> Seq(
309377
)
310378

311379
```
312-
Again, all the operations within the query work on `Expr`s: `c` is a `City[Expr]`,
380+
Again, all the operations within the query work on `Expr`s:
381+
`c` is a `City[Expr]` (or `Record[City, Expr]` for `SimpleTable`),
313382
`c.population` is an `Expr[Int]`, `c.countryCode` is an `Expr[String]`, and
314383
`===` and `>` and `&&` on `Expr`s all return `Expr[Boolean]`s that represent
315384
a SQL expression that can be sent to the Database as part of your query.
@@ -427,8 +496,61 @@ db.run(query) ==>
427496
"SINGAPORE",
428497
4 // population in millions
429498
)
499+
430500
```
431501

502+
**Mapping with named tuples**
503+
> Note: only available in the `com.lihaoyi::scalasql-namedtuples` library, which supports Scala 3.7.0+
504+
505+
You can also use named tuples to map the results of a query.
506+
```scala
507+
import scalasql.namedtuples.NamedTupleQueryable.given
508+
509+
val query = Country.select.map(c =>
510+
(name = c.name, continent = c.continent)
511+
)
512+
513+
db.run(query).take(5) ==> Seq(
514+
(name = "Afghanistan", continent = "Asia"),
515+
(name = "Netherlands", continent = "Europe"),
516+
(name = "Netherlands Antilles", continent = "North America"),
517+
(name = "Albania", continent = "Europe"),
518+
(name = "Algeria", continent = "Africa")
519+
)
520+
```
521+
522+
**Updating `Record` fields**
523+
> Note: only relevant when using the `SimpleTable` encoding.
524+
525+
When using `SimpleTable`, within the `.map` query `c` is of type
526+
`Record[Country, Expr]`. Records are converted back to their associated case class
527+
(e.g. `Country`) with `db.run`.
528+
529+
If you want to apply updates to any of the fields before returning, the `Record` class
530+
provides an `updates` method. This lets you provide an arbitrary sequence of updates to
531+
apply in-order to the record. You can either provide a value with `:=`,
532+
or provide a function that transforms the old value. For example:
533+
534+
```scala
535+
val query = Country.select.map(c =>
536+
c.updates(
537+
_.population := 0L,
538+
_.name(old => Expr("🌐 ") + old)
539+
)
540+
)
541+
542+
db.run(query).take(5).match {
543+
case Seq(
544+
Country(name = "🌐 Afghanistan", population = 0L),
545+
Country(name = "🌐 Netherlands", population = 0L),
546+
Country(name = "🌐 Netherlands Antilles", population = 0L),
547+
Country(name = "🌐 Albania", population = 0L),
548+
Country(name = "🌐 Algeria", population = 0L)
549+
) =>
550+
} ==> ()
551+
```
552+
553+
432554
### Aggregates
433555

434556
You can perform simple aggregates like `.sum` as below, where we
@@ -1361,6 +1483,23 @@ db.run(
13611483
13621484
db.run(City.select.filter(_.id === 313373).single) ==>
13631485
City[Sc](CityId(313373), "test", "XYZ", "district", 1000000)
1486+
1487+
1488+
```
1489+
You can also use `TypeMapper#bimap` for the common case where you want the
1490+
new `TypeMapper` to behave the same as an existing `TypeMapper`, just with
1491+
conversion functions to convert back and forth between the old type and new type:
1492+
1493+
```scala
1494+
case class CityId2(value: Int)
1495+
1496+
object CityId2 {
1497+
implicit def tm: TypeMapper[CityId2] = TypeMapper[Int].bimap[CityId2](
1498+
city => city.value,
1499+
int => CityId2(int)
1500+
)
1501+
}
1502+
13641503
```
13651504
13661505
```scala
@@ -1387,8 +1526,7 @@ db.run(
13871526
13881527
db.run(City2.select.filter(_.id === 31337).single) ==>
13891528
City2[Sc](CityId2(31337), "test", "XYZ", "district", 1000000)
1390-
1391-
st("customTableColumnNames") {
1529+
```
13921530
13931531
## Customizing Table and Column Names
13941532

readme.md

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ dbClient.transaction{ db =>
5656
}
5757
```
5858

59-
ScalaSql supports database connections to PostgreSQL, MySQL, Sqlite, and H2 databases.
59+
ScalaSql supports database connections to PostgreSQL, MySQL, Sqlite, and H2 databases.
6060
Support for additional databases can be easily added.
6161

6262
ScalaSql is a relatively new library, so please try it out, but be aware you may hit bugs
6363
or missing features! Please open [Discussions](https://github.com/com-lihaoyi/scalasql/discussions)
64-
for any questions, file [Issues](https://github.com/com-lihaoyi/scalasql/issues) for any
64+
for any questions, file [Issues](https://github.com/com-lihaoyi/scalasql/issues) for any
6565
bugs you hit, or send [Pull Requests](https://github.com/com-lihaoyi/scalasql/pulls) if
6666
you are able to investigate and fix them!
6767

@@ -76,10 +76,56 @@ ivy"com.lihaoyi::scalasql:0.1.19"
7676

7777
ScalaSql supports Scala 2.13.x and >=3.6.2
7878

79+
### SimpleTable variant based on named tuples
80+
81+
For Scala versions >=3.7.0 supporting named tuples, an alternative way to define tables is supported.
82+
83+
Add the following to your `build.sc` file as follows:
84+
85+
<!-- TODO: scalasql-simple? -->
86+
```scala
87+
ivy"com.lihaoyi::scalasql-namedtuples:0.1.19"
88+
```
89+
90+
And taking the example above, the only thing that needs to change is the following:
91+
```diff
92+
-import scalasql._, SqliteDialect._
93+
+import scalasql.simple._, SqliteDialect._
94+
95+
// Define your table model classes
96+
-case class City[T[_]](
97+
- id: T[Int],
98+
- name: T[String],
99+
- countryCode: T[String],
100+
- district: T[String],
101+
- population: T[Long]
102+
-)
103+
-object City extends Table[City]
104+
+case class City(
105+
+ id: Int,
106+
+ name: String,
107+
+ countryCode: String,
108+
+ district: String,
109+
+ population: Long
110+
+)
111+
+object City extends SimpleTable[City]
112+
```
113+
114+
And you now have the option to return named tuples from queries:
115+
```diff
116+
val fewLargestCities = db.run(
117+
City.select
118+
.sortBy(_.population).desc
119+
.drop(5).take(3)
120+
- .map(c => (c.name, c.population))
121+
+ .map(c => (name = c.name, pop = c.population))
122+
)
123+
```
124+
79125
## Documentation
80126

81127
* ScalaSql Quickstart Examples: self-contained files showing how to set up ScalaSql to
82-
connect your Scala code to a variety of supported databases and perform simple DDL and
128+
connect your Scala code to a variety of supported databases and perform simple DDL and
83129
`SELECT`/`INSERT`/`UPDATE`/`DELETE` operations:
84130
* [Postgres](scalasql/test/src/example/PostgresExample.scala)
85131
* [MySql](scalasql/test/src/example/MySqlExample.scala)
@@ -104,7 +150,7 @@ ScalaSql supports Scala 2.13.x and >=3.6.2
104150
to execute queries
105151
* [Transaction](docs/reference.md#transaction), covering usage of transactions
106152
and savepoints
107-
* [Select](docs/reference.md#select), [Insert](docs/reference.md#insert),
153+
* [Select](docs/reference.md#select), [Insert](docs/reference.md#insert),
108154
[Update](docs/reference.md#update), [Delete](docs/reference.md#delete):
109155
covering operations on the primary queries you are likely to use
110156
* [Join](docs/reference.md#join), covering different kinds of joins
@@ -113,14 +159,14 @@ ScalaSql supports Scala 2.13.x and >=3.6.2
113159
* [Expression Operations](docs/reference.md#exprops), covering the different
114160
types of `Expr[T]` values and the different operations you can do on each one
115161
* [Option Operations](docs/reference.md#optional), operations on `Expr[Option[T]`
116-
* [Window Functions](docs/reference.md#windowfunctions),
162+
* [Window Functions](docs/reference.md#windowfunctions),
117163
[With-Clauses/Common-Table-Expressions](docs/reference.md#withcte)
118164
* [Postgres](docs/reference.md#postgresdialect), [MySql](docs/reference.md#mysqldialect),
119165
[Sqlite](docs/reference.md#sqlitedialect), [H2](docs/reference.md#h2dialect) Dialects:
120166
operations that are specific to each database that may not be generally applicable
121167

122168
* [ScalaSql Design](docs/design.md): discusses the design of the ScalaSql library, why it
123-
is built the way it is, what tradeoffs it makes, and how it compares to other
169+
is built the way it is, what tradeoffs it makes, and how it compares to other
124170
common Scala database query libraries. Ideal for contributors who want to understand
125171
the structure of the ScalaSql codebase, or for advanced users who may need to
126172
understand enough to extend ScalaSql with custom functionality.

0 commit comments

Comments
 (0)