qubu 0.6.0 → 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 (95) hide show
  1. package/README.md +112 -0
  2. package/dist/{complete-DP7pliuY.mjs → canonical-B5_ouh-c.mjs} +466 -241
  3. package/dist/codegen.d.mts +2 -2
  4. package/dist/codegen.mjs +180 -90
  5. package/dist/column-Cyc2CMnG.mjs +116 -0
  6. package/dist/column-DDRvD7SF.mjs +721 -0
  7. package/dist/{constraints-YGyNPQ_z.mjs → constraints-CAmi18Uk.mjs} +8 -3
  8. package/dist/core.d.mts +3 -3
  9. package/dist/core.mjs +4 -5
  10. package/dist/diagnostics-I9vVtXkc.mjs +40 -0
  11. package/dist/diff.d.mts +9 -9
  12. package/dist/diff.mjs +152 -102
  13. package/dist/{explain-CkIK13L_.mjs → explain-BIqEmZha.mjs} +1 -1
  14. package/dist/expressions-_6JF_J77.mjs +130 -0
  15. package/dist/index-CaxrMD1A.d.mts +1 -0
  16. package/dist/index.d.mts +2 -2
  17. package/dist/index.mjs +414 -77
  18. package/dist/introspection/mysql.d.mts +26 -0
  19. package/dist/introspection/mysql.mjs +1278 -0
  20. package/dist/introspection/postgres.d.mts +43 -0
  21. package/dist/introspection/postgres.mjs +1889 -0
  22. package/dist/introspection/sqlite.d.mts +15 -0
  23. package/dist/introspection/sqlite.mjs +1172 -0
  24. package/dist/introspection.d.mts +26 -87
  25. package/dist/introspection.mjs +3 -4351
  26. package/dist/mysql.d.mts +3 -3
  27. package/dist/mysql.mjs +8 -7
  28. package/dist/{errors-Dxv73YJu.mjs → omit-VEr9Ydux.mjs} +5 -1
  29. package/dist/{on-conflict-DZQ85f1t.mjs → on-conflict-B2rFyHGF.mjs} +78 -8
  30. package/dist/on-duplicate-key-update-Czsw1Yq-.mjs +95 -0
  31. package/dist/postgres-hFhd0I9n.mjs +68 -0
  32. package/dist/postgres.d.mts +3 -3
  33. package/dist/postgres.mjs +3 -52
  34. package/dist/registry-BXE_4M9P.mjs +127 -0
  35. package/dist/{relational-CxnLCqZQ.mjs → relational-CoPBETjI.mjs} +3 -3
  36. package/dist/schema.d.mts +2 -2
  37. package/dist/schema.mjs +8 -7
  38. package/dist/{serialize-BN07IK0v.mjs → serialize-CyobNEx-.mjs} +174 -30
  39. package/dist/serialize-Du2UPZMt.d.mts +92 -0
  40. package/dist/snapshot/mysql.d.mts +5 -5
  41. package/dist/snapshot/mysql.mjs +32 -32
  42. package/dist/snapshot/postgres.d.mts +5 -5
  43. package/dist/snapshot/postgres.mjs +25 -25
  44. package/dist/snapshot/sqlite.d.mts +5 -5
  45. package/dist/snapshot/sqlite.mjs +30 -30
  46. package/dist/snapshot-mIb-Zzb5.mjs +1489 -0
  47. package/dist/snapshot.d.mts +4 -5
  48. package/dist/snapshot.mjs +3 -4
  49. package/dist/{source-SqrKWjFJ.mjs → source-DYSUqzvb.mjs} +2 -2
  50. package/dist/sqlite.d.mts +2 -2
  51. package/dist/sqlite.mjs +10 -10
  52. package/dist/{table-BwflqeAj.mjs → table-B8zEq0az.mjs} +4 -4
  53. package/dist/{types-BIJsj2fJ.mjs → types-CYHpSPwj.mjs} +10 -7
  54. package/dist/{types-CTCqtFlS.d.mts → types-CiMvKi5V.d.mts} +14 -4
  55. package/dist/{types-C0VkiwpR.d.mts → types-Dqr4o2I1.d.mts} +640 -189
  56. package/dist/value-CpaUFtjw.mjs +45 -0
  57. package/dist/vite/ambient.d.ts +2 -0
  58. package/dist/vite.d.mts +1 -1
  59. package/dist/vite.mjs +2 -0
  60. package/docs/dialects-and-execution.md +61 -11
  61. package/docs/getting-started.md +1 -1
  62. package/docs/guides/compose-queries.md +22 -0
  63. package/docs/guides/drizzle.md +11 -11
  64. package/docs/guides/extensions/typed-expressions.md +5 -3
  65. package/docs/guides/json.md +95 -2
  66. package/docs/guides/mutations.md +125 -0
  67. package/docs/guides/valtio-sync.md +113 -0
  68. package/docs/index.md +1 -1
  69. package/docs/migrations/adapters.md +58 -21
  70. package/docs/migrations/index.md +50 -12
  71. package/docs/migrations/operations.md +20 -6
  72. package/docs/migrations/recovery.md +7 -1
  73. package/docs/query-model/fragments.md +21 -1
  74. package/docs/reference/introspection-support.md +16 -18
  75. package/docs/reference/supported-surface.md +23 -11
  76. package/docs/schema/catalog-model.md +20 -7
  77. package/docs/schema/code-generation.md +4 -3
  78. package/docs/schema/diff.md +2 -2
  79. package/docs/schema/introspection.md +24 -16
  80. package/docs/schema/snapshots.md +18 -8
  81. package/docs/schema/storage-and-schema-sql.md +7 -2
  82. package/docs/sql-semantic-types.md +8 -0
  83. package/package.json +14 -1
  84. package/dist/canonical-DMvR9yBe.mjs +0 -972
  85. package/dist/column-Da37jYSD.mjs +0 -309
  86. package/dist/column-r1Y4ivwt.mjs +0 -327
  87. package/dist/complete-types-CY0KbzNw.d.mts +0 -371
  88. package/dist/index-B2rZf3-2.d.mts +0 -32
  89. package/dist/json-Db7XRD91.mjs +0 -169
  90. package/dist/omit-OxV58AwX.mjs +0 -5
  91. package/dist/registry-BRcUuazJ.mjs +0 -256
  92. package/dist/serialize-CEIIlWhC.d.mts +0 -66
  93. package/dist/snapshot-Xam8-q0j.mjs +0 -392
  94. package/dist/standard-DfcZEVOj.mjs +0 -12
  95. package/dist/value-D14I_XgL.mjs +0 -29
