qubu 0.6.1 → 0.6.2

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 (84) hide show
  1. package/dist/{complete-DP7pliuY.mjs → canonical-B5_ouh-c.mjs} +466 -241
  2. package/dist/codegen.d.mts +2 -2
  3. package/dist/codegen.mjs +180 -90
  4. package/dist/column-Cyc2CMnG.mjs +116 -0
  5. package/dist/column-DDRvD7SF.mjs +721 -0
  6. package/dist/{constraints-DM_tarXc.mjs → constraints-CAmi18Uk.mjs} +8 -3
  7. package/dist/core.d.mts +3 -3
  8. package/dist/core.mjs +4 -4
  9. package/dist/diff.d.mts +9 -9
  10. package/dist/diff.mjs +152 -102
  11. package/dist/{explain-CkIK13L_.mjs → explain-BIqEmZha.mjs} +1 -1
  12. package/dist/{expressions-BCjc08zw.mjs → expressions-_6JF_J77.mjs} +2 -1
  13. package/dist/index-CaxrMD1A.d.mts +1 -0
  14. package/dist/index.d.mts +2 -2
  15. package/dist/index.mjs +368 -74
  16. package/dist/introspection/mysql.d.mts +1 -1
  17. package/dist/introspection/mysql.mjs +156 -23
  18. package/dist/introspection/postgres.d.mts +6 -3
  19. package/dist/introspection/postgres.mjs +388 -53
  20. package/dist/introspection/sqlite.d.mts +1 -1
  21. package/dist/introspection/sqlite.mjs +198 -12
  22. package/dist/introspection.d.mts +26 -12
  23. package/dist/introspection.mjs +2 -672
  24. package/dist/mysql.d.mts +3 -3
  25. package/dist/mysql.mjs +6 -5
  26. package/dist/{errors-Dxv73YJu.mjs → omit-VEr9Ydux.mjs} +5 -1
  27. package/dist/{on-conflict-CnaY5qso.mjs → on-conflict-B2rFyHGF.mjs} +6 -8
  28. package/dist/on-duplicate-key-update-Czsw1Yq-.mjs +95 -0
  29. package/dist/{postgres-Dey7QXPL.mjs → postgres-hFhd0I9n.mjs} +4 -5
  30. package/dist/postgres.d.mts +2 -2
  31. package/dist/postgres.mjs +2 -2
  32. package/dist/{registry-oWDiqD7i.mjs → registry-BXE_4M9P.mjs} +1 -1
  33. package/dist/{relational-DSAJ-l58.mjs → relational-CoPBETjI.mjs} +3 -2
  34. package/dist/schema.d.mts +2 -2
  35. package/dist/schema.mjs +8 -8
  36. package/dist/{serialize-CE-gw5_s.mjs → serialize-CyobNEx-.mjs} +174 -30
  37. package/dist/serialize-Du2UPZMt.d.mts +92 -0
  38. package/dist/snapshot/mysql.d.mts +5 -5
  39. package/dist/snapshot/mysql.mjs +8 -8
  40. package/dist/snapshot/postgres.d.mts +3 -3
  41. package/dist/snapshot/postgres.mjs +9 -9
  42. package/dist/snapshot/sqlite.d.mts +3 -3
  43. package/dist/snapshot/sqlite.mjs +7 -7
  44. package/dist/snapshot-mIb-Zzb5.mjs +1489 -0
  45. package/dist/snapshot.d.mts +4 -5
  46. package/dist/snapshot.mjs +3 -4
  47. package/dist/{source-BDuUXmAk.mjs → source-DYSUqzvb.mjs} +2 -2
  48. package/dist/sqlite.d.mts +2 -2
  49. package/dist/sqlite.mjs +6 -6
  50. package/dist/{table-C1QGNe4P.mjs → table-B8zEq0az.mjs} +4 -4
  51. package/dist/{types-BLNRatG_.mjs → types-CYHpSPwj.mjs} +10 -5
  52. package/dist/{types-BEn0N_al.d.mts → types-CiMvKi5V.d.mts} +14 -4
  53. package/dist/{types-DUe6eeI0.d.mts → types-Dqr4o2I1.d.mts} +590 -168
  54. package/dist/value-CpaUFtjw.mjs +45 -0
  55. package/dist/vite/ambient.d.ts +2 -0
  56. package/dist/vite.d.mts +1 -1
  57. package/dist/vite.mjs +2 -0
  58. package/docs/dialects-and-execution.md +61 -11
  59. package/docs/getting-started.md +1 -1
  60. package/docs/guides/extensions/typed-expressions.md +5 -3
  61. package/docs/guides/json.md +95 -2
  62. package/docs/guides/mutations.md +36 -0
  63. package/docs/index.md +1 -1
  64. package/docs/migrations/adapters.md +58 -21
  65. package/docs/migrations/recovery.md +7 -1
  66. package/docs/query-model/fragments.md +21 -1
  67. package/docs/reference/introspection-support.md +16 -18
  68. package/docs/reference/supported-surface.md +8 -7
  69. package/docs/schema/catalog-model.md +20 -7
  70. package/docs/schema/code-generation.md +1 -1
  71. package/docs/schema/diff.md +2 -2
  72. package/docs/schema/introspection.md +21 -14
  73. package/docs/schema/snapshots.md +18 -8
  74. package/docs/schema/storage-and-schema-sql.md +7 -2
  75. package/package.json +2 -1
  76. package/dist/canonical-DMvR9yBe.mjs +0 -972
  77. package/dist/column-BzN8KFJa.mjs +0 -364
  78. package/dist/column-CFvSbil0.mjs +0 -309
  79. package/dist/complete-types-CNMWBWap.d.mts +0 -371
  80. package/dist/index-CGui70hi.d.mts +0 -32
  81. package/dist/json-Db7XRD91.mjs +0 -169
  82. package/dist/omit-OxV58AwX.mjs +0 -5
  83. package/dist/serialize-OvXCLzjm.d.mts +0 -66
  84. package/dist/snapshot-DgsOhf_8.mjs +0 -354
