qubu 0.3.4 → 0.4.0

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 (56) hide show
  1. package/dist/codegen.d.mts +1 -1
  2. package/dist/codegen.mjs +1 -1
  3. package/dist/column-D-8OqGuV.mjs +328 -0
  4. package/dist/{table-llv9tsZ8.mjs → column-LV7oGQde.mjs} +9 -97
  5. package/dist/{complete-types-BdFqUfbb.d.mts → complete-types-BavBtv8J.d.mts} +1 -1
  6. package/dist/core.d.mts +1 -1
  7. package/dist/core.mjs +3 -3
  8. package/dist/ddl.d.mts +1 -1
  9. package/dist/ddl.mjs +3 -3
  10. package/dist/diff.d.mts +1 -1
  11. package/dist/drizzle-mysql.d.mts +2 -2
  12. package/dist/drizzle-mysql.mjs +2 -2
  13. package/dist/drizzle-postgres.d.mts +2 -2
  14. package/dist/drizzle-postgres.mjs +2 -2
  15. package/dist/drizzle-sqlite.d.mts +36 -3
  16. package/dist/drizzle-sqlite.mjs +30 -4
  17. package/dist/drizzle.d.mts +1 -1
  18. package/dist/{index-Dug5HnLB.d.mts → index-D3ZOwPT-.d.mts} +2 -2
  19. package/dist/{index-Ds7-mhJi.d.mts → index-DH0qV6aS.d.mts} +2 -2
  20. package/dist/index.d.mts +2 -2
  21. package/dist/index.mjs +64 -34
  22. package/dist/introspection.d.mts +2 -2
  23. package/dist/introspection.mjs +1 -1
  24. package/dist/migration.d.mts +2 -2
  25. package/dist/{mysql-DqkqXB6A.mjs → mysql-CKEKGnj4.mjs} +3 -3
  26. package/dist/mysql.d.mts +1 -1
  27. package/dist/{on-conflict-BxnxubMb.mjs → on-conflict-4MOBl51J.mjs} +4 -3
  28. package/dist/{postgres-DEBBeh52.mjs → postgres-BK2APkKP.mjs} +3 -2
  29. package/dist/postgres.d.mts +1 -1
  30. package/dist/postgres.mjs +3 -3
  31. package/dist/{registry-BufIskVN.mjs → registry-CXV8u7Pt.mjs} +3 -108
  32. package/dist/{relational-DCZrrNia.mjs → relational-DuQ9IHSb.mjs} +4 -4
  33. package/dist/{runtime-BTr-MTlo.mjs → runtime-Cn_Xgzta.mjs} +1 -1
  34. package/dist/schema.d.mts +2 -2
  35. package/dist/schema.mjs +6 -4
  36. package/dist/{serialize-PF1cfH2P.mjs → serialize-CFtYYAdk.mjs} +2 -2
  37. package/dist/{snapshot-CWPgzxNx.mjs → snapshot-DJpfmxhQ.mjs} +1 -1
  38. package/dist/snapshot.d.mts +3 -3
  39. package/dist/snapshot.mjs +4 -4
  40. package/dist/{source-DUoJVXmL.mjs → source-DGO3DRgg.mjs} +3 -3
  41. package/dist/{sqlite-BU6DBxef.mjs → sqlite-CrsK0Fza.mjs} +3 -3
  42. package/dist/sqlite.d.mts +1 -1
  43. package/dist/sqlite.mjs +1 -1
  44. package/dist/table-D6rcs8SB.mjs +96 -0
  45. package/dist/{types-BX0mckiU.d.mts → types-CSNJTYaM.d.mts} +1 -1
  46. package/dist/{types-H4vyCw8_.d.mts → types-DZsueVoI.d.mts} +1 -1
  47. package/dist/{types-4Q076HKo.d.mts → types-Deo_q43Y.d.mts} +1442 -916
  48. package/dist/{types-Cec0xzo4.mjs → types-g-vVvj1B.mjs} +4 -3
  49. package/dist/{value-BvilP0oz.mjs → value-b6OFZXVS.mjs} +1 -1
  50. package/dist/vite/ambient.d.ts +13 -61
  51. package/docs/dialects-and-execution.md +84 -34
  52. package/docs/guides/drizzle.md +27 -0
  53. package/docs/reference/supported-surface.md +7 -7
  54. package/package.json +1 -1
  55. package/dist/column-CXMxx8Hq.mjs +0 -118
  56. package/dist/naming-QVCOnSj2.mjs +0 -20