@@ -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.
@@ -28,6 +28,28 @@ The rendered statement includes the `WITH` clause before `SELECT`. Selected
28
28
  camelCase keys use snake_case while they belong to the CTE relation; the outer
29
29
  result projection aliases them back to camelCase for the returned row.
30
30
 
31
+ Attach the same clause to an insert, update, or delete when the mutation reads
32
+ through the CTE. For example, an insert can consume a filtered CTE through
33
+ `insertSelect()`:
34
+
35
+ ```ts
36
+ import { insertInto, insertSelect, returning } from "qubu"
37
+
38
+ const archivedUsers = table("archived_users", { name: text() })
39
+ const archive = insertInto(
40
+ archivedUsers,
41
+ insertSelect(select({ name: activeUsers.name }, from(activeUsers)), ["name"]),
42
+ withCte(activeUsers),
43
+ returning({ name: archivedUsers.name }),
44
+ )
45
+ ```
46
+
47
+ Qubu renders `WITH ... INSERT` and keeps CTE parameters before mutation
48
+ parameters. For update and delete predicates, select from the CTE in a scalar,
49
+ `IN`, or `EXISTS` subquery; `withCte()` defines the relation but does not add it
50
+ directly to the mutation target's column scope. Recursive CTEs use the same
51
+ pattern and render `WITH RECURSIVE` before the mutation keyword.
52
+
31
53
  ## Build a recursive CTE
32
54
 
33
55
  `recursiveCte()` uses the anchor projection as the contract for a recursive
@@ -22,7 +22,7 @@ your database:
22
22
 
