@zio.dev/zio-blocks 0.0.51 → 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.
Files changed (164) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +6 -0
  4. package/guides/getting-started-with-mux.md +0 -112
  5. package/guides/query-dsl-extending.md +1 -1
  6. package/guides/query-dsl-fluent-builder.md +1 -1
  7. package/guides/query-dsl-reified-optics.md +1 -1
  8. package/guides/query-dsl-sql.md +395 -1
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +131 -70
  12. package/guides/zio-schema-migration.md +6 -6
  13. package/index.md +200 -583
  14. package/package.json +1 -1
  15. package/reference/async.md +1379 -531
  16. package/reference/chunk.md +3 -3
  17. package/reference/codegen/index.md +1 -1
  18. package/reference/combinators.md +4 -4
  19. package/reference/config/config-decoder.md +460 -0
  20. package/reference/config/config-source.md +489 -0
  21. package/reference/config/errors.md +278 -0
  22. package/reference/config/flags.md +369 -0
  23. package/reference/config/formats.md +314 -0
  24. package/reference/config/index.md +304 -0
  25. package/reference/config/rollout.md +336 -0
  26. package/reference/context.md +6 -49
  27. package/reference/data-migration.md +269 -0
  28. package/reference/datastar/attributes.md +302 -0
  29. package/reference/datastar/events.md +234 -0
  30. package/reference/datastar/index.md +256 -0
  31. package/reference/datastar/signals.md +230 -0
  32. package/reference/datastar/sse.md +295 -0
  33. package/reference/datastar.md +2 -2
  34. package/reference/docs.md +2 -2
  35. package/reference/endpoint/bulk-creation.md +96 -0
  36. package/reference/endpoint/index.md +9 -89
  37. package/reference/endpoint/path-codec.md +12 -24
  38. package/reference/endpoint/route-pattern.md +4 -6
  39. package/reference/endpoint/segment-codec.md +19 -32
  40. package/reference/html.md +313 -9
  41. package/reference/htmx/index.md +4 -52
  42. package/reference/htmx/response-headers.md +240 -0
  43. package/reference/http-model/headers.md +735 -0
  44. package/reference/http-model/index.md +3 -1
  45. package/reference/http-model/model.md +107 -71
  46. package/reference/http-model/schema-codecs.md +522 -0
  47. package/reference/http-model/schema.md +6 -3
  48. package/reference/http-model/server-sent-event.md +341 -0
  49. package/reference/jwt.md +195 -0
  50. package/reference/maybe.md +128 -11
  51. package/reference/media-type.md +2 -2
  52. package/reference/mux.md +254 -0
  53. package/reference/mux.mdx +7 -2
  54. package/reference/openapi.md +3 -3
  55. package/reference/projection.md +654 -0
  56. package/reference/resource-management/resource.md +2 -98
  57. package/reference/resource-management/scope.md +1 -209
  58. package/reference/resource-management/wire.md +4 -50
  59. package/reference/ringbuffer/advanced.mdx +1 -1
  60. package/reference/ringbuffer/index.mdx +3 -3
  61. package/reference/ringbuffer/mpmc.mdx +38 -4
  62. package/reference/ringbuffer/mpsc.mdx +36 -4
  63. package/reference/ringbuffer/spmc.mdx +1 -1
  64. package/reference/ringbuffer/spsc.mdx +87 -15
  65. package/reference/schema/allows.md +0 -96
  66. package/reference/schema/binding.md +2 -2
  67. package/reference/schema/built-in-codecs/avro.md +2 -2
  68. package/reference/schema/built-in-codecs/bson.md +50 -20
  69. package/reference/schema/built-in-codecs/csv.md +2 -2
  70. package/reference/schema/built-in-codecs/index.md +3 -3
  71. package/reference/schema/built-in-codecs/json/index.md +2 -2
  72. package/reference/schema/built-in-codecs/messagepack.md +3 -3
  73. package/reference/schema/built-in-codecs/thrift.md +2 -2
  74. package/reference/schema/built-in-codecs/toon.md +3 -3
  75. package/reference/schema/built-in-codecs/yaml.md +2 -2
  76. package/reference/schema/codec.md +11 -11
  77. package/reference/schema/dynamic-optic.md +48 -3
  78. package/reference/schema/dynamic-schema.md +3 -3
  79. package/reference/schema/index.md +2 -0
  80. package/reference/schema/path-interpolator.md +2 -0
  81. package/reference/schema/reflect-transformer.md +140 -0
  82. package/reference/schema/schema-evolution/as.md +4 -4
  83. package/reference/schema/schema-evolution/into.md +2 -2
  84. package/reference/schema/schema-expr.md +2 -2
  85. package/reference/schema/schema-search.md +263 -0
  86. package/reference/schema/schema.md +10 -2
  87. package/reference/schema/type-class-derivation.md +1 -1
  88. package/reference/smithy.md +502 -3
  89. package/reference/sql/db-codec-deriver.md +3 -3
  90. package/reference/sql/db-codec.md +22 -22
  91. package/reference/sql/db-con.md +4 -4
  92. package/reference/sql/db-connection.md +1 -1
  93. package/reference/sql/db-param.md +1 -1
  94. package/reference/sql/db-result-reader.md +4 -2
  95. package/reference/sql/db-tx.md +46 -14
  96. package/reference/sql/ddl.md +1 -1
  97. package/reference/sql/frag.md +44 -10
  98. package/reference/sql/index.md +7 -7
  99. package/reference/sql/repo.md +15 -15
  100. package/reference/sql/sql-dialect.md +1 -1
  101. package/reference/sql/sql-logger.md +1 -1
  102. package/reference/sql/sql-name-mapper.md +3 -3
  103. package/reference/sql/table-metadata.md +3 -3
  104. package/reference/sql/table.md +10 -10
  105. package/reference/sql/transactor-zio.md +1 -1
  106. package/reference/sql/transactor.md +21 -11
  107. package/reference/sql-zio.md +1 -1
  108. package/reference/streams/core/index.md +32 -0
  109. package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
  110. package/reference/streams/{sink.md → core/sink.md} +331 -353
  111. package/reference/streams/{stream.md → core/stream.md} +919 -209
  112. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  113. package/reference/streams/execution-and-compatibility/index.md +35 -0
  114. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  115. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  116. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  117. package/reference/streams/index.md +140 -67
  118. package/reference/streams/primitives/index.md +30 -0
  119. package/reference/streams/primitives/reader.md +1992 -0
  120. package/reference/streams/{writer.md → primitives/writer.md} +254 -98
  121. package/reference/telemetry/common/any-value.md +90 -0
  122. package/reference/telemetry/common/attribute-key.md +87 -0
  123. package/reference/telemetry/common/attributes.md +118 -0
  124. package/reference/telemetry/common/index.md +39 -0
  125. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  126. package/reference/telemetry/common/resource.md +34 -0
  127. package/reference/telemetry/index.md +311 -0
  128. package/reference/telemetry/logging/index.md +197 -0
  129. package/reference/telemetry/logging/log-enrichment.md +72 -0
  130. package/reference/telemetry/logging/log-formatter.md +100 -0
  131. package/reference/telemetry/logging/log-record-processor.md +56 -0
  132. package/reference/telemetry/logging/log-record.md +44 -0
  133. package/reference/telemetry/logging/log-writer.md +64 -0
  134. package/reference/telemetry/logging/logger-provider.md +142 -0
  135. package/reference/telemetry/logging/logger.md +83 -0
  136. package/reference/telemetry/logging/severity.md +62 -0
  137. package/reference/telemetry/metrics/index.md +150 -0
  138. package/reference/telemetry/metrics/instruments.md +183 -0
  139. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  140. package/reference/telemetry/metrics/meter-provider.md +76 -0
  141. package/reference/telemetry/metrics/meter.md +98 -0
  142. package/reference/telemetry/metrics/metric-data.md +57 -0
  143. package/reference/telemetry/otel/custom-exporter.md +216 -0
  144. package/reference/telemetry/otel/index.md +212 -0
  145. package/reference/telemetry/tracing/index.md +155 -0
  146. package/reference/telemetry/tracing/sampler.md +89 -0
  147. package/reference/telemetry/tracing/span-builder.md +57 -0
  148. package/reference/telemetry/tracing/span-context.md +39 -0
  149. package/reference/telemetry/tracing/span-data.md +32 -0
  150. package/reference/telemetry/tracing/span-kind.md +55 -0
  151. package/reference/telemetry/tracing/span-processor.md +53 -0
  152. package/reference/telemetry/tracing/span-status.md +47 -0
  153. package/reference/telemetry/tracing/span.md +117 -0
  154. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  155. package/reference/telemetry/tracing/tracer.md +52 -0
  156. package/reference/typeid.md +0 -64
  157. package/sidebars.js +150 -12
  158. package/undocumented-report.md +528 -270
  159. package/reference/config.md +0 -158
  160. package/reference/streams/concurrent-operators.md +0 -106
  161. package/reference/streams/reader.md +0 -1284
  162. package/reference/streams/scala-2-compatibility.md +0 -55
  163. package/reference/streams/zero-boxing.md +0 -275
  164. package/reference/telemetry.md +0 -693
