@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
package/guides/query-dsl-sql.md
CHANGED
|
@@ -9,7 +9,7 @@ This is Part 2 of the Query DSL series. [Part 1](./query-dsl-reified-optics.md)
|
|
|
9
9
|
|
|
10
10
|
**What we'll cover:**
|
|
11
11
|
|
|
12
|
-
- Interpreting `SchemaExpr`
|
|
12
|
+
- Interpreting `SchemaExpr` while keeping the typed API at the boundary
|
|
13
13
|
- Extracting column names from optic paths using `DynamicOptic`
|
|
14
14
|
- Translating relational, logical, arithmetic, and string operations to SQL
|
|
15
15
|
- Building complete `SELECT ... FROM ... WHERE ...` statements
|
|
@@ -36,14 +36,14 @@ def findProducts(category: Option[String], maxPrice: Option[Double], inStock: Op
|
|
|
36
36
|
|
|
37
37
|
This is fragile, repetitive, and vulnerable to SQL injection. Every new query shape requires new string-building code. The query logic is duplicated -- once as a `SchemaExpr` for in-memory filtering, and again as hand-written SQL for the database.
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
`SchemaExpr` is the user-facing query API. Internally it wraps a `DynamicSchemaExpr` — a sealed trait whose cases represent the full expression AST. That means we can write a single interpreter that accepts `SchemaExpr`, then crosses into the dynamic AST internally to translate *any* query expression into SQL. Write the interpreter once, and every query you build with the Part 1 DSL automatically gets a SQL translation.
|
|
40
40
|
|
|
41
41
|
## Prerequisites
|
|
42
42
|
|
|
43
43
|
This guide builds on [Part 1: Expressions](./query-dsl-reified-optics.md). You should be comfortable building `SchemaExpr` values with optic operators (`===`, `>`, `&&`, etc.).
|
|
44
44
|
|
|
45
45
|
```scala
|
|
46
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.
|
|
46
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.55"
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
```scala
|
|
@@ -74,24 +74,33 @@ object Product extends CompanionOptics[Product] {
|
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
## The SchemaExpr
|
|
77
|
+
## The SchemaExpr API
|
|
78
78
|
|
|
79
|
-
Before we build the interpreter,
|
|
79
|
+
Before we build the interpreter, keep the API boundary in mind: application code builds `SchemaExpr[A, B]` values, while interpreter code may inspect the underlying `DynamicSchemaExpr` through `.dynamic`.
|
|
80
80
|
|
|
81
81
|
```
|
|
82
|
-
SchemaExpr[A, B]
|
|
83
|
-
|
|
84
|
-
├──
|
|
85
|
-
├──
|
|
86
|
-
├──
|
|
87
|
-
├──
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
├──
|
|
91
|
-
├──
|
|
92
|
-
|
|
93
|
-
|
|
82
|
+
SchemaExpr[A, B] -- user-facing, typed API
|
|
83
|
+
└── .dynamic: DynamicSchemaExpr -- interpreter/runtime boundary
|
|
84
|
+
├── Select(path: DynamicOptic) -- field reference
|
|
85
|
+
├── Literal(value: DynamicValue, schema: Schema[_]) -- constant value
|
|
86
|
+
├── Relational(left, right, op) -- comparisons
|
|
87
|
+
├── Logical(left, right, op) -- boolean operators
|
|
88
|
+
├── Not(expr) -- negation
|
|
89
|
+
├── Arithmetic(left, right, op, _) -- numeric operators
|
|
90
|
+
├── StringConcat(left, right) -- string concatenation
|
|
91
|
+
├── StringRegexMatch(regex, string) -- pattern matching
|
|
92
|
+
└── StringLength(string) -- string length
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Most users never need to construct `DynamicSchemaExpr` directly. The normal workflow is:
|
|
96
|
+
|
|
97
|
+
1. Build a typed `SchemaExpr` with optics and operators.
|
|
98
|
+
2. Pass that `SchemaExpr` to your interpreter.
|
|
99
|
+
3. Let the interpreter read `.dynamic` internally.
|
|
94
100
|
|
|
101
|
+
The dynamic cases are still worth understanding because they are what your interpreter will pattern-match on:
|
|
102
|
+
|
|
103
|
+
```
|
|
95
104
|
RelationalOperator
|
|
96
105
|
├── LessThan
|
|
97
106
|
├── GreaterThan
|
|
@@ -110,7 +119,7 @@ ArithmeticOperator
|
|
|
110
119
|
└── Multiply
|
|
111
120
|
```
|
|
112
121
|
|
|
113
|
-
Each case carries enough information to produce SQL: `
|
|
122
|
+
Each dynamic case carries enough information to produce SQL: `Select` nodes carry field paths, `Literal` nodes carry values, and operator nodes carry the operation type. Our interpreter walks this tree and emits SQL fragments.
|
|
114
123
|
|
|
115
124
|
## Extracting Column Names from Optics
|
|
116
125
|
|
|
@@ -121,6 +130,9 @@ def columnName(optic: zio.blocks.schema.Optic[?, ?]): String = {
|
|
|
121
130
|
val nodes = optic.toDynamic.nodes
|
|
122
131
|
nodes.collect { case f: DynamicOptic.Node.Field => f.name }.mkString("_")
|
|
123
132
|
}
|
|
133
|
+
|
|
134
|
+
def columnName(path: DynamicOptic): String =
|
|
135
|
+
path.nodes.collect { case f: DynamicOptic.Node.Field => f.name }.mkString("_")
|
|
124
136
|
```
|
|
125
137
|
|
|
126
138
|
This converts the optic path to a column name. For a simple field like `Product.price`, it produces `"price"`. For a nested path, it joins field names with underscores (we will refine this for table-qualified names later).
|
|
@@ -136,92 +148,106 @@ columnName(Product.category)
|
|
|
136
148
|
|
|
137
149
|
## Translating Literals to SQL
|
|
138
150
|
|
|
139
|
-
|
|
151
|
+
Once we cross the interpreter boundary, literal values appear as `DynamicValue`. We need a function to format them as SQL:
|
|
140
152
|
|
|
141
153
|
```scala
|
|
142
|
-
def
|
|
143
|
-
case
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
def sqlLiteralDV(dv: DynamicValue): String = dv match {
|
|
155
|
+
case DynamicValue.Primitive(pv) =>
|
|
156
|
+
pv match {
|
|
157
|
+
case PrimitiveValue.String(s) => s"'${s.replace("'", "''")}'"
|
|
158
|
+
case PrimitiveValue.Boolean(b) => if (b) "TRUE" else "FALSE"
|
|
159
|
+
case PrimitiveValue.Int(n) => n.toString
|
|
160
|
+
case PrimitiveValue.Long(n) => n.toString
|
|
161
|
+
case PrimitiveValue.Double(n) => n.toString
|
|
162
|
+
case PrimitiveValue.Float(n) => n.toString
|
|
163
|
+
case PrimitiveValue.Short(n) => n.toString
|
|
164
|
+
case PrimitiveValue.Byte(n) => n.toString
|
|
165
|
+
case other => other.toString
|
|
166
|
+
}
|
|
167
|
+
case other => other.toString
|
|
147
168
|
}
|
|
148
169
|
```
|
|
149
170
|
|
|
150
171
|
## Building the SQL Interpreter
|
|
151
172
|
|
|
152
|
-
Now we build the core interpreter.
|
|
173
|
+
Now we build the core interpreter. The public entry point accepts `SchemaExpr`; the internal helper does the `DynamicSchemaExpr` pattern matching:
|
|
153
174
|
|
|
154
175
|
```scala
|
|
155
|
-
def toSql[A, B](expr: SchemaExpr[A, B]): String = expr
|
|
176
|
+
def toSql[A, B](expr: SchemaExpr[A, B]): String = toSqlDynamic(expr.dynamic)
|
|
177
|
+
|
|
178
|
+
private def toSqlDynamic(expr: DynamicSchemaExpr): String = expr match {
|
|
156
179
|
|
|
157
180
|
// Field reference → column name
|
|
158
|
-
case
|
|
159
|
-
columnName(
|
|
181
|
+
case DynamicSchemaExpr.Select(path) =>
|
|
182
|
+
columnName(path)
|
|
160
183
|
|
|
161
184
|
// Constant value → SQL literal
|
|
162
|
-
case
|
|
163
|
-
|
|
185
|
+
case DynamicSchemaExpr.Literal(value, _) =>
|
|
186
|
+
sqlLiteralDV(value)
|
|
164
187
|
|
|
165
188
|
// Comparison operators → SQL relational operators
|
|
166
|
-
case
|
|
189
|
+
case DynamicSchemaExpr.Relational(left, right, op) =>
|
|
167
190
|
val sqlOp = op match {
|
|
168
|
-
case
|
|
169
|
-
case
|
|
170
|
-
case
|
|
171
|
-
case
|
|
172
|
-
case
|
|
173
|
-
case
|
|
191
|
+
case DynamicSchemaExpr.RelationalOperator.Equal => "="
|
|
192
|
+
case DynamicSchemaExpr.RelationalOperator.NotEqual => "<>"
|
|
193
|
+
case DynamicSchemaExpr.RelationalOperator.LessThan => "<"
|
|
194
|
+
case DynamicSchemaExpr.RelationalOperator.LessThanOrEqual => "<="
|
|
195
|
+
case DynamicSchemaExpr.RelationalOperator.GreaterThan => ">"
|
|
196
|
+
case DynamicSchemaExpr.RelationalOperator.GreaterThanOrEqual => ">="
|
|
174
197
|
}
|
|
175
|
-
s"(${
|
|
198
|
+
s"(${toSqlDynamic(left)} $sqlOp ${toSqlDynamic(right)})"
|
|
176
199
|
|
|
177
200
|
// Boolean operators → AND / OR
|
|
178
|
-
case
|
|
201
|
+
case DynamicSchemaExpr.Logical(left, right, op) =>
|
|
179
202
|
val sqlOp = op match {
|
|
180
|
-
case
|
|
181
|
-
case
|
|
203
|
+
case DynamicSchemaExpr.LogicalOperator.And => "AND"
|
|
204
|
+
case DynamicSchemaExpr.LogicalOperator.Or => "OR"
|
|
182
205
|
}
|
|
183
|
-
s"(${
|
|
206
|
+
s"(${toSqlDynamic(left)} $sqlOp ${toSqlDynamic(right)})"
|
|
184
207
|
|
|
185
208
|
// Negation → NOT
|
|
186
|
-
case
|
|
187
|
-
s"NOT (${
|
|
209
|
+
case DynamicSchemaExpr.Not(inner) =>
|
|
210
|
+
s"NOT (${toSqlDynamic(inner)})"
|
|
188
211
|
|
|
189
212
|
// Arithmetic → SQL math operators
|
|
190
|
-
case
|
|
213
|
+
case DynamicSchemaExpr.Arithmetic(left, right, op, _) =>
|
|
191
214
|
val sqlOp = op match {
|
|
192
|
-
case
|
|
193
|
-
case
|
|
194
|
-
case
|
|
215
|
+
case DynamicSchemaExpr.ArithmeticOperator.Add => "+"
|
|
216
|
+
case DynamicSchemaExpr.ArithmeticOperator.Subtract => "-"
|
|
217
|
+
case DynamicSchemaExpr.ArithmeticOperator.Multiply => "*"
|
|
218
|
+
case _ => "?"
|
|
195
219
|
}
|
|
196
|
-
s"(${
|
|
220
|
+
s"(${toSqlDynamic(left)} $sqlOp ${toSqlDynamic(right)})"
|
|
197
221
|
|
|
198
222
|
// String concatenation → CONCAT()
|
|
199
|
-
case
|
|
200
|
-
s"CONCAT(${
|
|
223
|
+
case DynamicSchemaExpr.StringConcat(left, right) =>
|
|
224
|
+
s"CONCAT(${toSqlDynamic(left)}, ${toSqlDynamic(right)})"
|
|
201
225
|
|
|
202
226
|
// Regex match → column LIKE pattern (simplified)
|
|
203
|
-
case
|
|
204
|
-
s"(${
|
|
227
|
+
case DynamicSchemaExpr.StringRegexMatch(regex, string) =>
|
|
228
|
+
s"(${toSqlDynamic(string)} LIKE ${toSqlDynamic(regex)})"
|
|
205
229
|
|
|
206
230
|
// String length → LENGTH()
|
|
207
|
-
case
|
|
208
|
-
s"LENGTH(${
|
|
231
|
+
case DynamicSchemaExpr.StringLength(string) =>
|
|
232
|
+
s"LENGTH(${toSqlDynamic(string)})"
|
|
233
|
+
|
|
234
|
+
case _ => "?"
|
|
209
235
|
}
|
|
210
236
|
```
|
|
211
237
|
|
|
212
|
-
The mapping from `
|
|
238
|
+
The mapping from `DynamicSchemaExpr` to SQL is direct, but that dynamic matching stays inside the interpreter implementation:
|
|
213
239
|
|
|
214
|
-
|
|
|
215
|
-
|
|
216
|
-
| `
|
|
217
|
-
| `Literal(
|
|
218
|
-
| `Relational(_, _, op)` | `=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
219
|
-
| `Logical(_, _, op)`
|
|
220
|
-
| `Not(expr)`
|
|
221
|
-
| `Arithmetic(_, _, op, _)` | `+`, `-`, `*`
|
|
222
|
-
| `StringConcat`
|
|
223
|
-
| `StringRegexMatch`
|
|
224
|
-
| `StringLength`
|
|
240
|
+
| DynamicSchemaExpr Case | SQL Output |
|
|
241
|
+
|------------------------|--------------------------------------|
|
|
242
|
+
| `Select(path)` | Column name from `DynamicOptic` |
|
|
243
|
+
| `Literal(value, schema)` | SQL literal (`'text'`, `42`, `TRUE`) |
|
|
244
|
+
| `Relational(_, _, op)` | `=`, `<>`, `<`, `>`, `<=`, `>=` |
|
|
245
|
+
| `Logical(_, _, op)` | `AND`, `OR` |
|
|
246
|
+
| `Not(expr)` | `NOT (...)` |
|
|
247
|
+
| `Arithmetic(_, _, op, _)` | `+`, `-`, `*` |
|
|
248
|
+
| `StringConcat` | `CONCAT(a, b)` |
|
|
249
|
+
| `StringRegexMatch` | `LIKE` (pattern matching) |
|
|
250
|
+
| `StringLength` | `LENGTH(col)` |
|
|
225
251
|
|
|
226
252
|
## Generating SQL from Queries
|
|
227
253
|
|
|
@@ -367,63 +393,80 @@ The `toSql` function above inlines literal values directly into the SQL string.
|
|
|
367
393
|
```scala
|
|
368
394
|
case class SqlQuery(sql: String, params: List[Any])
|
|
369
395
|
|
|
370
|
-
def toParameterized[A, B](expr: SchemaExpr[A, B]): SqlQuery = expr
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
396
|
+
def toParameterized[A, B](expr: SchemaExpr[A, B]): SqlQuery = toParameterizedDynamic(expr.dynamic)
|
|
397
|
+
|
|
398
|
+
private def toParameterizedDynamic(expr: DynamicSchemaExpr): SqlQuery = expr match {
|
|
399
|
+
|
|
400
|
+
case DynamicSchemaExpr.Select(path) =>
|
|
401
|
+
SqlQuery(columnName(path), Nil)
|
|
402
|
+
|
|
403
|
+
case DynamicSchemaExpr.Literal(value, _) =>
|
|
404
|
+
val param = value match {
|
|
405
|
+
case DynamicValue.Primitive(pv) => pv match {
|
|
406
|
+
case PrimitiveValue.String(s) => s
|
|
407
|
+
case PrimitiveValue.Boolean(b) => b
|
|
408
|
+
case PrimitiveValue.Int(n) => n
|
|
409
|
+
case PrimitiveValue.Long(n) => n
|
|
410
|
+
case PrimitiveValue.Double(n) => n
|
|
411
|
+
case PrimitiveValue.Float(n) => n
|
|
412
|
+
case PrimitiveValue.Short(n) => n
|
|
413
|
+
case PrimitiveValue.Byte(n) => n
|
|
414
|
+
case PrimitiveValue.BigInt(n) => n
|
|
415
|
+
case PrimitiveValue.BigDecimal(n) => n
|
|
416
|
+
case PrimitiveValue.Char(c) => c
|
|
417
|
+
case other => other.toString
|
|
418
|
+
}
|
|
419
|
+
case other => other.toString
|
|
420
|
+
}
|
|
421
|
+
SqlQuery("?", List(param))
|
|
377
422
|
|
|
378
|
-
case
|
|
379
|
-
val l =
|
|
380
|
-
val r = toParameterized(right)
|
|
423
|
+
case DynamicSchemaExpr.Relational(left, right, op) =>
|
|
424
|
+
val l = toParameterizedDynamic(left); val r = toParameterizedDynamic(right)
|
|
381
425
|
val sqlOp = op match {
|
|
382
|
-
case
|
|
383
|
-
case
|
|
384
|
-
case
|
|
385
|
-
case
|
|
386
|
-
case
|
|
387
|
-
case
|
|
426
|
+
case DynamicSchemaExpr.RelationalOperator.Equal => "="
|
|
427
|
+
case DynamicSchemaExpr.RelationalOperator.NotEqual => "<>"
|
|
428
|
+
case DynamicSchemaExpr.RelationalOperator.LessThan => "<"
|
|
429
|
+
case DynamicSchemaExpr.RelationalOperator.LessThanOrEqual => "<="
|
|
430
|
+
case DynamicSchemaExpr.RelationalOperator.GreaterThan => ">"
|
|
431
|
+
case DynamicSchemaExpr.RelationalOperator.GreaterThanOrEqual => ">="
|
|
388
432
|
}
|
|
389
433
|
SqlQuery(s"(${l.sql} $sqlOp ${r.sql})", l.params ++ r.params)
|
|
390
434
|
|
|
391
|
-
case
|
|
392
|
-
val l =
|
|
393
|
-
val r = toParameterized(right)
|
|
435
|
+
case DynamicSchemaExpr.Logical(left, right, op) =>
|
|
436
|
+
val l = toParameterizedDynamic(left); val r = toParameterizedDynamic(right)
|
|
394
437
|
val sqlOp = op match {
|
|
395
|
-
case
|
|
396
|
-
case
|
|
438
|
+
case DynamicSchemaExpr.LogicalOperator.And => "AND"
|
|
439
|
+
case DynamicSchemaExpr.LogicalOperator.Or => "OR"
|
|
397
440
|
}
|
|
398
441
|
SqlQuery(s"(${l.sql} $sqlOp ${r.sql})", l.params ++ r.params)
|
|
399
442
|
|
|
400
|
-
case
|
|
401
|
-
val i =
|
|
443
|
+
case DynamicSchemaExpr.Not(inner) =>
|
|
444
|
+
val i = toParameterizedDynamic(inner)
|
|
402
445
|
SqlQuery(s"NOT (${i.sql})", i.params)
|
|
403
446
|
|
|
404
|
-
case
|
|
405
|
-
val l =
|
|
406
|
-
val r = toParameterized(right)
|
|
447
|
+
case DynamicSchemaExpr.Arithmetic(left, right, op, _) =>
|
|
448
|
+
val l = toParameterizedDynamic(left); val r = toParameterizedDynamic(right)
|
|
407
449
|
val sqlOp = op match {
|
|
408
|
-
case
|
|
409
|
-
case
|
|
410
|
-
case
|
|
450
|
+
case DynamicSchemaExpr.ArithmeticOperator.Add => "+"
|
|
451
|
+
case DynamicSchemaExpr.ArithmeticOperator.Subtract => "-"
|
|
452
|
+
case DynamicSchemaExpr.ArithmeticOperator.Multiply => "*"
|
|
453
|
+
case _ => "?"
|
|
411
454
|
}
|
|
412
455
|
SqlQuery(s"(${l.sql} $sqlOp ${r.sql})", l.params ++ r.params)
|
|
413
456
|
|
|
414
|
-
case
|
|
415
|
-
val l =
|
|
416
|
-
val r = toParameterized(right)
|
|
457
|
+
case DynamicSchemaExpr.StringConcat(left, right) =>
|
|
458
|
+
val l = toParameterizedDynamic(left); val r = toParameterizedDynamic(right)
|
|
417
459
|
SqlQuery(s"CONCAT(${l.sql}, ${r.sql})", l.params ++ r.params)
|
|
418
460
|
|
|
419
|
-
case
|
|
420
|
-
val s =
|
|
421
|
-
val r = toParameterized(regex)
|
|
461
|
+
case DynamicSchemaExpr.StringRegexMatch(regex, string) =>
|
|
462
|
+
val s = toParameterizedDynamic(string); val r = toParameterizedDynamic(regex)
|
|
422
463
|
SqlQuery(s"(${s.sql} LIKE ${r.sql})", s.params ++ r.params)
|
|
423
464
|
|
|
424
|
-
case
|
|
425
|
-
val s =
|
|
465
|
+
case DynamicSchemaExpr.StringLength(string) =>
|
|
466
|
+
val s = toParameterizedDynamic(string)
|
|
426
467
|
SqlQuery(s"LENGTH(${s.sql})", s.params)
|
|
468
|
+
|
|
469
|
+
case _ => SqlQuery("?", Nil)
|
|
427
470
|
}
|
|
428
471
|
```
|
|
429
472
|
|
|
@@ -549,90 +592,128 @@ object Product extends CompanionOptics[Product] {
|
|
|
549
592
|
def columnName(optic: zio.blocks.schema.Optic[?, ?]): String =
|
|
550
593
|
optic.toDynamic.nodes.collect { case f: DynamicOptic.Node.Field => f.name }.mkString("_")
|
|
551
594
|
|
|
552
|
-
def
|
|
553
|
-
case
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
case
|
|
595
|
+
def columnName(path: DynamicOptic): String =
|
|
596
|
+
path.nodes.collect { case f: DynamicOptic.Node.Field => f.name }.mkString("_")
|
|
597
|
+
|
|
598
|
+
def sqlLiteralDV(dv: DynamicValue): String = dv match {
|
|
599
|
+
case DynamicValue.Primitive(pv) =>
|
|
600
|
+
pv match {
|
|
601
|
+
case PrimitiveValue.String(s) => s"'${s.replace("'", "''")}'"
|
|
602
|
+
case PrimitiveValue.Boolean(b) => if (b) "TRUE" else "FALSE"
|
|
603
|
+
case PrimitiveValue.Int(n) => n.toString
|
|
604
|
+
case PrimitiveValue.Long(n) => n.toString
|
|
605
|
+
case PrimitiveValue.Double(n) => n.toString
|
|
606
|
+
case PrimitiveValue.Float(n) => n.toString
|
|
607
|
+
case PrimitiveValue.Short(n) => n.toString
|
|
608
|
+
case PrimitiveValue.Byte(n) => n.toString
|
|
609
|
+
case other => other.toString
|
|
610
|
+
}
|
|
611
|
+
case other => other.toString
|
|
557
612
|
}
|
|
558
613
|
|
|
559
|
-
def toSql[A, B](expr: SchemaExpr[A, B]): String = expr
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
case
|
|
614
|
+
def toSql[A, B](expr: SchemaExpr[A, B]): String = toSqlDynamic(expr.dynamic)
|
|
615
|
+
|
|
616
|
+
private def toSqlDynamic(expr: DynamicSchemaExpr): String = expr match {
|
|
617
|
+
case DynamicSchemaExpr.Select(path) => columnName(path)
|
|
618
|
+
case DynamicSchemaExpr.Literal(value, _) => sqlLiteralDV(value)
|
|
619
|
+
case DynamicSchemaExpr.Relational(left, right, op) =>
|
|
563
620
|
val sqlOp = op match {
|
|
564
|
-
case
|
|
565
|
-
case
|
|
566
|
-
case
|
|
567
|
-
case
|
|
568
|
-
case
|
|
569
|
-
case
|
|
621
|
+
case DynamicSchemaExpr.RelationalOperator.Equal => "="
|
|
622
|
+
case DynamicSchemaExpr.RelationalOperator.NotEqual => "<>"
|
|
623
|
+
case DynamicSchemaExpr.RelationalOperator.LessThan => "<"
|
|
624
|
+
case DynamicSchemaExpr.RelationalOperator.LessThanOrEqual => "<="
|
|
625
|
+
case DynamicSchemaExpr.RelationalOperator.GreaterThan => ">"
|
|
626
|
+
case DynamicSchemaExpr.RelationalOperator.GreaterThanOrEqual => ">="
|
|
570
627
|
}
|
|
571
|
-
s"(${
|
|
572
|
-
case
|
|
628
|
+
s"(${toSqlDynamic(left)} $sqlOp ${toSqlDynamic(right)})"
|
|
629
|
+
case DynamicSchemaExpr.Logical(left, right, op) =>
|
|
573
630
|
val sqlOp = op match {
|
|
574
|
-
case
|
|
575
|
-
case
|
|
631
|
+
case DynamicSchemaExpr.LogicalOperator.And => "AND"
|
|
632
|
+
case DynamicSchemaExpr.LogicalOperator.Or => "OR"
|
|
576
633
|
}
|
|
577
|
-
s"(${
|
|
578
|
-
case
|
|
579
|
-
case
|
|
634
|
+
s"(${toSqlDynamic(left)} $sqlOp ${toSqlDynamic(right)})"
|
|
635
|
+
case DynamicSchemaExpr.Not(inner) => s"NOT (${toSqlDynamic(inner)})"
|
|
636
|
+
case DynamicSchemaExpr.Arithmetic(left, right, op, _) =>
|
|
580
637
|
val sqlOp = op match {
|
|
581
|
-
case
|
|
582
|
-
case
|
|
583
|
-
case
|
|
638
|
+
case DynamicSchemaExpr.ArithmeticOperator.Add => "+"
|
|
639
|
+
case DynamicSchemaExpr.ArithmeticOperator.Subtract => "-"
|
|
640
|
+
case DynamicSchemaExpr.ArithmeticOperator.Multiply => "*"
|
|
641
|
+
case _ => "?"
|
|
584
642
|
}
|
|
585
|
-
s"(${
|
|
586
|
-
case
|
|
587
|
-
case
|
|
588
|
-
case
|
|
643
|
+
s"(${toSqlDynamic(left)} $sqlOp ${toSqlDynamic(right)})"
|
|
644
|
+
case DynamicSchemaExpr.StringConcat(left, right) => s"CONCAT(${toSqlDynamic(left)}, ${toSqlDynamic(right)})"
|
|
645
|
+
case DynamicSchemaExpr.StringRegexMatch(regex, string) => s"(${toSqlDynamic(string)} LIKE ${toSqlDynamic(regex)})"
|
|
646
|
+
case DynamicSchemaExpr.StringLength(string) => s"LENGTH(${toSqlDynamic(string)})"
|
|
647
|
+
case _ => "?"
|
|
589
648
|
}
|
|
590
649
|
|
|
591
650
|
// --- Parameterized queries ---
|
|
592
651
|
|
|
593
652
|
case class SqlQuery(sql: String, params: List[Any])
|
|
594
653
|
|
|
595
|
-
def toParameterized[A, B](expr: SchemaExpr[A, B]): SqlQuery = expr
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
case
|
|
599
|
-
|
|
654
|
+
def toParameterized[A, B](expr: SchemaExpr[A, B]): SqlQuery = toParameterizedDynamic(expr.dynamic)
|
|
655
|
+
|
|
656
|
+
private def toParameterizedDynamic(expr: DynamicSchemaExpr): SqlQuery = expr match {
|
|
657
|
+
case DynamicSchemaExpr.Select(path) => SqlQuery(columnName(path), Nil)
|
|
658
|
+
case DynamicSchemaExpr.Literal(value, _) =>
|
|
659
|
+
val param = value match {
|
|
660
|
+
case DynamicValue.Primitive(pv) => pv match {
|
|
661
|
+
case PrimitiveValue.String(s) => s
|
|
662
|
+
case PrimitiveValue.Boolean(b) => b
|
|
663
|
+
case PrimitiveValue.Int(n) => n
|
|
664
|
+
case PrimitiveValue.Long(n) => n
|
|
665
|
+
case PrimitiveValue.Double(n) => n
|
|
666
|
+
case PrimitiveValue.Float(n) => n
|
|
667
|
+
case PrimitiveValue.Short(n) => n
|
|
668
|
+
case PrimitiveValue.Byte(n) => n
|
|
669
|
+
case PrimitiveValue.BigInt(n) => n
|
|
670
|
+
case PrimitiveValue.BigDecimal(n) => n
|
|
671
|
+
case PrimitiveValue.Char(c) => c
|
|
672
|
+
case other => other.toString
|
|
673
|
+
}
|
|
674
|
+
case other => other.toString
|
|
675
|
+
}
|
|
676
|
+
SqlQuery("?", List(param))
|
|
677
|
+
case DynamicSchemaExpr.Relational(left, right, op) =>
|
|
678
|
+
val l = toParameterizedDynamic(left); val r = toParameterizedDynamic(right)
|
|
600
679
|
val sqlOp = op match {
|
|
601
|
-
case
|
|
602
|
-
case
|
|
603
|
-
case
|
|
604
|
-
case
|
|
605
|
-
case
|
|
606
|
-
case
|
|
680
|
+
case DynamicSchemaExpr.RelationalOperator.Equal => "="
|
|
681
|
+
case DynamicSchemaExpr.RelationalOperator.NotEqual => "<>"
|
|
682
|
+
case DynamicSchemaExpr.RelationalOperator.LessThan => "<"
|
|
683
|
+
case DynamicSchemaExpr.RelationalOperator.LessThanOrEqual => "<="
|
|
684
|
+
case DynamicSchemaExpr.RelationalOperator.GreaterThan => ">"
|
|
685
|
+
case DynamicSchemaExpr.RelationalOperator.GreaterThanOrEqual => ">="
|
|
607
686
|
}
|
|
608
687
|
SqlQuery(s"(${l.sql} $sqlOp ${r.sql})", l.params ++ r.params)
|
|
609
|
-
case
|
|
610
|
-
val l =
|
|
688
|
+
case DynamicSchemaExpr.Logical(left, right, op) =>
|
|
689
|
+
val l = toParameterizedDynamic(left); val r = toParameterizedDynamic(right)
|
|
611
690
|
val sqlOp = op match {
|
|
612
|
-
case
|
|
613
|
-
case
|
|
691
|
+
case DynamicSchemaExpr.LogicalOperator.And => "AND"
|
|
692
|
+
case DynamicSchemaExpr.LogicalOperator.Or => "OR"
|
|
614
693
|
}
|
|
615
694
|
SqlQuery(s"(${l.sql} $sqlOp ${r.sql})", l.params ++ r.params)
|
|
616
|
-
case
|
|
617
|
-
val i =
|
|
695
|
+
case DynamicSchemaExpr.Not(inner) =>
|
|
696
|
+
val i = toParameterizedDynamic(inner)
|
|
618
697
|
SqlQuery(s"NOT (${i.sql})", i.params)
|
|
619
|
-
case
|
|
620
|
-
val l =
|
|
698
|
+
case DynamicSchemaExpr.Arithmetic(left, right, op, _) =>
|
|
699
|
+
val l = toParameterizedDynamic(left); val r = toParameterizedDynamic(right)
|
|
621
700
|
val sqlOp = op match {
|
|
622
|
-
case
|
|
623
|
-
case
|
|
624
|
-
case
|
|
701
|
+
case DynamicSchemaExpr.ArithmeticOperator.Add => "+"
|
|
702
|
+
case DynamicSchemaExpr.ArithmeticOperator.Subtract => "-"
|
|
703
|
+
case DynamicSchemaExpr.ArithmeticOperator.Multiply => "*"
|
|
704
|
+
case _ => "?"
|
|
625
705
|
}
|
|
626
706
|
SqlQuery(s"(${l.sql} $sqlOp ${r.sql})", l.params ++ r.params)
|
|
627
|
-
case
|
|
628
|
-
val l =
|
|
707
|
+
case DynamicSchemaExpr.StringConcat(left, right) =>
|
|
708
|
+
val l = toParameterizedDynamic(left); val r = toParameterizedDynamic(right)
|
|
629
709
|
SqlQuery(s"CONCAT(${l.sql}, ${r.sql})", l.params ++ r.params)
|
|
630
|
-
case
|
|
631
|
-
val s =
|
|
710
|
+
case DynamicSchemaExpr.StringRegexMatch(regex, string) =>
|
|
711
|
+
val s = toParameterizedDynamic(string); val r = toParameterizedDynamic(regex)
|
|
632
712
|
SqlQuery(s"(${s.sql} LIKE ${r.sql})", s.params ++ r.params)
|
|
633
|
-
case
|
|
634
|
-
val s =
|
|
713
|
+
case DynamicSchemaExpr.StringLength(string) =>
|
|
714
|
+
val s = toParameterizedDynamic(string)
|
|
635
715
|
SqlQuery(s"LENGTH(${s.sql})", s.params)
|
|
716
|
+
case _ => SqlQuery("?", Nil)
|
|
636
717
|
}
|
|
637
718
|
|
|
638
719
|
// --- Complete SELECT builder ---
|
|
@@ -668,13 +749,407 @@ println(toSql(Product.price * 0.9))
|
|
|
668
749
|
// (price * 0.9)
|
|
669
750
|
```
|
|
670
751
|
|
|
752
|
+
## Upsert (ON CONFLICT)
|
|
753
|
+
|
|
754
|
+
Upsert (insert-or-update) combines an `INSERT` with a conflict handler so the
|
|
755
|
+
statement is idempotent. When a row with the conflicting key already exists, the
|
|
756
|
+
database either skips the insert or updates specified columns.
|
|
757
|
+
|
|
758
|
+
All identifiers (table name, column names, conflict column) are validated through
|
|
759
|
+
`SqlIdentifier.validate` and assignment columns are additionally checked against
|
|
760
|
+
`Table.columns`. Invalid or unknown names throw `IllegalArgumentException` at
|
|
761
|
+
build time, not at execution time.
|
|
762
|
+
|
|
763
|
+
### Table-aware builders
|
|
764
|
+
|
|
765
|
+
The high-level `Upsert` builders accept a `Table[A]` and an entity. The table
|
|
766
|
+
provides column names and a codec that extracts `DbValue` parameters.
|
|
767
|
+
|
|
768
|
+
`Upsert.insertDoNothing` builds `INSERT ... ON CONFLICT ("id") DO NOTHING`:
|
|
769
|
+
|
|
770
|
+
```scala
|
|
771
|
+
import zio.blocks.sql.*
|
|
772
|
+
import zio.blocks.schema.Schema
|
|
773
|
+
|
|
774
|
+
case class User(id: Int, name: String, email: String)
|
|
775
|
+
object User { implicit val schema: Schema[User] = Schema.derived }
|
|
776
|
+
|
|
777
|
+
val table: Table[User] = Table.derived[User]
|
|
778
|
+
val user = User(42, "Alice", "alice@example.com")
|
|
779
|
+
|
|
780
|
+
// INSERT INTO "user" ("id", "name", "email") VALUES (?, ?, ?) ON CONFLICT ("id") DO NOTHING
|
|
781
|
+
val frag: Frag = Upsert.insertDoNothing(table, user, conflictColumn = "id")
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
`Upsert.insertDoUpdate` builds `INSERT ... ON CONFLICT ("id") DO UPDATE SET`
|
|
785
|
+
for **all** non-conflict columns:
|
|
786
|
+
|
|
787
|
+
```scala
|
|
788
|
+
import zio.blocks.sql.*
|
|
789
|
+
import zio.blocks.schema.Schema
|
|
790
|
+
|
|
791
|
+
case class User(id: Int, name: String, email: String)
|
|
792
|
+
object User { implicit val schema: Schema[User] = Schema.derived }
|
|
793
|
+
|
|
794
|
+
val table: Table[User] = Table.derived[User]
|
|
795
|
+
val user = User(42, "Alice", "alice@example.com")
|
|
796
|
+
|
|
797
|
+
// INSERT INTO "user" ("id", "name", "email") VALUES (?, ?, ?)
|
|
798
|
+
// ON CONFLICT ("id") DO UPDATE SET "name" = ?, "email" = ?
|
|
799
|
+
val frag: Frag = Upsert.insertDoUpdate(table, user, conflictColumn = "id")
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
Pass `updateColumns` to restrict which columns are overwritten on conflict:
|
|
803
|
+
|
|
804
|
+
```scala
|
|
805
|
+
import zio.blocks.sql.*
|
|
806
|
+
import zio.blocks.schema.Schema
|
|
807
|
+
|
|
808
|
+
case class User(id: Int, name: String, email: String)
|
|
809
|
+
object User { implicit val schema: Schema[User] = Schema.derived }
|
|
810
|
+
|
|
811
|
+
val table: Table[User] = Table.derived[User]
|
|
812
|
+
val user = User(42, "Alice", "alice@example.com")
|
|
813
|
+
|
|
814
|
+
// Only "name" is updated on conflict; "email" keeps its original value
|
|
815
|
+
val frag: Frag = Upsert.insertDoUpdate(table, user, conflictColumn = "id", updateColumns = Seq("name"))
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### Low-level builders
|
|
819
|
+
|
|
820
|
+
When you need full control over column names and values (e.g. computed or
|
|
821
|
+
transformed data), use the low-level `Upsert.doNothing`, `Upsert.doNothingRaw`,
|
|
822
|
+
and `Upsert.doUpdate` builders:
|
|
823
|
+
|
|
824
|
+
```scala
|
|
825
|
+
import zio.blocks.sql.*
|
|
826
|
+
|
|
827
|
+
// Low-level DO NOTHING with explicit columns and values
|
|
828
|
+
val frag1: Frag = Upsert.doNothing(
|
|
829
|
+
tableName = "users",
|
|
830
|
+
columns = IndexedSeq("id", "name", "email"),
|
|
831
|
+
values = IndexedSeq(DbValue.DbInt(1), DbValue.DbString("Bob"), DbValue.DbString("bob@example.com")),
|
|
832
|
+
conflictColumn = "id"
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
// Comma-joined column string variant
|
|
836
|
+
val frag2: Frag = Upsert.doNothingRaw(
|
|
837
|
+
tableName = "users",
|
|
838
|
+
allColumns = "id, name, email",
|
|
839
|
+
values = IndexedSeq(DbValue.DbInt(1), DbValue.DbString("Bob"), DbValue.DbString("bob@example.com")),
|
|
840
|
+
conflictColumn = "id"
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
// Low-level DO UPDATE with explicit assignments
|
|
844
|
+
val frag3: Frag = Upsert.doUpdate(
|
|
845
|
+
tableName = "users",
|
|
846
|
+
columns = IndexedSeq("id", "name", "email"),
|
|
847
|
+
values = IndexedSeq(DbValue.DbInt(1), DbValue.DbString("Bob"), DbValue.DbString("bob@example.com")),
|
|
848
|
+
conflictColumn = "id",
|
|
849
|
+
assignments = IndexedSeq("name" -> DbValue.DbString("Bob"), "email" -> DbValue.DbString("bob@example.com"))
|
|
850
|
+
)
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### Suffix builders
|
|
854
|
+
|
|
855
|
+
To append an `ON CONFLICT` clause to an existing `INSERT` `Frag`, use the suffix
|
|
856
|
+
builders:
|
|
857
|
+
|
|
858
|
+
```scala
|
|
859
|
+
import zio.blocks.sql.*
|
|
860
|
+
|
|
861
|
+
val base: Frag = Frag.literal("INSERT INTO users (id, name) VALUES (?, ?)")
|
|
862
|
+
|
|
863
|
+
// Append DO NOTHING suffix
|
|
864
|
+
val withNothing: Frag = base ++ Upsert.doNothingSuffix(conflictColumn = "id")
|
|
865
|
+
|
|
866
|
+
// Append DO UPDATE suffix with explicit assignments
|
|
867
|
+
val withUpdate: Frag = base ++ Upsert.doUpdateSuffix(
|
|
868
|
+
conflictColumn = "id",
|
|
869
|
+
assignments = IndexedSeq("name" -> DbValue.DbString("updated"))
|
|
870
|
+
)
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
### Repository integration
|
|
874
|
+
|
|
875
|
+
`Repo` provides `insertOrUpdate` and `insertOrUpdateBatch` as convenience
|
|
876
|
+
wrappers that use `Upsert.insertDoUpdate` under the hood. The conflict target is
|
|
877
|
+
the repository's validated ID column, and all non-ID columns are overwritten with
|
|
878
|
+
the entity's values.
|
|
879
|
+
|
|
880
|
+
```scala
|
|
881
|
+
import zio.blocks.sql.*
|
|
882
|
+
import zio.blocks.schema.Schema
|
|
883
|
+
|
|
884
|
+
case class User(id: Int, name: String, email: String)
|
|
885
|
+
object User { implicit val schema: Schema[User] = Schema.derived }
|
|
886
|
+
|
|
887
|
+
given DbCon = ???
|
|
888
|
+
|
|
889
|
+
val table: Table[User] = Table.derived[User]
|
|
890
|
+
val repo: Repo[User, Int] = ???
|
|
891
|
+
|
|
892
|
+
val user = User(42, "Alice", "alice@example.com")
|
|
893
|
+
|
|
894
|
+
// Single upsert
|
|
895
|
+
val affected: Int = repo.insertOrUpdate(user)
|
|
896
|
+
|
|
897
|
+
// Batch upsert
|
|
898
|
+
val users: List[User] = List(user, User(43, "Bob", "bob@example.com"))
|
|
899
|
+
val totalAffected: Int = repo.insertOrUpdateBatch(users)
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
These generate SQL like:
|
|
903
|
+
|
|
904
|
+
```sql
|
|
905
|
+
INSERT INTO "user" ("id", "name", "email") VALUES (?, ?, ?)
|
|
906
|
+
ON CONFLICT ("id") DO UPDATE SET "name" = ?, "email" = ?
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
`insertOrUpdateBatch` uses a JDBC batch for efficiency, mirroring the pattern of
|
|
910
|
+
`insertBatch`. Both return the total affected row count.
|
|
911
|
+
|
|
912
|
+
## Keyset Pagination
|
|
913
|
+
|
|
914
|
+
Keyset (cursor) pagination avoids the cost and drift of `OFFSET` by seeking
|
|
915
|
+
after the last seen key: `WHERE id > ? ORDER BY id ASC LIMIT n`. The row
|
|
916
|
+
identified by the cursor is excluded (`>` not `>=`) so consecutive pages do
|
|
917
|
+
not duplicate the boundary row.
|
|
918
|
+
|
|
919
|
+
`Repo` exposes this directly for primary-key cursors:
|
|
920
|
+
|
|
921
|
+
```scala
|
|
922
|
+
import zio.blocks.sql.*
|
|
923
|
+
import zio.blocks.schema.Schema
|
|
924
|
+
|
|
925
|
+
case class User(id: Int, name: String, email: String)
|
|
926
|
+
object User { implicit val schema: Schema[User] = Schema.derived }
|
|
927
|
+
|
|
928
|
+
given DbCon = ???
|
|
929
|
+
|
|
930
|
+
val repo: Repo[User, Int] = ??? // e.g. Repo(table, "id", idCodec, _.id)
|
|
931
|
+
|
|
932
|
+
val firstPage: List[User] = repo.pageAfter(cursorId = 0, limit = 20)
|
|
933
|
+
val nextPage: List[User] = repo.pageAfter(cursorId = firstPage.last.id, limit = 20)
|
|
934
|
+
// when cursorId == last id, nextPage is empty
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
It renders as:
|
|
938
|
+
|
|
939
|
+
```sql
|
|
940
|
+
SELECT "id", "name", "email" FROM "user" WHERE "id" > ? ORDER BY "id" ASC LIMIT 20
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
where `?` is bound via `idCodec.toDbValues(cursorId)`. `limit` must be `> 0`.
|
|
944
|
+
|
|
945
|
+
For non-ID orderings or ad-hoc queries, `Frag.keysetAfter` builds the
|
|
946
|
+
portable `WHERE col > ? ORDER BY col ASC LIMIT n` fragment without a `Repo`:
|
|
947
|
+
|
|
948
|
+
```scala
|
|
949
|
+
import zio.blocks.sql.*
|
|
950
|
+
|
|
951
|
+
case class User(id: Int, name: String, email: String)
|
|
952
|
+
import zio.blocks.schema.Schema
|
|
953
|
+
object User { implicit val schema: Schema[User] = Schema.derived }
|
|
954
|
+
val table: Table[User] = Table.derived[User]
|
|
955
|
+
|
|
956
|
+
// Table-validated: rejects unknown columns
|
|
957
|
+
val frag: Frag = Frag.keysetAfter(table, orderCol = "id", lastValue = DbValue.DbInt(42), limit = 20)
|
|
958
|
+
// frag.sql(dialect) == " WHERE id > ? ORDER BY id ASC LIMIT 20"
|
|
959
|
+
|
|
960
|
+
// Without a table: identifier-only validation
|
|
961
|
+
val frag2: Frag = Frag.keysetAfter(orderCol = "created_at", lastValue = DbValue.DbLong(1000L), limit = 10)
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
- `Frag.keysetAfter(table, orderCol, lastValue, limit)` validates `orderCol`
|
|
965
|
+
with `SqlIdentifier.validate` and checks membership in `table.columns`;
|
|
966
|
+
unknown columns throw `IllegalArgumentException`.
|
|
967
|
+
- `Frag.keysetAfter(orderCol, lastValue, limit)` validates the identifier only.
|
|
968
|
+
- `limit` must be `> 0`; single-column cursors only (composite cursors are v2).
|
|
969
|
+
|
|
970
|
+
Compose with a base `SELECT`:
|
|
971
|
+
|
|
972
|
+
```scala
|
|
973
|
+
import zio.blocks.sql.*
|
|
974
|
+
import zio.blocks.schema.Schema
|
|
975
|
+
|
|
976
|
+
case class User(id: Int, name: String, email: String)
|
|
977
|
+
object User { implicit val schema: Schema[User] = Schema.derived }
|
|
978
|
+
val table: Table[User] = Table.derived[User]
|
|
979
|
+
|
|
980
|
+
val base = Frag.literal("SELECT id, name, email FROM user")
|
|
981
|
+
val pageFrag = base ++ Frag.keysetAfter(table, "id", DbValue.DbInt(42), 20)
|
|
982
|
+
// Rendering the SQL does not require a DbCon:
|
|
983
|
+
val sql: String = pageFrag.sql(SqlDialect.SQLite) // SELECT id, name, email FROM user WHERE id > ? ORDER BY id ASC LIMIT 20
|
|
984
|
+
// Executing needs givens at the call site:
|
|
985
|
+
// given DbCon = ???
|
|
986
|
+
// given DbCodec[User] = table.codec
|
|
987
|
+
// val rows: List[User] = pageFrag.query[User]
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
## Inspecting SQL
|
|
991
|
+
|
|
992
|
+
The custom interpreter above produces raw strings useful for debugging. The `sql` module's query IR (`zio.blocks.sql.query.SqlQuery`) provides richer inspection APIs.
|
|
993
|
+
|
|
994
|
+
### explain(dialect): String
|
|
995
|
+
|
|
996
|
+
The `explain` method renders the full SQL text with numbered parameter placeholders (`?1`, `?2`, ...) and a trailing comment listing each parameter's position and type:
|
|
997
|
+
|
|
998
|
+
```scala
|
|
999
|
+
import zio.blocks.sql._
|
|
1000
|
+
import zio.blocks.sql.query.{Rel, SqlQuery => Select}
|
|
1001
|
+
|
|
1002
|
+
val userTable = Table.derived[User]
|
|
1003
|
+
val repoTable = Table.derived[Repo]
|
|
1004
|
+
|
|
1005
|
+
val q = Select
|
|
1006
|
+
.from(userTable)
|
|
1007
|
+
.innerJoin(Rel(repoTable, "owner_id", userTable, "id"))
|
|
1008
|
+
.filter(Frag(IndexedSeq("""t0."name" = """, ""), IndexedSeq(DbValue.DbString("alice"))))
|
|
1009
|
+
|
|
1010
|
+
println(q.explain(SqlDialect.PostgreSQL))
|
|
1011
|
+
// SELECT t0."id", t0."name", t1."id", t1."owner_id", t1."name" FROM "user" AS t0 INNER JOIN "repo" AS t1 ON t1."owner_id" = t0."id" WHERE t0."name" = ?1
|
|
1012
|
+
// -- params: 1:String
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
`explain` renders a single-line SQL string with numbered `?N` placeholders. The `?N`
|
|
1016
|
+
placeholders correspond one-to-one with the parameter list you can obtain separately via
|
|
1017
|
+
`toFrag(dialect).params`. This makes `explain` useful for logging and visual debugging without touching a
|
|
1018
|
+
database.
|
|
1019
|
+
|
|
1020
|
+
### Inspecting the query IR
|
|
1021
|
+
|
|
1022
|
+
The query itself is a case class, so every part is available programmatically:
|
|
1023
|
+
|
|
1024
|
+
```scala
|
|
1025
|
+
q.source // Table[User] for "user"
|
|
1026
|
+
q.joins // Vector(JoinNode(repoTable, "t1", Inner, <on-frag>))
|
|
1027
|
+
q.filters // Vector(Frag(t0."name" = ?, DbString(alice)))
|
|
1028
|
+
q.groupBy // Vector.empty
|
|
1029
|
+
q.orderBy // Vector.empty
|
|
1030
|
+
q.limit // None
|
|
1031
|
+
q.offset // None
|
|
1032
|
+
q.toFrag // Frag (re-renderable to SQL via frag.sql(dialect))
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
Inspecting the IR directly lets you examine joins, filters, ordering, and limits programmatically. This is useful for building monitoring dashboards, query analyzers, or dynamic query modification layers.
|
|
1036
|
+
|
|
1037
|
+
### sql(dialect): String
|
|
1038
|
+
|
|
1039
|
+
The query IR also provides a simpler `sql` method that renders `?` placeholders for execution:
|
|
1040
|
+
|
|
1041
|
+
```scala
|
|
1042
|
+
println(q.sql(SqlDialect.PostgreSQL))
|
|
1043
|
+
// SELECT t0."id", t0."name", t1."id", t1."owner_id", t1."name" FROM "user" AS t0 INNER JOIN "repo" AS t1 ON t1."owner_id" = t0."id" WHERE t0."name" = ?
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
### previewSql() for Migrations
|
|
1047
|
+
|
|
1048
|
+
When using `SmallMigrator` or `LargeMigrator` from the `data-migration` module, `previewSql()` returns the full sequence of SQL statements the migrator would execute, without opening any database connection:
|
|
1049
|
+
|
|
1050
|
+
```scala
|
|
1051
|
+
import zio.blocks.data.migration._
|
|
1052
|
+
|
|
1053
|
+
given transactor: Transactor = tx
|
|
1054
|
+
|
|
1055
|
+
val migrator = SmallMigrator(
|
|
1056
|
+
repoV1 = userRepo,
|
|
1057
|
+
repoV2 = userRepoV2,
|
|
1058
|
+
migration = userMigration,
|
|
1059
|
+
queueTable = "migration_queue",
|
|
1060
|
+
batchSize = 100,
|
|
1061
|
+
target = TargetStrategy.InPlace
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
val statements: Vector[String] = migrator.previewSql()
|
|
1065
|
+
// Vector(
|
|
1066
|
+
// "CREATE TABLE ...", -- queue DDL
|
|
1067
|
+
// "CREATE TABLE ...", -- shadow table
|
|
1068
|
+
// "CREATE TRIGGER ...", -- capture triggers
|
|
1069
|
+
// "SELECT ...", -- dequeue template
|
|
1070
|
+
// "ALTER TABLE ..." -- finalize (rename)
|
|
1071
|
+
// )
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
This gives you a dry run of the migration SQL before any schema changes are applied.
|
|
1075
|
+
|
|
1076
|
+
## Compile-time SQL Dumps
|
|
1077
|
+
|
|
1078
|
+
The `Dump` object emits SQL files at compile time. When the JVM property `zib.sql.dumpDir` is set, inline macro calls to `Dump.dumpTable` or `Dump.dumpQuery` write `.sql` files to that directory. When the property is absent, the calls become no-ops with zero runtime cost.
|
|
1079
|
+
|
|
1080
|
+
### Enabling Dumps
|
|
1081
|
+
|
|
1082
|
+
The `Dump` macros read `System.getProperty("zib.sql.dumpDir")` at compile time from the JVM running sbt's compiler. This is a JVM system property, not a scalac flag. Passing it via `scalacOptions` does nothing.
|
|
1083
|
+
|
|
1084
|
+
The simplest way to set it is as a JVM flag on the sbt command line:
|
|
1085
|
+
|
|
1086
|
+
```bash
|
|
1087
|
+
sbt -Dzib.sql.dumpDir=target/sql-dumps "++3.8.3; sqlJVM/compile"
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
If you prefer not to type `-D` every time, export it through `SBT_OPTS`:
|
|
1091
|
+
|
|
1092
|
+
```bash
|
|
1093
|
+
SBT_OPTS="-Dzib.sql.dumpDir=target/sql-dumps" sbt compile
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
Both approaches set the property on the sbt process, which is the same JVM that runs the Scala compiler and the macros within it.
|
|
1097
|
+
|
|
1098
|
+
### Entry Points
|
|
1099
|
+
|
|
1100
|
+
There are two inline macro entry points, all in `zio.blocks.sql.Dump`:
|
|
1101
|
+
|
|
1102
|
+
```scala
|
|
1103
|
+
import zio.blocks.sql._
|
|
1104
|
+
|
|
1105
|
+
// Dump a Table's CREATE TABLE DDL (both PostgreSQL and SQLite)
|
|
1106
|
+
Dump.dumpTable(userTable)
|
|
1107
|
+
|
|
1108
|
+
// Dump a query IR's SELECT
|
|
1109
|
+
Dump.dumpQuery(queryIr)
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
Each call emits one file per dialect (PostgreSQL and SQLite by default).
|
|
1113
|
+
|
|
1114
|
+
### Naming Scheme
|
|
1115
|
+
|
|
1116
|
+
Files are named `<owner>-<dialect>.sql` where `<owner>` is derived from the enclosing symbol and `<dialect>` is the lowercased dialect name:
|
|
1117
|
+
|
|
1118
|
+
```
|
|
1119
|
+
target/sql-dumps/
|
|
1120
|
+
user-postgresql.sql
|
|
1121
|
+
user-sqlite.sql
|
|
1122
|
+
repo-postgresql.sql
|
|
1123
|
+
repo-sqlite.sql
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
For `dumpTable`, the owner comes from the `Table`'s type name. For `dumpQuery`, the macro walks the call site to extract the enclosing method, val name, or argument name. If the macro cannot determine a meaningful name, it falls back to `query`.
|
|
1127
|
+
|
|
1128
|
+
### Content-hash Skip
|
|
1129
|
+
|
|
1130
|
+
Each dump file is written only when its content differs from the existing file. The macro compares the new bytes against any existing file at the target path. If they match, the write is skipped. This means incremental compilations do not produce noisy diffs or unnecessary filesystem writes.
|
|
1131
|
+
|
|
1132
|
+
All files use UTF-8 encoding with a trailing newline.
|
|
1133
|
+
|
|
1134
|
+
### Limitations
|
|
1135
|
+
|
|
1136
|
+
Compile-time dumps work well for statically constructed queries, but some SQL patterns cannot be dumped:
|
|
1137
|
+
|
|
1138
|
+
- **Dynamic `Frag` chains.** Fragments built at runtime from user input, database lookups, or conditional branching are invisible to the macro. Only the static structure known at compile time appears in the dump.
|
|
1139
|
+
- **Repo internals.** The `Repo` abstraction's generated queries (insert, update, delete, select-by-id) are assembled at runtime from the `DbCodec` and `Table` metadata. `Dump.dumpTable` captures the DDL, but the CRUD queries themselves are not emitted.
|
|
1140
|
+
- **Phase 2 note.** A future phase may extend `Dump` to cover `Repo`-level CRUD operations and dynamic fragment composition. For now, treat the dump as a DDL and static-query snapshot, not a complete representation of every SQL statement your application will execute.
|
|
1141
|
+
|
|
1142
|
+
:::tip
|
|
1143
|
+
Pair `Dump.dumpTable` with `previewSql()` for a fuller picture: `dumpTable` captures the schema DDL at compile time, while `previewSql` captures the migration SQL at runtime before execution.
|
|
1144
|
+
:::
|
|
1145
|
+
|
|
671
1146
|
## Going Further
|
|
672
1147
|
|
|
673
1148
|
- **[Part 1: Expressions](./query-dsl-reified-optics.md)** -- Building query expressions with reified optics
|
|
674
1149
|
- **[Part 3: Extending the Expression Language](./query-dsl-extending.md)** -- Adding custom operators (IN, BETWEEN, aggregates) beyond SchemaExpr
|
|
675
1150
|
- **[Part 4: A Fluent SQL Builder](./query-dsl-fluent-builder.md)** -- Type-safe SELECT, UPDATE, INSERT, DELETE with seamless condition mixing
|
|
676
|
-
- **[SchemaExpr Reference](../reference/schema-expr.md)** -- Full API coverage of expression types
|
|
677
|
-
- **[Optics Reference](../reference/optics.md)** -- Lens, Prism, Optional, and Traversal
|
|
678
|
-
- **[DynamicOptic Reference](../reference/dynamic-optic.md)** -- Runtime optic paths for programmatic field extraction
|
|
1151
|
+
- **[SchemaExpr Reference](../reference/schema/schema-expr.md)** -- Full API coverage of expression types
|
|
1152
|
+
- **[Optics Reference](../reference/schema/optics.md)** -- Lens, Prism, Optional, and Traversal
|
|
1153
|
+
- **[DynamicOptic Reference](../reference/schema/dynamic-optic.md)** -- Runtime optic paths for programmatic field extraction
|
|
679
1154
|
|
|
680
|
-
The interpreter pattern shown here extends naturally to other query targets. Because `SchemaExpr`
|
|
1155
|
+
The interpreter pattern shown here extends naturally to other query targets. Because `SchemaExpr` wraps a `DynamicSchemaExpr` sealed trait and `DynamicOptic` carries full path metadata, you can write interpreters for MongoDB filters, Elasticsearch queries, GraphQL filters, or any other query language using the same approach: access `.dynamic`, pattern match on the AST, map operators, and extract field names from optic paths.
|