23
23
  ```ts
24
24
  import { integer, schema, table, text } from "qubu"
25
- import { toPostgresDrizzleSchema } from "@qubu/drizzle/postgres"
25
+ import { toDrizzleSchema } from "@qubu/drizzle/postgres"
26
26
 
27
27
  const users = table("user_records", {
28
28
  id: integer({ generated: true }),
@@ -31,17 +31,17 @@ const users = table("user_records", {
31
31
  })
32
32
 
33
33
  const appSchema = schema({ users }, { namespace: "app" })
34
- const drizzleTables = toPostgresDrizzleSchema(appSchema)
34
+ const drizzleTables = toDrizzleSchema(appSchema)
35
35
  ```
36
36
 
37
37
  The import path selects the dialect. Each module imports only its matching
38
38
  Drizzle core package:
39
39
 
40
- | Database | Import | Converter |
41
- | ---------- | ------------------------ | --------------------------- |
42
- | PostgreSQL | `@qubu/drizzle/postgres` | `toPostgresDrizzleSchema()` |
43
- | MySQL | `@qubu/drizzle/mysql` | `toMysqlDrizzleSchema()` |
44
- | SQLite | `@qubu/drizzle/sqlite` | `toSqliteDrizzleSchema()` |
40
+ | Database | Import | Converter |
41
+ | ---------- | ------------------------ | ------------------- |
42
+ | PostgreSQL | `@qubu/drizzle/postgres` | `toDrizzleSchema()` |
43
+ | MySQL | `@qubu/drizzle/mysql` | `toDrizzleSchema()` |
44
+ | SQLite | `@qubu/drizzle/sqlite` | `toDrizzleSchema()` |
45
45
 
46
46
  `@qubu/drizzle` exports the shared conversion error and dialect types. It does
47
47
  not import a dialect core or provide a universal runtime converter.
@@ -75,7 +75,7 @@ Unix timestamps and Drizzle must continue reading and writing `Date` values:
75
75
 
76
76
  ```ts
77
77
  import { schema, table } from "qubu"
78
- import { toSqliteDrizzleSchema } from "@qubu/drizzle/sqlite"
78
+ import { toDrizzleSchema } from "@qubu/drizzle/sqlite"
79
79
  import { sqliteTimestamp } from "qubu/sqlite"
80
80
 
81
81
  const events = table("events", {
@@ -85,7 +85,7 @@ const events = table("events", {
85
85
  }),
86
86
  })
87
87
 
88
- const drizzleTables = toSqliteDrizzleSchema(schema({ events }))
88
+ const drizzleTables = toDrizzleSchema(schema({ events }))
89
89
  ```
90
90
 
91
91
  `timestamp` stores Unix seconds and `timestamp_ms` stores Unix milliseconds.
@@ -129,13 +129,13 @@ belong to the selected dialect:
129
129
 
130
130
  ```ts
131
131
  import { nativeColumn, schema, table } from "qubu"
132
- import { toPostgresDrizzleSchema } from "@qubu/drizzle/postgres"
132
+ import { toDrizzleSchema } from "@qubu/drizzle/postgres"
133
133
 
134
134
  const records = table("records", {
135
135
  handle: nativeColumn("postgresql", "CITEXT"),
136
136
  })
137
137
 
138
- const tables = toPostgresDrizzleSchema(schema({ records }))
138
+ const tables = toDrizzleSchema(schema({ records }))
139
139
  ```
140
140
 
141
141
  Conversion first runs Qubu's snapshot validation for the selected dialect.
@@ -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:
@@ -44,6 +44,63 @@ render(query)
44
44
 
45
45
  Use `defaultValues()` only when every non-generated column has a database
46
46
  default. Use `insertSelect(query, columns)` for an `INSERT ... SELECT` source.
