qubu 0.0.0 → 0.3.3

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 (109) hide show
  1. package/dist/canonical-BbnqavJm.mjs +972 -0
  2. package/dist/codegen.d.mts +107 -0
  3. package/dist/codegen.mjs +1199 -0
  4. package/dist/column-CXMxx8Hq.mjs +118 -0
  5. package/dist/complete-D5Djh-zo.mjs +1788 -0
  6. package/dist/complete-types-BdFqUfbb.d.mts +371 -0
  7. package/dist/core.d.mts +33 -0
  8. package/dist/core.mjs +36 -0
  9. package/dist/ddl.d.mts +118 -0
  10. package/dist/ddl.mjs +1120 -0
  11. package/dist/dialect-b2-Z6uBF.mjs +13 -0
  12. package/dist/diff.d.mts +2 -0
  13. package/dist/diff.mjs +883 -0
  14. package/dist/drizzle-mysql.d.mts +24 -0
  15. package/dist/drizzle-mysql.mjs +72 -0
  16. package/dist/drizzle-postgres.d.mts +24 -0
  17. package/dist/drizzle-postgres.mjs +73 -0
  18. package/dist/drizzle-sqlite.d.mts +24 -0
  19. package/dist/drizzle-sqlite.mjs +82 -0
  20. package/dist/drizzle.d.mts +13 -0
  21. package/dist/drizzle.mjs +2 -0
  22. package/dist/errors-BGCoLe_r.mjs +14 -0
  23. package/dist/errors-Dxv73YJu.mjs +26 -0
  24. package/dist/explain-CkIK13L_.mjs +98 -0
  25. package/dist/index-Ds7-mhJi.d.mts +146 -0
  26. package/dist/index-Dug5HnLB.d.mts +204 -0
  27. package/dist/index.d.mts +2 -0
  28. package/dist/index.mjs +1276 -0
  29. package/dist/introspection.d.mts +117 -0
  30. package/dist/introspection.mjs +4341 -0
  31. package/dist/json-CUZlv4HT.mjs +169 -0
  32. package/dist/migration.d.mts +215 -0
  33. package/dist/migration.mjs +1161 -0
  34. package/dist/mysql-DqkqXB6A.mjs +355 -0
  35. package/dist/mysql.d.mts +5 -0
  36. package/dist/mysql.mjs +50 -0
  37. package/dist/naming-QVCOnSj2.mjs +20 -0
  38. package/dist/omit-OxV58AwX.mjs +5 -0
  39. package/dist/on-conflict-BxnxubMb.mjs +143 -0
  40. package/dist/postgres-DEBBeh52.mjs +235 -0
  41. package/dist/postgres.d.mts +123 -0
  42. package/dist/postgres.mjs +52 -0
  43. package/dist/registry-BufIskVN.mjs +569 -0
  44. package/dist/relational-DCZrrNia.mjs +77 -0
  45. package/dist/runtime-BTr-MTlo.mjs +193 -0
  46. package/dist/schema.d.mts +2 -0
  47. package/dist/schema.mjs +5 -0
  48. package/dist/serialize-PF1cfH2P.mjs +556 -0
  49. package/dist/snapshot-CWPgzxNx.mjs +392 -0
  50. package/dist/snapshot.d.mts +4 -0
  51. package/dist/snapshot.mjs +8 -0
  52. package/dist/source-DUoJVXmL.mjs +68 -0
  53. package/dist/sqlite-BU6DBxef.mjs +320 -0
  54. package/dist/sqlite.d.mts +5 -0
  55. package/dist/sqlite.mjs +48 -0
  56. package/dist/standard-BTVYKh_F.mjs +12 -0
  57. package/dist/table-llv9tsZ8.mjs +384 -0
  58. package/dist/types-4Q076HKo.d.mts +3636 -0
  59. package/dist/types-BX0mckiU.d.mts +644 -0
  60. package/dist/types-CO1KaRNc.d.mts +46 -0
  61. package/dist/types-Cec0xzo4.mjs +116 -0
  62. package/dist/value-BvilP0oz.mjs +29 -0
  63. package/dist/vite/ambient.d.ts +449 -0
  64. package/dist/vite.d.mts +35 -0
  65. package/dist/vite.mjs +498 -0
  66. package/docs/config.json +21 -0
  67. package/docs/dialects-and-execution.md +427 -0
  68. package/docs/getting-started.md +96 -0
  69. package/docs/guides/compose-queries.md +225 -0
  70. package/docs/guides/drizzle.md +129 -0
  71. package/docs/guides/extensions/dialects.md +40 -0
  72. package/docs/guides/extensions/overview.md +36 -0
  73. package/docs/guides/extensions/sources-and-clauses.md +82 -0
  74. package/docs/guides/extensions/typed-expressions.md +94 -0
  75. package/docs/guides/extensions/unsafe-syntax.md +27 -0
  76. package/docs/guides/json.md +65 -0
  77. package/docs/guides/mutations.md +138 -0
  78. package/docs/guides/select/conditions.md +110 -0
  79. package/docs/guides/select/grouping-and-windows.md +82 -0
  80. package/docs/guides/select/ordering-and-pagination.md +78 -0
  81. package/docs/guides/select/overview.md +139 -0
  82. package/docs/guides/sql-templates.md +157 -0
  83. package/docs/guides/vite-plugin.md +92 -0
  84. package/docs/index.md +111 -0
  85. package/docs/query-model/fragments.md +109 -0
  86. package/docs/query-model/result-shapes.md +125 -0
  87. package/docs/query-model/source-scope.md +182 -0
  88. package/docs/reference/introspection-support.md +196 -0
  89. package/docs/reference/mysql-snapshot.md +55 -0
  90. package/docs/reference/postgres-snapshot.md +61 -0
  91. package/docs/reference/sqlite-snapshot.md +53 -0
  92. package/docs/reference/supported-surface.md +104 -0
  93. package/docs/schema/catalog-model.md +58 -0
  94. package/docs/schema/code-generation.md +139 -0
  95. package/docs/schema/columns-and-writes.md +130 -0
  96. package/docs/schema/constraints-and-indexes.md +180 -0
  97. package/docs/schema/ddl-emission.md +75 -0
  98. package/docs/schema/diff.md +75 -0
  99. package/docs/schema/introspection.md +214 -0
  100. package/docs/schema/migration-plans.md +76 -0
  101. package/docs/schema/snapshots.md +93 -0
  102. package/docs/schema/storage-and-schema-sql.md +104 -0
  103. package/docs/schema/tables-and-names.md +106 -0
  104. package/docs/sql-semantic-types.md +141 -0
  105. package/docs/troubleshooting.md +151 -0
  106. package/package.json +132 -10
  107. package/skills/qubu/SKILL.md +42 -0
  108. package/skills/qubu/agents/openai.yaml +6 -0
  109. package/readme.md +0 -1
