@zio.dev/zio-blocks 0.0.33 → 0.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +16 -17
  2. package/guides/getting-started-with-mux.md +1507 -0
  3. package/guides/query-dsl-extending.md +161 -102
  4. package/guides/query-dsl-fluent-builder.md +217 -157
  5. package/guides/query-dsl-reified-optics.md +12 -10
  6. package/guides/query-dsl-sql.md +246 -165
  7. package/guides/telemetry-guide.md +1069 -0
  8. package/guides/zio-schema-migration.md +29 -22
  9. package/index.md +292 -50
  10. package/package.json +1 -1
  11. package/plans/config-follow-up-prs.md +188 -0
  12. package/plans/config-pr-assessment-roadmap.md +310 -0
  13. package/reference/MuxDataFlow.jsx +250 -0
  14. package/reference/async.md +651 -0
  15. package/reference/chunk.md +3533 -308
  16. package/reference/codegen/case-class.md +436 -0
  17. package/reference/codegen/emitter-config.md +383 -0
  18. package/reference/codegen/examples.md +664 -0
  19. package/reference/codegen/field.md +316 -0
  20. package/reference/codegen/index.md +317 -0
  21. package/reference/codegen/scala-emitter.md +392 -0
  22. package/reference/codegen/scala-file.md +276 -0
  23. package/reference/codegen/sealed-trait.md +408 -0
  24. package/reference/codegen/type-definition.md +340 -0
  25. package/reference/codegen/type-ref.md +201 -0
  26. package/reference/combinators.md +347 -117
  27. package/reference/config.md +158 -0
  28. package/reference/context.md +4 -4
  29. package/reference/datastar.md +346 -0
  30. package/reference/docs.md +1461 -345
  31. package/reference/endpoint/auth-type.md +146 -0
  32. package/reference/endpoint/endpoint.md +297 -0
  33. package/reference/endpoint/http-codec.md +249 -0
  34. package/reference/endpoint/index.md +825 -0
  35. package/reference/endpoint/path-codec.md +237 -0
  36. package/reference/endpoint/route-pattern.md +196 -0
  37. package/reference/endpoint/route-tree.md +111 -0
  38. package/reference/endpoint/segment-codec.md +212 -0
  39. package/reference/html.md +1120 -0
  40. package/reference/htmx/attribute-values.md +359 -0
  41. package/reference/htmx/hx-encoding.md +111 -0
  42. package/reference/htmx/hx-params.md +204 -0
  43. package/reference/htmx/hx-swap.md +276 -0
  44. package/reference/htmx/hx-sync.md +251 -0
  45. package/reference/htmx/hx-target.md +314 -0
  46. package/reference/htmx/hx-trigger.md +457 -0
  47. package/reference/htmx/hx-url-update.md +239 -0
  48. package/reference/htmx/index.md +855 -0
  49. package/reference/http-model/index.md +47 -0
  50. package/reference/http-model/model.md +1481 -0
  51. package/reference/http-model/schema.md +747 -0
  52. package/reference/maybe.md +826 -0
  53. package/reference/media-type.md +2 -2
  54. package/reference/mux.mdx +823 -0
  55. package/reference/openapi.md +1351 -0
  56. package/reference/resource-management/defer-handle.md +1 -1
  57. package/reference/resource-management/resource.md +31 -2
  58. package/reference/resource-management/scope.md +28 -12
  59. package/reference/resource-management/wire.md +3 -7
  60. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  61. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  62. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  63. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  64. package/reference/ringbuffer/advanced.mdx +109 -0
  65. package/reference/ringbuffer/index.mdx +145 -0
  66. package/reference/ringbuffer/mpmc.mdx +151 -0
  67. package/reference/ringbuffer/mpsc.mdx +132 -0
  68. package/reference/ringbuffer/spmc.mdx +108 -0
  69. package/reference/ringbuffer/spsc.mdx +344 -0
  70. package/reference/{allows.md → schema/allows.md} +4 -4
  71. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  72. package/reference/{binding.md → schema/binding.md} +2 -3
  73. package/reference/schema/built-in-codecs/avro.md +451 -0
  74. package/reference/schema/built-in-codecs/bson.md +480 -0
  75. package/reference/schema/built-in-codecs/csv.md +564 -0
  76. package/reference/schema/built-in-codecs/index.md +77 -0
  77. package/reference/schema/built-in-codecs/json/index.md +295 -0
  78. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  79. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  80. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  81. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  82. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  83. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  84. package/reference/schema/built-in-codecs/thrift.md +433 -0
  85. package/reference/schema/built-in-codecs/toon.md +1078 -0
  86. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  87. package/reference/schema/built-in-codecs/yaml.md +552 -0
  88. package/reference/{codec.md → schema/codec.md} +10 -10
  89. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
  90. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  91. package/reference/schema/format.md +92 -0
  92. package/reference/schema/index.md +50 -0
  93. package/reference/schema/migration.md +297 -0
  94. package/reference/{modifier.md → schema/modifier.md} +58 -7
  95. package/reference/{optics.md → schema/optics.md} +2 -2
  96. package/reference/{patch.md → schema/patch.md} +1 -1
  97. package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
  98. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  99. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  100. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  101. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  102. package/reference/{schema.md → schema/schema.md} +12 -0
  103. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  104. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  105. package/reference/smithy.md +533 -0
  106. package/reference/sql/db-codec-deriver.md +71 -0
  107. package/reference/sql/db-codec.md +687 -0
  108. package/reference/sql/db-con.md +271 -0
  109. package/reference/sql/db-connection.md +153 -0
  110. package/reference/sql/db-param-writer.md +77 -0
  111. package/reference/sql/db-param.md +66 -0
  112. package/reference/sql/db-result-reader.md +146 -0
  113. package/reference/sql/db-tx.md +82 -0
  114. package/reference/sql/db-value.md +41 -0
  115. package/reference/sql/ddl.md +85 -0
  116. package/reference/sql/frag.md +254 -0
  117. package/reference/sql/index.md +341 -0
  118. package/reference/sql/repo.md +600 -0
  119. package/reference/sql/sql-dialect.md +73 -0
  120. package/reference/sql/sql-logger.md +62 -0
  121. package/reference/sql/sql-name-mapper.md +70 -0
  122. package/reference/sql/table-metadata.md +134 -0
  123. package/reference/sql/table.md +448 -0
  124. package/reference/sql/transactor-zio.md +399 -0
  125. package/reference/sql/transactor.md +353 -0
  126. package/reference/sql-zio.md +112 -0
  127. package/reference/streams/concurrent-operators.md +106 -0
  128. package/reference/streams/index.md +653 -0
  129. package/reference/streams/pipeline.md +718 -0
  130. package/reference/streams/reader.md +1284 -0
  131. package/reference/streams/scala-2-compatibility.md +55 -0
  132. package/reference/streams/sink.md +1426 -0
  133. package/reference/streams/stream.md +2526 -0
  134. package/reference/streams/writer.md +1045 -0
  135. package/reference/streams/zero-boxing.md +275 -0
  136. package/reference/telemetry.md +693 -0
  137. package/reference/typeid.md +5 -19
  138. package/sidebars.js +238 -43
  139. package/reference/formats.md +0 -694
  140. package/reference/http-model.md +0 -1716
  141. package/reference/streams.md +0 -989
  142. package/ringbuffer.md +0 -249
  143. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  144. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  145. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  146. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  147. /package/reference/{registers.md → schema/registers.md} +0 -0
  148. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  149. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  150. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -0,0 +1,600 @@
