qubu 0.3.5 → 0.4.1

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 (104) hide show
  1. package/dist/{canonical-BbnqavJm.mjs → canonical-BXUguqfo.mjs} +23 -23
  2. package/dist/codegen.d.mts +25 -33
  3. package/dist/codegen.mjs +38 -45
  4. package/dist/column-hqKr7-1I.mjs +620 -0
  5. package/dist/{complete-D5Djh-zo.mjs → complete-WYyVozgK.mjs} +50 -50
  6. package/dist/{complete-types-B2PO6wQD.d.mts → complete-types-IjEn5VPN.d.mts} +56 -56
  7. package/dist/core.d.mts +2 -2
  8. package/dist/core.mjs +4 -4
  9. package/dist/ddl.d.mts +12 -12
  10. package/dist/ddl.mjs +13 -14
  11. package/dist/{dialect-b2-Z6uBF.mjs → dialect-wUKrnPMB.mjs} +2 -2
  12. package/dist/diff.d.mts +1 -1
  13. package/dist/diff.mjs +6 -7
  14. package/dist/{index-7qc6OcIC.d.mts → index-1DpA3mUh.d.mts} +20 -20
  15. package/dist/{index-CqWnouTK.d.mts → index-CPvfEheG.d.mts} +13 -15
  16. package/dist/index.d.mts +2 -2
  17. package/dist/index.mjs +148 -120
  18. package/dist/introspection.d.mts +8 -8
  19. package/dist/introspection.mjs +29 -30
  20. package/dist/{json-CUZlv4HT.mjs → json-Db7XRD91.mjs} +2 -2
  21. package/dist/migration.d.mts +20 -21
  22. package/dist/migration.mjs +5 -6
  23. package/dist/{mysql-DqkqXB6A.mjs → mysql-B_cYzzX2.mjs} +237 -7
  24. package/dist/mysql.d.mts +1 -1
  25. package/dist/mysql.mjs +3 -3
  26. package/dist/{on-conflict-BxnxubMb.mjs → on-conflict-hfPW0KmQ.mjs} +4 -4
  27. package/dist/postgres.d.mts +5 -5
  28. package/dist/postgres.mjs +6 -6
  29. package/dist/{registry-BufIskVN.mjs → registry-BRMLYwDp.mjs} +27 -135
  30. package/dist/{relational-DCZrrNia.mjs → relational-BZ3WDPzC.mjs} +4 -4
  31. package/dist/schema.d.mts +2 -2
  32. package/dist/schema.mjs +6 -6
  33. package/dist/{snapshot-CWPgzxNx.mjs → snapshot-C-W65HEd.mjs} +5 -5
  34. package/dist/snapshot.d.mts +3 -3
  35. package/dist/snapshot.mjs +5 -7
  36. package/dist/{source-DUoJVXmL.mjs → source-BcS2AsIg.mjs} +7 -9
  37. package/dist/{serialize-PF1cfH2P.mjs → sqlite-Cg0nwYEH.mjs} +326 -12
  38. package/dist/sqlite.d.mts +2 -2
  39. package/dist/sqlite.mjs +2 -2
  40. package/dist/{standard-BTVYKh_F.mjs → standard-DfcZEVOj.mjs} +1 -1
  41. package/dist/{table-CCUJ60rB.mjs → table-Bp5irMSj.mjs} +5 -7
  42. package/dist/{types-D8M1yZF4.d.mts → types-BK1COGZe.d.mts} +2661 -2150
  43. package/dist/{types-Cec0xzo4.mjs → types-JM3FcAnX.mjs} +8 -8
  44. package/dist/{types-LBt5rclR.d.mts → types-JSZHpUEj.d.mts} +83 -84
  45. package/dist/{value-BvilP0oz.mjs → value-Bi71Agyf.mjs} +1 -1
  46. package/dist/vite/ambient.d.ts +311 -390
  47. package/dist/vite.d.mts +5 -6
  48. package/dist/vite.mjs +3 -4
  49. package/docs/dialects-and-execution.md +121 -81
  50. package/docs/getting-started.md +4 -4
  51. package/docs/guides/better-auth.md +57 -0
  52. package/docs/guides/compose-queries.md +24 -67
  53. package/docs/guides/drizzle.md +23 -23
  54. package/docs/guides/extensions/dialects.md +4 -4
  55. package/docs/guides/extensions/sources-and-clauses.md +15 -15
  56. package/docs/guides/extensions/typed-expressions.md +13 -17
  57. package/docs/guides/extensions/unsafe-syntax.md +3 -3
  58. package/docs/guides/json.md +7 -7
  59. package/docs/guides/mutations.md +14 -21
  60. package/docs/guides/select/conditions.md +6 -8
  61. package/docs/guides/select/grouping-and-windows.md +4 -15
  62. package/docs/guides/select/ordering-and-pagination.md +6 -19
  63. package/docs/guides/select/overview.md +12 -26
  64. package/docs/guides/sql-templates.md +20 -24
  65. package/docs/guides/vite-plugin.md +9 -13
  66. package/docs/index.md +5 -7
  67. package/docs/query-model/fragments.md +4 -15
  68. package/docs/query-model/result-shapes.md +11 -14
  69. package/docs/query-model/source-scope.md +31 -49
  70. package/docs/reference/mysql-snapshot.md +2 -5
  71. package/docs/reference/postgres-snapshot.md +3 -3
  72. package/docs/reference/sqlite-snapshot.md +2 -5
  73. package/docs/reference/supported-surface.md +33 -33
  74. package/docs/schema/catalog-model.md +2 -5
  75. package/docs/schema/code-generation.md +18 -18
  76. package/docs/schema/columns-and-writes.md +17 -25
  77. package/docs/schema/constraints-and-indexes.md +36 -52
  78. package/docs/schema/ddl-emission.md +6 -6
  79. package/docs/schema/diff.md +5 -5
  80. package/docs/schema/introspection.md +7 -7
  81. package/docs/schema/migration-plans.md +9 -9
  82. package/docs/schema/snapshots.md +3 -3
  83. package/docs/schema/storage-and-schema-sql.md +11 -11
  84. package/docs/schema/tables-and-names.md +11 -14
  85. package/docs/sql-semantic-types.md +10 -10
  86. package/docs/troubleshooting.md +10 -10
  87. package/package.json +21 -33
  88. package/skills/qubu/agents/openai.yaml +3 -3
  89. package/dist/column-CXMxx8Hq.mjs +0 -118
  90. package/dist/column-CYMbKbOy.mjs +0 -290
  91. package/dist/drizzle-mysql.d.mts +0 -24
  92. package/dist/drizzle-mysql.mjs +0 -72
  93. package/dist/drizzle-postgres.d.mts +0 -24
  94. package/dist/drizzle-postgres.mjs +0 -73
  95. package/dist/drizzle-sqlite.d.mts +0 -50
  96. package/dist/drizzle-sqlite.mjs +0 -108
  97. package/dist/drizzle.d.mts +0 -13
  98. package/dist/drizzle.mjs +0 -2
  99. package/dist/errors-BGCoLe_r.mjs +0 -14
  100. package/dist/naming-QVCOnSj2.mjs +0 -20
  101. package/dist/postgres-DEBBeh52.mjs +0 -235
  102. package/dist/runtime-Cn_Xgzta.mjs +0 -193
  103. package/dist/sqlite-BU6DBxef.mjs +0 -320
  104. package/dist/types-Ctlxz1ip.d.mts +0 -45