@@ -0,0 +1,225 @@
1
+ # Compose queries
2
+
3
+ > Reuse a query's inferred row shape as a typed source for CTEs, derived tables, subqueries, and set operations.
4
+
5
+ ## Turn a query into a CTE
6
+
7
+ `cte()` exposes the selected fields of a query as columns on a new source.
8
+ Attach it with `withCte()` and use the CTE source in `from()`:
9
+
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', {
24
+ id: integer(),
25
+ name: text(),
26
+ })
27
+
28
+ 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
+ )
35
+ )
36
+
37
+ const report = select(
38
+ { displayName: activeUsers.name },
39
+ withCte(activeUsers),
40
+ from(activeUsers)
41
+ )
42
+ ```
43
+
44
+ `activeUsers.name` is a typed column derived from the first query's row shape.
45
+ The rendered statement includes the `WITH` clause before `SELECT`. Selected
46
+ camelCase keys use snake_case while they belong to the CTE relation; the outer
47
+ result projection aliases them back to camelCase for the returned row.
48
+
49
+ ## Build a recursive CTE
50
+
51
+ `recursiveCte()` uses the anchor projection as the contract for a recursive
52
+ member. The callback receives a typed self-reference; introduce it through
53
+ `from()` or a join before selecting its fields:
54
+
55
+ ```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)))
74
+ )
75
+
76
+ const query = select({ value: numbers.value }, withCte(numbers), from(numbers))
77
+ ```
78
+
79
+ The anchor names the fields, application types, nullability, and SQL domains
80
+ that the returned source exposes. The member must project those same fields
81
+ with compatible types. Give bound anchor values an explicit SQL type with
82
+ `cast()` when the database cannot infer it from surrounding columns; PostgreSQL
83
+ requires this for recursive CTE anchors. Qubu renders `WITH RECURSIVE`, an
84
+ explicit relation column list, and `anchor UNION ALL member`; ordinary and
85
+ recursive CTEs can share one `withCte()` clause.
86
+
87
+ ## Use a derived table
88
+
89
+ Alias a query when it should be used as an inline source:
90
+
91
+ ```ts
92
+ import { alias, from, lower, select } from 'qubu'
93
+ import type { SqlTypeOf } from 'qubu'
94
+
95
+ const names = select({ name: lower(users.name) }, from(users))
96
+ const namesSource = alias(names, 'names')
97
+
98
+ const query = select({ name: namesSource.name }, from(namesSource))
99
+ type NameSqlDomain = SqlTypeOf<typeof namesSource.name>
100
+ // SqlText
101
+ ```
102
+
103
+ The alias gets the selected query's fields, while the new source identity keeps
104
+ scope checks from confusing `namesSource.name` with `users.name`.
105
+
106
+ Its SQL domain is retained too. The projected `lower(users.name)` remains
107
+ `SqlText` through this query alias, and the same preservation applies to a CTE,
108
+ so downstream text operations remain checked without redeclaring the field.
109
+
110
+ ## Nest a scalar subquery
111
+
112
+ `scalar()` turns a query with exactly one selected field into an expression:
113
+
114
+ ```ts
115
+ import { from, scalar, select, value } from 'qubu'
116
+
117
+ const firstId = select({ id: users.id }, from(users))
118
+ const query = select(
119
+ {
120
+ name: users.name,
121
+ firstId: scalar(firstId),
122
+ },
123
+ from(users)
124
+ )
125
+ ```
126
+
127
+ `scalar()` throws at runtime when the query selects more than one field. Its
128
+ type is the selected field's value type, widened with `null` when the query may
129
+ return no rows. An ordinary select and `fetchFirst(1)` are both nullable: the
130
+ limit proves at most one row, not that a row exists. A source-free select such
131
+ as `select({ value: value(42) })` is known to produce exactly one row.
132
+
133
+ Qubu does not treat an arbitrary predicate as proof of exactness. Use
134
+ `exists()`, `notExists()`, or `inQuery()` for boolean subquery predicates.
135
+
136
+ ## Combine compatible queries
137
+
138
+ Set operations preserve the left query's row shape. Both queries must select
139
+ compatible rows:
140
+
141
+ ```ts
142
+ import { eq, from, select, unionAll, where } from 'qubu'
143
+
144
+ const first = select({ id: users.id }, from(users))
145
+ const second = select({ id: users.id }, from(users), where(eq(users.id, 7)))
146
+
147
+ const allUsers = unionAll(first, second)
148
+ ```
149
+
150
+ Use `union`, `unionAll`, `intersect`, or `except` depending on the SQL
151
+ operation. Each input renderer still contributes its runtime parameters, which
152
+ are collected in traversal order.
153
+
154
+ ## Prefer values over a builder chain
155
+
156
+ Build reusable pieces as ordinary values and pass them into the final query:
157
+
158
+ ```ts
159
+ import { desc, eq, from, orderBy, select, where } from 'qubu'
160
+
161
+ const byId = where(eq(users.id, 7))
162
+ const newest = orderBy(desc(users.id))
163
+
164
+ const query = select(
165
+ { id: users.id, name: users.name },
166
+ from(users),
167
+ byId,
168
+ newest
169
+ )
170
+ ```
171
+
172
+ This makes it possible to share a predicate or projection without mutating a
173
+ query object. The final `select()` call remains the place where source scope
174
+ and result shape are checked. Qubu also accepts these independent values in
175
+ another order, but SQL order is the canonical visual style for finished query
176
+ code.
177
+
178
+ ## Constrain a reusable fragment by required fields
179
+
180
+ Use `TableLike` when a fragment requires a physical table and `SourceLike`
181
+ when aliases, CTEs, derived tables, or custom sources are also valid. Both are
182
+ lower-bound constraints: the source may contain additional fields, and the
183
+ generic function retains its exact source identity.
184
+
185
+ For an application-level requirement, describe the required JavaScript row:
186
+
187
+ ```ts
188
+ import { eq, where } from 'qubu'
189
+ import type { TableLike } from 'qubu'
190
+
191
+ function byStringId<TTable extends TableLike<{ id: string }>>(
192
+ table: TTable,
193
+ id: string
194
+ ) {
195
+ return where(eq(table.columns.id, id))
196
+ }
197
+ ```
198
+
199
+ `{ id: string }` means a non-null selected string. It accepts a table with
200
+ extra fields and rejects `string | null`, but it does not distinguish
201
+ `SqlText` from `SqlUuid` because both have a JavaScript output of `string`.
202
+
203
+ Use `FieldLike` when the fragment depends on SQL semantics:
204
+
205
+ ```ts
206
+ import { eq, where } from 'qubu'
207
+ import type { FieldLike, SourceLike, SqlTextLike } from 'qubu'
208
+
209
+ type NonNullTextId = FieldLike<{
210
+ sqlType: SqlTextLike
211
+ nullable: false
212
+ }>
213
+
214
+ function byTextId<TSource extends SourceLike<{ id: NonNullTextId }>>(
215
+ source: TSource,
216
+ id: string
217
+ ) {
218
+ return where(eq(source.columns.id, id))
219
+ }
220
+ ```
221
+
222
+ This version accepts known text-like and permissive `SqlUnknown` fields. It
223
+ rejects nullable text and known non-text domains such as `SqlUuid`. Add an
224
+ `output` property to the `FieldLike` descriptor when the fragment also needs a
225
+ specific JavaScript result type.
@@ -0,0 +1,129 @@
1
+ # Use a Qubu schema with Drizzle
2
+
3
+ > Derive Drizzle tables at runtime so Qubu and Drizzle queries can share one
4
+ > schema declaration during a gradual migration.
5
+
6
+ ## Install the optional integration
7
+
8
+ Install Drizzle next to Qubu. The Drizzle entrypoints use an optional peer
9
+ dependency, so importing the rest of Qubu does not load the ORM.
10
+
11
+ ```bash
12
+ pnpm add qubu drizzle-orm
13
+ ```
14
+
15
+ The converter supports PostgreSQL, MySQL, and SQLite with Drizzle 0.45.2 through
16
+ the current 0.x line.
17
+
18
+ ## Convert the schema
19
+
20
+ Declare tables and the root registry with Qubu, then import the converter for
21
+ your database:
22
+
23
+ ```ts
24
+ import { integer, schema, table, text } from 'qubu'
25
+ import { toPostgresDrizzleSchema } from 'qubu/drizzle/postgres'
26
+
27
+ const users = table('user_records', {
28
+ id: integer({ generated: true }),
29
+ name: text(),
30
+ nickname: text({ nullable: true }),
31
+ })
32
+
33
+ const appSchema = schema({ users }, { namespace: 'app' })
34
+ const drizzleTables = toPostgresDrizzleSchema(appSchema)
35
+ ```
36
+
37
+ The import path selects the dialect. Each module imports only its matching
38
+ Drizzle core package:
39
+
40
+ | Database | Import | Converter |
41
+ | ---------- | ----------------------- | --------------------------- |
42
+ | PostgreSQL | `qubu/drizzle/postgres` | `toPostgresDrizzleSchema()` |
43
+ | MySQL | `qubu/drizzle/mysql` | `toMysqlDrizzleSchema()` |
44
+ | SQLite | `qubu/drizzle/sqlite` | `toSqliteDrizzleSchema()` |
45
+
46
+ `qubu/drizzle` exports the shared conversion error and dialect types. It does
47
+ not import a dialect core or provide a universal runtime converter.
48
+
49
+ `drizzleTables.users` is a real Drizzle `PgTable`. The logical `users` key,
50
+ physical `user_records` name, `app` namespace, field keys, and physical column
51
+ names all come from the Qubu declaration.
52
+
53
+ Pass the converted record to Drizzle and use it in ordinary queries:
54
+
55
+ ```ts
56
+ import { eq } from 'drizzle-orm'
57
+ import { drizzle } from 'drizzle-orm/node-postgres'
58
+
59
+ const db = drizzle(pool, { schema: drizzleTables })
60
+
61
+ const rows = await db
62
+ .select({ id: drizzleTables.users.id, name: drizzleTables.users.name })
63
+ .from(drizzleTables.users)
64
+ .where(eq(drizzleTables.users.id, 7))
65
+ ```
66
+
67
+ Existing Qubu queries can keep importing `users`. Move call sites to
68
+ `drizzleTables.users` one at a time without duplicating the table declaration.
69
+
70
+ ## Type behavior
71
+
72
+ The converted columns preserve selected values, nullability, required insert
73
+ keys, defaults, and generated-column omission:
74
+
75
+ ```ts
76
+ type User = typeof drizzleTables.users.$inferSelect
77
+ // { id: number; name: string; nickname: string | null }
78
+
79
+ type NewUser = typeof drizzleTables.users.$inferInsert
80
+ // { name: string; nickname: string | null }
81
+ ```
82
+
83
+ Qubu requires a nullable insert field unless that field has a default. The
84
+ adapter retains that rule even though hand-written Drizzle schemas normally
85
+ make nullable fields optional.
86
+
87
+ Drizzle has one application value type per column. Qubu can instead declare
88
+ different select, insert, and update types with
89
+ `column<Output, Insert, Update>()`. That declaration has no lossless Drizzle
90
+ equivalent, so each dialect converter rejects it at compile time. Built-in
91
+ Qubu columns and `$type()` narrowing use one value type and convert without an
92
+ override.
93
+
94
+ ## Runtime metadata
95
+
96
+ Each dialect adapter maps Qubu storage descriptors to its own Drizzle builders.
97
+ It also transfers concrete defaults, generated expressions, common primary and
98
+ unique constraints, checks, foreign keys, and indexes. Native storage must
99
+ belong to the selected dialect:
100
+
101
+ ```ts
102
+ import { nativeColumn, schema, table } from 'qubu'
103
+ import { toPostgresDrizzleSchema } from 'qubu/drizzle/postgres'
104
+
105
+ const records = table('records', {
106
+ handle: nativeColumn('postgresql', 'CITEXT'),
107
+ })
108
+
109
+ const tables = toPostgresDrizzleSchema(schema({ records }))
110
+ ```
111
+
112
+ Conversion first runs Qubu's snapshot validation for the selected dialect.
113
+ Metadata that Drizzle 0.45 cannot express, such as deferred constraints or
114
+ included index columns, raises `DrizzleSchemaConversionError` with a `code` and
115
+ `path`.
116
+
117
+ PostgreSQL schemas and MySQL databases become Drizzle table namespaces. Drizzle
118
+ SQLite tables have no namespace field, so a Qubu SQLite namespace remains
119
+ snapshot metadata and is not attached to the converted table.
120
+
121
+ > [!IMPORTANT]
122
+ > An external default or generated descriptor records that another authority
123
+ > owns the SQL definition. The adapter preserves its Drizzle write types but
124
+ > cannot invent the missing expression. Keep Qubu snapshots and migration plans
125
+ > as the DDL authority while those descriptors remain external.
126
+
127
+ The returned record contains tables, not Drizzle `relations()` declarations.
128
+ Add relations beside the converted record if the application uses Drizzle's
129
+ relational query API.
@@ -0,0 +1,40 @@
1
+ # Add a dialect policy
2
+
3
+ > Change identifiers, placeholders, pagination, or cast targets at the rendering boundary without changing portable query construction.
4
+
5
+ Use `createDialect()` when the query is portable but the driver changes
6
+ identifiers, placeholders, or pagination:
7
+
8
+ The examples assume a query has already been built.
9
+
10
+ ```ts
11
+ import { render } from 'qubu'
12
+ import { createDialect } from 'qubu/core'
13
+
14
+ const colonDialect = createDialect({
15
+ name: 'colon',
16
+ placeholder: position => ':p' + position,
17
+ })
18
+
19
+ render(query, colonDialect)
20
+ // ... WHERE ... = :p1
21
+ ```
22
+
23
+ `createDialect()` uses Qubu's standard double-quoted identifier policy unless
24
+ you provide `quoteIdentifier`. A complete pagination policy can be supplied
25
+ through the optional `pagination` renderer.
26
+
27
+ For the full rendering and execution boundary, read
28
+ [Dialects and execution](../../dialects-and-execution.md). It covers capability
29
+ requirements, the driver adapter, and the values that cross the boundary.
30
+
31
+ For syntax that is not a small policy decision, add a
32
+ [custom fragment or clause](sources-and-clauses.md) instead of making the
33
+ standard dialect pretend that vendor behavior is portable.
34
+
35
+ ## Read next
36
+
37
+ - [Add sources and clauses](sources-and-clauses.md) covers custom relations and
38
+ clause placement.
39
+ - [Add typed expressions](typed-expressions.md) covers custom SQL domains and
40
+ cast targets.
@@ -0,0 +1,36 @@
1
+ # Extend Qubu
2
+
3
+ > Choose an extension boundary when the built-in API does not cover a driver-specific or uncommon SQL feature.
4
+
5
+ Qubu extensions are values that render SQL and carry the metadata later
6
+ composition needs. Choose the page that matches the thing you are adding:
7
+
8
+ Ordinary query code stays on the `qubu` root entrypoint. Import fragment,
9
+ dialect, and extension constructors from `qubu/core`; import custom sources and
10
+ schema-expression constructors from `qubu/schema`. The [supported
11
+ surface](../../reference/supported-surface.md) keeps the full entrypoint map in
12
+ one place.
13
+
14
+ | You need to add... | Read... |
15
+ | ----------------------------------- | --------------------------------------------- |
16
+ | A custom relation or clause | [Sources and clauses](sources-and-clauses.md) |
17
+ | A dialect-specific rendering policy | [A dialect policy](dialects.md) |
18
+ | A typed expression or SQL domain | [Typed expressions](typed-expressions.md) |
19
+ | Parameterized trusted SQL syntax | [SQL templates](../sql-templates.md) |
20
+ | Syntax Qubu does not model | [Unsafe syntax](unsafe-syntax.md) |
21
+
22
+ Use `context.parameter()` for runtime values. Use the typed forms when a
23
+ custom expression or source must preserve source, result, nullability, or SQL
24
+ domain metadata. The [sources and clauses](sources-and-clauses.md) page starts
25
+ with a complete custom-clause example.
26
+
27
+ ## Read the concept pages
28
+
29
+ - [Source scope](../../query-model/source-scope.md) explains the identities
30
+ custom sources must provide.
31
+ - [Fragments and metadata](../../query-model/fragments.md) explains the facts
32
+ composition carries between fragments.
33
+ - [Dialects and execution](../../dialects-and-execution.md) explains the
34
+ rendering and driver boundary.
35
+ - [SQL semantic types](../../sql-semantic-types.md) explains capability checks
36
+ for typed expressions.
@@ -0,0 +1,82 @@
1
+ # Add sources and clauses
2
+
3
+ > Publish a custom SQL clause or relation while preserving parameter order and source-scope checks.
4
+
5
+ ## Add a custom clause
6
+
7
+ Qubu extensions are ordinary fragments. A custom clause supplies a placement,
8
+ render order, and renderer:
9
+
10
+ ```ts
11
+ import { from, render, select, table, text } from 'qubu'
12
+ import { customClause } from 'qubu/core'
13
+
14
+ const users = table('users', { name: text() })
15
+
16
+ const fetchWithTies = customClause({
17
+ name: 'fetch-with-ties',
18
+ order: 100,
19
+ render(context) {
20
+ context.append('FETCH FIRST ')
21
+ context.parameter(10)
22
+ context.append(' ROWS WITH TIES')
23
+ },
24
+ })
25
+
26
+ const query = select({ name: users.name }, from(users), fetchWithTies)
27
+
28
+ render(query)
29
+ // ... FETCH FIRST ? ROWS WITH TIES
30
+ ```
31
+
32
+ `context.parameter(10)` binds the value at render time, and the rendered
33
+ parameter is still ordered with every other parameter in the query. Parameter
34
+ value types are intentionally not part of fragment metadata.
35
+
36
+ ## Add a typed custom FROM source
37
+
38
+ Use `customSource()` for a table-valued function or another relation whose row
39
+ shape is known to the application but cannot be declared with `table()`. Its
40
+ `columns` definitions create the same direct and `.columns` references as a
41
+ table, while `from()` or a join supplies the source to the query scope:
42
+
43
+ ```ts
44
+ import { from, integer, render, select, text } from 'qubu'
45
+ import { identifier } from 'qubu/core'
46
+ import { customSource } from 'qubu/schema'
47
+
48
+ const rows = customSource({
49
+ identity: { sourceKind: 'table-function', name: 'json_each', alias: 'row' },
50
+ sourceKind: 'table-function',
51
+ reference: identifier('row'),
52
+ columns: {
53
+ key: integer(),
54
+ value: text({ nullable: true }),
55
+ },
56
+ render(context) {
57
+ context.append('json_each(')
58
+ context.parameter('{"a":1}')
59
+ context.append(') AS ')
60
+ context.render(identifier('row'))
61
+ },
62
+ })
63
+
64
+ const query = select({ value: rows.value }, from(rows))
65
+ render(query)
66
+ // ... FROM json_each(?) AS "row"
67
+ ```
68
+
69
+ `identity` is the source-scope key; `reference` is the SQL qualifier used by
70
+ the generated columns. A nullable column remains nullable intrinsically, and a
71
+ `leftJoin(rows, ...)` adds outer-join nullability to every selected row
72
+ column. Render the complete relation in the producer and bind values with
73
+ `context.parameter()`; the normal renderer preserves parameter order.
74
+
75
+ ## Read next
76
+
77
+ - [Add typed expressions](typed-expressions.md) covers custom SQL domains and
78
+ metadata-preserving expression wrappers.
79
+ - [Add a dialect policy](dialects.md) covers identifier, placeholder, and
80
+ pagination policies.
81
+ - [Source scope](../../query-model/source-scope.md) explains the identity rules
82
+ custom sources must satisfy.
@@ -0,0 +1,94 @@
1
+ # Add typed expressions
2
+
3
+ > Extend Qubu with expressions that retain source, nullability, result, and SQL-domain metadata.
4
+
5
+ ## Build expressions from public helpers
6
+
7
+ The examples use the `users` table from [Sources and clauses](sources-and-clauses.md).
8
+
9
+ `fragment()`, `makeExpression()`, `parameter()`, `identifier()`, `syntax()`,
10
+ `customClause()`, and `customSource()` are public extension building blocks.
11
+ Import fragment, dialect, and expression constructors from `qubu/core`; import
12
+ custom source and schema metadata constructors from `qubu/schema`. Preserve the
13
+ same metadata model that built-ins use:
14
+
15
+ - use `RequiresSourceMeta<Source>` for every source that the expression reads;
16
+ - use `ResultMeta<Output, NullableFrom, SqlType>` when the fragment exposes a
17
+ typed result, or accept its default `SqlUnknown` domain intentionally;
18
+ - inherit child source and nullability facts when composing fragments; and
19
+ - use `context.parameter()` for values instead of concatenating them into SQL.
20
+
21
+ `sequence()` is useful for a reusable fragment assembled from arbitrary child
22
+ fragments. Its `const` type parameter preserves the children's metadata, so
23
+ source-scope checking continues to work without `as const` at the call site.
24
+
25
+ ## Declare a custom SQL domain
26
+
27
+ Extend `SqlSemanticType` and only the portable capabilities the database type
28
+ actually supports. Compatibility groups allow a dialect-specific domain to
29
+ interoperate with a built-in family:
30
+
31
+ ```ts
32
+ import { cast, column } from 'qubu'
33
+ import type {
34
+ SqlEqualityComparable,
35
+ SqlOrderable,
36
+ SqlSemanticType,
37
+ SqlTextLike,
38
+ } from 'qubu'
39
+
40
+ interface SqlCitext
41
+ extends SqlSemanticType<'postgres.citext'>,
42
+ SqlTextLike,
43
+ SqlOrderable<'text'>,
44
+ SqlEqualityComparable<'text'> {}
45
+
46
+ const citext = column<string, string, string, SqlCitext>({
47
+ castType: 'CITEXT',
48
+ })
49
+
50
+ const nameAsCitext = cast(users.name, citext)
51
+ ```
52
+
53
+ The first three `column` type arguments are output, insert, and update values;
54
+ the fourth is the SQL domain. The `text` equality and ordering groups make the
55
+ custom domain compatible with `SqlText`. Use a distinct group when cross-type
56
+ comparison is not portable. `castType` also makes this definition a cast
57
+ target; its SQL text is emitted verbatim, so keep it in trusted extension code.
58
+ Definitions with schema flags are not accepted as cast targets because cast
59
+ nullability comes from the operand and write flags have no cast meaning.
60
+
61
+ Declare result domains at other extension boundaries too:
62
+
63
+ ```ts
64
+ import { typedCall, typedCast, typedValue, unsafeExpression } from 'qubu/core'
65
+ import type { SqlText, SqlUuid } from 'qubu'
66
+
67
+ const id = typedValue<SqlUuid, string>('108cb836-20d2-41b2-8c23-f0c94700aa7e')
68
+ const normalized = typedCall<SqlText, string>()('custom_text', users.name)
69
+ const rawNameAsText = typedCast<string, SqlText>()(users.name, 'TEXT')
70
+ const generated = unsafeExpression<string, SqlText>('custom_text()')
71
+ ```
72
+
73
+ `typedCall()` preserves source requirements from its arguments. `typedCast()`
74
+ is the fallback when no reusable definition describes the target. It preserves
75
+ operand nullability and source metadata while emitting its supplied type name
76
+ verbatim. `typedValue()` binds a parameter. `unsafeExpression()` emits its
77
+ string unchanged and should remain a last resort.
78
+
79
+ The lower-level forms also expose the SQL domain in their generic lists:
80
+ `call<Output, Name, Arguments, NullableFrom, SqlType>()` and
81
+ `cast<Output, SqlType>()`. They are useful when an extension already computes
82
+ argument or nullability types in its own generic signature.
83
+
84
+ Untyped `column()`, `value()`, `call()`, and custom expressions use
85
+ `SqlUnknown`, which stays permissive for backward compatibility. Declaring a
86
+ known domain opts the extension into incompatible-operation errors. See
87
+ [SQL semantic types](../../sql-semantic-types.md) for the capability model and
88
+ its limits.
89
+
90
+ ## Read next
91
+
92
+ - [Add sources and clauses](sources-and-clauses.md) covers custom relations and
93
+ clause renderers.
94
+ - [Use unsafe syntax](unsafe-syntax.md) covers the raw-SQL boundary.
@@ -0,0 +1,27 @@
1
+ # Use unsafe syntax
2
+
3
+ > Keep raw SQL visible and trusted when Qubu does not model the syntax you need.
4
+
5
+ `unsafeExpression()` and related escape hatches exist for syntax that cannot be
6
+ modeled yet. They do not quote identifiers or bind values for you:
7
+
8
+ ```ts
9
+ import { select } from 'qubu'
10
+ import { unsafeExpression } from 'qubu/core'
11
+
12
+ const query = select({
13
+ today: unsafeExpression('CURRENT_DATE'),
14
+ })
15
+ ```
16
+
17
+ Keep raw identifiers and values out of the string. Prefer a typed custom
18
+ fragment when the syntax will be reused. Use the [`sql` template
19
+ tag](../sql-templates.md) when fixed trusted syntax needs bound runtime values
20
+ or existing Qubu fragments. Keep dynamic SQL text on `unsafeExpression()` and
21
+ runtime identifiers on `identifier()` or `qualifiedIdentifier()` from
22
+ `qubu/core`.
23
+
24
+ Read [Dialects and execution](../../dialects-and-execution.md) for the boundary
25
+ between rendering and driver behavior. Read [Add typed
26
+ expressions](typed-expressions.md) when the extension needs a result domain or
27
+ source metadata.