@@ -634,22 +634,6 @@ This example creates a mux and opens your first stream, demonstrating the basic
634
634
  <summary>mux-examples/src/main/scala/mux/Example1CreatingAMux.scala</summary>
635
635
 
636
636
  ```scala title="mux-examples/src/main/scala/mux/Example1CreatingAMux.scala" showLineNumbers
637
- /*
638
- * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
639
- *
640
- * Licensed under the Apache License, Version 2.0 (the "License");
641
- * you may not use this file except in compliance with the License.
642
- * You may obtain a copy of the License at
643
- *
644
- * http://www.apache.org/licenses/LICENSE-2.0
645
- *
646
- * Unless required by applicable law or agreed to in writing, software
647
- * distributed under the License is distributed on an "AS IS" BASIS,
648
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
649
- * See the License for the specific language governing permissions and
650
- * limitations under the License.
651
- */
652
-
653
637
  package mux
654
638
 
655
639
  import zio.blocks.mux._
@@ -698,22 +682,6 @@ This example demonstrates the two-perspective communication model where applicat
698
682
  <summary>mux-examples/src/main/scala/mux/Example2UnderstandingStreamsAndMessageQueues.scala</summary>
699
683
 
700
684
  ```scala title="mux-examples/src/main/scala/mux/Example2UnderstandingStreamsAndMessageQueues.scala" showLineNumbers