@@ -0,0 +1,45 @@
1
+ import { A as fragment, E as makeSchemaExpression, U as resultValue, Y as standardJson, Z as createDialect } from "./column-DDRvD7SF.mjs";
2
+ //#region src/dialects/standard.ts
3
+ /** SQL:2008-style rendering defaults used by the core builder. */
4
+ function standardDialect() {
5
+ return createDialect({
6
+ name: "standard-sql",
7
+ placeholder: () => "?",
8
+ json: standardJson
9
+ });
10
+ }
11
+ //#endregion
12
+ //#region src/core/primitives/parameter.ts
13
+ function parameter(_value, sqlType) {
14
+ return fragment((context) => context.parameter(_value, sqlType));
15
+ }
16
+ //#endregion
17
+ //#region src/expressions/value.ts
18
+ /** Build a parameterized value expression with an optional runtime SQL domain for the adapter. */
19
+ function value(input, sqlType) {
20
+ const expression = makeSchemaExpression("value", (context) => context.render(parameter(input, sqlType)), resultValue(void 0, void 0, sqlType));
21
+ return Object.freeze({
22
+ ...expression,
23
+ value: input
24
+ });
25
+ }
26
+ /**
27
+ * Bind a value while declaring its compile-time and runtime SQL semantic domain.
28
+ *
29
+ * @remarks
30
+ * The domain is a binding hint; it does not select a JavaScript result decoder.
31
+ */
32
+ function typedValue(input, sqlType) {
33
+ return value(input, sqlType);
34
+ }
35
+ function isExpressionValue(valueToCheck) {
36
+ return typeof valueToCheck === "object" && valueToCheck !== null && "expressionKind" in valueToCheck && "render" in valueToCheck && typeof valueToCheck.render === "function";
37
+ }
38
+ function isValueExpression(valueToCheck) {
39
+ return isExpressionValue(valueToCheck) && valueToCheck.expressionKind === "value" && "value" in valueToCheck;
40
+ }
41
+ function asValue(input, sqlType) {
42
+ return isExpressionValue(input) ? input : value(input, sqlType);
43
+ }
44
+ //#endregion
45
+ export { parameter as a, value as i, isValueExpression as n, standardDialect as o, typedValue as r, asValue as t };
@@ -68,6 +68,8 @@ declare global {
68
68
  const isNull: typeof import("qubu").isNull
69
69
  const isTrue: typeof import("qubu").isTrue
70
70
  const json: typeof import("qubu").json
71
+ const jsonArrayFrom: typeof import("qubu").jsonArrayFrom
72
+ const jsonObjectFrom: typeof import("qubu").jsonObjectFrom
71
73
  const jsonBoolean: typeof import("qubu").jsonBoolean
72
74
  const jsonExists: typeof import("qubu").jsonExists
73
75
  const jsonNumber: typeof import("qubu").jsonNumber
package/dist/vite.d.mts CHANGED
@@ -4,7 +4,7 @@
4
4
  * with the ordinary authoring surface of `qubu`; fragment internals, dialect construction, and
5
5
  * schema extensions belong to the layered entrypoints.
6
6
  */
7
- declare const qubuGlobals: readonly ["add", "alias", "all", "allowAll", "and", "asc", "asValue", "avg", "bigint", "between", "binary", "boolean", "call", "caseWhen", "cast", "check", "coalesce", "column", "concat", "count", "countDistinct", "correlate", "crossJoin", "cte", "date", "denseRank", "defaultValues", "deleteFrom", "desc", "distinct", "divide", "eq", "except", "execute", "executeRows", "externalDefault", "externalGeneratedColumn", "exists", "fetchFirst", "fetchNext", "foreignKey", "from", "fullJoin", "generatedColumn", "gt", "gte", "groupBy", "having", "identityColumn", "inList", "inQuery", "index", "innerJoin", "insertInto", "insertSelect", "integer", "intersect", "isDistinctFrom", "isNotDistinctFrom", "isNotNull", "isNull", "isTrue", "json", "jsonBoolean", "jsonExists", "jsonNumber", "jsonPath", "jsonText", "lateral", "leftJoin", "like", "lower", "lt", "lte", "max", "min", "modulo", "multiply", "naturalJoin", "nativeColumn", "nativeStorage", "ne", "not", "notExists", "notIn", "notLike", "nullsFirst", "nullsLast", "numeric", "nullable", "offset", "omit", "or", "order", "orderBy", "over", "portableStorage", "primaryKey", "qubu", "references", "recursiveCte", "render", "returning", "rightJoin", "rank", "rowNumber", "scalar", "schema", "schemaCall", "select", "sql", "stream", "subtract", "sum", "table", "text", "timestamp", "union", "unionAll", "unique", "uniqueConstraint", "update", "upper", "value", "values", "where", "withCte", "uuid"];
7
+ declare const qubuGlobals: readonly ["add", "alias", "all", "allowAll", "and", "asc", "asValue", "avg", "bigint", "between", "binary", "boolean", "call", "caseWhen", "cast", "check", "coalesce", "column", "concat", "count", "countDistinct", "correlate", "crossJoin", "cte", "date", "denseRank", "defaultValues", "deleteFrom", "desc", "distinct", "divide", "eq", "except", "execute", "executeRows", "externalDefault", "externalGeneratedColumn", "exists", "fetchFirst", "fetchNext", "foreignKey", "from", "fullJoin", "generatedColumn", "gt", "gte", "groupBy", "having", "identityColumn", "inList", "inQuery", "index", "innerJoin", "insertInto", "insertSelect", "integer", "intersect", "isDistinctFrom", "isNotDistinctFrom", "isNotNull", "isNull", "isTrue", "json", "jsonArrayFrom", "jsonObjectFrom", "jsonBoolean", "jsonExists", "jsonNumber", "jsonPath", "jsonText", "lateral", "leftJoin", "like", "lower", "lt", "lte", "max", "min", "modulo", "multiply", "naturalJoin", "nativeColumn", "nativeStorage", "ne", "not", "notExists", "notIn", "notLike", "nullsFirst", "nullsLast", "numeric", "nullable", "offset", "omit", "or", "order", "orderBy", "over", "portableStorage", "primaryKey", "qubu", "references", "recursiveCte", "render", "returning", "rightJoin", "rank", "rowNumber", "scalar", "schema", "schemaCall", "select", "sql", "stream", "subtract", "sum", "table", "text", "timestamp", "union", "unionAll", "unique", "uniqueConstraint", "update", "upper", "value", "values", "where", "withCte", "uuid"];
8
8
  type QubuGlobal = (typeof qubuGlobals)[number];
9
9
  //#endregion
10
10
  //#region src/vite/index.d.ts
package/dist/vite.mjs CHANGED
@@ -138,6 +138,8 @@ const qubuGlobals = [
138
138
  "isNull",
139
139
  "isTrue",
140
140
  "json",
141
+ "jsonArrayFrom",
142
+ "jsonObjectFrom",
141
143
  "jsonBoolean",
142
144
  "jsonExists",
143
145
  "jsonNumber",
@@ -79,6 +79,11 @@ rows. Qubu then uses the query's result shape and the adapter's decoder policy
79
79
  to produce the typed `ExecutionResult`. A `TransactionalQueryAdapter` can also
80
80
  pin one driver connection for a callback transaction:
81
81
 
82
+ When present, `request.statement.parameterSqlTypes` is an optional sidecar
83
+ aligned with `statement.parameters`. Adapters can pass each domain to their
84
+ value encoder or driver binding layer when a client distinguishes values such
85
+ as `DATE`, `TIMESTAMP`, `UUID`, and `DECIMAL`.
86
+
82
87
  ```ts
83
88
  import { qubu } from "qubu"
84
89
  import { postgresDialect } from "qubu/postgres"
@@ -147,10 +152,12 @@ lifecycle.
147
152
 
148
153
  ### Decode schema-aware result values
149
154
 
150
- Portable boolean, date, timestamp, and JSON columns retain their logical
151
- result domains through projection aliases, derived queries, CTEs, set
152
- operations, and mutation `RETURNING`. Register only the conversions required
153
- by the selected driver configuration:
155
+ Portable boolean, date, timestamp, JSON, and bigint columns retain their
156
+ logical result domains through projection aliases, derived queries, CTEs, set
157
+ operations, and mutation `RETURNING`. The result field exposes that domain as
158
+ `sqlType` before execution. Register only the conversions required by the
159
+ selected driver configuration. `sqlType` identifies the SQL domain; `type` and
160
+ the selected decoder determine whether Qubu converts the returned value:
154
161
 
155
162
  ```ts
156
163
  import {
@@ -180,6 +187,11 @@ const adapter: QueryAdapter = {
180
187
  }
181
188
  ```
182
189
 
190
+ Qubu can decode bigint values exactly when a driver returns a bigint, a safe
191
+ integer, or an integer string. For arbitrary-precision `DECIMAL` values, keep
192
+ the driver's exact representation (usually a string or decimal object) rather
193
+ than converting it to a JavaScript number.
194
+
183
195
  Do not register `jsonTextResultDecoder` when the driver already returns parsed
184
196
  JSON. A JSON string is otherwise ambiguous: it may be serialized JSON or an
185
197
  already-decoded JSON string scalar. With no registered decoder, Qubu preserves
@@ -459,13 +471,51 @@ back after it rejects, and releases the connection in every case. Qubu only
459
471
  creates the scoped client and passes the callback result through. It never
460
472
  emits `BEGIN`, `COMMIT`, or `ROLLBACK` itself.
461
473
 
462
- The transaction client exposes `execute()` and `rows()` but no public
463
- `transaction()` method, so nested transactions are not part of this contract.
464
- When the adapter also implements `StreamingQueryAdapter`, the scoped client
465
- also exposes `stream()` and its streams follow the cleanup rule above. Use
466
- adapter-specific savepoints when a driver needs nested partial rollback.
467
- `TransactionOptions.signal` is passed to the adapter. Isolation levels and
468
- other driver-specific settings remain adapter-specific.
474
+ A scoped client's methods follow its adapter's capabilities: `execute()` and
475
+ `rows()` are always available; EXPLAIN and streaming require their respective
476
+ capabilities. `TransactionOptions.signal` is passed to the adapter. Isolation
477
+ levels and other driver-specific settings remain adapter-specific.
478
+
479
+ ### Roll back part of a transaction
480
+
481
+ The pg, mysql2, and node:sqlite adapters expose `transaction()` on scoped
482
+ clients through the shared `NestedTransactionalQueryAdapter` capability.
483
+ Other adapters retain their existing transaction surface. For example, with a
484
+ bound pg client, catch a nested failure to keep earlier work:
485
+
486
+ ```ts
487
+ await db.transaction(async (outer) => {
488
+ await outer.execute(firstMutation)
489
+ try {
490
+ await outer.transaction(async (inner) => {
491
+ await inner.execute(optionalMutation)
492
+ })
493
+ } catch (error) {
494
+ // The nested work was rolled back; the outer transaction can continue.
495
+ }
496
+ await outer.execute(secondMutation)
497
+ })
498
+ ```
499
+
500
+ Each nested callback uses a uniquely named savepoint on the same connection.
501
+ Success releases it; failure rolls back to it and releases it. Letting the
502
+ failure escape also rolls back the outer transaction. Failed savepoint creation
503
+ or recovery makes the entire transaction unsafe to commit, even if the callback
504
+ catches the error. Primary and cleanup failures are retained in `AggregateError`.
505
+
506
+ Await every query and nested transaction before returning. These three adapters
507
+ reject finished scoped clients, overlapping sibling scopes, a child started
508
+ while its parent has pending queries, and parent queries while a child is active.
509
+ If a callback finishes with work still pending, the adapter waits for that work
510
+ and rolls back instead of committing. Use the active scoped client for all work
511
+ on a directly supplied connection; its root client rejects unrelated operations
512
+ during the transaction. A pg pool still accepts independent queries and
513
+ transactions on other acquired connections. Raw driver calls and separately
514
+ constructed adapters remain the application's responsibility.
515
+
516
+ EXPLAIN and result decoding remain available at every depth. Nested transaction
517
+ hooks identify their enclosing transaction with `parentId`; queries identify
518
+ their immediate scope. Cancellation does not interrupt savepoint recovery.
469
519
 
470
520
  The standalone functions remain useful when the adapter varies by call or a
471
521
  small module does not need a bound client:
@@ -91,6 +91,6 @@ type UserRow = typeof query.row
91
91
  inputs.
92
92
  - [Choose a database dialect](dialects-and-execution.md) when the
93
93
  driver expects different identifier, placeholder, or pagination syntax.
94
- - [Read JSON scalars](guides/json.md) from a JSON column.
94
+ - [Query nested JSON](guides/json.md) or read scalars from a JSON column.
95
95
  - [Use the Vite compiler hint](guides/vite-plugin.md) for directive-based
96
96
  imports.
@@ -60,7 +60,7 @@ Declare result domains at other extension boundaries too:
60
60
  import { typedCall, typedCast, typedValue, unsafeExpression } from "qubu/core"
61
61
  import type { SqlText, SqlUuid } from "qubu"
62
62
 
63
- const id = typedValue<SqlUuid, string>("108cb836-20d2-41b2-8c23-f0c94700aa7e")
63
+ const id = typedValue<SqlUuid, string>("108cb836-20d2-41b2-8c23-f0c94700aa7e", "uuid")
64
64
  const normalized = typedCall<SqlText, string>()("custom_text", users.name)
65
65
  const rawNameAsText = typedCast<string, SqlText>()(users.name, "TEXT")
66
66
  const generated = unsafeExpression<string, SqlText>("custom_text()")
@@ -69,8 +69,10 @@ const generated = unsafeExpression<string, SqlText>("custom_text()")
69
69
  `typedCall()` preserves source requirements from its arguments. `typedCast()`
70
70
  is the fallback when no reusable definition describes the target. It preserves
71
71
  operand nullability and source metadata while emitting its supplied type name
72
- verbatim. `typedValue()` binds a parameter. `unsafeExpression()` emits its
73
- string unchanged and should remain a last resort.
72
+ verbatim. `typedValue()` binds a parameter and declares its runtime SQL domain
73
+ for the adapter; it does not select a JavaScript result decoder. Schema columns
74
+ carry result-decoder metadata separately. `unsafeExpression()` emits its string
75
+ unchanged and should remain a last resort.
74
76
 
75
77
  The lower-level forms also expose the SQL domain in their generic lists:
76
78
  `call<Output, Name, Arguments, NullableFrom, SqlType>()` and
@@ -1,6 +1,99 @@
1
- # Read JSON scalars
1
+ # Query nested JSON
2
2
 
3
- > Extract a string, number, or boolean from a JSON column without writing a raw SQL path.
3
+ > Build inferred nested results, or read scalar values from stored JSON documents.
4
+
5
+ Use jsonArrayFrom() to nest a query's rows and jsonObjectFrom() for a query
6
+ proven to return at most one row. Both preserve filtering, correlation,
7
+ ordering, and pagination:
8
+
9
+ ```ts
10
+ import {
11
+ correlate,
12
+ desc,
13
+ eq,
14
+ fetchFirst,
15
+ from,
16
+ integer,
17
+ jsonArrayFrom,
18
+ jsonObjectFrom,
19
+ orderBy,
20
+ select,
21
+ table,
22
+ text,
23
+ where,
24
+ } from "qubu"
25
+
26
+ const users = table("users", { id: integer(), name: text() })
27
+ const posts = table("posts", {
28
+ id: integer(),
29
+ authorId: integer(),
30
+ title: text(),
31
+ })
32
+ const latestPosts = select(
33
+ { id: posts.id, title: posts.title },
34
+ from(posts),
35
+ correlate(users),
36
+ where(eq(posts.authorId, users.id)),
37
+ orderBy(desc(posts.id)),
38
+ fetchFirst(3),
39
+ )
40
+ const latestPost = select(
41
+ { title: posts.title },
42
+ from(posts),
43
+ correlate(users),
44
+ where(eq(posts.authorId, users.id)),
45
+ orderBy(desc(posts.id)),
46
+ fetchFirst(1),
47
+ )
48
+ const query = select(
49
+ {
50
+ name: users.name,
51
+ posts: jsonArrayFrom(latestPosts),
52
+ latestPost: jsonObjectFrom(latestPost),
53
+ },
54
+ from(users),
55
+ )
56
+ // Row: { name: string; posts: { id: number; title: string }[];
57
+ // latestPost: { title: string } | null }
58
+ ```
59
+
60
+ Execute the query through a Qubu adapter to decode nested results. An empty
61
+ array query returns []; an empty object query returns null. A source-free
62
+ query proven to return exactly one row produces a non-null object type.
63
+ Object queries need Qubu's cardinality proof: an unconditional fetchFirst(1)
64
+ or fetchFirst(0) establishes the bound. A conditional limit does not.
65
+
66
+ The helpers compose inside further select() projections, so nesting can
67
+ continue without result-type assertions. correlate() and the outer query's
68
+ FROM/JOIN scope remain checked at every level.
69
+
70
+ ### Preserve ordering and logical values
71
+
72
+ Nested arrays retain explicit ORDER BY and pagination. Tied sort keys retain
73
+ SQL's unspecified tie order; add a unique tie-breaker when order matters.
74
+ DISTINCT ordering must use the same expressions as the selection. Without an
75
+ ORDER BY, array order is unspecified.
76
+
77
+ Nested results support PostgreSQL, MySQL 8.0.21+, and SQLite 3.45+. Other
78
+ dialects fail during rendering. SQLite's minimum includes the JSON aggregate
79
+ ordering fix needed to retain object values.
80
+
81
+ Built-in column domains decode to their declared types, including bigint,
82
+ Uint8Array, Date, boolean, and nested JSON. Qubu transports precision-sensitive
83
+ values as text and rejects numbers that lose significant decimal digits or
84
+ exceed JavaScript's safe integer range. Use bigint columns for exact large
85
+ integers. Unknown or custom SQL domains need a supported explicit cast, for
86
+ example cast(value(7), integer()); declaring a TypeScript result alone does
87
+ not provide runtime decoding information.
88
+
89
+ Custom mapResult() and column decoders receive the JSON transport value as
90
+ unknown: bigint and decimal strings, hexadecimal binary strings, serialized
91
+ JSON strings, or ordinary JSON scalar values. They own conversion to their
92
+ advertised application type. Adapter-wide decoders do not run inside nested
93
+ objects. Keep arbitrary stored JSON within JavaScript's numeric precision;
94
+ unsupported numeric representations fail instead of silently rounding.
95
+
96
+ ## Read stored JSON scalars
4
97
 
5
98
  Use a structured jsonPath() when a query needs a scalar or an existence check
6
99
  inside a JSON document:
@@ -176,6 +176,42 @@ before rendering when every field is omitted. Possible expression branches
176
176
  remain source- and capability-aware even when their runtime alternative is
177
177
  `omit`.
178
178
 
179
+ ## Update duplicate keys in MySQL
180
+
181
+ Use `onDuplicateKeyUpdate()` from `qubu/mysql` to update a row when an insert
182
+ conflicts with any primary or unique key. MySQL chooses the conflicting key;
183
+ this clause has no conflict-target argument or `RETURNING` support.
184
+
185
+ ```ts
186
+ import { insertInto, values } from "qubu"
187
+ import { incoming, onDuplicateKeyUpdate } from "qubu/mysql"
188
+
189
+ const proposed = incoming(users)
190
+ const query = insertInto(
191
+ users,
192
+ values({ name: "Ada", email: "ada@example.com" }),
193
+ onDuplicateKeyUpdate(users, { name: proposed.name }),
194
+ )
195
+ await db.execute(query)
196
+ ```
197
+
198
+ Declare a unique key on `email` in the database for this example. A duplicate
199
+ email replaces the existing name with the proposed name. Assignments accept
200
+ writable target columns, target-table expressions, `incoming(table)` columns,
201
+ or `omit`. Raw values retain the target column's parameter encoder. At least
202
+ one assignment must remain after omissions.
203
+
204
+ Incoming references are available only inside the matching table's duplicate-key
205
+ assignments. Qubu renders MySQL row aliases for `values()` and `defaultValues()`.
206
+ For `insertSelect()`, it wraps the query in a derived projection and maps its
207
+ fields positionally to the target-column list. Incoming references then cover
208
+ only that list; referencing an omitted target column fails during rendering.
209
+ This syntax requires MySQL 8.0.19 or later.
210
+
211
+ The result retains mysql2's `affectedRows`, `changedRows`, and `insertId` metadata
212
+ when supplied by the driver. These facts do not identify which branch ran for
213
+ every row of a batch. Read rows with a separate query when needed.
214
+
179
215
  ## Delete with a predicate
180
216
 
181
217
  ```ts
package/docs/index.md CHANGED
@@ -33,7 +33,7 @@ define a table, build a `SELECT`, and inspect its SQL and parameters.
33
33
  derivation and a native transactional database adapter.
34
34
  - [Extend Qubu](guides/extensions/overview.md) with a custom source, clause,
35
35
  dialect policy, or typed expression.
36
- - [Read JSON scalars](guides/json.md) from structured JSON paths.
36
+ - [Query nested JSON](guides/json.md) or read scalars from structured paths.
37
37
  - [Enable the Vite compiler hint](guides/vite-plugin.md) when query modules
38
38
  should opt into named imports through a directive.
39
39
  - [Inspect an existing database](schema/introspection.md) through the optional
@@ -2,38 +2,47 @@
2
2
 
3
3
  > Select a migration adapter from capabilities proven by its driver and environment, not from dialect name alone.
4
4
 
5
- Every executable migration adapter opens one pinned migration session and
5
+ Every executable migration adapter opens a migration session and
6
6
  advertises the exact behavior the executor may use:
7
7
 
8
- | Field | Contract |
9
- | -------------------------------------- | ----------------------------------------------------------------------------------- |
10
- | `dialect`, `serverVersion` | Physical target and optional version used for compatibility checks |
11
- | `session` | Must be `pinned` until `close()` resolves |
12
- | `transactionalDdl` | Whether DDL effects can roll back |
13
- | `optionalTransactions`, `transactions` | Whether optional phases join a transaction and which requirements are proven |
14
- | `lease`, `leaseKind` | Database-backed exclusion of another migration runner |
15
- | `locks` | Independently supported program DDL lock requirements |
16
- | `journal` | Database storage, head compare-and-swap, and atomic applied-record/head advancement |
17
- | `parameters` | Supported tagged parameter kinds |
18
- | `commitAmbiguity` | Ambiguous commit becomes `recovery-required` |
19
- | `forbiddenPhases` | Checkpointed support or explicit rejection |
20
- | `features` | Named constraints an artifact may require |
8
+ | Field | Contract |
9
+ | -------------------------------------- | -------------------------------------------------------------------------------------- |
10
+ | `dialect`, `serverVersion` | Physical target and optional version used for compatibility checks |
11
+ | `session` | `pinned` for the full lifecycle, or `atomic-batch` for one complete artifact per batch |
12
+ | `transactionalDdl` | Whether DDL effects can roll back |
13
+ | `optionalTransactions`, `transactions` | Whether optional phases join a transaction and which requirements are proven |
14
+ | `lease`, `leaseKind` | Database-backed exclusion of another migration runner |
15
+ | `locks` | Independently supported program DDL lock requirements |
16
+ | `journal` | Database storage, head compare-and-swap, and atomic applied-record/head advancement |
17
+ | `parameters` | Supported tagged parameter kinds |
18
+ | `commitAmbiguity` | Ambiguous commit becomes `recovery-required` |
19
+ | `forbiddenPhases` | Checkpointed support or explicit rejection |
20
+ | `features` | Named constraints an artifact may require |
21
21
 
22
22
  The migrator lease and a program's DDL lock are different controls. The lease
23
23
  excludes another Qubu runner; a DDL lock protects the database operation. The
24
24
  executor never treats one as proof of the other.
25
25
 
26
+ ## Trusted migration SQL
27
+
28
+ Migration SQL, including SQL conditions, is trusted across all adapters. Qubu
29
+ validates program structure and adapter capabilities, but does not parse SQL
30
+ to enforce safety. Callers must preserve executor-owned transactions, connection
31
+ settings, and migration journal state. For example, an explicit `COMMIT` can
32
+ leave schema changes applied without their journal record; atomicity and recovery
33
+ guarantees depend on respecting this contract.
34
+
26
35
  ## Current profiles
27
36
 
28
37
  The following stable profiles have live conformance coverage in this checkout:
29
38
 
30
- | Migration entrypoint | Dialect | Transactions | Locks | Forbidden phases | Notes |
31
- | ------------------------------------- | ---------- | ----------------------------- | --------------- | ---------------- | --------------------------------------------------- |
32
- | `@qubu/adapter-libsql/migration` | SQLite | required, optional | none, exclusive | unsupported | Pinned application-owned client |
33
- | `@qubu/adapter-node-sqlite/migration` | SQLite | required, optional | none, exclusive | unsupported | Pinned application-owned `DatabaseSync` |
34
- | `@qubu/adapter-pg/migration` | PostgreSQL | required, optional, forbidden | none, exclusive | checkpointed | Caller supplies an already-pinned client |
35
- | `@qubu/adapter-postgresjs/migration` | PostgreSQL | required, optional, forbidden | none, exclusive | checkpointed | Reserves and releases one connection |
36
- | `@qubu/adapter-pglite/migration` | PostgreSQL | required, optional, forbidden | none, exclusive | checkpointed | Uses the database query queue as the pinned session |
39
+ | Migration entrypoint | Dialect | Transactions | Locks | Forbidden phases | Notes |
40
+ | ------------------------------------- | ---------- | ----------------------------- | --------------- | ---------------- | ------------------------------------------------------ |
41
+ | `@qubu/adapter-libsql/migration` | SQLite | required, optional | none, exclusive | unsupported | Single-phase atomic batches through `client.migrate()` |
42
+ | `@qubu/adapter-node-sqlite/migration` | SQLite | required, optional | none, exclusive | unsupported | Pinned application-owned `DatabaseSync` |
43
+ | `@qubu/adapter-pg/migration` | PostgreSQL | required, optional, forbidden | none, exclusive | checkpointed | Caller supplies an already-pinned client |
44
+ | `@qubu/adapter-postgresjs/migration` | PostgreSQL | required, optional, forbidden | none, exclusive | checkpointed | Reserves and releases one connection |
45
+ | `@qubu/adapter-pglite/migration` | PostgreSQL | required, optional, forbidden | none, exclusive | checkpointed | Uses the database query queue as the pinned session |
37
46
 
38
47
  All five support every current tagged parameter kind (`null`, `boolean`,
39
48
  `string`, `number`, `bigint`, `bytes`, and `json`), a database journal and
@@ -68,3 +77,31 @@ const adapter = libsqlMigrationAdapter(client, {
68
77
 
69
78
  `DATABASE_URL` remains application configuration; neither the adapter nor CLI
70
79
  assigns deployment-provider meaning to it.
80
+
81
+ ## libSQL batch execution
82
+
83
+ Each executable artifact must contain exactly one phase and an embedded before
84
+ snapshot. The adapter submits its statements, SQL assertions, applied-history
85
+ record, head update, and terminal attempt state in one `client.migrate()` call.
86
+ For example, creating a table and recording that migration either both commit
87
+ or both roll back. Multiple artifacts are separate batches; earlier successful
88
+ artifacts remain applied if a later one fails.
89
+
90
+ Preparation reads the schema in a read transaction. The submitted batch checks
91
+ that the catalog still matches that inspection, the lease is still owned, and
92
+ the head still equals the expected parent. Foreign-key validation runs before
93
+ commit because libSQL temporarily disables enforcement during `migrate()`.
94
+
95
+ Schema fingerprint and property preconditions are checked against the embedded
96
+ before snapshot, whose physical facts are verified during preparation and
97
+ guarded by the in-batch catalog assertion. Object-presence and scalar SQL checks
98
+ run inside the batch. Postconditions must be object-presence/absence checks
99
+ without fingerprints, or scalar SQL checks returning `1`. Unsupported conditions
100
+ and multiple phases are rejected. SQL content is passed to the driver without
101
+ safety validation; each program entry must follow the driver's statement contract.
102
+
103
+ The database-row lease has no expiry or heartbeat. A process crash can leave
104
+ it held; ownership must be resolved before another runner can proceed. A lost
105
+ batch response is an uncertain outcome requiring journal inspection and, when
106
+ the attempt remains unresolved, explicit recovery. It is never assumed to be
107
+ a successful rollback.
@@ -46,13 +46,19 @@ head, or a non-prefix repository fail before any statement executes.
46
46
  ## Execution and concurrency guarantees
47
47
 
48
48
  For each invocation, the executor verifies the entire repository, opens one
49
- pinned session, checks capabilities, acquires the migrator lease, validates the
49
+ migration session, checks capabilities, acquires the migrator lease, validates the
50
50
  journal and repository prefix, checks the live before-snapshot digest, then
51
51
  applies each pending artifact. Within an artifact it creates an attempt,
52
52
  executes ordered phases with preconditions and postconditions, writes durable
53
53
  checkpoints, appends immutable history, and compare-and-swaps the head.
54
54
  Resources are released in reverse order: DDL lock, migrator lease, then session.
55
55
 
56
+ An `atomic-batch` profile instead applies one single-phase artifact in one
57
+ database transaction, including its checks and terminal journal writes. It
58
+ records a completed phase checkpoint rather than intermediate statement
59
+ checkpoints. See [libSQL batch execution](./adapters.md#libsql-batch-execution)
60
+ for its supported conditions and concurrency guards.
61
+
56
62
  A second runner cannot rely on the lease alone. Atomic applied-record/head
57
63
  advancement uses the expected parent as a compare-and-swap guard. A runner that
58
64
  observes the already-matching head exits idempotently; a conflicting head is a
@@ -51,7 +51,8 @@ producer, consumer, and regression test all exist.
51
51
  ## Parameters are runtime data
52
52
 
53
53
  Parameter values are not fragment metadata. A renderer calls
54
- `context.parameter(value)`, and `render()` collects values in placeholder order:
54
+ `context.parameter(value)`, and `render()` collects values in placeholder order.
55
+ Pass a second argument when the adapter needs the runtime SQL domain too:
55
56
 
56
57
  ```ts
57
58
  import { and, eq, from, integer, like, render, select, table, text, where } from "qubu"
@@ -72,6 +73,25 @@ render(query)
72
73
  // parameters: [7, '%Ada%']
73
74
  ```
74
75
 
76
+ The optional domain metadata stays in a sidecar aligned with `parameters`:
77
+
78
+ ```ts
79
+ import { typedValue } from "qubu/core"
80
+ import type { SqlUuid } from "qubu"
81
+
82
+ const id = typedValue<SqlUuid, string>("108cb836-20d2-41b2-8c23-f0c94700aa7e", "uuid")
83
+ render(id)
84
+ // parameterSqlTypes: ["uuid"]
85
+ ```
86
+
87
+ The domain is a binding hint, not an instruction to convert the returned
88
+ JavaScript value. Result decoding comes from a field's `type` or an explicit
89
+ decoder.
90
+
91
+ Plain JavaScript values remain untyped at runtime. Use an explicit domain when
92
+ `Date` could mean either `DATE` or `TIMESTAMP`, or when a string is a UUID
93
+ rather than text.
94
+
75
95
  The parameter array follows the placeholders in the rendered text. `select()`
76
96
  normalizes independent clause values, but keep the final call in SQL order in
77
97
  new code so source scope and repair hints are visible at a glance.
@@ -4,7 +4,7 @@
4
4
 
5
5
  The optional `qubu/introspection` entrypoint reads one selected database
6
6
  namespace through a user-owned `CatalogConnection`. It returns normalized
7
- catalog data and can map that data to canonical Snapshot v1 or v2. The
7
+ catalog data and can map that data to canonical Snapshot v1. The
8
8
  application owns the driver and connection lifecycle. Snapshot diffing,
9
9
  migration planning, and DDL emission are separate Qubu capabilities; see the
10
10
  [ownership map](supported-surface.md#ownership-boundary).
@@ -46,7 +46,7 @@ SQL.
46
46
  ## PostgreSQL complete catalog surface
47
47
 
48
48
  The PostgreSQL reader also fills the complete normalized catalog used by
49
- Snapshot v2. It keeps PostgreSQL OIDs in current-run physical references and
49
+ Snapshot v1. It keeps PostgreSQL OIDs in current-run physical references and
50
50
  keeps decompiler output such as `pg_get_viewdef`, `pg_get_triggerdef`, and
51
51
  `pg_get_functiondef` as tagged SQL data. It does not evaluate that text.
52
52
 
@@ -55,9 +55,9 @@ The query and normalization layout follows the catalog-oriented parts of the
55
55
  while Qubu keeps the result as typed data instead of generating TypeScript
56
56
  declarations.
57
57
 
58
- Use `mapCatalogToCompleteSnapshot()` for this object set. Use
59
- `mapCatalogToSnapshot()` when a caller explicitly needs the existing table-only
60
- Snapshot v1.
58
+ Use `mapCatalogToSnapshot()` or its explicit
59
+ `mapCatalogToCompleteSnapshot()` alias for this object set. Both produce the
60
+ canonical Snapshot v1 shape.
61
61
 
62
62
  ## SQLite complete catalog surface
63
63
 
@@ -86,10 +86,9 @@ the selected namespace. SQLite does not provide the PostgreSQL object families
86
86
  such as routines, materialized views, policies, or ownership, so the reader
87
87
  does not fabricate them.
88
88
 
89
- `mapCatalogToCompleteSnapshot()` retains the typed views, triggers, deferred
90
- objects, opaque boundaries, and dialect extensions in Snapshot v2. The existing
91
- `mapCatalogToSnapshot()` still maps only tables and preserves Snapshot v1
92
- behavior.
89
+ `mapCatalogToSnapshot()` delegates to `mapCatalogToCompleteSnapshot()` and
90
+ retains typed views, triggers, deferred objects, opaque boundaries, and dialect
91
+ extensions in Snapshot v1.
93
92
 
94
93
  The query and normalization seams follow the catalog-reading portions of the
95
94
  [Drizzle SQLite introspector](https://github.com/drizzle-team/drizzle-orm/blob/main/drizzle-kit/src/introspect-sqlite.ts).
@@ -110,13 +109,13 @@ MySQL has typed complete records for views, routines and their parameters,
110
109
  triggers, partitions, collations used by selected tables or columns, and
111
110
  comments. View definitions come from `INFORMATION_SCHEMA.VIEWS`; each view's
112
111
  columns are joined back to the matching `COLUMNS` rows by physical table name,
113
- so the complete Snapshot v2 cross-reference points at the view's own column
112
+ so the complete Snapshot v1 cross-reference points at the view's own column
114
113
  IDs. Missing view definitions or unresolved trigger, partition, or other
115
114
  object references become deferred records with diagnostics.
116
115
 
117
116
  Scheduled events are retained as `CatalogOpaqueObject` records. Their event
118
117
  metadata and definition remain opaque, and the reader emits an
119
- `unmodeled-object` warning. Snapshot v2 keeps these records in
118
+ `unmodeled-object` warning. Snapshot v1 keeps these records in
120
119
  `opaqueObjects`; they are not treated as typed routines, triggers, or
121
120
  migration operations.
122
121
 
@@ -132,13 +131,12 @@ The query and normalization layout follows the catalog-reading portions of the
132
131
  while Qubu keeps the result as typed data instead of generating TypeScript
133
132
  declarations.
134
133
 
135
- Use `mapCatalogToCompleteSnapshot()` to retain these typed MySQL families and
136
- the opaque or deferred boundaries in Snapshot v2. Use `mapCatalogToSnapshot()`
137
- when a caller explicitly needs the existing table-only Snapshot v1.
134
+ Use `mapCatalogToSnapshot()` or `mapCatalogToCompleteSnapshot()` to retain these
135
+ typed MySQL families and the opaque or deferred boundaries in Snapshot v1.
138
136
 
139
137
  ## Snapshot v1 surface
140
138
 
141
- The mapper can emit these facts in canonical Snapshot v1:
139
+ The mapper emits these facts in canonical Snapshot v1:
142
140
 
143
141
  - one namespace and ordinary tables;
144
142
  - exact dialect-native column storage;
@@ -162,14 +160,14 @@ Snapshot v1 objects:
162
160
  - views and materialized views;
163
161
  - sequences, enums, domains, routines, triggers, policies, extensions,
164
162
  collations, comments, and partition metadata;
165
- - PostgreSQL identity sequence options that have no typed Snapshot v1 field;
163
+ - PostgreSQL identity sequence options that have no typed field;
166
164
  - SQLite virtual/shadow tables, attached namespaces, and unrecoverable
167
165
  generated or expression definitions;
168
166
  - MySQL/MariaDB differences, prefix indexes, invisible indexes, and advanced
169
167
  functional, full-text, or spatial index semantics.
170
168
 
171
- Use the complete catalog and Snapshot v2 mapper to retain supported PostgreSQL
172
- and MySQL families. MySQL scheduled events stay opaque, and MySQL sequences,
169
+ Use the Snapshot v1 mapper to retain supported PostgreSQL and MySQL families.
170
+ MySQL scheduled events stay opaque, and MySQL sequences,
173
171
  materialized views, row-level security (RLS) policies, extension objects, and
174
172
  ownership stay unsupported or deferred. When a row cannot be normalized safely, the reader
175
173
  keeps a typed deferred or opaque record and emits a diagnostic instead of