@@ -1,7 +1,8 @@
1
1
  import { s as resolveCastTarget } from "./json-CUZlv4HT.mjs";
2
2
  import { t as standardDialect } from "./standard-BTVYKh_F.mjs";
3
- import { c as makeExpression, l as makeSchemaExpression, p as fragment } from "./column-CXMxx8Hq.mjs";
4
- import { t as asValue } from "./value-BvilP0oz.mjs";
3
+ import { C as fragment, v as makeExpression, y as makeSchemaExpression } from "./column-D-8OqGuV.mjs";
4
+ import { a as columnResultValue } from "./column-LV7oGQde.mjs";
5
+ import { t as asValue } from "./value-b6OFZXVS.mjs";
5
6
  //#region src/core/render.ts
6
7
  function render(query, options = {}) {
7
8
  const dialect = isDialect(options) ? options : options.dialect ?? standardDialect();
@@ -51,7 +52,7 @@ function cast(expression, target) {
51
52
  context.append(" AS ");
52
53
  context.append(typeof target === "string" ? target : resolveCastTarget(context.dialect, target.castTarget));
53
54
  context.append(")");
54
- });
55
+ }, typeof target === "string" ? void 0 : columnResultValue(target));
55
56
  }
56
57
  /** Create a cast whose JS output and SQL result domain are declared up front. */
