@zio.dev/zio-blocks 0.0.33 → 0.0.55
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.
- package/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +21 -16
- package/guides/getting-started-with-mux.md +1395 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +640 -165
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +1130 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +248 -389
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +1499 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +9 -52
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +745 -0
- package/reference/endpoint/path-codec.md +225 -0
- package/reference/endpoint/route-pattern.md +194 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +199 -0
- package/reference/html.md +1424 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +807 -0
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +49 -0
- package/reference/http-model/model.md +1517 -0
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +750 -0
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +943 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +828 -0
- package/reference/openapi.md +1351 -0
- package/reference/projection.md +654 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -98
- package/reference/resource-management/scope.md +28 -220
- package/reference/resource-management/wire.md +5 -55
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +185 -0
- package/reference/ringbuffer/mpsc.mdx +164 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +416 -0
- package/reference/{allows.md → schema/allows.md} +4 -100
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +3 -4
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +510 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +11 -11
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +52 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/schema/schema-search.md +263 -0
- package/reference/{schema.md → schema/schema.md} +22 -2
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +1032 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +148 -0
- package/reference/sql/db-tx.md +114 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +288 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +363 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/core/pipeline.md +854 -0
- package/reference/streams/core/sink.md +1404 -0
- package/reference/streams/core/stream.md +3236 -0
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +726 -0
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/primitives/writer.md +1201 -0
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +5 -83
- package/sidebars.js +376 -43
- package/undocumented-report.md +528 -270
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /package/reference/{validation.md → schema/validation.md} +0 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sql-checked-interpolation
|
|
3
|
+
title: "sql(table) Checked Interpolation"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> **Note**: Checked form is `StringContext(...).sql(table, ...)(...)`; no `sqlChecked` alias (removed pre-1.0).
|
|
7
|
+
|
|
8
|
+
`sql` with tables gives you compile-time typo protection for hand-written SQL: it validates table and column identifiers against the `Table[?]` values you hand it, with did-you-mean suggestions. Without tables it still validates quotes/parentheses. The escape hatch `SqlLiteral` lets you splice unchecked raw SQL when needed. It is lint-grade, not a full SQL parser — zero runtime overhead when unused.
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
The `sql` interpolator composes two compile-time checks:
|
|
13
|
+
|
|
14
|
+
1. Quote/paren validation from `SqlValidator` (unclosed quotes, unbalanced parentheses) — always.
|
|
15
|
+
2. Identifier checking via `SqlIdentifierChecker` when tables are provided: tokenizes literal parts respecting `'...'` / `"..."` and `__HOLE__` placeholders, subtracts the SQL keyword/function allowlist (~160 entries, case-insensitive), tracks aliases introduced via `AS alias`, and requires every remaining identifier to be a known table name or column.
|
|
16
|
+
|
|
17
|
+
Unknown identifiers emit compile-time errors via `report.error` (up to 5 diagnostics), each message containing the identifier and a Levenshtein `<=2` suggestion when close.
|
|
18
|
+
|
|
19
|
+
## Usage without and with Table
|
|
20
|
+
|
|
21
|
+
Tables are derived from `Schema` as elsewhere:
|
|
22
|
+
|
|
23
|
+
```scala
|
|
24
|
+
import zio.blocks.schema._
|
|
25
|
+
import zio.blocks.sql._
|
|
26
|
+
|
|
27
|
+
case class User(id: Int, name: String, email: String)
|
|
28
|
+
object User { implicit val schema: Schema[User] = Schema.derived }
|
|
29
|
+
|
|
30
|
+
case class Order(id: Int, userId: Int, amount: Double)
|
|
31
|
+
object Order { implicit val schema: Schema[Order] = Schema.derived }
|
|
32
|
+
|
|
33
|
+
val usersTable = Table.derived[User] // name "user"
|
|
34
|
+
val ordersTable = Table.derived[Order] // name "order"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Without tables, only quote/paren checks run (identifier checking skipped):
|
|
38
|
+
|
|
39
|
+
```scala
|
|
40
|
+
val plain: Frag = sql"SELECT * FROM users WHERE email = ${"a@b.com"}"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
With tables, identifiers are checked. Pass the relevant tables before the interpolated args — the macro extracts table/column names from their case class fields at compile time:
|
|
44
|
+
|
|
45
|
+
```scala
|
|
46
|
+
val q1: Frag = StringContext("SELECT * FROM user WHERE email = ", "").sql(usersTable)("a@b.com")
|
|
47
|
+
val q2: Frag = StringContext("SELECT user.id, order.amount FROM user JOIN order ON user.id = order.user_id").sql(usersTable, ordersTable)()
|
|
48
|
+
val q3: Frag = StringContext("SELECT u.id FROM user AS u WHERE u.email = ", "").sql(usersTable)("x")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Positive examples compile and render identically to the plain `sql` form:
|
|
52
|
+
|
|
53
|
+
```scala
|
|
54
|
+
q1.sql(SqlDialect.PostgreSQL)
|
|
55
|
+
// res0: String = "SELECT * FROM user WHERE email = ?"
|
|
56
|
+
q2.sql(SqlDialect.PostgreSQL)
|
|
57
|
+
// res1: String = "SELECT user.id, order.amount FROM user JOIN order ON user.id = order.user_id"
|
|
58
|
+
q3.sql(SqlDialect.PostgreSQL)
|
|
59
|
+
// res2: String = "SELECT u.id FROM user AS u WHERE u.email = ?"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Parameters are handled identically regardless of checking — any `DbParam[T]` (or `DbValue`) can be interpolated and becomes a `?` placeholder:
|
|
63
|
+
|
|
64
|
+
```scala
|
|
65
|
+
val email: String = "alice@example.com"
|
|
66
|
+
val frag: Frag = StringContext("SELECT * FROM user WHERE email = ", " AND id = ", "").sql(usersTable)(email, 42)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Did-you-mean example
|
|
70
|
+
|
|
71
|
+
Typos fail at compile time with a suggestion (only when tables are provided):
|
|
72
|
+
|
|
73
|
+
```scala
|
|
74
|
+
StringContext("SELECT * FROM usre").sql(usersTable)()
|
|
75
|
+
// error:
|
|
76
|
+
// Unknown identifier 'usre' at position 14; did you mean 'user'?
|
|
77
|
+
// StringContext("SELECT * FROM usre").sql(usersTable)()
|
|
78
|
+
// ^
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
// error: Unknown identifier 'usre' at position 14; did you mean 'user'?
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```scala
|
|
86
|
+
StringContext("SELECT emial FROM user").sql(usersTable)()
|
|
87
|
+
// error:
|
|
88
|
+
// Unknown identifier 'emial' at position 7; did you mean 'email'?
|
|
89
|
+
// StringContext("SELECT emial FROM user").sql(usersTable)()
|
|
90
|
+
// ^
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
// error: Unknown identifier 'emial' at position 7; did you mean 'email'?
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```scala
|
|
98
|
+
StringContext("SELECT * FROM user WHERE badcol = 1").sql(usersTable)()
|
|
99
|
+
// error:
|
|
100
|
+
// Unknown identifier 'badcol' at position 25
|
|
101
|
+
// StringContext("SELECT * FROM user WHERE badcol = 1").sql(usersTable)()
|
|
102
|
+
// ^
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
// error: Unknown identifier 'badcol' at position 28
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Up to 5 diagnostics are reported per interpolator invocation; fix them iteratively.
|
|
110
|
+
|
|
111
|
+
## Limits — lint-grade, not a parser
|
|
112
|
+
|
|
113
|
+
The checked `sql(tables)` is intentionally lightweight:
|
|
114
|
+
|
|
115
|
+
- **No expression typing.** It does not type-check `WHERE` expressions or enforce that `amount > "foo"` is ill-typed.
|
|
116
|
+
- **Subquery internals out of scope.** Identifiers inside nested `SELECT` subqueries are checked as flat tokens; column scoping across subqueries is not modeled.
|
|
117
|
+
- **Aliases trusted.** Once an alias is introduced via `AS alias` (case-insensitive, quoted aliases supported), further uses of that alias and `alias.column` are trusted without verifying the column belongs to the aliased table.
|
|
118
|
+
- **Allowlist coverage.** ~160 SQL keywords, types, and common functions (SELECT/FROM/WHERE/JOIN/COUNT/SUM/AVG/COALESCE/CASE/etc.) are allowlisted case-insensitively. Uncommon dialect-specific functions may need the escape hatch.
|
|
119
|
+
- **Keyword-named tables.** Tables named like `order`/`group` overlap the keyword allowlist; they are trusted as keywords unless passed as a `Table`, so ensure such tables are included in the `Table[?]` arguments — known tables/columns are checked before the allowlist, so `order` is recognized when it is in `knownTables`.
|
|
120
|
+
- **Table extraction.** Column names are derived from case class fields via `SqlNameMapper.SnakeCase`. Custom renames (`@Modifier.rename`) or complex `Table("name", codec, cols)` constructions are not fully reflected — prefer `Table.derived` for checked queries. In particular, `Table.name` from `@Modifier.config("sql.table_name")` is only reflected in the checker when the `Table` is constructed with an explicit name literal (e.g. `Table.derived[User]("my_table")` or `Table("my_table", ...)`); otherwise the macro falls back to `SnakeCase(typeName)` which may mismatch the runtime name — see `SqlMacros.addTableMeta` limitation.
|
|
121
|
+
- **String literals respected.** Identifiers inside `'...'` are never flagged; `"quoted identifiers"` are validated as identifiers.
|
|
122
|
+
|
|
123
|
+
If a valid query is flagged, use the escape hatch below.
|
|
124
|
+
|
|
125
|
+
## Escape hatch: `SqlLiteral`
|
|
126
|
+
|
|
127
|
+
> **Warning**: `SqlLiteral` is spliced verbatim without escaping — never construct it from untrusted input (user data, request params). Use `DbParam`/`?` placeholders for values; reserve `SqlLiteral` for trusted, dialect-specific SQL.
|
|
128
|
+
|
|
129
|
+
When you need dynamic SQL or a dialect-specific construct that the lint cannot model, use `SqlLiteral` — unchecked raw SQL.
|
|
130
|
+
|
|
131
|
+
Standalone unchecked fragment (no validation, no `Table` needed):
|
|
132
|
+
|
|
133
|
+
```scala
|
|
134
|
+
val raw: Frag = SqlLiteral("SELECT MY_CUSTOM_FUNC(id) FROM user").toFrag
|
|
135
|
+
// also: SqlLiteral.frag("SELECT ...")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Splicing raw SQL inside the `sql` interpolator as a verbatim fragment (not a `?` parameter):
|
|
139
|
+
|
|
140
|
+
```scala
|
|
141
|
+
val qRaw: Frag = sql"SELECT ${SqlLiteral("MY_CUSTOM_FUNC(id)")} FROM user"
|
|
142
|
+
val qMixed: Frag = sql"SELECT ${SqlLiteral("MY_FUNC()")}, email FROM user WHERE id = ${42}"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`Frag` values are also spliced verbatim: `sql"SELECT * FROM (${myFrag}) WHERE id = ${id}"`.
|
|
146
|
+
|
|
147
|
+
Spliced `SqlLiteral`/`Frag` content is not identifier-checked; the surrounding literal parts still are (when tables are provided). Interpolated values that are not `SqlLiteral`/`Frag` always become `?` placeholders, so dialect-specific SQL must be spliced as `SqlLiteral`/`Frag`, not as a bound parameter.
|
|
148
|
+
|
|
149
|
+
```scala
|
|
150
|
+
qRaw.sql(SqlDialect.PostgreSQL)
|
|
151
|
+
// res6: String = "SELECT MY_CUSTOM_FUNC(id) FROM user"
|
|
152
|
+
qMixed.sql(SqlDialect.PostgreSQL)
|
|
153
|
+
// res7: String = "SELECT MY_FUNC(), email FROM user WHERE id = ?"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Prefer checked `sql(tables)` for all hand-written queries; reserve `SqlLiteral` for genuinely dynamic or dialect-specific cases.
|
|
157
|
+
|
|
158
|
+
## Comparison
|
|
159
|
+
|
|
160
|
+
| Form | Quote/paren check | Identifier check | Needs `Table` | Use when |
|
|
161
|
+
|------|-------------------|------------------|---------------|----------|
|
|
162
|
+
| `sql"..."` (no tables) | yes | no | no | Default, no schema coupling |
|
|
163
|
+
| `StringContext(...).sql(table)(...)` | yes | yes (first 5) | yes | Hand-written SQL you want typo-protected |
|
|
164
|
+
| `SqlLiteral("...")` / spliced `SqlLiteral` / `Frag` | no | no | no | Escape hatch for dynamic/dialect SQL |
|
|
165
|
+
|
|
166
|
+
Neither form changes `Frag` rendering; all produce the same `Frag(parts, params)` structure and `sql(dialect)` output.
|
|
167
|
+
|
|
168
|
+
## See also
|
|
169
|
+
|
|
170
|
+
- `SqlIdentifierChecker` — pure checker core and `DefaultAllowlist`
|
|
171
|
+
- `SqlValidator` — quote/paren validation
|
|
172
|
+
- `SqlLiteral` — unchecked raw SQL holder
|
|
173
|
+
- `Table.derived` — table derivation and `TableNamingPolicy`
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sql-transactions
|
|
3
|
+
title: "SQL Transactions: Isolation, Savepoints & Hikari on Loom"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
This guide covers ZIO Blocks' SQL transaction system: how `transact` works under the hood, which isolation levels are available on each database, how nested transactions use savepoints, and how to wire HikariCP into a `JdbcTransactor` for virtual-thread-friendly connection pooling.
|
|
7
|
+
|
|
8
|
+
**What we'll cover:**
|
|
9
|
+
|
|
10
|
+
- Opening transactions with `transactor.transact { ... }` and tuning isolation + readOnly
|
|
11
|
+
- The `TransactionIsolation` enum and how SQLite vs PostgreSQL handle each level
|
|
12
|
+
- Nested transactions via SQL savepoints (`zib_tx_1 .. zib_tx_N`)
|
|
13
|
+
- Error semantics: inner rollback vs outer commit
|
|
14
|
+
- HikariCP connection pooling with `JdbcTransactor.fromDataSource`
|
|
15
|
+
- Virtual threads (JDK 25+ Loom): why blocking JDBC is fine, and what pinning caveats remain
|
|
16
|
+
|
|
17
|
+
## Basic Usage
|
|
18
|
+
|
|
19
|
+
Every database interaction goes through a `Transactor`. The simplest form opens a connection, disables auto-commit, runs your code, and commits on success (or rolls back on failure):
|
|
20
|
+
|
|
21
|
+
```scala
|
|
22
|
+
transactor.transact {
|
|
23
|
+
sql"INSERT INTO users (name) VALUES ('Alice')".update
|
|
24
|
+
sql"INSERT INTO orders (user_id, total) VALUES (1, 99.50)".update
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Both inserts run inside a single transaction. If either fails, both are rolled back.
|
|
29
|
+
|
|
30
|
+
To control isolation level and read-only mode, pass them explicitly:
|
|
31
|
+
|
|
32
|
+
```scala
|
|
33
|
+
transactor.transact(TransactionIsolation.RepeatableRead, readOnly = false) {
|
|
34
|
+
val users = sql"SELECT * FROM users WHERE active = true".query[User].toList
|
|
35
|
+
// ... process users ...
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The default overload uses `Serializable` isolation with `readOnly = false`, which matches the standard SQL default for transactional databases.
|
|
40
|
+
|
|
41
|
+
## TransactionIsolation
|
|
42
|
+
|
|
43
|
+
The `TransactionIsolation` enum has four values, each mapping to the corresponding `java.sql.Connection.TRANSACTION_*` constant:
|
|
44
|
+
|
|
45
|
+
| Enum value | JDBC constant |
|
|
46
|
+
|----------------------|--------------------------------------|
|
|
47
|
+
| `ReadUncommitted` | `TRANSACTION_READ_UNCOMMITTED` |
|
|
48
|
+
| `ReadCommitted` | `TRANSACTION_READ_COMMITTED` |
|
|
49
|
+
| `RepeatableRead` | `TRANSACTION_REPEATABLE_READ` |
|
|
50
|
+
| `Serializable` | `TRANSACTION_SERIALIZABLE` |
|
|
51
|
+
|
|
52
|
+
When you pass a level to `transactor.transact`, it calls `Connection.setTransactionIsolation` before starting the transaction. The previous level is restored in a `finally` block after the transaction completes.
|
|
53
|
+
|
|
54
|
+
### SQLite vs PostgreSQL
|
|
55
|
+
|
|
56
|
+
The two supported dialects differ significantly in how they honor isolation levels:
|
|
57
|
+
|
|
58
|
+
| Level | PostgreSQL | SQLite |
|
|
59
|
+
|--------------------|-----------------------------------|-------------------------------------------|
|
|
60
|
+
| `ReadUncommitted` | Full support (dirty reads) | Accepted, treated as `SERIALIZABLE` |
|
|
61
|
+
| `ReadCommitted` | Full support (default PG level) | Accepted, treated as `SERIALIZABLE` |
|
|
62
|
+
| `RepeatableRead` | Full support (MVCC snapshot) | Accepted, treated as `SERIALIZABLE` |
|
|
63
|
+
| `Serializable` | Full support (SSI) | Native (default and only true level) |
|
|
64
|
+
|
|
65
|
+
SQLite natively supports only `SERIALIZABLE`. The driver accepts the `setTransactionIsolation` call without error, but the engine ignores the requested level and behaves as serializable. This is a SQLite limitation, not a ZIO Blocks one. If your application relies on weaker isolation guarantees (e.g., `ReadCommitted` for higher concurrency), test on PostgreSQL where those semantics are real.
|
|
66
|
+
|
|
67
|
+
PostgreSQL supports all four levels natively through its MVCC implementation. `ReadCommitted` is the default for non-transactional statements; `RepeatableRead` gives you a consistent snapshot for the duration of the transaction; `Serializable` adds serialization conflict detection via Snapshot Isolation (SSI).
|
|
68
|
+
|
|
69
|
+
## Read-Only Transactions
|
|
70
|
+
|
|
71
|
+
Passing `readOnly = true` marks the connection as read-only at the JDBC level:
|
|
72
|
+
|
|
73
|
+
```scala
|
|
74
|
+
transactor.transact(TransactionIsolation.ReadCommitted, readOnly = true) {
|
|
75
|
+
sql"SELECT COUNT(*) FROM users".query[Int].toOne
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
PostgreSQL uses the read-only flag to enable optimization paths (e.g., avoiding WAL writes for read-only transactions). SQLite may ignore the flag depending on the driver version, but it's still set on the connection for consistency. The previous `readOnly` value is always restored after the transaction, regardless of dialect.
|
|
80
|
+
|
|
81
|
+
## Nested Transactions via Savepoints
|
|
82
|
+
|
|
83
|
+
Nested transactions are emulated via SQL savepoints on the same JDBC connection. When you're already inside `transactor.transact { ... }`, you can nest further work using the ambient `DbTx`:
|
|
84
|
+
|
|
85
|
+
```scala
|
|
86
|
+
transactor.transact {
|
|
87
|
+
sql"INSERT INTO orders (total) VALUES (100)".update
|
|
88
|
+
|
|
89
|
+
summon[DbTx].transact {
|
|
90
|
+
sql"INSERT INTO order_items (order_id, product_id) VALUES (1, 42)".update
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
sql"UPDATE inventory SET stock = stock - 1 WHERE product_id = 42".update
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The nested block runs inside a savepoint named `zib_tx_1`. If it fails, only the inner work is rolled back (via `ROLLBACK TO SAVEPOINT`). The outer transaction continues and can still commit.
|
|
98
|
+
|
|
99
|
+
### How Savepoints Work
|
|
100
|
+
|
|
101
|
+
Each nesting level gets a savepoint name derived from the current depth: `zib_tx_1`, `zib_tx_2`, and so on. The mechanism:
|
|
102
|
+
|
|
103
|
+
1. **Depth increments** before creating the savepoint
|
|
104
|
+
2. **`SAVEPOINT zib_tx_<depth>`** is issued on the connection
|
|
105
|
+
3. The nested body executes with the ambient `DbTx` in scope
|
|
106
|
+
4. On **success**: `RELEASE SAVEPOINT zib_tx_<depth>` is called
|
|
107
|
+
5. On **failure**: `ROLLBACK TO SAVEPOINT zib_tx_<depth>` is called, then the exception is rethrown
|
|
108
|
+
6. **Depth decrements** in a `finally` block, guaranteeing the counter resets even after exceptions
|
|
109
|
+
|
|
110
|
+
The depth counter resets after each inner block finishes, so sibling nested transactions reuse the same name sequence without leaking savepoints.
|
|
111
|
+
|
|
112
|
+
### Three-Level Example
|
|
113
|
+
|
|
114
|
+
```scala
|
|
115
|
+
transactor.transact {
|
|
116
|
+
// Depth 0: outer transaction (real COMMIT/ROLLBACK)
|
|
117
|
+
sql"INSERT INTO t VALUES (1)".update
|
|
118
|
+
|
|
119
|
+
summon[DbTx].transact {
|
|
120
|
+
// Depth 1: savepoint zib_tx_1
|
|
121
|
+
sql"INSERT INTO t VALUES (2)".update
|
|
122
|
+
|
|
123
|
+
summon[DbTx].transact {
|
|
124
|
+
// Depth 2: savepoint zib_tx_2
|
|
125
|
+
sql"INSERT INTO t VALUES (3)".update
|
|
126
|
+
}
|
|
127
|
+
// zib_tx_2 released here
|
|
128
|
+
|
|
129
|
+
sql"INSERT INTO t VALUES (4)".update
|
|
130
|
+
}
|
|
131
|
+
// zib_tx_1 released here
|
|
132
|
+
|
|
133
|
+
sql"INSERT INTO t VALUES (5)".update
|
|
134
|
+
}
|
|
135
|
+
// COMMIT (all 5 inserts)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
If the depth-2 block fails, only `INSERT INTO t VALUES (3)` is rolled back. Inserts 1, 2, 4, and 5 remain and will be committed.
|
|
139
|
+
|
|
140
|
+
### Error Semantics
|
|
141
|
+
|
|
142
|
+
When an inner block throws:
|
|
143
|
+
|
|
144
|
+
- The inner savepoint is rolled back via `ROLLBACK TO SAVEPOINT`
|
|
145
|
+
- The exception is rethrown to the caller
|
|
146
|
+
- The outer transaction is **not** rolled back automatically
|
|
147
|
+
- The outer block can catch the exception and continue, or let it propagate (triggering a full rollback at the top level)
|
|
148
|
+
|
|
149
|
+
```scala
|
|
150
|
+
transactor.transact {
|
|
151
|
+
sql"INSERT INTO t VALUES (1)".update
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
summon[DbTx].transact {
|
|
155
|
+
sql"INSERT INTO t VALUES (2)".update
|
|
156
|
+
throw new RuntimeException("inner failure")
|
|
157
|
+
sql"INSERT INTO t VALUES (3)".update // never reached
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
case _: RuntimeException => () // swallow inner failure
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
sql"INSERT INTO t VALUES (4)".update
|
|
164
|
+
}
|
|
165
|
+
// COMMIT: rows 1 and 4 only (row 2 rolled back by savepoint)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### The `transactNested` Helper
|
|
169
|
+
|
|
170
|
+
For call sites that prefer `using` parameters over extension receivers, `transactNested` is available as an alias:
|
|
171
|
+
|
|
172
|
+
```scala
|
|
173
|
+
import zio.blocks.sql.DbTx
|
|
174
|
+
|
|
175
|
+
transactor.transact {
|
|
176
|
+
DbTx.transactNested {
|
|
177
|
+
// same as summon[DbTx].transact { ... }
|
|
178
|
+
sql"INSERT INTO t VALUES (1)".update
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Both forms are equivalent. The extension method (`summon[DbTx].transact { ... }`) is more common in practice because it reads naturally inside a `transact` block.
|
|
184
|
+
|
|
185
|
+
### Given-Priority Trick
|
|
186
|
+
|
|
187
|
+
When a `DbTx` is already in scope (inside `transactor.transact`), the extension `summon[DbTx].transact { ... }` resolves via the more specific `DbTx` receiver and reuses the same connection with a savepoint. Using `transactor.transact { ... }` instead would open a **new** connection, which is almost never what you want inside an existing transaction. The explicit `summon[DbTx]` form keeps semantics clear without hidden implicit resolution.
|
|
188
|
+
|
|
189
|
+
## HikariCP Recipe
|
|
190
|
+
|
|
191
|
+
For production applications, use HikariCP for connection pooling. Create a `HikariDataSource` and pass it to `JdbcTransactor.fromDataSource`:
|
|
192
|
+
|
|
193
|
+
```scala
|
|
194
|
+
import com.zaxxer.hikari.HikariDataSource
|
|
195
|
+
import zio.blocks.sql.{JdbcTransactor, SqlDialect}
|
|
196
|
+
|
|
197
|
+
val ds = new HikariDataSource()
|
|
198
|
+
ds.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb")
|
|
199
|
+
ds.setUsername("app_user")
|
|
200
|
+
ds.setPassword("secret")
|
|
201
|
+
ds.setMaximumPoolSize(10)
|
|
202
|
+
|
|
203
|
+
val transactor = JdbcTransactor.fromDataSource(ds, SqlDialect.PostgreSQL)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Convenience helpers are available for the built-in dialects:
|
|
207
|
+
|
|
208
|
+
```scala
|
|
209
|
+
val pgTransactor = JdbcTransactor.postgres(ds) // shorthand for fromDataSource(ds, PostgreSQL)
|
|
210
|
+
val sqliteTransactor = JdbcTransactor.sqlite(ds) // shorthand for fromDataSource(ds, SQLite)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Pool Sizing for Virtual Threads
|
|
214
|
+
|
|
215
|
+
With virtual threads (JDK 25+), the pool size rule is straightforward: **pool size equals database max connections, not thread count**. Virtual threads are cheap to create and block, so you don't need a thread pool to limit concurrency. The HikariCP pool limits how many database connections are open simultaneously, which is the only resource that actually needs capping.
|
|
216
|
+
|
|
217
|
+
A typical configuration:
|
|
218
|
+
|
|
219
|
+
```scala
|
|
220
|
+
ds.setMaximumPoolSize(20) // match your DB's max_connections / app instances
|
|
221
|
+
ds.setMinimumIdle(5) // keep a few warm connections
|
|
222
|
+
ds.setConnectionTimeout(3000) // fail fast if pool exhausted
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Don't set `maximumPoolSize` to the number of CPU cores or virtual threads. Size it based on your database's connection limit and how many concurrent queries your application actually runs. Most applications need 10-30 connections, not thousands.
|
|
226
|
+
|
|
227
|
+
Queue sizing via `connectionTimeout` (default 30 seconds) controls how long a request waits for a pooled connection when the pool is exhausted. With virtual threads, you can afford to wait longer since you're not blocking platform threads, but 30 seconds via `ds.setConnectionTimeout(30000)` is usually a good default.
|
|
228
|
+
|
|
229
|
+
### Pinning Notes (Post-JEP491)
|
|
230
|
+
|
|
231
|
+
JEP 491 (JDK 25+) eliminates virtual-thread pinning on most blocking I/O operations. Blocking JDBC calls like `Statement.executeQuery()` no longer pin the virtual thread to its carrier thread. This means you can run thousands of concurrent queries without needing an async JDBC wrapper.
|
|
232
|
+
|
|
233
|
+
However, some drivers still have internal `synchronized` blocks or native calls that can pin:
|
|
234
|
+
|
|
235
|
+
- **SQLite driver**: The `busy_timeout` implementation uses `synchronized` internally. Under high concurrency with short timeouts, this can still pin virtual threads. Set `busy_timeout` high (e.g., 5000ms) to reduce contention, or use PostgreSQL for concurrent workloads.
|
|
236
|
+
|
|
237
|
+
- **PostgreSQL JDBC driver**: The driver's `Object.wait()` calls are safe on virtual threads post-JEP491. Earlier JDK versions (21-24) may pin during `Object.wait` in the driver's connection handshake, but this is resolved in JDK 25+.
|
|
238
|
+
|
|
239
|
+
- **HikariCP itself**: HikariCP's internal `ConcurrentBag` uses `ThreadLocal`-based tracking, which works correctly with virtual threads. The pool doesn't need special configuration for Loom.
|
|
240
|
+
|
|
241
|
+
If you're on JDK 25+, just use blocking JDBC directly. No need for `ZIO.attemptBlocking` or async wrappers to avoid pinning.
|
|
242
|
+
|
|
243
|
+
## Virtual Threads (JDK 25+ Loom)
|
|
244
|
+
|
|
245
|
+
JDK 25 brings Loom to general availability on the JVM-first path. Virtual threads handle blocking I/O naturally: when a JDBC call blocks, the virtual thread unmounts from its carrier thread and the carrier is free to run other work.
|
|
246
|
+
|
|
247
|
+
This changes the connection-pooling calculus. Before Loom, you needed to size your thread pool and connection pool carefully to avoid thread starvation. With virtual threads, the thread pool concern disappears. Your only constraint is the database's connection limit.
|
|
248
|
+
|
|
249
|
+
```scala
|
|
250
|
+
// No special thread pool needed. Just use virtual threads directly.
|
|
251
|
+
import java.util.concurrent.Executors
|
|
252
|
+
import scala.concurrent.ExecutionContext
|
|
253
|
+
|
|
254
|
+
val vtExecutor = Executors.newVirtualThreadPerTaskExecutor()
|
|
255
|
+
val ec = ExecutionContext.fromExecutor(vtExecutor)
|
|
256
|
+
|
|
257
|
+
// Run hundreds of concurrent queries
|
|
258
|
+
val results = Future.traverse(userIds) { id =>
|
|
259
|
+
Future {
|
|
260
|
+
transactor.transact {
|
|
261
|
+
sql"SELECT * FROM users WHERE id = $id".query[User].toOne
|
|
262
|
+
}
|
|
263
|
+
}(ec)
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
With virtual threads, you don't need async JDBC drivers, reactive wrappers, or thread pool tuning. Blocking is fine. The database connection pool is the only thing you need to manage.
|
|
268
|
+
|
|
269
|
+
## Summary
|
|
270
|
+
|
|
271
|
+
| Concept | Key Point |
|
|
272
|
+
|----------------------------|---------------------------------------------------------|
|
|
273
|
+
| `transactor.transact` | Opens connection, disables auto-commit, commits/rollbacks |
|
|
274
|
+
| `TransactionIsolation` | Four levels; SQLite only true `SERIALIZABLE` |
|
|
275
|
+
| `readOnly` flag | Sets JDBC read-only; PG optimizes, SQLite may ignore |
|
|
276
|
+
| Nested via `summon[DbTx]` | Savepoints `zib_tx_1..N`, inner rollback isolated |
|
|
277
|
+
| `transactNested` | Alias using `using` parameter instead of extension |
|
|
278
|
+
| HikariCP | `fromDataSource(ds, dialect)`, pool = DB connections |
|
|
279
|
+
| Virtual threads | Blocking JDBC is fine on JDK 25+ Loom |
|
|
280
|
+
| Pinning | Post-JEP491 mostly resolved; SQLite `busy_timeout` caveat |
|
|
281
|
+
|
|
282
|
+
## Going Further
|
|
283
|
+
|
|
284
|
+
- **[Query DSL with SQL](./query-dsl-sql.md)** -- Building parameterized SQL from `SchemaExpr` queries
|
|
285
|
+
- **[Transactor Reference](../reference/sql/transactor)** -- `JdbcTransactor.fromDataSource` / `fromUrl` and `transact(isolation, readOnly)` overloads
|
|
286
|
+
- **[DbTx Reference](../reference/sql/db-tx)** -- Savepoint API (`savepoint` / `release` / `rollbackTo`) and `summon[DbTx].transact`
|