701
- /*
702
- * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
703
- *
704
- * Licensed under the Apache License, Version 2.0 (the "License");
705
- * you may not use this file except in compliance with the License.
706
- * You may obtain a copy of the License at
707
- *
708
- * http://www.apache.org/licenses/LICENSE-2.0
709
- *
710
- * Unless required by applicable law or agreed to in writing, software
711
- * distributed under the License is distributed on an "AS IS" BASIS,
712
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
713
- * See the License for the specific language governing permissions and
714
- * limitations under the License.
715
- */
716
-
717
685
  package mux
718
686
 
719
687
  import zio.blocks.mux._
@@ -791,22 +759,6 @@ This example shows all three ways to close a stream: graceful two-phase closure
791
759
  <summary>mux-examples/src/main/scala/mux/Example3StreamLifecycle.scala</summary>
792
760
 
793
761
  ```scala title="mux-examples/src/main/scala/mux/Example3StreamLifecycle.scala" showLineNumbers
794
- /*
795
- * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
796
- *
797
- * Licensed under the Apache License, Version 2.0 (the "License");
798
- * you may not use this file except in compliance with the License.
799
- * You may obtain a copy of the License at
800
- *
801
- * http://www.apache.org/licenses/LICENSE-2.0
802
- *
803
- * Unless required by applicable law or agreed to in writing, software
804
- * distributed under the License is distributed on an "AS IS" BASIS,
805
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
806
- * See the License for the specific language governing permissions and
807
- * limitations under the License.
808
- */
809
-
810
762
  package mux
811
763
 
812
764
  import zio.blocks.mux._
@@ -904,22 +856,6 @@ This example opens three independent streams and exchanges messages on each one,
904
856
  <summary>mux-examples/src/main/scala/mux/Example4WorkingWithMultipleStreams.scala</summary>
905
857
 
906
858
  ```scala title="mux-examples/src/main/scala/mux/Example4WorkingWithMultipleStreams.scala" showLineNumbers
907
- /*
908
- * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
909
- *
910
- * Licensed under the Apache License, Version 2.0 (the "License");
911
- * you may not use this file except in compliance with the License.
912
- * You may obtain a copy of the License at
913
- *
914
- * http://www.apache.org/licenses/LICENSE-2.0
915
- *
916
- * Unless required by applicable law or agreed to in writing, software
917
- * distributed under the License is distributed on an "AS IS" BASIS,
918
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
919
- * See the License for the specific language governing permissions and
920
- * limitations under the License.
921
- */
922
-
923
859
  package mux
924
860
 
925
861
  import zio.blocks.mux._