57
58
  function typedCast() {
@@ -1,4 +1,4 @@
1
- import { l as makeSchemaExpression, p as fragment } from "./column-CXMxx8Hq.mjs";
1
+ import { C as fragment, y as makeSchemaExpression } from "./column-D-8OqGuV.mjs";
2
2
  //#region src/core/primitives/parameter.ts
3
3
  function parameter(_value) {
4
4
  return fragment((context) => context.parameter(_value));
@@ -133,41 +133,9 @@ declare global {
133
133
  const withCte: typeof import('qubu').withCte
134
134
  const uuid: typeof import('qubu').uuid
135
135
 
136
- type ColumnDefinition<
137
- TOutput = unknown,
138
- TNullable extends boolean = false,
139
- TInsert = TOutput,
140
- TUpdate = TInsert,
141
- THasDefault extends boolean = false,
142
- TGenerated extends boolean = false,
143
- TSqlType extends import('qubu').AnySqlType = import('qubu').SqlUnknown,
144
- TStorage extends import('qubu').ColumnStorage | undefined = undefined,
145
- TDefault extends import('qubu').ColumnDefault | undefined =
146
- | import('qubu').ColumnDefault
147
- | undefined,
148
- TGeneratedColumn extends
149
- | import('qubu').GeneratedColumnDescriptor
150
- | undefined = import('qubu').GeneratedColumnDescriptor | undefined,
151
- TIdentity extends import('qubu').IdentityDescriptor | undefined =
152
- | import('qubu').IdentityDescriptor
153
- | undefined,
154
- TOnUpdate extends import('qubu').AnySchemaExpression | undefined =
155
- | import('qubu').AnySchemaExpression
156
- | undefined,
157
- > = import('qubu').ColumnDefinition<
158
- TOutput,
159
- TNullable,
160
- TInsert,
161
- TUpdate,
162
- THasDefault,
163
- TGenerated,
164
- TSqlType,
165
- TStorage,
166
- TDefault,
167
- TGeneratedColumn,
168
- TIdentity,
169
- TOnUpdate
170
- >
136
+ type ColumnDefinitionConfig = import('qubu').ColumnDefinitionConfig
137
+ type ColumnDefinition<TConfig extends ColumnDefinitionConfig = {}> =
138
+ import('qubu').ColumnDefinition<TConfig>
171
139
  type ColumnSqlType<T> = import('qubu').ColumnSqlType<T>
172
140
  type ColumnDefault = import('qubu').ColumnDefault
173
141
  type ColumnDefaultInput = import('qubu').ColumnDefaultInput
@@ -317,32 +285,16 @@ declare global {
317
285
  type StreamingTransactionalQueryAdapter =
318
286
  import('qubu').StreamingTransactionalQueryAdapter
319
287
  type RenderedQuery = import('qubu').RenderedQuery
320
- type MutationQuery<
321
- TRow extends object = Record<string, unknown>,
322
- TKind extends import('qubu').MutationKind = import('qubu').MutationKind,
323
- TMetadata = never,
324
- > = import('qubu').MutationQuery<TRow, TKind, TMetadata>
325
- type Query<
326
- TRow extends object = import('qubu').Row,
327
- TCardinality extends
328
- import('qubu').QueryCardinality = import('qubu').QueryCardinality,
329
- TMetadata = never,
330
- > = import('qubu').Query<TRow, TCardinality, TMetadata>
331
- type SelectQuery<
332
- TRow extends object = Record<string, unknown>,
333
- TCardinality extends
334
- import('qubu').QueryCardinality = import('qubu').QueryCardinality,
335
- TMetadata = never,
336
- > = import('qubu').SelectQuery<TRow, TCardinality, TMetadata>
337
- type Source<
338
- TIdentity = unknown,
339
- TRow extends object = Record<string, unknown>,
340
- TMetadata = never,
341
- TSqlTypes extends {
342
- readonly [K in keyof TRow]: import('qubu').AnySqlType
343
- } = { readonly [K in keyof TRow]: import('qubu').SqlUnknown },
344
- TConstraints extends import('qubu').SourceConstraintsRecord = {},
345
- > = import('qubu').Source<TIdentity, TRow, TMetadata, TSqlTypes, TConstraints>
288
+ type MutationQueryConfig = import('qubu').MutationQueryConfig
289
+ type MutationQuery<TConfig extends MutationQueryConfig = {}> =
290
+ import('qubu').MutationQuery<TConfig>
291
+ type QueryConfig = import('qubu').QueryConfig
292
+ type Query<TConfig extends QueryConfig = {}> = import('qubu').Query<TConfig>
293
+ type SelectQuery<TConfig extends QueryConfig = {}> =
294
+ import('qubu').SelectQuery<TConfig>
295
+ type SourceConfig = import('qubu').SourceConfig
296
+ type Source<TConfig extends SourceConfig = {}> =
297
+ import('qubu').Source<TConfig>
346
298
  type SourceConstraint = import('qubu').SourceConstraint
347
299
  type SourceConstraintsRecord = import('qubu').SourceConstraintsRecord
348
300
  type SourceIndexesRecord = import('qubu').SourceIndexesRecord
@@ -81,15 +81,16 @@ does not re-export concrete dialect constructors.
81
81
 
82
82
  ## The adapter owns the driver
83
83
 
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:
84
+ Qubu does not open connections or bind values for a particular client. An
85
+ adapter receives an `ExecutionRequest` and returns driver-normalized object
86
+ rows. Qubu then uses the query's result shape and the adapter's decoder policy
87
+ to produce the typed `ExecutionResult`. A `TransactionalQueryAdapter` can also
88
+ pin one driver connection for a callback transaction:
88
89
 
89
90
  ```ts
90
91
  import { qubu } from 'qubu'
91
92
  import { postgresDialect } from 'qubu/postgres'
92
- import type { ExecutionRequest, ExecutionResult, QueryAdapter } from 'qubu'
93
+ import type { ExecutionRequest, QueryAdapter } from 'qubu'
93
94
 
94
95
  declare const driver: {
95
96
  query<TRow extends object>(
@@ -101,9 +102,9 @@ declare const driver: {
101
102
 
102
103
  const adapter: QueryAdapter = {
103
104
  dialect: postgresDialect(),
104
- async execute<TRow extends object>(request: ExecutionRequest) {
105
+ async execute(request: ExecutionRequest) {
105
106
  const { statement, queryKind, signal } = request
106
- const result = await driver.query<TRow>(
107
+ const result = await driver.query<Record<string, unknown>>(
107
108
  statement.text,
108
109
  statement.parameters,
109
110
  { signal }
@@ -115,11 +116,61 @@ const adapter: QueryAdapter = {
115
116
  result.rowCount !== null
116
117
  ? { affectedRows: result.rowCount }
117
118
  : {}),
118
- } satisfies ExecutionResult<TRow>
119
+ }
119
120
  },
120
121
  }
121
122
  ```
122
123
 
124
+ ### Decode schema-aware result values
125
+
126
+ Portable boolean, date, timestamp, and JSON columns retain their logical
127
+ result domains through projection aliases, derived queries, CTEs, set
128
+ operations, and mutation `RETURNING`. Register only the conversions required
129
+ by the selected driver configuration:
130
+
131
+ ```ts
132
+ import {
133
+ booleanResultDecoder,
134
+ dateResultDecoder,
135
+ jsonTextResultDecoder,
136
+ timestampResultDecoder,
137
+ } from 'qubu'
138
+ import type { AdapterExecutionResult, QueryAdapter, RenderedQuery } from 'qubu'
139
+ import { sqliteDialect } from 'qubu/sqlite'
140
+
141
+ declare const sqliteDriver: {
142
+ execute(statement: RenderedQuery): Promise<AdapterExecutionResult>
143
+ }
144
+
145
+ const adapter: QueryAdapter = {
146
+ dialect: sqliteDialect(),
147
+ decoders: {
148
+ boolean: booleanResultDecoder,
149
+ date: dateResultDecoder,
150
+ timestamp: timestampResultDecoder,
151
+ json: jsonTextResultDecoder,
152
+ },
153
+ async execute(request) {
154
+ return sqliteDriver.execute(request.statement)
155
+ },
156
+ }
157
+ ```
158
+
159
+ Do not register `jsonTextResultDecoder` when the driver already returns parsed
160
+ JSON. A JSON string is otherwise ambiguous: it may be serialized JSON or an
161
+ already-decoded JSON string scalar. With no registered decoder, Qubu preserves
162
+ the driver's value.
163
+
164
+ Use a column decoder for a custom stored type, or `mapResult()` for one
165
+ expression. Both override adapter policy for that field:
166
+
167
+ ```ts
168
+ import { column, mapResult, value } from 'qubu'
169
+
170
+ const score = column<number>({ decode: value => Number(value) })
171
+ const decodedTotal = mapResult(value('42'), value => Number(value))
172
+ ```
173
+
123
174
  ## Stream read results
124
175
 
125
176
  Add `StreamingQueryAdapter` when a driver can return rows through an
@@ -130,11 +181,7 @@ Mutations stay on `execute()` and `executeRows()`, including mutations with
130
181
 
131
182
  ```ts
132
183
  import { qubu } from 'qubu'
133
- import type {
134
- ExecutionRequest,
135
- ExecutionResult,
136
- StreamingQueryAdapter,
137
- } from 'qubu'
184
+ import type { ExecutionRequest, StreamingQueryAdapter } from 'qubu'
138
185
  import { postgresDialect } from 'qubu/postgres'
139
186
 
140
187
  declare const driver: {
@@ -152,8 +199,8 @@ declare const driver: {
152
199
 
153
200
  const adapter: StreamingQueryAdapter = {
154
201
  dialect: postgresDialect(),
155
- async execute<TRow extends object>(request: ExecutionRequest) {
156
- const result = await driver.query<TRow>(
202
+ async execute(request: ExecutionRequest) {
203
+ const result = await driver.query<Record<string, unknown>>(
157
204
  request.statement.text,
158
205
  request.statement.parameters,
159
206
  { signal: request.signal }
@@ -161,10 +208,10 @@ const adapter: StreamingQueryAdapter = {
161
208
  return {
162
209
  rows: result.rows,
163
210
  ...(result.rowCount === null ? {} : { affectedRows: result.rowCount }),
164
- } satisfies ExecutionResult<TRow>
211
+ }
165
212
  },
166
- stream<TRow extends object>(request: ExecutionRequest) {
167
- return driver.stream<TRow>(
213
+ stream(request: ExecutionRequest) {
214
+ return driver.stream<Record<string, unknown>>(
168
215
  request.statement.text,
169
216
  request.statement.parameters,
170
217
  { signal: request.signal }
@@ -178,11 +225,11 @@ for await (const row of db.stream(readQuery)) {
178
225
  }
179
226
  ```
180
227
 
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.
228
+ Qubu renders the query with the selected dialect before calling `stream()`. It
229
+ passes ordered raw parameters, query kind, result shape, and the optional
230
+ `AbortSignal` in the same `ExecutionRequest` used by `execute()`. The adapter
231
+ binds values and returns the iterable. Qubu lazily decodes each row without
232
+ opening a cursor or connection, buffering rows, or imposing a fetch size.
186
233
 
187
234
  The adapter owns the iterator's cleanup contract:
188
235
 
@@ -359,12 +406,12 @@ const result = await execute(query, adapter)
359
406
  const rows = await executeRows(readQuery, adapter)
360
407
  ```
361
408
 
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 |
409
+ | Result field | Adapter type | Contract |
410
+ | -------------- | ------------------------------------ | ------------------------------------------------------------------------------------- |
411
+ | `rows` | `readonly Record<string, unknown>[]` | Key by rendered aliases; Qubu returns the decoded `readonly TRow[]` |
412
+ | `affectedRows` | `number \| bigint` | Rows inserted, updated, or deleted when the driver reports an affected count |
413
+ | `changedRows` | `number \| bigint` | Rows whose stored values changed when the driver distinguishes them from matched rows |
414
+ | `insertId` | `string \| number \| bigint` | One insert identifier when the driver reports it |
368
415
 
369
416
  The last three fields are optional. For example, an adapter can map PostgreSQL
370
417
  `rowCount`, MySQL `affectedRows`, `changedRows`, and `insertId`, or SQLite
@@ -373,9 +420,11 @@ report accurately. Qubu does not derive mutation metadata from returned rows.
373
420
 
374
421
  The adapter's `dialect` becomes the default for standalone and bound execution.
375
422
  A `dialect` in the execution options overrides that rendering policy. Qubu
376
- passes `signal` and the query's `queryKind` to the adapter without changing
423
+ passes `signal`, `queryKind`, and `resultShape` to the adapter without changing
377
424
  them. The adapter decides whether and how its driver supports cancellation.
378
- Driver errors pass through unchanged.
425
+ Driver errors pass through unchanged. Decoder failures become a
426
+ `ResultDecodingError` that identifies the row and field without exposing the
427
+ raw value.
379
428
 
380
429
  ```mermaid
381
430
  sequenceDiagram
@@ -386,11 +435,12 @@ sequenceDiagram
386
435
 
387
436
  App->>Qubu: db.execute(query, options)
388
437
  Qubu->>Qubu: render with selected dialect
389
- Qubu->>Adapter: statement + queryKind + signal
438
+ Qubu->>Adapter: statement + queryKind + resultShape + signal
390
439
  Adapter->>Driver: bind and execute
391
440
  Driver-->>Adapter: driver result or error
392
- Adapter-->>Qubu: typed rows + optional mutation facts
393
- Qubu-->>App: result envelope or rows
441
+ Adapter-->>Qubu: normalized object rows + optional mutation facts
442
+ Qubu->>Qubu: apply field and adapter decoders
443
+ Qubu-->>App: typed result envelope or rows
394
444
  ```
395
445
 
396
446
  ## Create a small custom dialect
@@ -68,6 +68,33 @@ const rows = await db
68
68
  Existing Qubu queries can keep importing `users`. Move call sites to
69
69
  `drizzleTables.users` one at a time without duplicating the table declaration.
70
70
 
71
+ ### Keep SQLite integer timestamps
72
+
73
+ Use `sqliteTimestamp()` when an existing SQLite schema stores dates as integer
74
+ Unix timestamps and Drizzle must continue reading and writing `Date` values:
75
+
76
+ ```ts
77
+ import { schema, table } from 'qubu'
78
+ import { sqliteTimestamp, toSqliteDrizzleSchema } from 'qubu/drizzle/sqlite'
79
+
80
+ const events = table('events', {
81
+ createdAt: sqliteTimestamp({
82
+ mode: 'timestamp',
83
+ defaultFn: () => new Date(),
84
+ }),
85
+ })
86
+
87
+ const drizzleTables = toSqliteDrizzleSchema(schema({ events }))
88
+ ```
89
+
90
+ `timestamp` stores Unix seconds and `timestamp_ms` stores Unix milliseconds.
91
+ Both modes expose `Date` values through Drizzle. `defaultFn` runs when a
92
+ Drizzle insert omits the column; it is runtime behavior and is not emitted as a
93
+ database `DEFAULT` or serialized into a Qubu snapshot.
94
+
95
+ Use Qubu's root `timestamp()` helper for portable schemas. SQLite stores that
96
+ portable form as ISO text instead of an integer.
97
+
71
98
  ## Type behavior
72
99
 
73
100
  The converted columns preserve selected values, nullability, required insert
@@ -70,13 +70,13 @@ Snapshot creation, diffing, migration planning, and DDL emission are pure.
70
70
  interfaces the application provides. Qubu can emit DDL, but it never applies
71
71
  that DDL to a database.
72
72
 
73
- | Boundary | Qubu side | Application side |
74
- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
75
- | Query rendering | Builds a typed query and renders SQL text with ordered raw parameter values | Keeps the runtime database schema aligned with query definitions and validates any dynamic syntax passed to an unsafe helper |
76
- | Query execution | Binds an adapter with `qubu()` when requested; renders and passes statements to `QueryAdapter`, `ExplainableQueryAdapter`, or a read-only `StreamingQueryAdapter`; scopes `db.transaction()` callbacks through `TransactionalQueryAdapter`; returns `ExecutionResult`, `ExplainResult`, rows, or an adapter-owned `AsyncIterable` | Owns the adapter, driver, connections, pools, cursors, stream cleanup, transaction begin/commit/rollback, savepoints, retries, parameter encoding, application-row and plan-row decoding, backpressure, cancellation behavior, driver error translation, and database lifecycle |
77
- | Catalog introspection | Selects fixed parameterized catalog queries, normalizes rows, and maps catalog data to snapshots | Supplies `CatalogConnection`, credentials, already-decoded catalog rows, logging, and connection lifecycle |
78
- | Schema source generation | Prints deterministic TypeScript from complete, non-lossy Snapshot v1 introspection without writing files | Owns generated-file writes, replacement policy, hand-edit merging, and CLI integration |
79
- | Schema changes | Creates snapshots, compares them, builds deterministic migration plans, and emits DDL from approved plans with preflight diagnostics | Reviews decisions, executes or rolls back statements, acquires locks, manages transactions and migration journals, and owns database lifecycle |
73
+ | Boundary | Qubu side | Application side |
74
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
75
+ | Query rendering | Builds a typed query and renders SQL text with ordered raw parameter values | Keeps the runtime database schema aligned with query definitions and validates any dynamic syntax passed to an unsafe helper |
76
+ | Query execution | Binds an adapter with `qubu()` when requested; passes rendered statements and result shapes to execution adapters; applies registered logical field decoders to buffered or streamed object rows; scopes transaction callbacks; returns typed results, plans, rows, or streams | Owns the adapter, driver, connections, pools, cursors, stream cleanup, transactions, savepoints, retries, parameter encoding, proprietary row normalization, decoder policy, plan-row decoding, backpressure, cancellation, driver error translation, and database lifecycle |
77
+ | Catalog introspection | Selects fixed parameterized catalog queries, normalizes rows, and maps catalog data to snapshots | Supplies `CatalogConnection`, credentials, already-decoded catalog rows, logging, and connection lifecycle |
78
+ | Schema source generation | Prints deterministic TypeScript from complete, non-lossy Snapshot v1 introspection without writing files | Owns generated-file writes, replacement policy, hand-edit merging, and CLI integration |
79
+ | Schema changes | Creates snapshots, compares them, builds deterministic migration plans, and emits DDL from approved plans with preflight diagnostics | Reviews decisions, executes or rolls back statements, acquires locks, manages transactions and migration journals, and owns database lifecycle |
80
80
 
81
81
  Start with [Dialects and execution](../dialects-and-execution.md) for the query
82
82
  adapter contract. The schema path is documented in [Canonical schema
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qubu",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/aleclarson/qubu"
@@ -1,118 +0,0 @@
1
- import { a as assertDialectCapability } from "./json-CUZlv4HT.mjs";
2
- //#region src/core/fragment.ts
3
- function fragment(render) {
4
- return Object.freeze({ render });
5
- }
6
- function sequence(parts, separator = " ") {
7
- return fragment((context) => {
8
- let first = true;
9
- for (const part of parts) {
10
- if (!first) context.append(separator);
11
- context.render(part);
12
- first = false;
13
- }
14
- });
15
- }
16
- function parenthesize(part) {
17
- return fragment((context) => {
18
- context.append("(");
19
- context.render(part);
20
- context.append(")");
21
- });
22
- }
23
- function isFragment(value) {
24
- return typeof value === "object" && value !== null && "render" in value && typeof value.render === "function";
25
- }
26
- //#endregion
27
- //#region src/expressions/types.ts
28
- /** Runtime/type-level proof that an expression is safe to use in schema SQL. */
29
- const schemaExpressionBrand = Symbol("qubu.schema-expression");
30
- /** Add a concrete dialect requirement without dropping expression metadata. */
31
- function withDialectCapability(expression, capability) {
32
- const wrapped = makeExpression(expression.expressionKind, (context) => {
33
- assertDialectCapability(context.dialect, capability);
34
- context.render(expression);
35
- });
36
- return isSchemaExpression(expression) ? markSchemaExpression(wrapped) : wrapped;
37
- }
38
- function makeExpression(expressionKind, render, expressionCategory) {
39
- return Object.freeze({
40
- expressionKind,
41
- ...expressionCategory ? { expressionCategory } : {},
42
- ...fragment(render)
43
- });
44
- }
45
- /** Mark a query-only expression category without changing its SQL renderer. */
46
- function markExpressionCategory(expression, category) {
47
- return Object.freeze({
48
- ...expression,
49
- expressionCategory: category
50
- });
51
- }
52
- /**
53
- * Mark a built-in or explicitly audited renderer as schema-deterministic.
54
- * Prefer {@link defineSchemaExpression} for application extensions because it
55
- * supplies a restricted schema rendering context.
56
- */
57
- function makeSchemaExpression(expressionKind, render) {
58
- const expression = makeExpression(expressionKind, render);
59
- return Object.freeze({
60
- ...expression,
61
- [schemaExpressionBrand]: true
62
- });
63
- }
64
- /** Add the schema-determinism brand to an explicitly audited expression. */
65
- function markSchemaExpression(expression) {
66
- if (isSchemaExpression(expression)) return expression;
67
- return Object.freeze({
68
- ...expression,
69
- [schemaExpressionBrand]: true
70
- });
71
- }
72
- /** Test the runtime brand used by schema rendering and validation. */
73
- function isSchemaExpression(value) {
74
- return typeof value === "object" && value !== null && schemaExpressionBrand in value && value[schemaExpressionBrand] === true;
75
- }
76
- function isExpression(value) {
77
- return typeof value === "object" && value !== null && "expressionKind" in value && "render" in value && typeof value.render === "function";
78
- }
79
- //#endregion
80
- //#region src/core/primitives/identifier.ts
81
- function identifier(name) {
82
- return fragment((context) => context.append(context.dialect.quoteIdentifier(name)));
83
- }
84
- function qualifiedIdentifier(...parts) {
85
- return fragment((context) => {
86
- parts.forEach((part, index) => {
87
- if (index > 0) context.append(".");
88
- context.append(context.dialect.quoteIdentifier(part));
89
- });
90
- });
91
- }
92
- //#endregion
93
- //#region src/expressions/column.ts
94
- function createColumnReference(columnName, sourceReference, fieldName) {
95
- const expression = makeSchemaExpression("column", (context) => {
96
- if (context.renderColumnReference) {
97
- context.renderColumnReference(columnName);
98
- return;
99
- }
100
- context.render(sourceReference);
101
- context.append(".");
102
- context.render(identifier(columnName));
103
- });
104
- return Object.freeze({
105
- ...expression,
106
- fieldName,
107
- columnName
108
- });
109
- }
110
- function isColumnReference(value) {
111
- return typeof value === "object" && value !== null && "expressionKind" in value && value.expressionKind === "column";
112
- }
113
- /** Turn an expression into a fragment that renders it without changing it. */
114
- function expressionFragment(expression) {
115
- return fragment((context) => context.render(expression));
116
- }
117
- //#endregion
118
- export { qualifiedIdentifier as a, makeExpression as c, markSchemaExpression as d, withDialectCapability as f, sequence as g, parenthesize as h, identifier as i, makeSchemaExpression as l, isFragment as m, expressionFragment as n, isExpression as o, fragment as p, isColumnReference as r, isSchemaExpression as s, createColumnReference as t, markExpressionCategory as u };
@@ -1,20 +0,0 @@
1
- //#region src/core/naming.ts
2
- /** Convert an application-facing field name to a SQL-facing identifier. */
3
- function snakeCaseIdentifier(name) {
4
- return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
5
- }
6
- function resolveSqlNames(fields) {
7
- const sqlNames = {};
8
- const fieldsBySqlName = /* @__PURE__ */ new Map();
9
- for (const field of fields) {
10
- const sqlName = field.sqlName ?? snakeCaseIdentifier(field.fieldName);
11
- if (sqlName.length === 0) throw new Error(`SQL name for field "${field.fieldName}" cannot be empty`);
12
- const existingField = fieldsBySqlName.get(sqlName);
13
- if (existingField) throw new Error(`Fields "${existingField}" and "${field.fieldName}" both resolve to SQL name "${sqlName}"`);
14
- fieldsBySqlName.set(sqlName, field.fieldName);
15
- sqlNames[field.fieldName] = sqlName;
16
- }
17
- return Object.freeze(sqlNames);
18
- }
19
- //#endregion
20
- export { snakeCaseIdentifier as n, resolveSqlNames as t };