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.
- package/README.md +112 -0
- package/dist/{complete-DP7pliuY.mjs → canonical-B5_ouh-c.mjs} +466 -241
- package/dist/codegen.d.mts +2 -2
- package/dist/codegen.mjs +180 -90
- package/dist/column-Cyc2CMnG.mjs +116 -0
- package/dist/column-DDRvD7SF.mjs +721 -0
- package/dist/{constraints-YGyNPQ_z.mjs → constraints-CAmi18Uk.mjs} +8 -3
- package/dist/core.d.mts +3 -3
- package/dist/core.mjs +4 -5
- package/dist/diagnostics-I9vVtXkc.mjs +40 -0
- package/dist/diff.d.mts +9 -9
- package/dist/diff.mjs +152 -102
- package/dist/{explain-CkIK13L_.mjs → explain-BIqEmZha.mjs} +1 -1
- package/dist/expressions-_6JF_J77.mjs +130 -0
- package/dist/index-CaxrMD1A.d.mts +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +414 -77
- package/dist/introspection/mysql.d.mts +26 -0
- package/dist/introspection/mysql.mjs +1278 -0
- package/dist/introspection/postgres.d.mts +43 -0
- package/dist/introspection/postgres.mjs +1889 -0
- package/dist/introspection/sqlite.d.mts +15 -0
- package/dist/introspection/sqlite.mjs +1172 -0
- package/dist/introspection.d.mts +26 -87
- package/dist/introspection.mjs +3 -4351
- package/dist/mysql.d.mts +3 -3
- package/dist/mysql.mjs +8 -7
- package/dist/{errors-Dxv73YJu.mjs → omit-VEr9Ydux.mjs} +5 -1
- package/dist/{on-conflict-DZQ85f1t.mjs → on-conflict-B2rFyHGF.mjs} +78 -8
- package/dist/on-duplicate-key-update-Czsw1Yq-.mjs +95 -0
- package/dist/postgres-hFhd0I9n.mjs +68 -0
- package/dist/postgres.d.mts +3 -3
- package/dist/postgres.mjs +3 -52
- package/dist/registry-BXE_4M9P.mjs +127 -0
- package/dist/{relational-CxnLCqZQ.mjs → relational-CoPBETjI.mjs} +3 -3
- package/dist/schema.d.mts +2 -2
- package/dist/schema.mjs +8 -7
- package/dist/{serialize-BN07IK0v.mjs → serialize-CyobNEx-.mjs} +174 -30
- package/dist/serialize-Du2UPZMt.d.mts +92 -0
- package/dist/snapshot/mysql.d.mts +5 -5
- package/dist/snapshot/mysql.mjs +32 -32
- package/dist/snapshot/postgres.d.mts +5 -5
- package/dist/snapshot/postgres.mjs +25 -25
- package/dist/snapshot/sqlite.d.mts +5 -5
- package/dist/snapshot/sqlite.mjs +30 -30
- package/dist/snapshot-mIb-Zzb5.mjs +1489 -0
- package/dist/snapshot.d.mts +4 -5
- package/dist/snapshot.mjs +3 -4
- package/dist/{source-SqrKWjFJ.mjs → source-DYSUqzvb.mjs} +2 -2
- package/dist/sqlite.d.mts +2 -2
- package/dist/sqlite.mjs +10 -10
- package/dist/{table-BwflqeAj.mjs → table-B8zEq0az.mjs} +4 -4
- package/dist/{types-BIJsj2fJ.mjs → types-CYHpSPwj.mjs} +10 -7
- package/dist/{types-CTCqtFlS.d.mts → types-CiMvKi5V.d.mts} +14 -4
- package/dist/{types-C0VkiwpR.d.mts → types-Dqr4o2I1.d.mts} +640 -189
- package/dist/value-CpaUFtjw.mjs +45 -0
- package/dist/vite/ambient.d.ts +2 -0
- package/dist/vite.d.mts +1 -1
- package/dist/vite.mjs +2 -0
- package/docs/dialects-and-execution.md +61 -11
- package/docs/getting-started.md +1 -1
- package/docs/guides/compose-queries.md +22 -0
- package/docs/guides/drizzle.md +11 -11
- package/docs/guides/extensions/typed-expressions.md +5 -3
- package/docs/guides/json.md +95 -2
- package/docs/guides/mutations.md +125 -0
- package/docs/guides/valtio-sync.md +113 -0
- package/docs/index.md +1 -1
- package/docs/migrations/adapters.md +58 -21
- package/docs/migrations/index.md +50 -12
- package/docs/migrations/operations.md +20 -6
- package/docs/migrations/recovery.md +7 -1
- package/docs/query-model/fragments.md +21 -1
- package/docs/reference/introspection-support.md +16 -18
- package/docs/reference/supported-surface.md +23 -11
- package/docs/schema/catalog-model.md +20 -7
- package/docs/schema/code-generation.md +4 -3
- package/docs/schema/diff.md +2 -2
- package/docs/schema/introspection.md +24 -16
- package/docs/schema/snapshots.md +18 -8
- package/docs/schema/storage-and-schema-sql.md +7 -2
- package/docs/sql-semantic-types.md +8 -0
- package/package.json +14 -1
- package/dist/canonical-DMvR9yBe.mjs +0 -972
- package/dist/column-Da37jYSD.mjs +0 -309
- package/dist/column-r1Y4ivwt.mjs +0 -327
- package/dist/complete-types-CY0KbzNw.d.mts +0 -371
- package/dist/index-B2rZf3-2.d.mts +0 -32
- package/dist/json-Db7XRD91.mjs +0 -169
- package/dist/omit-OxV58AwX.mjs +0 -5
- package/dist/registry-BRcUuazJ.mjs +0 -256
- package/dist/serialize-CEIIlWhC.d.mts +0 -66
- package/dist/snapshot-Xam8-q0j.mjs +0 -392
- package/dist/standard-DfcZEVOj.mjs +0 -12
- 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 };
|
package/dist/vite/ambient.d.ts
CHANGED
|
@@ -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
|
@@ -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
|
|
151
|
-
result domains through projection aliases, derived queries, CTEs, set
|
|
152
|
-
operations, and mutation `RETURNING`.
|
|
153
|
-
|
|
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
|
-
|
|
463
|
-
`
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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:
|
package/docs/getting-started.md
CHANGED
|
@@ -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
|
-
- [
|
|
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
|
package/docs/guides/drizzle.md
CHANGED
|
@@ -22,7 +22,7 @@ your database:
|
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
import { integer, schema, table, text } from "qubu"
|
|
25
|
-
import {
|
|
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 =
|
|
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` | `
|
|
43
|
-
| MySQL | `@qubu/drizzle/mysql` | `
|
|
44
|
-
| SQLite | `@qubu/drizzle/sqlite` | `
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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
|
|
73
|
-
|
|
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
|
package/docs/guides/json.md
CHANGED
|
@@ -1,6 +1,99 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Query nested JSON
|
|
2
2
|
|
|
3
|
-
>
|
|
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:
|
package/docs/guides/mutations.md
CHANGED
|
@@ -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.
|