package/dist/vite.d.mts CHANGED
@@ -1,9 +1,8 @@
1
1
  //#region src/vite/globals.d.ts
2
2
  /**
3
- * Public runtime names that the `"use qubu"` transform can auto-import.
4
- * Keep this catalog aligned with the ordinary authoring surface of `qubu`;
5
- * fragment internals, dialect construction, and schema extensions belong to
6
- * the layered entrypoints.
3
+ * Public runtime names that the `"use qubu"` transform can auto-import. Keep this catalog aligned
4
+ * with the ordinary authoring surface of `qubu`; fragment internals, dialect construction, and
5
+ * schema extensions belong to the layered entrypoints.
7
6
  */
8
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"];
9
8
  type QubuGlobal = (typeof qubuGlobals)[number];
@@ -25,8 +24,8 @@ interface QubuViteTransformResult {
25
24
  }
26
25
  /** A Vite-compatible plugin without a runtime dependency on Vite itself. */
27
26
  interface QubuVitePlugin {
28
- readonly name: 'qubu:compiler-hint';
29
- readonly enforce: 'pre';
27
+ readonly name: "qubu:compiler-hint";
28
+ readonly enforce: "pre";
30
29
  transform(code: string, id: string): QubuViteTransformResult | null;
31
30
  }
32
31
  declare function qubu(options?: QubuVitePluginOptions): QubuVitePlugin;