@@ -1037,22 +973,6 @@ This example demonstrates both mux-level capacity (controlling concurrent stream
1037
973
  <summary>mux-examples/src/main/scala/mux/Example5ManagingCapacity.scala</summary>
1038
974
 
1039
975
  ```scala title="mux-examples/src/main/scala/mux/Example5ManagingCapacity.scala" showLineNumbers
1040
- /*
1041
- * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1042
- *
1043
- * Licensed under the Apache License, Version 2.0 (the "License");
1044
- * you may not use this file except in compliance with the License.
1045
- * You may obtain a copy of the License at
1046
- *
1047
- * http://www.apache.org/licenses/LICENSE-2.0
1048
- *
1049
- * Unless required by applicable law or agreed to in writing, software
1050
- * distributed under the License is distributed on an "AS IS" BASIS,
1051
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1052
- * See the License for the specific language governing permissions and
1053
- * limitations under the License.
1054
- */
1055
-
1056
976
  package mux
1057
977
 
1058
978
  import zio.blocks.mux._
@@ -1172,22 +1092,6 @@ This example documents which operations are thread-safe (mux-level and send/offe
1172
1092
  <summary>mux-examples/src/main/scala/mux/Example6ThreadSafety.scala</summary>
1173
1093
 
1174
1094
  ```scala title="mux-examples/src/main/scala/mux/Example6ThreadSafety.scala"
1175
- /*
1176
- * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1177
- *
1178
- * Licensed under the Apache License, Version 2.0 (the "License");
1179
- * you may not use this file except in compliance with the License.
1180
- * You may obtain a copy of the License at
1181
- *
1182
- * http://www.apache.org/licenses/LICENSE-2.0
1183
- *
1184
- * Unless required by applicable law or agreed to in writing, software
1185
- * distributed under the License is distributed on an "AS IS" BASIS,
1186
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1187
- * See the License for the specific language governing permissions and
1188
- * limitations under the License.
1189
- */
1190
-
1191
1095
  package mux
1192
1096
 
1193
1097
  import zio.blocks.mux._
@@ -1310,22 +1214,6 @@ This comprehensive example demonstrates a complete request-response system that
1310
1214
  <summary>mux-examples/src/main/scala/mux/CompleteExample.scala</summary>
1311
1215
 
1312
1216
  ```scala title="mux-examples/src/main/scala/mux/CompleteExample.scala" showLineNumbers
1313
- /*
1314
- * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1315
- *
1316
- * Licensed under the Apache License, Version 2.0 (the "License");
1317
- * you may not use this file except in compliance with the License.
1318
- * You may obtain a copy of the License at
1319
- *
1320
- * http://www.apache.org/licenses/LICENSE-2.0
1321
- *
1322
- * Unless required by applicable law or agreed to in writing, software
1323
- * distributed under the License is distributed on an "AS IS" BASIS,
1324
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1325
- * See the License for the specific language governing permissions and
1326
- * limitations under the License.
1327
- */
1328
-
1329
1217
  package mux
1330
1218
 
1331
1219
  import zio.blocks.mux._
@@ -52,7 +52,7 @@ None of these can be expressed with `SchemaExpr` alone. You could generate the S
52
52
  This guide builds on [Part 1: Expressions](./query-dsl-reified-optics.md) and [Part 2: SQL Generation](./query-dsl-sql.md). You should be comfortable building `SchemaExpr` values and translating them to SQL.
53
53
 
54
54
  ```scala
55
- libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.51"
55
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.55"
56
56
  ```
57
57
 
58
58
  ```scala
@@ -57,7 +57,7 @@ In this guide, we solve both problems: bridge extensions eliminate `.toExpr`, an
57
57
  This guide builds on [Part 1: Expressions](./query-dsl-reified-optics.md), [Part 2: SQL Generation](./query-dsl-sql.md), and [Part 3: Extending the Expression Language](./query-dsl-extending.md).
58
58
 
59
59
  ```scala
60
- libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.51"
60
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.55"
61
61
  ```
62
62
 
63
63
  ## Domain Setup
@@ -43,7 +43,7 @@ In this guide, we'll solve this by using ZIO Blocks' `SchemaExpr` and reified op
43
43
  Add the ZIO Blocks Schema dependency:
44
44
 
45
45
  ```scala
46
- libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.51"
46
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.55"
47
47
  ```
48
48
 
49
49
  ```scala
@@ -43,7 +43,7 @@ This is fragile, repetitive, and vulnerable to SQL injection. Every new query sh
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.51"
46
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.55"
47
47
  ```
48
48
 
49
49
  ```scala
@@ -749,6 +749,400 @@ println(toSql(Product.price * 0.9))
749
749
  // (price * 0.9)
750
750
  ```
751
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
+
752
1146
  ## Going Further
753
1147
 
754
1148
  - **[Part 1: Expressions](./query-dsl-reified-optics.md)** -- Building query expressions with reified optics