47
+ An ordinary or recursive `withCte()` clause can prefix any mutation; see
48
+ [Compose queries](compose-queries.md#turn-a-query-into-a-cte) for the typed
49
+ `WITH ... INSERT`, `WITH ... UPDATE`, and `WITH ... DELETE` patterns.
50
+
51
+ Each field may also be a typed expression whose output is compatible with the
52
+ target column. Expressions render directly and retain their dialect
53
+ requirements; ordinary application values still pass through the column's
54
+ parameter encoder:
55
+
56
+ ```ts
57
+ import { upper } from "qubu"
58
+
59
+ insertInto(users, values({ name: upper("Ada") }))
60
+ ```
61
+
62
+ An `INSERT ... VALUES` row does not introduce a relational source, so its
63
+ expressions cannot reference columns from the target table or another table.
64
+ Use `insertSelect()` when inserted values need a query source.
65
+
66
+ ## Resolve PostgreSQL conflicts
67
+
68
+ PostgreSQL upserts can target a primary key, a `unique()` constraint, or a
69
+ declared unique index from the inserted table. A partial unique index carries
70
+ its predicate into the conflict target so PostgreSQL can infer the same index:
71
+
72
+ ```ts
73
+ import { boolean, eq, index, table, text, value } from "qubu"
74
+ import { doUpdate, excluded, onConflict } from "qubu/postgres"
75
+
76
+ const accounts = table(
77
+ "accounts",
78
+ { email: text(), active: boolean(), name: text() },
79
+ (accounts) => ({
80
+ constraints: {},
81
+ indexes: {
82
+ activeEmail: index([accounts.email], {
83
+ unique: true,
84
+ where: eq(accounts.active, value(true)),
85
+ dialect: { dialect: "postgresql" },
86
+ }),
87
+ },
88
+ }),
89
+ )
90
+
91
+ const incoming = excluded(accounts)
92
+ const conflict = onConflict(
93
+ accounts,
94
+ accounts.indexes.activeEmail,
95
+ doUpdate({ name: incoming.name }),
96
+ )
97
+ ```
98
+
99
+ The index must be unique, declared on the insert target, and portable or marked
100
+ for PostgreSQL. Index expressions and predicates use the same deterministic,
101
+ parameter-free schema-expression rules as the declared index. Constraint-based
102
+ targets keep their existing cross-dialect behavior; unique-index inference is
103
+ PostgreSQL-specific.
47
104
 
48
105
  ## Update with a predicate
49
106
 
@@ -64,6 +121,38 @@ const query = update(
64
121
  The assignment expression is source-aware, so a column from an unrelated table
65
122
  cannot silently enter the update.
66
123
 
124
+ PostgreSQL updates can introduce one or more typed sources with `updateFrom()`.
125
+ Those sources are available to assignments, the predicate, and `RETURNING`:
126
+
127
+ ```ts
128
+ import { eq, integer, render, returning, table, text, update, where } from "qubu"
129
+ import { postgresDialect, updateFrom } from "qubu/postgres"
130
+
131
+ const changes = table("user_changes", {
132
+ userId: integer(),
133
+ name: text(),
134
+ })
135
+
136
+ const query = update(
137
+ users,
138
+ { name: changes.name },
139
+ updateFrom(changes),
140
+ where(eq(users.id, changes.userId)),
141
+ returning({ id: users.id, sourceName: changes.name }),
142
+ )
143
+
144
+ render(query, postgresDialect())
145
+ // UPDATE "users" SET "name" = "user_changes"."name"
146
+ // FROM "user_changes"
147
+ // WHERE ("users"."id" = "user_changes"."user_id")
148
+ // RETURNING "users"."id" AS "id", "user_changes"."name" AS "sourceName"
149
+ ```
150
+
151
+ `UPDATE ... FROM` carries a dialect capability requirement, so rendering it
152
+ with the default, SQLite, or MySQL dialect is rejected. Qubu still requires a
153
+ predicate or an explicit `allowAll()` marker; introducing a source does not
154
+ authorize an unrestricted update.
155
+
67
156
  Use `omit` for a runtime-conditional assignment. Qubu removes omitted fields
68
157
  before validating and rendering the effective assignment set:
69
158
 
@@ -87,6 +176,42 @@ before rendering when every field is omitted. Possible expression branches
87
176
  remain source- and capability-aware even when their runtime alternative is
88
177
  `omit`.
89
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
+
90
215
  ## Delete with a predicate
91
216
 
92
217
  ```ts
@@ -0,0 +1,113 @@
1
+ # Valtio Sync
2
+
3
+ > Check Valtio Sync fields against Qubu tables and apply each client mutation
4
+ > with its sync event in one Qubu-owned transaction.
5
+
6
+ Install the optional integration beside Qubu, Valtio Sync, and Zod:
7
+
8
+ ```sh
9
+ pnpm add qubu @qubu/valtio-sync valtio-sync zod
10
+ ```
11
+
12
+ ## Check synced fields against Qubu tables
13
+
14
+ Use the integration's schema wrappers instead of `valtio-sync/schema` when a
15
+ definition corresponds to a Qubu table. Every selected table field must appear
16
+ in `fields`. Mark persistence-only or server-controlled fields with
17
+ `serverOnly()` so they are excluded from Valtio Sync validation and records.
18
+
19
+ ```ts
20
+ import { $type, defineCollection, serverOnly } from "@qubu/valtio-sync"
21
+ import { boolean, integer, table, text } from "qubu"
22
+ import { z } from "zod"
23
+
24
+ const todosTable = table("todos", {
25
+ ownerId: integer(),
26
+ id: text(),
27
+ title: text(),
28
+ done: boolean(),
29
+ })
30
+
31
+ export const todos = defineCollection({
32
+ dbType: $type<typeof todosTable>(),
33
+ fields: {
34
+ ownerId: serverOnly(),
35
+ id: z.string(),
36
+ title: z.string().default(""),
37
+ done: z.boolean().default(false),
38
+ },
39
+ })
40
+ ```
41
+
42
+ The Zod output for each synced field must be assignable to its Qubu selected
43
+ value. Narrow schemas are allowed, such as a Zod enum for a Qubu text field.
44
+ Missing fields, extra fields, and wider outputs fail type checking. The same
45
+ rules apply to `defineAccount()`.
46
+
47
+ ## Apply mutations in transactions
48
+
49
+ `applyOpsWithQubu()` converts Qubu-aware mutation handlers into the public
50
+ `ServerHandlers` contract accepted by `valtio-sync/server`. Supply a bound Qubu
51
+ client whose adapter supports transactions:
52
+
53
+ ```ts
54
+ import { applyOpsWithQubu } from "@qubu/valtio-sync"
55
+ import { and, eq, insertInto, returning, update, values, where } from "qubu"
56
+ import { valtioSync } from "valtio-sync/server"
57
+
58
+ type SyncContext = { user: { id: number } }
59
+
60
+ const handlers = applyOpsWithQubu<SyncContext>({
61
+ db,
62
+ syncEvents: {
63
+ write: async ({ tx, ctx, collection, recordId, op }) => {
64
+ const [event] = await tx.rows(
65
+ insertInto(
66
+ syncEvents,
67
+ values({ userId: ctx.user.id, collection, recordId, op }),
68
+ returning({ seq: syncEvents.seq }),
69
+ ),
70
+ )
71
+ return event.seq
72
+ },
73
+ },
74
+ authorize: ({ ctx, collection, op }) => assertCanSync(ctx.user, collection, op),
75
+ checkConflict: ({ tx, ctx, collection, op }) =>
76
+ assertFreshBaseVersion(tx, ctx.user.id, collection, op),
77
+ handlers: {
78
+ todos: {
79
+ readChanges: ({ ctx, since }) => readTodoChanges(ctx.user.id, since),
80
+ create: async ({ tx, ctx, record }) => {
81
+ const value = todos.recordSchema.parse(record)
82
+
83
+ await tx.execute(insertInto(todosTable, values({ ...value, ownerId: ctx.user.id })))
84
+ return {}
85
+ },
86
+ update: async ({ tx, ctx, op, patch }) => {
87
+ const value = todos.recordSchema.partial().parse(patch)
88
+
89
+ await tx.execute(
90
+ update(
91
+ todosTable,
92
+ value,
93
+ where(and(eq(todosTable.id, op.id), eq(todosTable.ownerId, ctx.user.id))),
94
+ ),
95
+ )
96
+ return {}
97
+ },
98
+ },
99
+ },
100
+ })
101
+
102
+ export const sync = valtioSync({ schema: { todos }, handlers })
103
+ ```
104
+
105
+ Authorization, conflict checks, the application mutation, and
106
+ `syncEvents.write()` run in that order inside one Qubu transaction. If any step
107
+ fails, the adapter rolls the transaction back. The event sequence becomes
108
+ `serverVersion` unless the mutation handler returns an explicit version. Read
109
+ handlers pass through unchanged.
110
+
111
+ The integration does not define persistence tables, import Drizzle, or execute
112
+ driver APIs. The application owns table design, authorization, conflict policy,
113
+ event retention, and every query issued through the supplied Qubu transaction.