package/dist/vite.mjs CHANGED
@@ -70,10 +70,9 @@ function isWhitespace$1(character) {
70
70
  //#endregion
71
71
  //#region src/vite/globals.ts
72
72
  /**
73
- * Public runtime names that the `"use qubu"` transform can auto-import.
74
- * Keep this catalog aligned with the ordinary authoring surface of `qubu`;
75
- * fragment internals, dialect construction, and schema extensions belong to
76
- * the layered entrypoints.
73
+ * Public runtime names that the `"use qubu"` transform can auto-import. Keep this catalog aligned
74
+ * with the ordinary authoring surface of `qubu`; fragment internals, dialect construction, and
75
+ * schema extensions belong to the layered entrypoints.
77
76
  */
78
77
  const qubuGlobals = [
79
78
  "add",
@@ -20,8 +20,8 @@ Construct the query without choosing a driver, then render it with the policy
20
20
  the adapter expects:
21
21
 
22
22
  ```ts
23
- import { render } from 'qubu'
24
- import { postgresDialect } from 'qubu/postgres'
23
+ import { render } from "qubu"
24
+ import { postgresDialect } from "qubu/postgres"
25
25
 
26
26
  const standard = render(query)
27
27
  const postgres = render(query, postgresDialect())
@@ -40,15 +40,11 @@ capability requirement to the rendering boundary. PostgreSQL's `ilike()` is
40
40
  the first such feature:
41
41
 
42
42
  ```ts
43
- import { from, like, render, select, where } from 'qubu'
44
- import { ilike, postgresDialect } from 'qubu/postgres'
45
- import { sqliteDialect } from 'qubu/sqlite'
43
+ import { from, like, render, select, where } from "qubu"
44
+ import { ilike, postgresDialect } from "qubu/postgres"
45
+ import { sqliteDialect } from "qubu/sqlite"
46
46
 
47
- const postgresQuery = select(
48
- { name: users.name },
49
- from(users),
50
- where(ilike(users.name, '%ada%'))
51
- )
47
+ const postgresQuery = select({ name: users.name }, from(users), where(ilike(users.name, "%ada%")))
52
48
 
53
49
  render(postgresQuery, postgresDialect()) // supported
54
50
  render(postgresQuery, sqliteDialect()) // TypeScript error
@@ -59,14 +55,10 @@ received from an untyped integration. Use the portable operator when the
59
55
  query must render across dialects:
60
56
 
61
57
  ```ts
62
- import { from, like, render, select, where } from 'qubu'
63
- import { sqliteDialect } from 'qubu/sqlite'
58
+ import { from, like, render, select, where } from "qubu"
59
+ import { sqliteDialect } from "qubu/sqlite"
64
60
 
65
- const portableQuery = select(
66
- { name: users.name },
67
- from(users),
68
- where(like(users.name, '%ada%'))
69
- )
61
+ const portableQuery = select({ name: users.name }, from(users), where(like(users.name, "%ada%")))
70
62
 
71
63
  render(portableQuery)
72
64
  render(portableQuery, sqliteDialect())
@@ -81,45 +73,94 @@ does not re-export concrete dialect constructors.
81
73
 
82
74
  ## The adapter owns the driver
83
75
 
84
- Qubu does not open connections, bind values for a particular client, or decode
85
- rows. An adapter receives an `ExecutionRequest` and returns an
86
- `ExecutionResult`. A `TransactionalQueryAdapter` can also pin one driver
87
- connection for a callback transaction:
76
+ Qubu does not open connections or bind values for a particular client. An
77
+ adapter receives an `ExecutionRequest` and returns driver-normalized object
78
+ rows. Qubu then uses the query's result shape and the adapter's decoder policy
79
+ to produce the typed `ExecutionResult`. A `TransactionalQueryAdapter` can also
80
+ pin one driver connection for a callback transaction:
88
81
 
89
82
  ```ts
90
- import { qubu } from 'qubu'
91
- import { postgresDialect } from 'qubu/postgres'
92
- import type { ExecutionRequest, ExecutionResult, QueryAdapter } from 'qubu'
83
+ import { qubu } from "qubu"
84
+ import { postgresDialect } from "qubu/postgres"
85
+ import type { ExecutionRequest, QueryAdapter } from "qubu"
93
86
 
94
87
  declare const driver: {
95
88
  query<TRow extends object>(
96
89
  text: string,
97
90
  parameters: readonly unknown[],
98
- options: { signal?: AbortSignal }
91
+ options: { signal?: AbortSignal },
99
92
  ): Promise<{ rows: readonly TRow[]; rowCount: number | null }>
100
93
  }
101
94
 
102
95
  const adapter: QueryAdapter = {
103
96
  dialect: postgresDialect(),
104
- async execute<TRow extends object>(request: ExecutionRequest) {
97
+ async execute(request: ExecutionRequest) {
105
98
  const { statement, queryKind, signal } = request
106
- const result = await driver.query<TRow>(
99
+ const result = await driver.query<Record<string, unknown>>(
107
100
  statement.text,
108
101
  statement.parameters,
109
- { signal }
102
+ { signal },
110
103
  )
111
104
  return {
112
105
  rows: result.rows,
113
- ...(queryKind !== 'select' &&
114
- queryKind !== 'set' &&
115
- result.rowCount !== null
106
+ ...(queryKind !== "select" && queryKind !== "set" && result.rowCount !== null
116
107
  ? { affectedRows: result.rowCount }
117
108
  : {}),
118
- } satisfies ExecutionResult<TRow>
109
+ }
119
110
  },
120
111
  }
121
112
  ```
122
113
 
114
+ ### Decode schema-aware result values
115
+
116
+ Portable boolean, date, timestamp, and JSON columns retain their logical
117
+ result domains through projection aliases, derived queries, CTEs, set
118
+ operations, and mutation `RETURNING`. Register only the conversions required
119
+ by the selected driver configuration:
120
+
121
+ ```ts
122
+ import {
123
+ booleanResultDecoder,
124
+ dateResultDecoder,
125
+ jsonTextResultDecoder,
126
+ timestampResultDecoder,
127
+ } from "qubu"
128
+ import type { AdapterExecutionResult, QueryAdapter, RenderedQuery } from "qubu"
129
+ import { sqliteDialect } from "qubu/sqlite"
130
+
131
+ declare const sqliteDriver: {
132
+ execute(statement: RenderedQuery): Promise<AdapterExecutionResult>
133
+ }
134
+
135
+ const adapter: QueryAdapter = {
136
+ dialect: sqliteDialect(),
137
+ decoders: {
138
+ boolean: booleanResultDecoder,
139
+ date: dateResultDecoder,
140
+ timestamp: timestampResultDecoder,
141
+ json: jsonTextResultDecoder,
142
+ },
143
+ async execute(request) {
144
+ return sqliteDriver.execute(request.statement)
145
+ },
146
+ }
147
+ ```
148
+
149
+ Do not register `jsonTextResultDecoder` when the driver already returns parsed
150
+ JSON. A JSON string is otherwise ambiguous: it may be serialized JSON or an
151
+ already-decoded JSON string scalar. With no registered decoder, Qubu preserves
152
+ the driver's value.
153
+
154
+ Use a column decoder for a custom stored type, or `mapResult()` for one
155
+ expression. Both override adapter policy for that field:
156
+
157
+ ```ts
158
+ import { column, mapResult, value } from "qubu"
159
+
160
+ const score = column<number>({ decode: (value) => Number(value) })
161
+ const decodedTotal = mapResult(value("42"), (value) => Number(value))
162
+ ```
163
+
123
164
  ## Stream read results
124
165
 
125
166
  Add `StreamingQueryAdapter` when a driver can return rows through an
@@ -129,45 +170,41 @@ Mutations stay on `execute()` and `executeRows()`, including mutations with
129
170
  `RETURNING`.
130
171
 
131
172
  ```ts
132
- import { qubu } from 'qubu'
133
- import type {
134
- ExecutionRequest,
135
- ExecutionResult,
136
- StreamingQueryAdapter,
137
- } from 'qubu'
138
- import { postgresDialect } from 'qubu/postgres'
173
+ import { qubu } from "qubu"
174
+ import type { ExecutionRequest, StreamingQueryAdapter } from "qubu"
175
+ import { postgresDialect } from "qubu/postgres"
139
176
 
140
177
  declare const driver: {
141
178
  query<TRow extends object>(
142
179
  text: string,
143
180
  parameters: readonly unknown[],
144
- options: { signal?: AbortSignal }
181
+ options: { signal?: AbortSignal },
145
182
  ): Promise<{ rows: readonly TRow[]; rowCount: number | null }>
146
183
  stream<TRow extends object>(
147
184
  text: string,
148
185
  parameters: readonly unknown[],
149
- options: { signal?: AbortSignal }
186
+ options: { signal?: AbortSignal },
150
187
  ): AsyncIterable<TRow>
151
188
  }
152
189
 
153
190
  const adapter: StreamingQueryAdapter = {
154
191
  dialect: postgresDialect(),
155
- async execute<TRow extends object>(request: ExecutionRequest) {
156
- const result = await driver.query<TRow>(
192
+ async execute(request: ExecutionRequest) {
193
+ const result = await driver.query<Record<string, unknown>>(
157
194
  request.statement.text,
158
195
  request.statement.parameters,
159
- { signal: request.signal }
196
+ { signal: request.signal },
160
197
  )
161
198
  return {
162
199
  rows: result.rows,
163
200
  ...(result.rowCount === null ? {} : { affectedRows: result.rowCount }),
164
- } satisfies ExecutionResult<TRow>
201
+ }
165
202
  },
166
- stream<TRow extends object>(request: ExecutionRequest) {
167
- return driver.stream<TRow>(
203
+ stream(request: ExecutionRequest) {
204
+ return driver.stream<Record<string, unknown>>(
168
205
  request.statement.text,
169
206
  request.statement.parameters,
170
- { signal: request.signal }
207
+ { signal: request.signal },
171
208
  )
172
209
  },
173
210
  }
@@ -178,11 +215,11 @@ for await (const row of db.stream(readQuery)) {
178
215
  }
179
216
  ```
180
217
 
181
- Qubu renders the query before calling `stream()` and passes the selected
182
- dialect, ordered raw parameters, query kind, and optional `AbortSignal` in the
183
- same `ExecutionRequest` used by `execute()`. The adapter binds values, decodes
184
- rows, and returns the iterable. Qubu does not open a cursor or connection,
185
- iterate the result, buffer rows, or impose a fetch size.
218
+ Qubu renders the query with the selected dialect before calling `stream()`. It
219
+ passes ordered raw parameters, query kind, result shape, and the optional
220
+ `AbortSignal` in the same `ExecutionRequest` used by `execute()`. The adapter
221
+ binds values and returns the iterable. Qubu lazily decodes each row without
222
+ opening a cursor or connection, buffering rows, or imposing a fetch size.
186
223
 
187
224
  The adapter owns the iterator's cleanup contract:
188
225
 
@@ -212,10 +249,10 @@ transaction callback receives a streaming client. The adapter must keep its
212
249
  cursor and connection valid until the callback's streams finish or close:
213
250
 
214
251
  ```ts
215
- declare const transactionalAdapter: import('qubu').StreamingTransactionalQueryAdapter
252
+ declare const transactionalAdapter: import("qubu").StreamingTransactionalQueryAdapter
216
253
  const transactionalDb = qubu(transactionalAdapter)
217
254
 
218
- await transactionalDb.transaction(async transaction => {
255
+ await transactionalDb.transaction(async (transaction) => {
219
256
  for await (const row of transaction.stream(readQuery)) {
220
257
  consume(row)
221
258
  }
@@ -234,17 +271,17 @@ standalone `explain()` function and the bound `db.explain()` method render a
234
271
  plan request without calling `execute()`:
235
272
 
236
273
  ```ts
237
- import { explain, qubu } from 'qubu'
238
- import type { ExplainableQueryAdapter } from 'qubu'
239
- import { postgresDialect } from 'qubu/postgres'
274
+ import { explain, qubu } from "qubu"
275
+ import type { ExplainableQueryAdapter } from "qubu"
276
+ import { postgresDialect } from "qubu/postgres"
240
277
 
241
- type PostgresPlanRow = { 'QUERY PLAN': string }
278
+ type PostgresPlanRow = { "QUERY PLAN": string }
242
279
 
243
280
  declare const driver: {
244
281
  query<TRow extends object>(
245
282
  text: string,
246
283
  parameters: readonly unknown[],
247
- options: { signal?: AbortSignal }
284
+ options: { signal?: AbortSignal },
248
285
  ): Promise<{ rows: readonly TRow[] }>
249
286
  }
250
287
 
@@ -257,7 +294,7 @@ const adapter: ExplainableQueryAdapter<PostgresPlanRow> = {
257
294
  const result = await driver.query<PostgresPlanRow>(
258
295
  request.statement.text,
259
296
  request.statement.parameters,
260
- { signal: request.signal }
297
+ { signal: request.signal },
261
298
  )
262
299
  return { rows: result.rows }
263
300
  },
@@ -322,13 +359,13 @@ Use a transactional adapter when several queries must share one commit or
322
359
  rollback boundary:
323
360
 
324
361
  ```ts
325
- import { qubu } from 'qubu'
326
- import type { TransactionalQueryAdapter } from 'qubu'
362
+ import { qubu } from "qubu"
363
+ import type { TransactionalQueryAdapter } from "qubu"
327
364
 
328
365
  declare const transactionalAdapter: TransactionalQueryAdapter
329
366
  const transactionalDb = qubu(transactionalAdapter)
330
367
 
331
- const result = await transactionalDb.transaction(async transaction => {
368
+ const result = await transactionalDb.transaction(async (transaction) => {
332
369
  await transaction.execute(firstMutation)
333
370
  await transaction.execute(secondMutation)
334
371
  return transaction.rows(readQuery)
@@ -353,18 +390,18 @@ The standalone functions remain useful when the adapter varies by call or a
353
390
  small module does not need a bound client:
354
391
 
355
392
  ```ts
356
- import { execute, executeRows } from 'qubu'
393
+ import { execute, executeRows } from "qubu"
357
394
 
358
395
  const result = await execute(query, adapter)
359
396
  const rows = await executeRows(readQuery, adapter)
360
397
  ```
361
398
 
362
- | Result field | Type | Adapter contract |
363
- | -------------- | ---------------------------- | ------------------------------------------------------------------------------------- |
364
- | `rows` | `readonly TRow[]` | Always present; use an empty array for a mutation without returned rows |
365
- | `affectedRows` | `number \| bigint` | Rows inserted, updated, or deleted when the driver reports an affected count |
366
- | `changedRows` | `number \| bigint` | Rows whose stored values changed when the driver distinguishes them from matched rows |
367
- | `insertId` | `string \| number \| bigint` | One insert identifier when the driver reports it |
399
+ | Result field | Adapter type | Contract |
400
+ | -------------- | ------------------------------------ | ------------------------------------------------------------------------------------- |
401
+ | `rows` | `readonly Record<string, unknown>[]` | Key by rendered aliases; Qubu returns the decoded `readonly TRow[]` |
402
+ | `affectedRows` | `number \| bigint` | Rows inserted, updated, or deleted when the driver reports an affected count |
403
+ | `changedRows` | `number \| bigint` | Rows whose stored values changed when the driver distinguishes them from matched rows |
404
+ | `insertId` | `string \| number \| bigint` | One insert identifier when the driver reports it |
368
405
 
369
406
  The last three fields are optional. For example, an adapter can map PostgreSQL
370
407
  `rowCount`, MySQL `affectedRows`, `changedRows`, and `insertId`, or SQLite
@@ -373,9 +410,11 @@ report accurately. Qubu does not derive mutation metadata from returned rows.
373
410
 
374
411
  The adapter's `dialect` becomes the default for standalone and bound execution.
375
412
  A `dialect` in the execution options overrides that rendering policy. Qubu
376
- passes `signal` and the query's `queryKind` to the adapter without changing
413
+ passes `signal`, `queryKind`, and `resultShape` to the adapter without changing
377
414
  them. The adapter decides whether and how its driver supports cancellation.
378
- Driver errors pass through unchanged.
415
+ Driver errors pass through unchanged. Decoder failures become a
416
+ `ResultDecodingError` that identifies the row and field without exposing the
417
+ raw value.
379
418
 
380
419
  ```mermaid
381
420
  sequenceDiagram
@@ -386,11 +425,12 @@ sequenceDiagram
386
425
 
387
426
  App->>Qubu: db.execute(query, options)
388
427
  Qubu->>Qubu: render with selected dialect
389
- Qubu->>Adapter: statement + queryKind + signal
428
+ Qubu->>Adapter: statement + queryKind + resultShape + signal
390
429
  Adapter->>Driver: bind and execute
391
430
  Driver-->>Adapter: driver result or error
392
- Adapter-->>Qubu: typed rows + optional mutation facts
393
- Qubu-->>App: result envelope or rows
431
+ Adapter-->>Qubu: normalized object rows + optional mutation facts
432
+ Qubu->>Qubu: apply field and adapter decoders
433
+ Qubu-->>App: typed result envelope or rows
394
434
  ```
395
435
 
396
436
  ## Create a small custom dialect
@@ -399,13 +439,13 @@ Use `createDialect()` when a driver needs a different policy but the query
399
439
  syntax stays portable:
400
440
 
401
441
  ```ts
402
- import { render } from 'qubu'
403
- import { createDialect } from 'qubu/core'
442
+ import { render } from "qubu"
443
+ import { createDialect } from "qubu/core"
404
444
 
405
445
  const namedParameters = createDialect({
406
- name: 'named-parameters',
407
- placeholder: position => `:p${position}`,
408
- castTypes: { text: 'STRING' },
446
+ name: "named-parameters",
447
+ placeholder: (position) => `:p${position}`,
448
+ castTypes: { text: "STRING" },
409
449
  })
410
450
 
411
451
  const statement = render(query, namedParameters)
@@ -24,9 +24,9 @@ Use `table()` once for each query-facing table. Column helpers describe the
24
24
  application values that can be selected and, for mutations, written.
25
25
 
26
26
  ```ts
27
- import { integer, table, text } from 'qubu'
27
+ import { integer, table, text } from "qubu"
28
28
 
29
- const users = table('users', {
29
+ const users = table("users", {
30
30
  id: integer(),
31
31
  name: text(),
32
32
  email: text({ nullable: true }),
@@ -44,7 +44,7 @@ it, then renders the normalized statement in SQL order. The example uses the
44
44
  `users` table from the previous section.
45
45
 
46
46
  ```ts
47
- import { eq, from, render, select, where } from 'qubu'
47
+ import { eq, from, render, select, where } from "qubu"
48
48
 
49
49
  const query = select(
50
50
  {
@@ -52,7 +52,7 @@ const query = select(
52
52
  displayName: users.name,
53
53
  },
54
54
  from(users),
55
- where(eq(users.id, 7))
55
+ where(eq(users.id, 7)),
56
56
  )
57
57
 
58
58
  const statement = render(query)
@@ -0,0 +1,57 @@
1
+ # Better Auth
2
+
3
+ > Derive Qubu-owned auth tables and run Better Auth through a transactional Qubu client.
4
+
5
+ Install the integration next to Qubu and Better Auth:
6
+
7
+ ```sh
8
+ pnpm add qubu @qubu/better-auth better-auth
9
+ ```
10
+
11
+ Define the Better Auth options once. The schema derivation reads Better Auth's
12
+ resolved public metadata, so core tables, renamed models and fields, additional
13
+ fields, plugin tables, references, unique constraints, and compound indexes all
14
+ participate.
15
+
16
+ ```ts
17
+ import { betterAuth } from "better-auth"
18
+ import type { BetterAuthOptions } from "better-auth/types"
19
+ import { twoFactor } from "better-auth/plugins"
20
+ import { betterAuthSchema, qubuAdapter } from "@qubu/better-auth"
21
+ import { qubu } from "qubu"
22
+ import { pgAdapter } from "@qubu/adapter-pg"
23
+
24
+ const options = {
25
+ user: {
26
+ modelName: "auth_users",
27
+ additionalFields: {
28
+ locale: { type: "string", required: false },
29
+ },
30
+ },
31
+ plugins: [twoFactor()],
32
+ } satisfies BetterAuthOptions
33
+
34
+ const authSchema = betterAuthSchema(options, "postgresql")
35
+ const db = qubu(pgAdapter(pool))
36
+
37
+ export const auth = betterAuth({
38
+ ...options,
39
+ database: qubuAdapter(db, { schema: authSchema }),
40
+ })
41
+ ```
42
+
43
+ `authSchema` is an ordinary Qubu `Schema`. Hand it to Qubu snapshot, diff,
44
+ migration-plan, and DDL workflows. The adapter's Better Auth `createSchema`
45
+ hook emits a TypeScript module that reconstructs the same Qubu-owned metadata.
46
+
47
+ The package never imports PostgreSQL, MySQL, or SQLite drivers. It executes
48
+ through Qubu's query and transaction boundaries. PostgreSQL and SQLite use one
49
+ limited mutation statement for atomic consume and guarded increment operations;
50
+ MySQL locks one selected row inside the Qubu-owned transaction. A client without
51
+ transaction support, or a dialect other than PostgreSQL, MySQL, or SQLite, is
52
+ rejected during adapter construction.
53
+
54
+ Better Auth enum metadata is currently rejected because Qubu cannot preserve
55
+ the closed value set as a portable column without adding a database constraint.
56
+ The error includes the model and field path instead of silently widening it to
57
+ text.
@@ -8,37 +8,19 @@
8
8
  Attach it with `withCte()` and use the CTE source in `from()`:
9
9
 
10
10
  ```ts
11
- import {
12
- cte,
13
- eq,
14
- from,
15
- integer,
16
- select,
17
- table,
18
- text,
19
- where,
20
- withCte,
21
- } from 'qubu'
22
-
23
- const users = table('users', {
11
+ import { cte, eq, from, integer, select, table, text, where, withCte } from "qubu"
12
+
13
+ const users = table("users", {
24
14
  id: integer(),
25
15
  name: text(),
26
16
  })
27
17
 
28
18
  const activeUsers = cte(
29
- 'active_users',
30
- select(
31
- { id: users.id, name: users.name },
32
- from(users),
33
- where(eq(users.id, 7))
34
- )
19
+ "active_users",
20
+ select({ id: users.id, name: users.name }, from(users), where(eq(users.id, 7))),
35
21
  )
36
22
 
37
- const report = select(
38
- { displayName: activeUsers.name },
39
- withCte(activeUsers),
40
- from(activeUsers)
41
- )
23
+ const report = select({ displayName: activeUsers.name }, withCte(activeUsers), from(activeUsers))
42
24
  ```
43
25
 
44
26
  `activeUsers.name` is a typed column derived from the first query's row shape.
@@ -53,24 +35,10 @@ member. The callback receives a typed self-reference; introduce it through
53
35
  `from()` or a join before selecting its fields:
54
36
 
55
37
  ```ts
56
- import {
57
- add,
58
- cast,
59
- from,
60
- integer,
61
- lt,
62
- recursiveCte,
63
- select,
64
- value,
65
- where,
66
- withCte,
67
- } from 'qubu'
68
-
69
- const numbers = recursiveCte(
70
- 'numbers',
71
- select({ value: cast(value(1), integer()) }),
72
- self =>
73
- select({ value: add(self.value, 1) }, from(self), where(lt(self.value, 3)))
38
+ import { add, cast, from, integer, lt, recursiveCte, select, value, where, withCte } from "qubu"
39
+
40
+ const numbers = recursiveCte("numbers", select({ value: cast(value(1), integer()) }), (self) =>
41
+ select({ value: add(self.value, 1) }, from(self), where(lt(self.value, 3))),
74
42
  )
75
43
 
76
44
  const query = select({ value: numbers.value }, withCte(numbers), from(numbers))
@@ -89,11 +57,11 @@ recursive CTEs can share one `withCte()` clause.
89
57
  Alias a query when it should be used as an inline source:
90
58
 
91
59
  ```ts
92
- import { alias, from, lower, select } from 'qubu'
93
- import type { SqlTypeOf } from 'qubu'
60
+ import { alias, from, lower, select } from "qubu"
61
+ import type { SqlTypeOf } from "qubu"
94
62
 
95
63
  const names = select({ name: lower(users.name) }, from(users))
96
- const namesSource = alias(names, 'names')
64
+ const namesSource = alias(names, "names")
97
65
 
98
66
  const query = select({ name: namesSource.name }, from(namesSource))
99
67
  type NameSqlDomain = SqlTypeOf<typeof namesSource.name>
@@ -112,7 +80,7 @@ so downstream text operations remain checked without redeclaring the field.
112
80
  `scalar()` turns a query with exactly one selected field into an expression:
113
81
 
114
82
  ```ts
115
- import { from, scalar, select, value } from 'qubu'
83
+ import { from, scalar, select, value } from "qubu"
116
84
 
117
85
  const firstId = select({ id: users.id }, from(users))
118
86
  const query = select(
@@ -120,7 +88,7 @@ const query = select(
120
88
  name: users.name,
121
89
  firstId: scalar(firstId),
122
90
  },
123
- from(users)
91
+ from(users),
124
92
  )
125
93
  ```
126
94
 
@@ -139,7 +107,7 @@ Set operations preserve the left query's row shape. Both queries must select
139
107
  compatible rows:
140
108
 
141
109
  ```ts
142
- import { eq, from, select, unionAll, where } from 'qubu'
110
+ import { eq, from, select, unionAll, where } from "qubu"
143
111
 
144
112
  const first = select({ id: users.id }, from(users))
145
113
  const second = select({ id: users.id }, from(users), where(eq(users.id, 7)))
@@ -156,17 +124,12 @@ are collected in traversal order.
156
124
  Build reusable pieces as ordinary values and pass them into the final query:
157
125
 
158
126
  ```ts
159
- import { desc, eq, from, orderBy, select, where } from 'qubu'
127
+ import { desc, eq, from, orderBy, select, where } from "qubu"
160
128
 
161
129
  const byId = where(eq(users.id, 7))
162
130
  const newest = orderBy(desc(users.id))
163
131
 
164
- const query = select(
165
- { id: users.id, name: users.name },
166
- from(users),
167
- byId,
168
- newest
169
- )
132
+ const query = select({ id: users.id, name: users.name }, from(users), byId, newest)
170
133
  ```
171
134
 
172
135
  This makes it possible to share a predicate or projection without mutating a
@@ -185,13 +148,10 @@ generic function retains its exact source identity.
185
148
  For an application-level requirement, describe the required JavaScript row:
186
149
 
187
150
  ```ts
188
- import { eq, where } from 'qubu'
189
- import type { TableLike } from 'qubu'
151
+ import { eq, where } from "qubu"
152
+ import type { TableLike } from "qubu"
190
153
 
191
- function byStringId<TTable extends TableLike<{ id: string }>>(
192
- table: TTable,
193
- id: string
194
- ) {
154
+ function byStringId<TTable extends TableLike<{ id: string }>>(table: TTable, id: string) {
195
155
  return where(eq(table.columns.id, id))
196
156
  }
197
157
  ```
@@ -203,18 +163,15 @@ extra fields and rejects `string | null`, but it does not distinguish
203
163
  Use `FieldLike` when the fragment depends on SQL semantics:
204
164
 
205
165
  ```ts
206
- import { eq, where } from 'qubu'
207
- import type { FieldLike, SourceLike, SqlTextLike } from 'qubu'
166
+ import { eq, where } from "qubu"
167
+ import type { FieldLike, SourceLike, SqlTextLike } from "qubu"
208
168
 
209
169
  type NonNullTextId = FieldLike<{
210
170
  sqlType: SqlTextLike
211
171
  nullable: false
212
172
  }>
213
173
 
214
- function byTextId<TSource extends SourceLike<{ id: NonNullTextId }>>(
215
- source: TSource,
216
- id: string
217
- ) {
174
+ function byTextId<TSource extends SourceLike<{ id: NonNullTextId }>>(source: TSource, id: string) {
218
175
  return where(eq(source.columns.id, id))
219
176
  }
220
177
  ```