1
+ ---
2
+ id: repo
3
+ title: "Repo"
4
+ description: "Reference for Repo[E, ID]: a type-safe CRUD repository in the sql module that pre-generates all SQL at construction time from a Schema-derived Table."
5
+ keywords:
6
+ - "Repo CRUD repository"
7
+ - "schema-driven SQL derivation"
8
+ - "insertBatch batch insert"
9
+ - "insertAll multi-row insert"
10
+ - "find primary key lookup"
11
+ - "DbCon implicit context"
12
+ - "Table entity mapping"
13
+ ---
14
+
15
+ `Repo[E, ID]` is a type-safe CRUD repository that provides `all`, `find`, `insert`, `update`, `delete`, and related operations for entities of type `E` identified by a primary key of type `ID`. In the `sql` module's layered architecture, `Repo` sits above `Table[E]` (which it wraps) and below the `Transactor` (which supplies the `DbCon` or `DbTx` context each operation requires). All SQL — `SELECT`, `INSERT`, `UPDATE`, and `DELETE` — is assembled from the `Table`'s column metadata at construction time; individual calls do only parameter binding.
16
+
17
+ Its primary constructor and public fields have this structure:
18
+
19
+ ```scala
20
+ class Repo[E, ID](
21
+ val table: Table[E],
22
+ val idColumn: String,
23
+ val idCodec: DbCodec[ID],
24
+ val getId: E => ID
25
+ ) {
26
+ // ... all the CRUD methods are defined here ...
27
+ }
28
+ ```
29
+
30
+ ## Motivation
31
+
32
+ Writing CRUD SQL by hand requires keeping column names, result-set positions, and prepared-statement parameter counts in sync with your Scala types — a maintenance burden that grows with every added field or renamed column. `Repo` eliminates that burden by deriving all standard CRUD statements from the same `Schema[E]` that already describes your domain type: `Repo.derived` inspects the schema's field names, types, and `@Modifier` annotations once at construction time and stores the resulting parameterized fragments for reuse.
33
+
34
+ When the standard operations — select-all, select-by-id, insert, update, delete — cover your needs, `Repo` means zero SQL maintenance. When you need custom queries (filtered selects, joins, aggregations), the `sql"..."` interpolator and `Frag` type compose freely alongside `Repo` inside the same `Transactor#connect` or `Transactor#transact` block, so you never have to choose between a repository pattern and hand-written SQL.
35
+
36
+ ## Usage
37
+
38
+ The example below shows the full lifecycle: deriving a repository from a `Schema`, setting up the table, writing and reading entities, and performing both bulk and single-entity operations — all inside one `transact` block:
39
+
40
+ ```scala
41
+ import zio.blocks.sql._
42
+ import zio.blocks.schema.Schema
43
+ import zio.blocks.maybe.Maybe
44
+
45
+ case class User(id: Int, name: String, email: String)
46
+ object User {
47
+ implicit val schema: Schema[User] = Schema.derived
48
+ }
49
+
50
+ // All SQL is generated here, once, from User's Schema.
51
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
52
+ // repo: Repo[User, Int] = zio.blocks.sql.Repo$DerivedRepo@16e9cebd
53
+ val tx = JdbcTransactor.fromUrl("jdbc:sqlite::memory:", SqlDialect.SQLite)
54
+ // tx: JdbcTransactor = zio.blocks.sql.JdbcTransactor@aae0228
55
+
56
+ tx.transact {
57
+ repo.table.createTable(summon[DbTx].dialect).update // CREATE TABLE IF NOT EXISTS users …
58
+
59
+ repo.insert(User(1, "Alice", "alice@example.com"))
60
+ repo.insert(User(2, "Bob", "bob@example.com"))
61
+
62
+ val allUsers: List[User] = repo.all // SELECT id, name, email FROM users
63
+ val one: Maybe[User] = repo.find(1) // SELECT … WHERE id = ?
64
+ val exists: Boolean = repo.exists(99) // SELECT … WHERE id = ?
65
+ val total: Long = repo.count // SELECT COUNT(*) FROM users
66
+
67
+ repo.update(User(1, "Alice Smith", "alice.smith@example.com"))
68
+ repo.delete(2)
69
+
70
+ // Single multi-row VALUES (…),(…) statement; returns the supplied IDs
71
+ val ids: Seq[Int] = repo.insertAll(Seq(User(3, "Carol", "carol@example.com")))
72
+
73
+ // JDBC addBatch/executeBatch; returns total row count
74
+ val n: Int = repo.insertBatch(List(User(4, "Dave", "dave@example.com")))
75
+
76
+ (allUsers, one, exists, total, ids, n)
77
+ }
78
+ // res1: Tuple6[List[User], Maybe[User], Boolean, Long, Seq[Int], Int] = (
79
+ // List(
80
+ // User(id = 1, name = "Alice", email = "alice@example.com"),
81
+ // User(id = 2, name = "Bob", email = "bob@example.com")
82
+ // ),
83
+ // User(id = 1, name = "Alice", email = "alice@example.com"),
84
+ // false,
85
+ // 2L,
86
+ // List(3),
87
+ // 1
88
+ // )
89
+ ```
90
+
91
+ Operations inside `transact` run under auto-commit disabled; on success the block commits and closes the connection, on any exception it rolls back.
92
+
93
+ ## Construction / Creating Instances
94
+
95
+ `Repo` exposes four factory methods, three of which auto-derive the underlying `Table[E]` from an implicit `Schema[E]`. The `Repo.derived` overloads differ only in how much naming information is supplied; `Repo.apply` accepts a fully pre-built `Table[E]` for cases where you have already constructed or derived a `Table` manually.
96
+
97
+ ### `Repo.derived` — Derive with ID column and getter
98
+
99
+ This overload derives the `Table[E]` from the implicit `Schema[E]` and uses the caller-supplied `idColumn` name and `getId` getter. The table name is computed by applying the default singular-snake-case policy to the type name — for example, `User` becomes `"user"` and `BlogPost` becomes `"blog_post"`.
100
+
101
+ ```scala
102
+ object Repo {
103
+ def derived[E, ID](idColumn: String, getId: E => ID)(using schema: Schema[E], idCodec: DbCodec[ID]): Repo[E, ID]
104
+ }
105
+ ```
106
+
107
+ A `DbCodec[ID]` instance is resolved automatically for all primitive ID types (`Int`, `Long`, `String`, `UUID`, and others) without any additional imports. The typical usage looks like this:
108
+
109
+ ```scala
110
+ import zio.blocks.sql._
111
+ import zio.blocks.schema.Schema
112
+
113
+ case class Article(id: Int, title: String, body: String)
114
+ object Article {
115
+ implicit val schema: Schema[Article] = Schema.derived
116
+ }
117
+
118
+ val repo = Repo.derived[Article, Int]("id", _.id)
119
+ // repo: Repo[Article, Int] = zio.blocks.sql.Repo$DerivedRepo@2f907618
120
+ repo.table.name
121
+ // res3: String = "article"
122
+ repo.table.codec.columns
123
+ // res4: IndexedSeq[String] = Vector("id", "title", "body")
124
+ ```
125
+
126
+ :::caution
127
+ The `idColumn` string must exactly match a column name in the derived table (after `SqlNameMapper` applies, so `camelCase` fields become `snake_case`). A mismatch throws `IllegalArgumentException` (via `require`) at construction time — not at query time.
128
+ :::
129
+
130
+ ### `Repo.derived` — Derive with table name, ID column, and getter
131
+
132
+ This overload is identical to the previous one except that it accepts an explicit `tableName`, bypassing the naming policy entirely. Use it when the default snake-case convention does not match the actual database table name — for example, when migrating from a legacy schema.
133
+
134
+ ```scala
135
+ object Repo {
136
+ def derived[E, ID](tableName: String, idColumn: String, getId: E => ID)(using schema: Schema[E], idCodec: DbCodec[ID]): Repo[E, ID]
137
+ }
138
+ ```
139
+
140
+ The supplied `tableName` is used verbatim (after SQL identifier validation) in all generated statements:
141
+
142
+ ```scala
143
+ import zio.blocks.sql._
144
+ import zio.blocks.schema.Schema
145
+
146
+ case class OrderLine(lineId: Int, qty: Int, unitPrice: BigDecimal)
147
+ object OrderLine {
148
+ implicit val schema: Schema[OrderLine] = Schema.derived
149
+ }
150
+
151
+ // Overrides the default "order_line" table name with the legacy one
152
+ val repo = Repo.derived[OrderLine, Int]("tbl_order_lines", "line_id", _.lineId)
153
+ // repo: Repo[OrderLine, Int] = zio.blocks.sql.Repo$DerivedRepo@2bc241ca
154
+ repo.table.name
155
+ // res6: String = "tbl_order_lines"
156
+ ```
157
+
158
+ ### `Repo.derived` — Fully automatic derivation
159
+
160
+ When called without any term arguments, `Repo.derived` requires an additional implicit `Schema[ID]` and locates the ID field in `E` using a four-priority rule, trying each in order until one matches:
161
+
162
+ 1. A field annotated `@Modifier.id` whose type matches `ID`.
163
+ 2. The unique field (among all fields) whose type matches `ID`, if there is exactly one.
164
+ 3. A field literally named `"id"` whose type matches `ID`.
165
+ 4. A field named `<entity>Id` (for example, `userId` for entity `User`) whose type matches `ID`.
166
+
167
+ The ID column name respects that field's `@Modifier.rename` annotation if present, or applies `SqlNameMapper.SnakeCase` to the field name otherwise. The table name uses the default naming policy.
168
+
169
+ ```scala
170
+ object Repo {
171
+ def derived[E, ID](using schema: Schema[E], idSchema: Schema[ID], idCodec: DbCodec[ID]): Repo[E, ID]
172
+ }
173
+ ```
174
+
175
+ This is the most concise form when the entity type has exactly one field of the `ID` type:
176
+
177
+ ```scala
178
+ import zio.blocks.sql._
179
+ import zio.blocks.schema.Schema
180
+
181
+ case class Tag(id: Long, label: String)
182
+ object Tag {
183
+ implicit val schema: Schema[Tag] = Schema.derived
184
+ }
185
+
186
+ // Inspects Tag's Schema, finds the unique Long field "id", maps it to column "id"
187
+ val repo = Repo.derived[Tag, Long]
188
+ // repo: Repo[Tag, Long] = zio.blocks.sql.Repo$DerivedRepo@589393f1
189
+ repo.idColumn
190
+ // res8: String = "id"
191
+ ```
192
+
193
+ :::caution
194
+ `Repo.derived` (fully auto) throws `IllegalArgumentException` at runtime if `E` is not a case class, if no field matches any of the four priorities, or if a priority level itself is ambiguous — multiple `@Modifier.id`-annotated fields of type `ID`, or multiple type-matching fields when none of them is named `"id"` or `<entity>Id`. When any ambiguity is possible, annotate the intended field with `@Modifier.id`, or prefer the explicit overload that names the ID column and getter directly.
195
+ :::
196
+
197
+ ### `Repo.apply` — Construct from an explicit Table
198
+
199
+ `Repo.apply` is the base constructor. It accepts a fully built `Table[E]` together with the ID column name, codec, and getter. Use it when you already have a `Table` — for example, one obtained from `Table.derived` — and want precise control over all four components.
200
+
201
+ ```scala
202
+ object Repo {
203
+ def apply[E, ID](table: Table[E], idColumn: String, idCodec: DbCodec[ID], getId: E => ID): Repo[E, ID]
204
+ }
205
+ ```
206
+
207
+ Combining `Table.derived` with `Repo.apply` gives access to DDL generation via `createTable` and `dropTable` while also enabling full CRUD:
208
+
209
+ ```scala
210
+ import zio.blocks.sql._
211
+ import zio.blocks.schema.Schema
212
+
213
+ case class Category(categoryId: Int, name: String)
214
+ object Category {
215
+ implicit val schema: Schema[Category] = Schema.derived
216
+ }
217
+
218
+ val table = Table.derived[Category]
219
+ val repo = Repo(table, "category_id", DbCodec[Int], _.categoryId)
220
+
221
+ given DbCon = ???
222
+
223
+ repo.table.createTable(summon[DbCon].dialect).update // DDL from Table
224
+ repo.all // pre-built SELECT from Repo
225
+ ```
226
+
227
+ ## Core Operations
228
+
229
+ Every `Repo` method requires a `DbCon` (or `DbTx`) given in scope, which is supplied by the enclosing `Transactor#connect` or `Transactor#transact` block. Using `DbTx` (from `Transactor#transact`) means all writes in a block are committed atomically or rolled back together on exception.
230
+
231
+ ### Read Operations
232
+
233
+ The read operations query the database without modifying it. `Repo#all`, `Repo#findAll`, and `Repo#find` decode and return entity values; `Repo#exists` and `Repo#count` return summary information without decoding full rows.
234
+
235
+ #### `all` — Retrieve all rows
236
+
237
+ `Repo#all` executes `SELECT <columns> FROM <table>` and decodes every result-set row into an `E` using the entity's `DbCodec`, returning all rows as a `List[E]` in database-native order.
238
+
239
+ ```scala
240
+ class Repo[E, ID] {
241
+ def all(using con: DbCon): List[E]
242
+ }
243
+ ```
244
+
245
+ Inside a `connect` or `transact` block, the call requires no arguments:
246
+
247
+ ```scala
248
+ import zio.blocks.sql._
249
+ import zio.blocks.schema.Schema
250
+
251
+ case class User(id: Int, name: String, email: String)
252
+ object User { implicit val schema: Schema[User] = Schema.derived }
253
+
254
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
255
+ given DbCon = ???
256
+
257
+ // Inside a Transactor#connect or #transact block:
258
+ val users: List[User] = repo.all
259
+ ```
260
+
261
+ :::caution
262
+ `all` loads the entire table into memory. For large tables, complement `Repo` with a custom `Frag` query that includes `LIMIT` and `OFFSET` clauses.
263
+ :::
264
+
265
+ #### `findAll` — Retrieve rows by a set of primary keys
266
+
267
+ `Repo#findAll` executes `SELECT <columns> FROM <table> WHERE <idColumn> IN (...)` for the given IDs and decodes every matching row into an `E`. It returns an empty `List` immediately, without executing any SQL, when `ids` is empty.
268
+
269
+ ```scala
270
+ class Repo[E, ID] {
271
+ def findAll(ids: Iterable[ID])(using con: DbCon): List[E]
272
+ }
273
+ ```
274
+
275
+ Use it to batch-fetch a known set of rows in a single round-trip instead of calling `find` in a loop:
276
+
277
+ ```scala
278
+ import zio.blocks.sql._
279
+ import zio.blocks.schema.Schema
280
+
281
+ case class User(id: Int, name: String, email: String)
282
+ object User { implicit val schema: Schema[User] = Schema.derived }
283
+
284
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
285
+ given DbCon = ???
286
+
287
+ val users: List[User] = repo.findAll(List(1, 2, 3))
288
+ ```
289
+
290
+ #### `find` — Find a row by primary key
291
+
292
+ `Repo#find` executes `SELECT <columns> FROM <table> WHERE <idColumn> = ?`, binding the ID through `idCodec`. It returns `Maybe.absent` if no row with the given key exists, or `Maybe(entity)` if a row is found.
293
+
294
+ ```scala
295
+ class Repo[E, ID] {
296
+ def find(id: ID)(using con: DbCon): Maybe[E]
297
+ }
298
+ ```
299
+
300
+ The ID value is bound as a parameterized `?` — no string formatting or concatenation:
301
+
302
+ ```scala
303
+ import zio.blocks.sql._
304
+ import zio.blocks.schema.Schema
305
+ import zio.blocks.maybe.Maybe
306
+
307
+ case class User(id: Int, name: String, email: String)
308
+ object User { implicit val schema: Schema[User] = Schema.derived }
309
+
310
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
311
+ given DbCon = ???
312
+
313
+ val alice: Maybe[User] = repo.find(1)
314
+ ```
315
+
316
+ #### `exists` — Check whether a row exists
317
+
318
+ `Repo#exists` returns `true` when a row with the given primary key exists in the table. It delegates to `find` and tests whether the result is defined, running a single parameterized `SELECT`.
319
+
320
+ ```scala
321
+ class Repo[E, ID] {
322
+ def exists(id: ID)(using con: DbCon): Boolean
323
+ }
324
+ ```
325
+
326
+ Use `exists` when you only need to confirm presence without loading the full entity:
327
+
328
+ ```scala
329
+ import zio.blocks.sql._
330
+ import zio.blocks.schema.Schema
331
+
332
+ case class User(id: Int, name: String, email: String)
333
+ object User { implicit val schema: Schema[User] = Schema.derived }
334
+
335
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
336
+ given DbCon = ???
337
+
338
+ val exists: Boolean = repo.exists(99)
339
+ ```
340
+
341
+ #### `count` — Count all rows
342
+
343
+ `Repo#count` executes `SELECT COUNT(*) FROM <table>` and returns the row count as a `Long`. The result is `0L` when the table is empty.
344
+
345
+ ```scala
346
+ class Repo[E, ID] {
347
+ def count(using con: DbCon): Long
348
+ }
349
+ ```
350
+
351
+ The `Long` result avoids boxing and handles tables with more than `Int.MaxValue` rows correctly:
352
+
353
+ ```scala
354
+ import zio.blocks.sql._
355
+ import zio.blocks.schema.Schema
356
+
357
+ case class User(id: Int, name: String, email: String)
358
+ object User { implicit val schema: Schema[User] = Schema.derived }
359
+
360
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
361
+ given DbCon = ???
362
+
363
+ val total: Long = repo.count
364
+ ```
365
+
366
+ ### Write Operations
367
+
368
+ The write operations insert, update, or delete rows in the database. `Repo#insert`, `Repo#insertBatch`, `Repo#insertAll`, `Repo#update`, `Repo#delete`, `Repo#deleteAll`, and `Repo#clear` each return an `Int` row count (`insertAll` returns the inserted IDs instead); `Repo#insertReturning` is the exception and returns the full inserted entity.
369
+
370
+ #### `insert` — Insert a single entity
371
+
372
+ `Repo#insert` encodes the entity with `DbCodec[E]` and executes `INSERT INTO <table> (<columns>) VALUES (?, …, ?)`, returning the number of affected rows — normally 1 on success.
373
+
374
+ ```scala
375
+ class Repo[E, ID] {
376
+ def insert(entity: E)(using con: DbCon): Int
377
+ }
378
+ ```
379
+
380
+ Each call uses the pre-built SQL string and binds fresh parameter values from the entity:
381
+
382
+ ```scala
383
+ import zio.blocks.sql._
384
+ import zio.blocks.schema.Schema
385
+
386
+ case class User(id: Int, name: String, email: String)
387
+ object User { implicit val schema: Schema[User] = Schema.derived }
388
+
389
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
390
+ given DbCon = ???
391
+
392
+ val rowsAffected: Int = repo.insert(User(1, "Alice", "alice@example.com"))
393
+ ```
394
+
395
+ #### `insertReturning` — Insert and return the inserted entity
396
+
397
+ `Repo#insertReturning` inserts the entity, retrieves the generated primary key via JDBC's `getGeneratedKeys`, and re-fetches the full row by calling `find` with that key. If the driver returns no generated key, it falls back to `find(getId(entity))`.
398
+
399
+ ```scala
400
+ class Repo[E, ID] {
401
+ def insertReturning(entity: E)(using con: DbCon): E
402
+ }
403
+ ```
404
+
405
+ This method is most useful when the database assigns the primary key — for example, via an auto-increment or sequence column — and the caller needs to read the assigned value back:
406
+
407
+ ```scala
408
+ import zio.blocks.sql._
409
+ import zio.blocks.schema.Schema
410
+
411
+ case class User(id: Int, name: String, email: String)
412
+ object User { implicit val schema: Schema[User] = Schema.derived }
413
+
414
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
415
+ given DbCon = ???
416
+
417
+ // Pass a placeholder ID; the returned entity carries the database-assigned key.
418
+ val inserted: User = repo.insertReturning(User(0, "Bob", "bob@example.com"))
419
+ ```
420
+
421
+ :::caution
422
+ `insertReturning` throws `NoSuchElementException` if the inserted row cannot be found after the insert. This can occur when the JDBC driver returns a key whose type does not match what `idCodec` expects. Verify that `idCodec` matches the database column type before relying on auto-generated keys.
423
+ :::
424
+
425
+ #### `insertBatch` — Batch-insert multiple entities
426
+
427
+ `Repo#insertBatch` inserts a collection of entities using JDBC batch execution (`addBatch` / `executeBatch`). Sending all parameter sets to the driver in a single round-trip is significantly faster than calling `insert` in a loop for large collections. It returns the total number of affected rows across all batched statements.
428
+
429
+ ```scala
430
+ class Repo[E, ID] {
431
+ def insertBatch(entities: Iterable[E])(using con: DbCon): Int
432
+ }
433
+ ```
434
+
435
+ The method returns 0 immediately when the input is empty, without opening a prepared statement:
436
+
437
+ ```scala
438
+ import zio.blocks.sql._
439
+ import zio.blocks.schema.Schema
440
+
441
+ case class User(id: Int, name: String, email: String)
442
+ object User { implicit val schema: Schema[User] = Schema.derived }
443
+
444
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
445
+ given DbCon = ???
446
+
447
+ val users = List(
448
+ User(1, "Alice", "alice@example.com"),
449
+ User(2, "Bob", "bob@example.com"),
450
+ User(3, "Carol", "carol@example.com")
451
+ )
452
+ val rowsAffected: Int = repo.insertBatch(users)
453
+ ```
454
+
455
+ `insertBatch` and `insertAll` solve different problems: `insertBatch` uses JDBC's multi-statement batch protocol (one prepared statement, multiple `executeBatch` rounds) and returns a row count; `insertAll` constructs a single multi-row `VALUES (…), (…)` SQL statement (one round-trip) and returns the primary keys in input order. For large collections or when relying on database-generated keys, prefer `insertBatch`. For moderate-sized collections where the caller controls the keys and wants a single SQL statement, prefer `insertAll`.
456
+
457
+ #### `insertAll` — Multi-row insert returning primary keys
458
+
459
+ `Repo#insertAll` assembles a single `INSERT INTO <table> (<columns>) VALUES (?, …, ?), …, (?, …, ?)` statement covering all rows and executes it in one database round-trip. It then extracts the primary keys from the input entities via `getId` and returns them in input order.
460
+
461
+ ```scala
462
+ class Repo[E, ID] {
463
+ def insertAll(rows: Seq[E])(using con: DbCon): Seq[ID]
464
+ }
465
+ ```
466
+
467
+ Because the generated SQL grows with the number of rows, `insertAll` is best suited for moderate-sized batches where the caller controls the primary keys:
468
+
469
+ ```scala
470
+ import zio.blocks.sql._
471
+ import zio.blocks.schema.Schema
472
+
473
+ case class User(id: Int, name: String, email: String)
474
+ object User { implicit val schema: Schema[User] = Schema.derived }
475
+
476
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
477
+ given DbCon = ???
478
+
479
+ val newUsers = Seq(
480
+ User(10, "Dave", "dave@example.com"),
481
+ User(11, "Eve", "eve@example.com")
482
+ )
483
+ // Returns Seq(10, 11) — the IDs extracted from the input entities via getId
484
+ val ids: Seq[Int] = repo.insertAll(newUsers)
485
+ ```
486
+
487
+ :::caution
488
+ `insertAll` throws `IllegalArgumentException` for an empty `Seq`. Always verify the input is non-empty before calling it, or use `insertBatch`, which accepts empty collections and returns 0 without error.
489
+ :::
490
+
491
+ #### `update` — Update an entity's non-ID columns
492
+
493
+ `Repo#update` executes `UPDATE <table> SET <col1> = ?, …, <colN> = ? WHERE <idColumn> = ?` for all non-ID columns of the entity, identifying the target row by its primary key. It returns the number of affected rows — 0 when no row with that ID exists.
494
+
495
+ ```scala
496
+ class Repo[E, ID] {
497
+ def update(entity: E)(using con: DbCon): Int
498
+ }
499
+ ```
500
+
501
+ Only non-ID columns appear in the `SET` clause; the ID column appears only in the `WHERE` clause, so it is never overwritten:
502
+
503
+ ```scala
504
+ import zio.blocks.sql._
505
+ import zio.blocks.schema.Schema
506
+
507
+ case class User(id: Int, name: String, email: String)
508
+ object User { implicit val schema: Schema[User] = Schema.derived }
509
+
510
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
511
+ given DbCon = ???
512
+
513
+ val rowsAffected: Int = repo.update(User(1, "Alice Smith", "alice.smith@example.com"))
514
+ ```
515
+
516
+ :::note
517
+ `update` returns 0 when the entity type has only an ID column and no other updatable fields. In that case the generated `SET` clause would be empty, and `Repo` skips the statement entirely.
518
+ :::
519
+
520
+ #### `delete` — Delete by primary key
521
+
522
+ `Repo#delete` executes `DELETE FROM <table> WHERE <idColumn> = ?`, binding the ID through `idCodec`. It returns the number of deleted rows — 0 if no row with the given ID exists.
523
+
524
+ ```scala
525
+ class Repo[E, ID] {
526
+ def delete(id: ID)(using con: DbCon): Int
527
+ }
528
+ ```
529
+
530
+ The ID value is bound as a parameterized `?`, not interpolated into the SQL string:
531
+
532
+ ```scala
533
+ import zio.blocks.sql._
534
+ import zio.blocks.schema.Schema
535
+
536
+ case class User(id: Int, name: String, email: String)
537
+ object User { implicit val schema: Schema[User] = Schema.derived }
538
+
539
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
540
+ given DbCon = ???
541
+
542
+ val rowsAffected: Int = repo.delete(42)
543
+ ```
544
+
545
+ To delete by an entity value rather than a bare ID, extract the key with `getId` first: `repo.delete(repo.getId(user))`.
546
+
547
+ #### `deleteAll` — Delete rows by a set of primary keys
548
+
549
+ `Repo#deleteAll` executes `DELETE FROM <table> WHERE <idColumn> IN (...)` for the given IDs in a single round-trip and returns the total number of deleted rows. It returns `0` immediately, without executing any SQL, when `ids` is empty.
550
+
551
+ ```scala
552
+ class Repo[E, ID] {
553
+ def deleteAll(ids: Iterable[ID])(using con: DbCon): Int
554
+ }
555
+ ```
556
+
557
+ Use it to batch-delete a known set of rows instead of calling `delete` in a loop:
558
+
559
+ ```scala
560
+ import zio.blocks.sql._
561
+ import zio.blocks.schema.Schema
562
+
563
+ case class User(id: Int, name: String, email: String)
564
+ object User { implicit val schema: Schema[User] = Schema.derived }
565
+
566
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
567
+ given DbCon = ???
568
+
569
+ val rowsAffected: Int = repo.deleteAll(List(1, 2, 3))
570
+ ```
571
+
572
+ #### `clear` — Remove all rows
573
+
574
+ `Repo#clear` executes `DELETE FROM <table>` without a `WHERE` clause and returns the number of deleted rows.
575
+
576
+ ```scala
577
+ class Repo[E, ID] {
578
+ def clear()(using con: DbCon): Int
579
+ }
580
+ ```
581
+
582
+ Despite the different name, this method issues a `DELETE FROM` statement rather than a SQL `TRUNCATE`, so the operation participates in transactions and the returned row count is exact:
583
+
584
+ ```scala
585
+ import zio.blocks.sql._
586
+ import zio.blocks.schema.Schema
587
+
588
+ case class User(id: Int, name: String, email: String)
589
+ object User { implicit val schema: Schema[User] = Schema.derived }
590
+
591
+ val repo = Repo.derived[User, Int]("users", "id", _.id)
592
+ given DbCon = ???
593
+
594
+ val rowsDeleted: Int = repo.clear()
595
+ ```
596
+
597
+ :::note
598
+ `DELETE FROM` is transactional and precise but may be slower than `TRUNCATE` for very large tables on databases that support the `TRUNCATE` statement. When performance for bulk deletes is critical, issue a `TRUNCATE` via a custom `Frag` query instead.
599
+ :::
600
+
@@ -0,0 +1,73 @@
1
+ ---
2
+ id: sql-dialect
3
+ title: "SqlDialect"
4
+ description: "Reference for SqlDialect, the interface for database-specific SQL type names and parameter placeholders."
5
+ keywords:
6
+ - "SQL Dialect"
7
+ - "PostgreSQL Dialect"
8
+ - "SQLite Dialect"
9
+ ---
10
+
11
+ `SqlDialect` tells the framework how to render SQL for a specific database. Two dialects are built in: `PostgreSQL` and `SQLite`.
12
+
13
+ ## Core API
14
+
15
+ ```scala
16
+ sealed trait SqlDialect {
17
+ def name: String
18
+ def typeName(dbValue: DbValue): String
19
+ def paramPlaceholder(index: Int): String
20
+ }
21
+
22
+ object SqlDialect {
23
+ case object PostgreSQL extends SqlDialect
24
+ case object SQLite extends SqlDialect
25
+ }
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ You can specify the dialect when creating your `Transactor`:
31
+
32
+ ```scala
33
+ import zio.blocks.sql._
34
+
35
+ val txPostgres = JdbcTransactor.fromUrl("jdbc:postgresql://...", SqlDialect.PostgreSQL)
36
+ val txSqlite = JdbcTransactor.fromUrl("jdbc:sqlite::memory:", SqlDialect.SQLite)
37
+ ```
38
+
39
+ The dialect is available through `DbCon#dialect` inside a transaction:
40
+
41
+ ```scala
42
+ import zio.blocks.sql._
43
+
44
+ val tx = JdbcTransactor.fromUrl("jdbc:sqlite::memory:", SqlDialect.SQLite)
45
+ // tx: JdbcTransactor = zio.blocks.sql.JdbcTransactor@f150688
46
+
47
+ tx.connect {
48
+ val dialect = summon[DbCon].dialect
49
+
50
+ (
51
+ dialect.typeName(DbValue.DbInt(0)),
52
+ dialect.typeName(DbValue.DbUUID(new java.util.UUID(0L, 0L))),
53
+ dialect.paramPlaceholder(1)
54
+ )
55
+ }
56
+ // res2: Tuple3[String, String, String] = ("INTEGER", "TEXT", "?")
57
+ ```
58
+
59
+ ## Type Mapping
60
+
61
+ PostgreSQL and SQLite map Scala types to SQL differently:
62
+
63
+ | Type | PostgreSQL | SQLite |
64
+ |------------|---------------|-----------|
65
+ | Long | `BIGINT` | `INTEGER` |
66
+ | Boolean | `BOOLEAN` | `INTEGER` |
67
+ | Instant | `TIMESTAMPTZ` | `TEXT` |
68
+ | LocalDate | `DATE` | `TEXT` |
69
+ | UUID | `UUID` | `TEXT` |
70
+ | BigDecimal | `NUMERIC` | `TEXT` |
71
+ | Bytes | `BYTEA` | `BLOB` |
72
+
73
+ PostgreSQL uses native types; SQLite uses `INTEGER` for all integers and `TEXT` for temporal/UUID types.