-
Notifications
You must be signed in to change notification settings - Fork 0
/
readme.scala.md
executable file
·238 lines (170 loc) · 6.83 KB
/
readme.scala.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#!/usr/bin/env -S scala-cli --power shebang
# Kelner
**Table of Contents**
- [About](#about)
- [Implementation](#implementation)
- [Usage](#usage)
- [Meta](#meta)
## About
Kelner is a helper for generating DML query params from domain objects.
## Implementation
<details>
<summary>Source code</summary>
```scala mdoc
type TupleConsistsOf[A <: Tuple, B] = A match
case B *: tail => TupleConsistsOf[tail, B]
case EmptyTuple => DummyImplicit
case _ => Nothing
type Of[A] = [X <: Tuple] =>> TupleConsistsOf[X, A]
type Column[VALUE] = (String & Singleton, VALUE)
type ColumnNames[T <: NonEmptyTuple] <: Tuple = T match
case (name, ?) *: tail => name *: ColumnNames[tail]
case _ => EmptyTuple
trait Mapping[-DOMAIN, ROW <: (String & Singleton, NonEmptyTuple)]:
def encode(a: DOMAIN): Tuple.Elem[ROW, 1]
trait Table[NAME <: String & Singleton : ValueOf, COLUMNS <: NonEmptyTuple : Of[Column[?]]]:
type Columns = COLUMNS
type Row = (NAME, COLUMNS)
given CanEqual[Tuple.Union[Columns], Tuple.Union[Columns]] = CanEqual.derived
def primaryKey: List[Tuple.Union[ColumnNames[Columns]]]
def name: NAME = valueOf[NAME]
def params[A](data: A)(using e: Mapping[A, Row]): List[Tuple.Union[Columns]] =
e.encode(data).toList
def diff[A](
before: A,
after: A,
includePrimaryKey: Boolean = false,
)(
using Mapping[A, Row],
): List[Tuple.Union[Columns]] =
val columnsBefore = params(before)
val columnsAfter = params(after)
columnsBefore.zip(columnsAfter).collect:
case ((name, _), y) if includePrimaryKey && primaryKey.contains(name) => y
case (x, y) if x != y => y
```
</details>
## Usage
Define a table and a way of transforming your domain objects into rows:
```scala mdoc
object Users extends Table["users", (("id", Int), ("name", String))]:
override def primaryKey = List("id")
case class User(id: Int, name: String)
given Mapping[User, Users.Row] = (user: User) => (
"id" -> user.id,
"name" -> user.name,
)
```
Use `Table`'s `params` and `diff` methods to prepare parameters for your queries:
```scala mdoc
val user = User(id = 0, name = "Adam Dąbrowski")
val insertParams = Users.params(user)
val updatedUser = user.copy(name = "A. D.")
val updateParams = Users.diff(user, updatedUser)
val updateParamsWithPrimaryKey = Users.diff(user, updatedUser, includePrimaryKey = true)
```
The compiler will check if `primaryKey` references defined columns.
```scala mdoc:fail ignore
object Reactions extends Table["reactions", (("post_id", Int), ("user_id", Int))]:
override def primaryKey = List("id", "user_id")
```
It will also ensure that you provide a valid transformation from domain objects to rows.
```scala mdoc:fail ignore
object Reactions extends Table["reactions", (("post_id", Int), ("user_id", Int))]:
override def primaryKey = List("post_id", "user_id")
case class Reaction(id: Int, userId: Int)
given Mapping[Reaction, Reactions.Row] = (reaction: Reaction) => (
"id" -> reaction.id,
"user_id" -> reaction.userId,
)
```
If you have two tables with the same columns, you can't accidentally swap them.
```scala mdoc:fail ignore
object Items extends Table["items", (("id", Int), ("name", String))]:
override def primaryKey = List("id")
case class Item(id: Int, name: String)
given Mapping[Item, Items.Row] = (item: Item) => (
"id" -> item.id,
"name" -> item.name,
)
Items.params(user)
```
# Use cases
## Generate partial updates
```scala mdoc
// Implementation making use of com.datastax.driver.core.BoundStatement is left as an exercise to the reader.
type CassandraQuery = String
trait Cassandra:
this: Table[?, ?] =>
def update[A](before: A, after: A)(using Mapping[A, this.Row]): CassandraQuery =
val params = this.diff(before, after, includePrimaryKey = true)
val columns = params.map:
case (name, _value) => name
val values = params.map:
case (_name, value: String) => s"'$value'"
case (_name, value: Int) => value
s"INSERT (${columns.mkString(", ")}) INTO ${this.name} VALUES (${values.mkString(", ")})"
```
```scala mdoc
object Posts extends Table[
"posts",
(
("id", Int),
("content", String),
("created_at", String),
)
] with Cassandra:
override def primaryKey = List("id")
case class Post(id: Int, content: String, createdAt: String)
given Mapping[Post, Posts.Row] = (post: Post) => (
"id" -> post.id,
"content" -> post.content,
"created_at" -> post.createdAt,
)
val post = Post(id = 0, content = "Lorem ipsum...", createdAt = "timestamp")
val updatedPost = post.copy(content = "Quidquid latine dictum sit, altum videtur.")
val cassandraUpdateQuery = Posts.update(post, updatedPost)
```
## Meta
This readme was generated from [readme.scala.md](readme.scala.md?plain=1).
First, make it executable (yes!) with `chmod +x readme.scala.md`, then just run it: `./readme.scala.md`. 😀
```scala mdoc:invisible raw
//> using jvm 22
//> using scala 3.5.0-RC7
//> using mainClass Main
//> using options -deprecation -feature -language:strictEquality
//> using options -Xmax-inlines:64 -Xkind-projector:underscores
//> using options -Yexplicit-nulls -Ysafe-init-global
//> using options -Wsafe-init -Wnonunit-statement -Wshadow:all
//> using dep org.scalameta::mdoc:2.5.4
```
```scala mdoc:invisible raw
import scala.util.{Try, Success, Failure, Using}
import scala.io.Source
import java.io.PrintWriter
object Main:
def main(args: Array[String]): Unit =
val classpath = System.getProperty("java.class.path")
val inputFile = "readme.scala.md"
val outputFile = inputFile.replace(".scala.md", ".md")
val mdocArgs = List(
"--classpath", classpath,
"--in", inputFile,
"--out", outputFile,
)
val settings = mdoc.MainSettings().withArgs(args.toList ++ mdocArgs)
val program =
for
_ <- mdoc.Main.process(settings) match
case 0 => Success(())
case exitCode => Failure(new RuntimeException(s"mdoc failed with exit code $exitCode"))
_ <- trimShebang(outputFile)
yield ()
val exitCode = program.fold(_failure => 1, _success => 0)
sys.exit(exitCode)
def trimShebang(filePath: String): Try[Unit] =
Using.Manager: use =>
val lines = use(Source.fromFile(filePath)).getLines().toList
val writer = use(new PrintWriter(filePath))
lines.dropWhile(_.startsWith("#!")).foreach(writer.println)
```